Radish alpha
r
Radicle web interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
Extract ToggleButton component
Rūdolfs Ošiņš committed 3 years ago
commit caac53a8f657d53d3bfc57f8111e3e8a9232df10
parent c5eddde23f733ad90bf3dd2f94dad2e722a7f4d6
9 files changed +216 -228
added src/ToggleButton.svelte
@@ -0,0 +1,68 @@
+
<script lang="ts" context="module">
+
  export interface ToggleButtonOption<T> {
+
    title?: string;
+
    count?: number;
+
    value: T;
+
  }
+
</script>
+

+
<script lang="ts" strictEvents>
+
  type T = $$Generic;
+

+
  import { createEventDispatcher } from "svelte";
+
  import { capitalize } from "@app/utils";
+

+
  export let options: ToggleButtonOption<T>[];
+
  export let active: T;
+

+
  const dispatch = createEventDispatcher<{ select: T }>();
+

+
  function onSelect(option: ToggleButtonOption<T>) {
+
    if (option.count !== 0) {
+
      dispatch("select", option.value);
+
    }
+
  }
+
</script>
+

+
<style>
+
  .wrapper {
+
    display: flex;
+
    gap: 1rem;
+
    user-select: none;
+
  }
+
  button {
+
    border-radius: var(--border-radius-small);
+
    color: var(--color-foreground-80);
+
    cursor: pointer;
+
    font-family: var(--font-family-monospace);
+
    font-size: 0.75rem;
+
    height: var(--button-tiny-height);
+
    padding: 0.25rem 0.5rem;
+
    border: none;
+
    min-width: 0;
+
  }
+
  button:hover, button.active {
+
    cursor: pointer;
+
    color: var(--color-foreground);
+
    background-color: var(--color-foreground-background);
+
  }
+
  button[disabled], button[disabled]:hover {
+
    cursor: not-allowed;
+
    color: var(--color-foreground-80);
+
  }
+
</style>
+

