Radish alpha
r
Radicle desktop app
Radicle
Git (anonymous pull)
Log in to clone via SSH
Implement issue and patch filters in second column
✓ CI success Rūdolfs Ošiņš committed 1 year ago
commit c2f38bb0cbb117a3c602c4ea14ae7546fb840cf3
parent 4571d466dbb4dbff8aba9cabe5e7b7704366dc78
1 passed (1 total) View logs
8 files changed +278 -68
modified src/components/IssueSecondColumn.svelte
@@ -3,11 +3,18 @@
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { IssueStatus } from "@app/views/repo/router";

+
  import capitalize from "lodash/capitalize";
+

  import * as router from "@app/lib/router";
+
  import { issueStatusColor } from "@app/lib/utils";

-
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
+
  import Border from "./Border.svelte";
+
  import DropdownList from "./DropdownList.svelte";
+
  import DropdownListItem from "./DropdownListItem.svelte";
  import Icon from "./Icon.svelte";
+
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
  import OutlineButton from "./OutlineButton.svelte";
+
  import Popover, { closeFocused } from "./Popover.svelte";

  const activeRouteStore = router.activeRouteStore;

@@ -17,11 +24,15 @@
    issues: Issue[];
    status: IssueStatus;
    title: string;
+
    changeFilter: (status: IssueStatus) => void;
  }

  /* eslint-disable prefer-const */
-
  let { repo, selectedIssueId, issues, status, title }: Props = $props();
+
  let { repo, selectedIssueId, issues, status, title, changeFilter }: Props =
+
    $props();
  /* eslint-enable prefer-const */
+

+
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>

<style>
@@ -40,25 +51,79 @@
  }
</style>

