mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-10-29 08:10:59 +08:00
Implement issue and patch filters in second column
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
@@ -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}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user