+
<div class="wrapper">
+
  {#each options as option}
+
    <button
+
      class="state-toggle"
+
      on:click={() => onSelect(option)}
+
      disabled={option.count === 0}
+
      class:active={active === option.value}>
+
      {#if option.count !== undefined}
+
        {option.count}
+
      {/if}
+
      {option.title ?? capitalize(`${option.value}`)}
+
    </button>
+
  {/each}
+
</div>
modified src/base/projects/Commit.svelte
@@ -27,6 +27,7 @@
    padding: 1rem;
    background: var(--color-foreground-background-subtle);
    border-radius: var(--border-radius);
+
    margin-bottom: 1.5rem;
  }
  .summary {
    display: flex;
deleted src/base/projects/Issue/IssueFilter.svelte
@@ -1,71 +0,0 @@
-
<script lang="ts">
-
  import { navigate } from "svelte-routing";
-
  import { groupIssues, Issue } from "@app/issue";
-
  import Placeholder from "@app/Placeholder.svelte";
-
  import { capitalize } from "@app/utils";
-

-
  export let issues: Issue[];
-
  export let state = "open";
-

-
  const { open, closed } = groupIssues(issues);
-

-
  $: filteredIssues = state === "open" ? open : closed;
-
</script>
-

-
<style>
-
  .filter {
-
    display: flex;
-
    flex-direction: row;
-
    align-items: center;
-
    margin: 1rem 0;
-
    border-radius: var(--border-radius-small);
-
  }
-
  .state-toggle {
-
    cursor: pointer;
-
    color: var(--color-foreground-80);
-
    font-family: var(--font-family-monospace);
-
    font-size: 0.75rem;
-
  }
-
  .state-toggle:hover {
-
    cursor: pointer;
-
    color: var(--color-foreground);
-
  }
-
  .state-toggle[disabled], .state-toggle[disabled]:hover {
-
    cursor: not-allowed;
-
    color: var(--color-foreground-80);
-
  }
-
  .active {
-
    color: var(--color-foreground);
-
  }
-
  .separator {
-
    color: var(--color-foreground-faded);
-
    margin: 0 0.5rem;
-
  }
-
</style>
-

-
<div class="filter">
-
  <button
-
    class="unstyled state-toggle"
-
    on:click={() => navigate("?state=open") }
-
    disabled={open.length === 0}
-
    class:active={state === "open"}>
-
    {open.length} Open
-
  </button>
-
  <span class="separator">&middot;</span>
-
  <button
-
    class="unstyled state-toggle"
-
    on:click={() => navigate("?state=closed")}
-
    disabled={closed.length === 0}
-
    class:active={state === "closed"}>
-
    {closed.length} Closed
-
  </button>
-
</div>
-

-
{#if filteredIssues.length}
-
  <slot {filteredIssues} />
-
{:else}
-
  <Placeholder icon="🍣">
-
    <div slot="title">{capitalize(state)} issues</div>
-
    <div slot="body">No issues matched the current filter</div>
-
  </Placeholder>
-
{/if}
modified src/base/projects/Issues.svelte
@@ -1,24 +1,42 @@
+
<script lang="ts" context="module">
+
  export type State = "open" | "closed";
+
</script>
+

<script lang="ts">
-
  import { Project, ProjectContent } from "@app/project";
  import type { Config } from "@app/config";
-
  import IssueTeaser from "@app/base/projects/Issue/IssueTeaser.svelte";
-
  import IssueFilter from "@app/base/projects/Issue/IssueFilter.svelte";
  import type { Issue } from "@app/issue";
+
  import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
+

+
  import { Project, ProjectContent } from "@app/project";
+
  import { capitalize } from "@app/utils";
+
  import { groupIssues } from "@app/issue";
+
  import { navigate } from "svelte-routing";
+

+
  import IssueTeaser from "@app/base/projects/Issue/IssueTeaser.svelte";
+
  import Placeholder from "@app/Placeholder.svelte";
+
  import ToggleButton from "@app/ToggleButton.svelte";

-
  export let project: Project;
  export let config: Config;
-
  export let state: string;
  export let issues: Issue[];
+
  export let project: Project;
+
  export let state: State;

-
  const navigate = (issue: string) => {
-
    project.navigateTo({
-
      content: ProjectContent.Issue,
-
      issue,
-
      patch: null,
-
      revision: null,
-
      path: null
-
    });
-
  };
+
  let options: ToggleButtonOption<State>[];
+
  const { open, closed } = groupIssues(issues);
+

+
  $: filteredIssues = state === "open" ? open : closed;
+
  $: sortedIssues = filteredIssues.sort(({ timestamp: t1 }, { timestamp: t2 }) => t2 - t1);
+

+
  $: options = [
+
    {
+
      value: "open",
+
      count: open.length
+
    },
+
    {
+
      value: "closed",
+
      count: closed.length
+
    },
+
  ];
</script>

<style>
@@ -42,14 +60,30 @@
</style>

<div class="issues">
-
  <IssueFilter {state} {issues} let:filteredIssues>
-
  {@const sortedIssues = filteredIssues.sort(({ timestamp: t1 }, { timestamp: t2 }) => t2 - t1)}
+
  <div style="margin-bottom: 1rem;">
+
    <ToggleButton {options} on:select={(e) => {navigate(`?state=${e.detail}`);}} active={state} />
+
  </div>
+

+
  {#if filteredIssues.length}
    <div class="issues-list">
      {#each sortedIssues as issue}
-
        <div class="teaser" on:click={() => navigate(issue.id)}>
+
        <div class="teaser" on:click={() => {
+
            project.navigateTo({
+
              content: ProjectContent.Issue,
+
              issue: issue.id,
+
              patch: null,
+
              revision: null,
+
              path: null
+
            });
+
          }}>
          <IssueTeaser {config} {issue} />
        </div>
      {/each}
    </div>
-
  </IssueFilter>
+
  {:else}
+
    <Placeholder icon="🍣">
+
      <div slot="title">{capitalize(state)} issues</div>
+
      <div slot="body">No issues matched the current filter</div>
+
    </Placeholder>
+
  {/if}
</div>
deleted src/base/projects/Patch/PatchFilter.svelte
@@ -1,74 +0,0 @@
-
<script lang="ts">
-
  import { groupPatches, Patch } from "@app/patch";
-
  import Placeholder from "@app/Placeholder.svelte";
-
  import { capitalize } from "@app/utils";
-

-
  export let patches: Patch[];
-
  export let state = "proposed";
-

-
  const sortedPatches = groupPatches(patches);
-

-
  $: filteredPatches = sortedPatches[state];
-
</script>
-

-
<style>
-
  .filter {
-
    display: flex;
-
    flex-direction: row;
-
    gap: 1rem;
-
    margin: 1rem 0;
-
  }
-
  .state-toggle {
-
    cursor: pointer;
-
    color: var(--color-foreground-80);
-
    border-radius: var(--border-radius-small);
-
    font-family: var(--font-family-monospace);
-
    font-size: 0.75rem;
-
    padding: 0.25rem 0.5rem;
-
  }
-
  .state-toggle:hover:not([disabled]), .state-toggle.active {
-
    cursor: pointer;
-
    color: var(--color-foreground);
-
    background-color: var(--color-foreground-background);
-
  }
-
  .state-toggle[disabled], .state-toggle[disabled]:hover {
-
    cursor: not-allowed;
-
    color: var(--color-foreground-80);
-
  }
-
  .active {
-
    color: var(--color-foreground);
-
  }
-
</style>
-

-
<div class="filter">
-
  <div
-
    class="unstyled state-toggle"
-
    on:click={() => state = "proposed"}
-
    disabled={sortedPatches.proposed.length === 0}
-
    class:active={state === "proposed"}>
-
    {sortedPatches.proposed.length} Proposed
-
  </div>
-
  <div
-
    class="unstyled state-toggle"
-
    on:click={() => state = "draft"}
-
    disabled={sortedPatches.draft.length === 0}
-
    class:active={state === "draft"}>
-
    {sortedPatches.draft.length} Draft
-
  </div>
-
  <div
-
    class="unstyled state-toggle"
-
    on:click={() => state = "archived"}
-
    disabled={sortedPatches.archived.length === 0}
-
    class:active={state === "archived"}>
-
    {sortedPatches.archived.length} Archived
-
  </div>
-
</div>
-

-
{#if filteredPatches.length}
-
  <slot {filteredPatches} />
-
{:else}
-
  <Placeholder icon="🍖">
-
    <div slot="title">{capitalize(state)} patches</div>
-
    <div slot="body">No patches matched the current filter</div>
-
  </Placeholder>
-
{/if}
modified src/base/projects/Patch/PatchTabBar.svelte
@@ -1,6 +1,10 @@
<script lang="ts">
+
  import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
+

  import Dropdown from "@app/Dropdown.svelte";
  import Floating from "@app/Floating.svelte";
+
  import ToggleButton from "@app/ToggleButton.svelte";
+

  import { PatchTab, Revision } from "@app/patch";
  import { formatCommit, formatTimestamp } from "@app/utils";
  import { createEventDispatcher } from "svelte";
@@ -27,41 +31,40 @@
  const onRevisionChange = ({ detail }: { detail: string }) => {
    dispatch("revisionChanged", detail);
  };
+

+
  let options: ToggleButtonOption<PatchTab>[];
+
  $: options = [
+
    {
+
      title: "Patch",
+
      value: PatchTab.Timeline,
+
    },
+
    {
+
      title: "Changeset",
+
      value: PatchTab.Diff,
+
    },
+
  ];
</script>

<style>
  .bar {
-
    margin: 1rem 0;
-
  }
-
  .tabs {
-
    display: flex;
-
    flex-direction: row;
    align-items: center;
+
    display: flex;
    gap: 1rem;
+
    margin: 1.5rem 0;
  }
-
  .tab {
-
    color: var(--color-foreground-80);
+
  .revision-toggle {
    border-radius: var(--border-radius-small);
+
    border: none;
+
    color: var(--color-foreground-80);
    font-family: var(--font-family-monospace);
    font-size: 0.75rem;
+
    height: var(--button-tiny-height);
    padding: 0.25rem 0.5rem;
  }
-
  .tab:hover, .tab.active {
-
    color: var(--color-foreground);
+
  .revision-toggle:hover {
    background-color: var(--color-foreground-background);
-
    cursor: pointer;
-
  }
-
  .active {
    color: var(--color-foreground);
-
    cursor: default !important;
-
  }
-
  .revision-toggle {
-
    color: var(--color-foreground-80);
-
    border: none;
-
    padding: 0;
-
  }
-
  .revision-toggle:hover {
-
    background: none;
+
    cursor: pointer;
  }
  .revision-toggle:disabled {
    color: var(--color-foreground-faded);
@@ -69,32 +72,20 @@
</style>

<div class="bar text-small">
-
  <div class="tabs">
-
    <div
-
      class="tab" class:active={activeTab === PatchTab.Timeline}
-
      on:click={() => dispatch("switchTab", PatchTab.Timeline)}>
-
      Patch
-
    </div>
-
    <div
-
      class="tab" class:active={activeTab === PatchTab.Diff}
-
      on:click={() => dispatch("switchTab", PatchTab.Diff)}>
-
      Changeset
-
    </div>
-
    <div class="revision-toggle">
-
      <Floating disabled={revisions.length <= 1}>
-
        <button
-
          slot="toggle"
-
          class:tab={revisions.length > 1}
-
          class="text-small revision-toggle"
-
          disabled={revisions.length <= 1}>
-
          {formatRevisionName(revisions[revisionNumber], revisionNumber)}
-
        </button>
-
        <svelte:fragment slot="modal">
-
          <Dropdown
-
            items={revisionList} selected={revisionNumber.toString()}
-
            on:select={onRevisionChange} />
-
        </svelte:fragment>
-
      </Floating>
-
    </div>
-
  </div>
+
  <ToggleButton {options} on:select={(e) => {dispatch("switchTab", e.detail);}} active={activeTab} />
+

+
  <Floating disabled={revisions.length <= 1}>
+
    <button
+
      slot="toggle"
+
      class="text-small revision-toggle"
+
      disabled={revisions.length <= 1}>
+
      {formatRevisionName(revisions[revisionNumber], revisionNumber)}
+
    </button>
+

+
    <svelte:fragment slot="modal">
+
      <Dropdown
+
        items={revisionList} selected={revisionNumber.toString()}
+
        on:select={onRevisionChange} />
+
    </svelte:fragment>
+
  </Floating>
</div>
modified src/base/projects/Patches.svelte
@@ -1,23 +1,41 @@
<script lang="ts">
+
  type State = "proposed" | "draft" | "archived";
+

  import type { Config } from "@app/config";
  import type { Patch } from "@app/patch";
-
  import { Project, ProjectContent } from "@app/project";
-
  import PatchFilter from "./Patch/PatchFilter.svelte";
+
  import type { ToggleButtonOption } from "@app/ToggleButton.svelte";
+

  import PatchTeaser from "./Patch/PatchTeaser.svelte";
+
  import Placeholder from "@app/Placeholder.svelte";
+
  import ToggleButton from "@app/ToggleButton.svelte";
+

+
  import { Project, ProjectContent } from "@app/project";
+
  import { capitalize } from "@app/utils";
+
  import { groupPatches } from "@app/patch";

+
  export let state: State = "proposed";
  export let config: Config;
  export let patches: Patch[];
  export let project: Project;

-
  const navigate = (patch: string) => {
-
    project.navigateTo({
-
      content: ProjectContent.Patch,
-
      patch,
-
      issue: null,
-
      revision: null,
-
      path: null
-
    });
-
  };
+
  let options: ToggleButtonOption<State>[];
+
  const sortedPatches = groupPatches(patches);
+

+
  $: filteredPatches = sortedPatches[state];
+
  $: options = [
+
    {
+
      value: "proposed",
+
      count: sortedPatches.proposed.length
+
    },
+
    {
+
      value: "draft",
+
      count: sortedPatches.draft.length
+
    },
+
    {
+
      value: "archived",
+
      count: sortedPatches.archived.length
+
    }
+
  ];
</script>

<style>
@@ -41,13 +59,30 @@
</style>

<div class="patches">
-
  <PatchFilter {patches} let:filteredPatches>
+
  <div style="margin-bottom: 1rem;">
+
    <ToggleButton {options} on:select={(e) => {state = e.detail;}} active={state} />
+
  </div>
+

+
  {#if filteredPatches.length}
    <div class="patches-list">
      {#each filteredPatches as patch}
-
        <div class="teaser" on:click={() => navigate(patch.id)}>
+
        <div class="teaser" on:click={() => {
+
            project.navigateTo({
+
              content: ProjectContent.Patch,
+
              patch: patch.id,
+
              issue: null,
+
              revision: null,
+
              path: null
+
            });
+
          }}>
          <PatchTeaser {config} {patch} />
        </div>
      {/each}
    </div>
-
  </PatchFilter>
+
  {:else}
+
    <Placeholder icon="🍖">
+
      <div slot="title">{capitalize(state)} patches</div>
+
      <div slot="body">No patches matched the current filter</div>
+
    </Placeholder>
+
  {/if}
</div>
modified src/base/projects/Project.svelte
@@ -1,5 +1,7 @@
<script lang="ts">
  import type { Config } from '@app/config';
+
  import type { State as IssueState } from './Issues.svelte';
+

  import * as proj from '@app/project';
  import Placeholder from '@app/Placeholder.svelte';
  import Loading from '@app/Loading.svelte';
@@ -30,6 +32,8 @@
  const parentName = project.profile ? formatProfile(project.profile.nameOrAddress, config) : null;
  let pageTitle = parentName ? `${parentName}/${project.name}` : project.name;

+
  $: issueFilter = $browserStore.search?.get("state") as IssueState ?? "open";
+

  const baseName = parentName
    ? `${parentName}/${project.name}`
    : project.name;
@@ -93,7 +97,7 @@

  {#if content === proj.ProjectContent.Issues}
    <Async fetch={issue.Issue.getIssues(project.urn, project.seed.api)} let:result>
-
      <Issues {project} state={$browserStore.search?.get("state") || "open"} {config} issues={result} />
+
      <Issues {project} state={issueFilter} {config} issues={result} />
    </Async>
  {:else if content === proj.ProjectContent.Issue && $browserStore.issue}
    <Async fetch={issue.Issue.getIssue(project.urn, $browserStore.issue, project.seed.api)} let:result>
modified src/base/projects/SourceBrowser/Changeset.svelte
@@ -24,7 +24,7 @@

<style>
  .changeset-summary {
-
    padding: 1.5rem 0;
+
    padding-bottom: 1.5rem;
    margin-left: 1rem;
  }
  .changeset-summary .additions {