+
{#snippet icons(status: IssueStatus)}
+
  <div
+
    class="icon"
+
    style:color={status === "all" ? undefined : issueStatusColor[status]}>
+
    <Icon name={status === "closed" ? "issue-closed" : "issue"} />
+
  </div>
+
{/snippet}
+

+
{#snippet counters(status: IssueStatus)}
+
  <div style:margin-left="auto" style:padding-left="0.25rem">
+
    {#if status === "all"}
+
      {project.meta.issues.open + project.meta.issues.closed}
+
    {:else}
+
      {project.meta.issues[status]}
+
    {/if}
+
  </div>
+
{/snippet}
+

<div class="container">
-
  <div class="txt-regular txt-semibold global-flex" style:gap="4px">
+
  <div
+
    class="txt-regular txt-semibold global-flex"
+
    style:gap="4px"
+
    style:white-space="nowrap">
    {title}
    <Icon name="chevron-right" />
    Issues
  </div>

-
  <OutlineButton
-
    variant="ghost"
-
    disabled={$activeRouteStore.resource === "repo.createIssue"}
-
    onclick={() => {
-
      void router.push({
-
        resource: "repo.createIssue",
-
        rid: repo.rid,
-
        status,
-
      });
-
    }}>
-
    <Icon name="plus" />New
-
  </OutlineButton>
+
  <div class="global-flex" style:margin-left="1rem">
+
    <Popover popoverPositionRight="0" popoverPositionTop="2.5rem">
+
      {#snippet toggle(onclick)}
+
        <OutlineButton variant="ghost" {onclick}>
+
          {@render icons(status)}
+
          {capitalize(status)}
+
          {@render counters(status)}
+
          <Icon name="chevron-down" />
+
        </OutlineButton>
+
      {/snippet}
+

+
      {#snippet popover()}
+
        <Border variant="ghost">
+
          <DropdownList items={["all", "open", "closed"] as IssueStatus[]}>
+
            {#snippet item(state)}
+
              <DropdownListItem
+
                style="gap: 0.5rem"
+
                selected={status === state}
+
                onclick={() => {
+
                  changeFilter(state);
+
                  closeFocused();
+
                }}>
+
                {@render icons(state)}
+
                {capitalize(state)}
+
                {@render counters(state)}
+
              </DropdownListItem>
+
            {/snippet}
+
          </DropdownList>
+
        </Border>
+
      {/snippet}
+
    </Popover>
+

+
    <OutlineButton
+
      variant="ghost"
+
      disabled={$activeRouteStore.resource === "repo.createIssue"}
+
      onclick={() => {
+
        void router.push({
+
          resource: "repo.createIssue",
+
          rid: repo.rid,
+
          status,
+
        });
+
      }}>
+
      <Icon name="plus" />New
+
    </OutlineButton>
+
  </div>
</div>
<div class="issue-list">
  {#each issues as issue}
@@ -69,4 +134,26 @@
      rid={repo.rid}
      selected={issue.id === selectedIssueId} />
  {/each}
+

+
  {#if issues.length === 0}
+
    <Border
+
      styleMinWidth="25rem"
+
      variant="ghost"
+
      styleAlignItems="center"
+
      styleJustifyContent="center">
+
      <div
+
        class="global-flex"
+
        style:height="74px"
+
        style:justify-content="center">
+
        <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
          <Icon name="none" />
+
          {#if status === "all"}
+
            No issues.
+
          {:else}
+
            No {status} issues.
+
          {/if}
+
        </div>
+
      </div>
+
    </Border>
+
  {/if}
</div>
modified src/components/IssuesSecondColumn.svelte
@@ -1,6 +1,5 @@
<script lang="ts">
  import type { IssueStatus } from "@app/views/repo/router";
-
  import type { ProjectPayload } from "@bindings/repo/ProjectPayload";
  import type { RepoInfo } from "@bindings/repo/RepoInfo";

  import Border from "./Border.svelte";
@@ -10,12 +9,13 @@
  import Settings from "./Settings.svelte";

  interface Props {
-
    project: ProjectPayload;
    status: IssueStatus;
    repo: RepoInfo;
  }

-
  const { project, status, repo }: Props = $props();
+
  const { status, repo }: Props = $props();
+

+
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>

<style>
modified src/components/Sidebar.svelte
@@ -2,8 +2,6 @@
  import type { IssueStatus, PatchStatus } from "@app/views/repo/router";

  import * as router from "@app/lib/router";
-
  import { patchStatusColor } from "@app/lib/utils";
-
  import { issueStatusColor } from "@app/lib/utils";

  import { storeLayout, getLayout } from "@app/views/repo/Layout.svelte";

@@ -61,16 +59,7 @@
      styleWidth="40px"
      styleHeight="40px"
      styleJustifyContent="center">
-
      <div
-
        style:color={activeTab.status === "all"
-
          ? undefined
-
          : issueStatusColor[activeTab.status]}>
-
        {#if activeTab.status === "open"}
-
          <Icon name="issue" />
-
        {:else}
-
          <Icon name="issue-closed" />
-
        {/if}
-
      </div>
+
      <Icon name="issue" />
    </Border>
  {:else}
    <button
@@ -100,16 +89,7 @@
      styleWidth="40px"
      styleHeight="40px"
      styleJustifyContent="center">
-
      <div
-
        style:color={activeTab.status
-
          ? patchStatusColor[activeTab.status]
-
          : undefined}>
-
        {#if activeTab.status === "open" || activeTab.status === undefined}
-
          <Icon name="patch" />
-
        {:else}
-
          <Icon name={`patch-${activeTab.status}`} />
-
        {/if}
-
      </div>
+
      <Icon name="patch" />
    </Border>
  {:else}
    <button
modified src/views/repo/CreateIssue.svelte
@@ -31,16 +31,35 @@
    status: IssueStatus;
  }

-
  const { repo, issues, config, status }: Props = $props();
+
  const {
+
    repo,
+
    issues: initialIssues,
+
    config,
+
    status: initialStatus,
+
  }: Props = $props();

  const project = $derived(repo.payloads["xyz.radicle.project"]!);

  let preview: boolean = $state(false);
  let title: string = $state("");
+
  let status = $state(initialStatus);
+
  let issues = $state(initialIssues);

  let assignees: Author[] = $state([]);
  let labels: string[] = $state([]);

+
  async function loadIssues(filter: IssueStatus) {
+
    try {
+
      issues = await invoke<Issue[]>("list_issues", {
+
        rid: repo.rid,
+
        status: filter,
+
      });
+
      status = filter;
+
    } catch (error) {
+
      console.error("Loading issue list failed", error);
+
    }
+
  }
+

  async function createIssue(
    description: string,
    embeds: Embed[],
@@ -97,7 +116,14 @@
  {/snippet}

  {#snippet secondColumn()}
-
    <IssueSecondColumn {repo} {issues} {status} title={project.data.name} />
+
    <IssueSecondColumn
+
      {repo}
+
      {issues}
+
      {status}
+
      title={project.data.name}
+
      changeFilter={async filter => {
+
        await loadIssues(filter);
+
      }} />
  {/snippet}

  <div class="content">
modified src/views/repo/Issue.svelte
@@ -60,12 +60,12 @@
    activity,
    config,
    threads,
-
    status,
+
    status: initialStatus,
  }: Props = $props();
  /* eslint-enable prefer-const */

-
  const issues = $state(initialIssues);
-

+
  let issues = $state(initialIssues);
+
  let status = $state(initialStatus);
  let topLevelReplyOpen = $state(false);
  let editingTitle = $state(false);
  let updatedTitle = $state("");
@@ -94,6 +94,18 @@

  const project = $derived(repo.payloads["xyz.radicle.project"]!);

+
  async function loadIssues(filter: IssueStatus) {
+
    try {
+
      issues = await invoke<Issue[]>("list_issues", {
+
        rid: repo.rid,
+
        status: filter,
+
      });
+
      status = filter;
+
    } catch (error) {
+
      console.error("Loading issue list failed", error);
+
    }
+
  }
+

  async function saveLabels(labels: string[]) {
    try {
      labelSaveInProgress = true;
@@ -318,7 +330,7 @@
    display: flex;
    align-items: center;
    justify-content: space-between;
-
    word-break: break-all;
+
    word-break: break-word;
    min-height: 40px;
  }
  .status {
@@ -397,6 +409,9 @@
      selectedIssueId={issue.id}
      {issues}
      {status}
+
      changeFilter={async filter => {
+
        await loadIssues(filter);
+
      }}
      title={project.data.name} />
  {/snippet}

modified src/views/repo/Issues.svelte
@@ -8,13 +8,13 @@

  import Layout from "./Layout.svelte";

+
  import Border from "@app/components/Border.svelte";
  import Button from "@app/components/Button.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
  import Icon from "@app/components/Icon.svelte";
  import IssueTeaser from "@app/components/IssueTeaser.svelte";
  import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte";
  import Sidebar from "@app/components/Sidebar.svelte";
-
  import Border from "@app/components/Border.svelte";

  interface Props {
    repo: RepoInfo;
@@ -24,8 +24,6 @@
  }

  const { repo, issues, config, status }: Props = $props();
-

-
  const project = $derived(repo.payloads["xyz.radicle.project"]!);
</script>

<style>
@@ -61,7 +59,7 @@
  {/snippet}

  {#snippet secondColumn()}
-
    <IssuesSecondColumn {project} {status} {repo} />
+
    <IssuesSecondColumn {status} {repo} />
  {/snippet}

  <div class="container">
modified src/views/repo/Layout.svelte
@@ -117,7 +117,7 @@

  .secondColumn {
    grid-column: 2 / 3;
-
    max-width: 28rem;
+
    max-width: 29rem;
    min-width: 14rem;
    padding: 1rem 1rem 1rem 0;
  }
modified src/views/repo/Patch.svelte
@@ -9,6 +9,8 @@
  import type { RepoInfo } from "@bindings/repo/RepoInfo";
  import type { Revision } from "@bindings/cob/patch/Revision";

+
  import capitalize from "lodash/capitalize";
+

  import * as roles from "@app/lib/roles";
  import { announce } from "@app/components/AnnounceSwitch.svelte";
  import {
@@ -22,14 +24,18 @@
  import AssigneeInput from "@app/components/AssigneeInput.svelte";
  import Border from "@app/components/Border.svelte";
  import CopyableId from "@app/components/CopyableId.svelte";
+
  import DropdownList from "@app/components/DropdownList.svelte";
+
  import DropdownListItem from "@app/components/DropdownListItem.svelte";
  import Icon from "@app/components/Icon.svelte";
  import InlineTitle from "@app/components/InlineTitle.svelte";
  import LabelInput from "@app/components/LabelInput.svelte";
  import Layout from "./Layout.svelte";
+
  import OutlineButton from "@app/components/OutlineButton.svelte";
  import PatchStateBadge from "@app/components/PatchStateBadge.svelte";
  import PatchStateButton from "@app/components/PatchStateButton.svelte";
  import PatchTeaser from "@app/components/PatchTeaser.svelte";
  import PatchTimeline from "@app/components/PatchTimeline.svelte";
+
  import Popover, { closeFocused } from "@app/components/Popover.svelte";
  import RevisionBadges from "@app/components/RevisionBadges.svelte";
  import RevisionComponent from "@app/components/Revision.svelte";
  import RevisionSelector from "@app/components/RevisionSelector.svelte";
@@ -51,7 +57,7 @@
  let {
    repo,
    patch,
-
    patches,
+
    patches: initialPatches,
    revisions,
    config,
    status: initialStatus,
@@ -59,9 +65,11 @@
  }: Props = $props();
  /* eslint-enable prefer-const */

-
  let cursor = patches.cursor;
-
  let more = patches.more;
-
  let items = $state(patches.content);
+
  let cursor: number = $state(0);
+
  let more: boolean = $state(false);
+
  let patchTeasers: Patch[] = $state([]);
+

+
  let patches = $state(initialPatches);
  let status = $state(initialStatus);
  let editingTitle = $state(false);
  let updatedTitle = $state("");
@@ -72,7 +80,7 @@
  let selectedRevision: Revision = $state(revisions.slice(-1)[0]);

  $effect(() => {
-
    items = patches.content;
+
    patchTeasers = patches.content;
    cursor = patches.cursor;
    more = patches.more;
  });
@@ -192,7 +200,7 @@
    }
  }

-
  async function loadMoreSecondColumn() {
+
  async function loadMoreTeasers() {
    if (more) {
      const p = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
        rid: repo.rid,
@@ -202,7 +210,7 @@

      cursor = p.cursor;
      more = p.more;
-
      items = [...items, ...p.content];
+
      patchTeasers = [...patchTeasers, ...p.content];
    }
  }

@@ -230,6 +238,18 @@
      }),
    ]);
  }
+

+
  async function loadPatches(filter: PatchStatus | undefined) {
+
    try {
+
      patches = await invoke<PaginatedQuery<Patch[]>>("list_patches", {
+
        rid: repo.rid,
+
        status: filter,
+
      });
+
      status = filter;
+
    } catch (error) {
+
      console.error("Loading patch list failed", error);
+
    }
+
  }
</script>

<style>
@@ -241,7 +261,7 @@
    display: flex;
    align-items: center;
    justify-content: space-between;
-
    word-break: break-all;
+
    word-break: break-word;
    min-height: 40px;
  }
  .title-icons {
@@ -291,7 +311,29 @@
  }
</style>

-
<Layout {loadMoreSecondColumn} publicKey={config.publicKey}>
+
{#snippet icons(status: PatchStatus | undefined)}
+
  <div class="icon" style:color={status ? patchStatusColor[status] : undefined}>
+
    <Icon
+
      name={status === undefined || status === "open"
+
        ? "patch"
+
        : `patch-${status}`} />
+
  </div>
+
{/snippet}
+

+
{#snippet counters(status: PatchStatus | undefined)}
+
  <div style:margin-left="auto" style:padding-left="0.25rem">
+
    {#if status}
+
      {project.meta.patches[status]}
+
    {:else}
+
      {project.meta.patches.draft +
+
        project.meta.patches.open +
+
        project.meta.patches.archived +
+
        project.meta.patches.merged}
+
    {/if}
+
  </div>
+
{/snippet}
+

+
<Layout loadMoreSecondColumn={loadMoreTeasers} publicKey={config.publicKey}>
  {#snippet headerCenter()}
    <CopyableId id={patch.id} />
  {/snippet}
@@ -303,22 +345,84 @@
  {#snippet secondColumn()}
    <div
      class="txt-regular txt-semibold global-flex"
-
      style:gap="4px"
-
      style:min-height="40px">
-
      {project.data.name}
-
      <Icon name="chevron-right" />
-
      Patches
+
      style:min-height="40px"
+
      style:justify-content="space-between">
+
      <div class="global-flex" style:gap="4px">
+
        {project.data.name}
+
        <Icon name="chevron-right" />
+
        Patches
+
      </div>
+

+
      <Popover popoverPositionRight="0" popoverPositionTop="2.5rem">
+
        {#snippet toggle(onclick)}
+
          <OutlineButton variant="ghost" {onclick}>
+
            {@render icons(status)}
+
            {status ? capitalize(status) : "All"}
+
            {@render counters(status)}
+
            <Icon name="chevron-down" />
+
          </OutlineButton>
+
        {/snippet}
+

+
        {#snippet popover()}
+
          <Border variant="ghost">
+
            <DropdownList
+
              items={[
+
                undefined,
+
                "draft",
+
                "open",
+
                "archived",
+
                "merged",
+
              ] as const}>
+
              {#snippet item(state)}
+
                <DropdownListItem
+
                  style="gap: 0.5rem"
+
                  selected={status === state}
+
                  onclick={async () => {
+
                    await loadPatches(state);
+
                    closeFocused();
+
                  }}>
+
                  {@render icons(state)}
+
                  {state ? capitalize(state) : "All"}
+
                  {@render counters(state)}
+
                </DropdownListItem>
+
              {/snippet}
+
            </DropdownList>
+
          </Border>
+
        {/snippet}
+
      </Popover>
    </div>
    <div class="patch-list">
-
      {#each items as p}
+
      {#each patchTeasers as teaser}
        <PatchTeaser
          compact
          {loadPatch}
-
          patch={p}
+
          patch={teaser}
          rid={repo.rid}
          {status}
-
          selected={patch && p.id === patch.id} />
+
          selected={patch && teaser.id === patch.id} />
      {/each}
+

+
      {#if patches.content.length === 0}
+
        <Border
+
          styleMinWidth="25rem"
+
          variant="ghost"
+
          styleAlignItems="center"
+
          styleJustifyContent="center">
+
          <div
+
            class="global-flex"
+
            style:height="74px"
+
            style:justify-content="center">
+
            <div class="txt-missing txt-small global-flex" style:gap="0.25rem">
+
              <Icon name="none" />
+
              {#if status === undefined}
+
                No patches.
+
              {:else}
+
                No {status} patches.
+
              {/if}
+
            </div>
+
          </div>
+
        </Border>
+
      {/if}
    </div>
  {/snippet}