From 83390aed59fc4b3d8090bb29773a0d79ec76b2b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C5=ABdolfs=20O=C5=A1i=C5=86=C5=A1?= Date: Fri, 28 Mar 2025 18:18:22 +0100 Subject: [PATCH] Implement issue and patch list fuzzy finder --- .../radicle-tauri/src/commands/cob/patch.rs | 44 +++++--- crates/test-http-api/src/api.rs | 23 +++- src/components/IssueTeaser.svelte | 52 ++++++--- src/components/PatchTeaser.svelte | 63 +++++++---- src/components/TextInput.svelte | 37 ++++--- src/views/repo/Issues.svelte | 94 +++++++++++++--- src/views/repo/Patches.svelte | 104 ++++++++++++++++-- src/views/repo/router.ts | 4 + 8 files changed, 325 insertions(+), 96 deletions(-) diff --git a/crates/radicle-tauri/src/commands/cob/patch.rs b/crates/radicle-tauri/src/commands/cob/patch.rs index 707b0abc8..609d78f41 100644 --- a/crates/radicle-tauri/src/commands/cob/patch.rs +++ b/crates/radicle-tauri/src/commands/cob/patch.rs @@ -22,32 +22,50 @@ pub async fn list_patches( rid: identity::RepoId, status: Option, skip: Option, + // None: return all patches, `skip` is ignored. take: Option, ) -> Result>, Error> { let profile = ctx.profile(); let cursor = skip.unwrap_or(0); - let take = take.unwrap_or(20); let aliases = profile.aliases(); + let patches = match status { None => sqlite_service.list(rid)?.collect::>(), Some(s) => sqlite_service .list_by_status(rid, s.into())? .collect::>(), }; - let more = cursor + take < patches.len(); - let patches = patches - .into_iter() - .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) - .skip(cursor) - .take(take) - .collect::>(); + match take { + None => { + let content = patches + .into_iter() + .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) + .collect::>(); - Ok::<_, Error>(cobs::PaginatedQuery { - cursor, - more, - content: patches, - }) + Ok::<_, Error>(cobs::PaginatedQuery { + cursor: 0, + more: false, + content, + }) + } + Some(take) => { + let more = cursor + take < patches.len(); + + let content = patches + .into_iter() + .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) + .skip(cursor) + .take(take) + .collect::>(); + + Ok::<_, Error>(cobs::PaginatedQuery { + cursor, + more, + content, + }) + } + } } #[tauri::command] diff --git a/crates/test-http-api/src/api.rs b/crates/test-http-api/src/api.rs index fcfa14e06..b89ab6e8d 100644 --- a/crates/test-http-api/src/api.rs +++ b/crates/test-http-api/src/api.rs @@ -339,7 +339,7 @@ async fn issue_threads_handler( struct PatchesBody { pub rid: identity::RepoId, pub skip: Option, - pub take: Option, + pub take: Option, pub status: Option, } @@ -354,8 +354,8 @@ async fn patches_handler( ) -> impl IntoResponse { let profile = ctx.profile; let cursor = skip.unwrap_or(0); - let take = take.unwrap_or(20); let aliases = profile.aliases(); + let patches = match status { None => ctx.patches.list(rid)?.collect::>(), Some(s) => ctx @@ -363,6 +363,25 @@ async fn patches_handler( .list_by_status(rid, s.into())? .collect::>(), }; + + if let Some(t) = take { + if t < 0 { + // Return all patches + let content = patches + .into_iter() + .map(|(id, patch)| models::patch::Patch::new(id, &patch, &aliases)) + .collect::>(); + + return Ok::<_, Error>(Json(cobs::PaginatedQuery { + cursor: 0, + more: false, + content, + })); + } + } + + let take = take.unwrap_or(20) as usize; + let more = cursor + take < patches.len(); let patches = patches diff --git a/src/components/IssueTeaser.svelte b/src/components/IssueTeaser.svelte index d14b7b3ac..aa91b9cf4 100644 --- a/src/components/IssueTeaser.svelte +++ b/src/components/IssueTeaser.svelte @@ -10,6 +10,7 @@ } from "@app/lib/utils"; import { push } from "@app/lib/router"; + import Border from "./Border.svelte"; import Icon from "./Icon.svelte"; import Id from "./Id.svelte"; import InlineTitle from "./InlineTitle.svelte"; @@ -17,19 +18,21 @@ import NodeId from "./NodeId.svelte"; interface Props { + compact?: boolean; + focussed?: boolean; issue: Issue; rid: string; - status: IssueStatus; selected?: boolean; - compact?: boolean; + status: IssueStatus; } const { + compact = false, + focussed, issue, rid, - status, selected = false, - compact = false, + status, }: Props = $props(); @@ -37,7 +40,6 @@ .issue-teaser { display: flex; align-items: center; - justify-content: space-between; gap: 0.25rem; min-height: 5rem; background-color: var(--color-background-float); @@ -67,16 +69,7 @@ } - -
{ - void push({ resource: "repo.issue", rid, issue: issue.id, status }); - }}> +{#snippet issueSnippet()}
-
+
{#if !compact} {#each issue.labels as label}
{/if}
-
+{/snippet} + +{#if focussed} + + {@render issueSnippet()} + +{:else} + +
{ + void push({ resource: "repo.issue", rid, issue: issue.id, status }); + }}> + {@render issueSnippet()} +
+{/if} diff --git a/src/components/PatchTeaser.svelte b/src/components/PatchTeaser.svelte index 5749150e1..e0db0a43d 100644 --- a/src/components/PatchTeaser.svelte +++ b/src/components/PatchTeaser.svelte @@ -18,9 +18,11 @@ import InlineTitle from "@app/components/InlineTitle.svelte"; import Label from "@app/components/Label.svelte"; import NodeId from "@app/components/NodeId.svelte"; + import Border from "./Border.svelte"; interface Props { compact?: boolean; + focussed?: boolean; loadPatch?: (patchId: string) => Promise; patch: Patch; rid: string; @@ -30,6 +32,7 @@ const { compact = false, + focussed, loadPatch, patch, rid, @@ -72,26 +75,7 @@ } - -
{ - if (loadPatch) { - await loadPatch(patch.id); - } else { - void push({ - resource: "repo.patch", - rid, - patch: patch.id, - status, - reviewId: undefined, - }); - } - }}> +{#snippet patchSnippet()}
-
+
{#if !compact} {#await invoke( "diff_stats", { rid, base: patch.base, head: patch.head }, ) then stats} @@ -134,4 +118,39 @@ {patch.revisionCount}
-
+{/snippet} + +{#if focussed} + + {@render patchSnippet()} + +{:else} + +
{ + if (loadPatch) { + await loadPatch(patch.id); + } else { + void push({ + resource: "repo.patch", + rid, + patch: patch.id, + status, + reviewId: undefined, + }); + } + }}> + {@render patchSnippet()} +
+{/if} diff --git a/src/components/TextInput.svelte b/src/components/TextInput.svelte index 7c977c81f..f0ddc20a5 100644 --- a/src/components/TextInput.svelte +++ b/src/components/TextInput.svelte @@ -7,36 +7,38 @@ import Border from "./Border.svelte"; interface Props { - name?: string; - placeholder?: string; - value?: string; - type?: string; autofocus?: boolean; autoselect?: boolean; disabled?: boolean; - onSubmit?: () => void; - onDismiss?: () => void; - valid?: boolean; - oninput?: FormEventHandler; keyShortcuts?: string; left?: Snippet; + name?: string; + onDismiss?: () => void; + onFocus?: () => void; + onSubmit?: () => void; + oninput?: FormEventHandler; + placeholder?: string; + type?: string; + valid?: boolean; + value?: string; } /* eslint-disable prefer-const */ let { - name, - placeholder, - value = $bindable(undefined), - type = "text", autofocus = false, autoselect = false, disabled = false, - onSubmit, - onDismiss, - valid = true, - oninput, keyShortcuts, left, + name, + onDismiss, + onFocus, + onSubmit, + oninput, + placeholder, + type = "text", + valid = true, + value = $bindable(undefined), }: Props = $props(); /* eslint-enable prefer-const */ @@ -102,6 +104,9 @@ style:padding={left ? "0.25rem 0.75rem 0.25rem 0" : "0.25rem 0.75rem"} aria-keyshortcuts={keyShortcuts} onfocus={() => { + if (onFocus) { + onFocus(); + } focussed = true; }} onblur={() => { diff --git a/src/views/repo/Issues.svelte b/src/views/repo/Issues.svelte index 34e9c4691..5b4b0e4fa 100644 --- a/src/views/repo/Issues.svelte +++ b/src/views/repo/Issues.svelte @@ -4,7 +4,10 @@ import type { IssueStatus } from "./router"; import type { RepoInfo } from "@bindings/repo/RepoInfo"; + import fuzzysort from "fuzzysort"; + import * as router from "@app/lib/router"; + import { modifierKey } from "@app/lib/utils"; import Layout from "./Layout.svelte"; @@ -15,6 +18,7 @@ import IssueTeaser from "@app/components/IssueTeaser.svelte"; import IssuesSecondColumn from "@app/components/IssuesSecondColumn.svelte"; import Sidebar from "@app/components/Sidebar.svelte"; + import TextInput from "@app/components/TextInput.svelte"; interface Props { repo: RepoInfo; @@ -23,7 +27,35 @@ status: IssueStatus; } - const { repo, issues, config, status }: Props = $props(); + /* eslint-disable prefer-const */ + let { repo, issues, config, status }: Props = $props(); + /* eslint-enable prefer-const */ + + let searchInput = $state(""); + + const searchableIssues = $derived( + issues + .flatMap(i => { + return { + issue: i, + labels: i.labels.join(" "), + assignees: i.assignees + .map(a => { + return a.alias ?? ""; + }) + .join(" "), + author: i.author.alias ?? "", + }; + }) + .filter((item): item is NonNullable => item !== undefined), + ); + + const searchResults = $derived( + fuzzysort.go(searchInput, searchableIssues, { + keys: ["issue.title", "labels", "assignees", "author"], + all: true, + }), + );