Implement issue and patch filters in second column

This commit is contained in:
Rūdolfs Ošiņš
2025-01-20 15:30:24 -06:00
parent 4571d466db
commit c2f38bb0cb
8 changed files with 279 additions and 69 deletions
+103 -16
View File
@@ -3,11 +3,18 @@
import type { RepoInfo } from "@bindings/repo/RepoInfo";
import type { IssueStatus } from "@app/views/repo/router";
import * as router from "@app/lib/router";
import capitalize from "lodash/capitalize";
import IssueTeaser from "@app/components/IssueTeaser.svelte";
import * as router from "@app/lib/router";
import { issueStatusColor } from "@app/lib/utils";
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>
+3 -3
View File
@@ -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>
+2 -22
View File
@@ -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
+28 -2
View File
@@ -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">
+19 -4
View File
@@ -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}
+2 -4
View File
@@ -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">
+1 -1
View File
@@ -117,7 +117,7 @@
.secondColumn {
grid-column: 2 / 3;
max-width: 28rem;
max-width: 29rem;
min-width: 14rem;
padding: 1rem 1rem 1rem 0;
}
+121 -17
View File
@@ -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}