diff --git a/Cargo.lock b/Cargo.lock index 3a6c3ac33..10e808539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4115,6 +4115,7 @@ dependencies = [ "serde_json", "sqlite", "ssh-key", + "tauri", "tauri-plugin-clipboard-manager", "tauri-plugin-fs", "tempfile", diff --git a/crates/radicle-tauri/src/commands/cob/issue.rs b/crates/radicle-tauri/src/commands/cob/issue.rs index 2a003d5ca..3830a895e 100644 --- a/crates/radicle-tauri/src/commands/cob/issue.rs +++ b/crates/radicle-tauri/src/commands/cob/issue.rs @@ -1,5 +1,8 @@ +use std::ops::ControlFlow; + use radicle::git; use radicle::identity; +use radicle::storage::ReadStorage; use radicle::issue::TYPENAME; use radicle_types as types; @@ -66,3 +69,36 @@ pub fn activity_by_issue( ) -> Result>, Error> { ctx.activity_by_id(rid, &TYPENAME, id) } + +#[tauri::command] +pub async fn rebuild_issue_cache( + ctx: tauri::State<'_, AppState>, + rid: identity::RepoId, + on_event: tauri::ipc::Channel, +) -> Result<(), Error> { + let repo = ctx.profile.storage.repository(rid)?; + let mut issues = ctx.profile.issues_mut(&repo)?; + on_event.send(types::cobs::CacheEvent::Started { rid })?; + issues.write_all(|result, progress| { + match result { + Ok((id, _)) => { + if on_event + .send(types::cobs::CacheEvent::Progress { + rid, + oid: **id, + current: progress.current(), + total: progress.total(), + }) + .is_err() + { + log::error!("Failed to send progress"); + } + } + Err(err) => log::warn!("Failed to retrieve issue: {err}"), + }; + ControlFlow::Continue(()) + })?; + on_event.send(types::cobs::CacheEvent::Finished { rid })?; + + Ok(()) +} diff --git a/crates/radicle-tauri/src/commands/cob/patch.rs b/crates/radicle-tauri/src/commands/cob/patch.rs index 22662bbef..8b40548de 100644 --- a/crates/radicle-tauri/src/commands/cob/patch.rs +++ b/crates/radicle-tauri/src/commands/cob/patch.rs @@ -1,4 +1,7 @@ +use std::ops::ControlFlow; + use radicle::patch::TYPENAME; +use radicle::storage::ReadStorage; use radicle::{cob, git, identity}; use radicle_types as types; @@ -125,3 +128,36 @@ pub fn activity_by_patch( ) -> Result>, Error> { ctx.activity_by_id(rid, &TYPENAME, id) } + +#[tauri::command] +pub async fn rebuild_patch_cache( + ctx: tauri::State<'_, AppState>, + rid: identity::RepoId, + on_event: tauri::ipc::Channel, +) -> Result<(), Error> { + let repo = ctx.profile.storage.repository(rid)?; + let mut patches = ctx.profile.patches_mut(&repo)?; + on_event.send(types::cobs::CacheEvent::Started { rid })?; + patches.write_all(|result, progress| { + match result { + Ok((id, _)) => { + if on_event + .send(cobs::CacheEvent::Progress { + rid, + oid: **id, + current: progress.current(), + total: progress.total(), + }) + .is_err() + { + log::error!("Failed to send progress"); + } + } + Err(err) => log::warn!("Failed to retrieve patch: {err}"), + }; + ControlFlow::Continue(()) + })?; + on_event.send(types::cobs::CacheEvent::Finished { rid })?; + + Ok(()) +} diff --git a/crates/radicle-tauri/src/lib.rs b/crates/radicle-tauri/src/lib.rs index fc4688b6e..e217c537b 100644 --- a/crates/radicle-tauri/src/lib.rs +++ b/crates/radicle-tauri/src/lib.rs @@ -31,11 +31,13 @@ pub fn run() { cob::issue::edit_issue, cob::issue::issue_by_id, cob::issue::list_issues, + cob::issue::rebuild_issue_cache, cob::patch::activity_by_patch, cob::patch::edit_patch, cob::patch::list_patches, cob::patch::patch_by_id, cob::patch::edit_patch, + cob::patch::rebuild_patch_cache, cob::patch::review_by_patch_and_revision_and_id, cob::patch::revisions_by_patch, cob::patch::revision_by_patch_and_id, diff --git a/crates/radicle-types/Cargo.toml b/crates/radicle-types/Cargo.toml index 49c662c63..66211e134 100644 --- a/crates/radicle-types/Cargo.toml +++ b/crates/radicle-types/Cargo.toml @@ -17,6 +17,7 @@ serde = { version = "1.0.0", features = ["derive"] } serde_json = { version = "1.0.0" } sqlite = { version = "0.32.0", features = ["bundled"] } ssh-key = { version = "0.6.3" } +tauri = { version = "2.5.0", features = ["isolation"] } tauri-plugin-clipboard-manager = { version = "2.2.2" } tauri-plugin-fs = { version = "2.2.0" } tempfile = { version = "3.19.0" } @@ -39,7 +40,11 @@ tree-sitter-rust = { version = "0.23.2" } tree-sitter-svelte-ng = { version = "1.0.2" } tree-sitter-toml-ng = { version = "0.7.0" } tree-sitter-typescript = { version = "0.23.2" } -ts-rs = { version = "10.1.0", features = ["serde-json-impl", "no-serde-warnings", "format"] } +ts-rs = { version = "10.1.0", features = [ + "serde-json-impl", + "no-serde-warnings", + "format", +] } [dev-dependencies] -radicle = { version = "0.15.0", features = ["test"] } \ No newline at end of file +radicle = { version = "0.15.0", features = ["test"] } diff --git a/crates/radicle-types/bindings/cob/CacheEvent.ts b/crates/radicle-types/bindings/cob/CacheEvent.ts new file mode 100644 index 000000000..30ce8142b --- /dev/null +++ b/crates/radicle-types/bindings/cob/CacheEvent.ts @@ -0,0 +1,6 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. + +export type CacheEvent = { "event": "started"; "data": { rid: string } } | { + "event": "progress"; + "data": { rid: string; oid: string; current: number; total: number }; +} | { "event": "finished"; "data": { rid: string } }; diff --git a/crates/radicle-types/src/cobs.rs b/crates/radicle-types/src/cobs.rs index 653fdca82..4aefe15c2 100644 --- a/crates/radicle-types/src/cobs.rs +++ b/crates/radicle-types/src/cobs.rs @@ -1,3 +1,5 @@ +use radicle::git::Oid; +use radicle::prelude::RepoId; use radicle::profile::Aliases; use serde::{Deserialize, Serialize}; use ts_rs::TS; @@ -12,6 +14,29 @@ pub mod repo; pub mod stream; pub mod thread; +#[derive(ts_rs::TS, Clone, Serialize)] +#[serde(rename_all = "camelCase", tag = "event", content = "data")] +#[ts(export)] +#[ts(export_to = "cob/")] +pub enum CacheEvent { + Started { + #[ts(as = "String")] + rid: RepoId, + }, + Progress { + #[ts(as = "String")] + rid: RepoId, + #[ts(as = "String")] + oid: Oid, + current: usize, + total: usize, + }, + Finished { + #[ts(as = "String")] + rid: RepoId, + }, +} + #[derive(Debug, Clone, Serialize, TS, Deserialize, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "camelCase")] #[ts(export)] diff --git a/crates/radicle-types/src/error.rs b/crates/radicle-types/src/error.rs index 7128b8c14..9f630dba6 100644 --- a/crates/radicle-types/src/error.rs +++ b/crates/radicle-types/src/error.rs @@ -43,6 +43,10 @@ pub enum Error { #[error(transparent)] TauriPluginFs(#[from] tauri_plugin_fs::Error), + /// Tauri error. + #[error(transparent)] + Tauri(#[from] tauri::Error), + /// Project error. #[error(transparent)] ProjectError(#[from] radicle::identity::project::ProjectError), diff --git a/src/components/Border.svelte b/src/components/Border.svelte index 62d501db9..3ae2aa63f 100644 --- a/src/components/Border.svelte +++ b/src/components/Border.svelte @@ -3,7 +3,14 @@ interface Props { children: Snippet; - variant: "primary" | "secondary" | "ghost" | "float" | "danger" | "success"; + variant: + | "primary" + | "secondary" + | "ghost" + | "float" + | "danger" + | "success" + | "outline"; hoverable?: boolean; onclick?: (e: MouseEvent) => void; stylePosition?: string; diff --git a/src/lib/issueCounts.svelte.ts b/src/lib/issueCounts.svelte.ts new file mode 100644 index 000000000..a2a8cf469 --- /dev/null +++ b/src/lib/issueCounts.svelte.ts @@ -0,0 +1,30 @@ +import type { IssueStatus } from "@app/views/repo/router"; +import type { ProjectPayload } from "@bindings/repo/ProjectPayload"; + +export const issueCounts = $state< + Record +>({ + all: { sidebar: null, total: null }, + open: { sidebar: null, total: null }, + closed: { sidebar: null, total: null }, +}); + +export function issueCountMismatch(status: IssueStatus) { + return issueCounts[status].total !== issueCounts[status].sidebar; +} + +export function resetIssueCounts() { + Object.values(issueCounts).forEach(count => { + count.sidebar = null; + count.total = null; + }); +} + +export function updateIssueCounts( + itemCount: number, + sidebarCount: ProjectPayload["meta"]["issues"] & { all: number }, + status: IssueStatus, +) { + issueCounts[status].total = itemCount; + issueCounts[status].sidebar = sidebarCount[status]; +} diff --git a/src/lib/patchCounts.svelte.ts b/src/lib/patchCounts.svelte.ts new file mode 100644 index 000000000..aada80736 --- /dev/null +++ b/src/lib/patchCounts.svelte.ts @@ -0,0 +1,41 @@ +import type { PatchStatus } from "@app/views/repo/router"; +import type { ProjectPayload } from "@bindings/repo/ProjectPayload"; + +import sum from "lodash/sum"; + +export const patchCounts = $state< + Record +>({ + draft: { sidebar: null, total: null }, + open: { sidebar: null, total: null }, + archived: { sidebar: null, total: null }, + merged: { sidebar: null, total: null }, + all: { sidebar: null, total: null }, +}); + +export function patchCountMismatch(status?: PatchStatus) { + return ( + patchCounts[status || "all"].total !== patchCounts[status || "all"].sidebar + ); +} + +export function resetPatchCounts() { + Object.values(patchCounts).forEach(count => { + count.sidebar = null; + count.total = null; + }); +} + +export function updatePatchCounts( + itemCount: number, + sidebarCount: ProjectPayload["meta"]["patches"], + status?: PatchStatus, +) { + if (status) { + patchCounts[status].total = itemCount; + patchCounts[status].sidebar = sidebarCount[status]; + } else { + patchCounts["all"].total = itemCount; + patchCounts["all"].sidebar = sum(Object.values(sidebarCount)); + } +} diff --git a/src/views/repo/Issues.svelte b/src/views/repo/Issues.svelte index 66536a85b..984e9cb8c 100644 --- a/src/views/repo/Issues.svelte +++ b/src/views/repo/Issues.svelte @@ -1,13 +1,21 @@