diff options
| author | Adam Malczewski <[email protected]> | 2026-09-20 16:55:12 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-09-20 16:55:12 +0900 |
| commit | caf7060ec571039102acd7b4fd73008fdecbe8a9 (patch) | |
| tree | 7eac6c55556d20021de4a6e19f63d8efaa21ce84 /src/features/workspaces/store.svelte.ts | |
| parent | 72663945ef0a0053fbfd83b168e90f12169a5c3a (diff) | |
| download | dispatch-web-caf7060ec571039102acd7b4fd73008fdecbe8a9.tar.gz dispatch-web-caf7060ec571039102acd7b4fd73008fdecbe8a9.zip | |
feat(workspaces): archive option + archived-workspaces view
Consume the backend's archive contract (feature/archive-workspaces):
- adapter/http: archive(id)/unarchive(id) (PUT/DELETE /workspaces/:id/archive);
list(archived?) passes the ?archived= query
- logic/view-model: pure applyArchived(list, id, archived)
- store.svelte: setArchived(id, archived) (optimistic + revert-on-error +
reconciling refresh); archivedList + refreshArchived() for the archived view
- ui/TileActionBar: new Archive action (the right-click→select action bar)
- ui/HomeHeader: new Archived button opens the archived view
- ui/ArchivedWorkspaces (new): modal listing archived workspaces with
Unarchive + View (navigates to /<id>)
- ui/WorkspacesHome: wire the Archive action + the Archived modal
- .dispatch/wire.reference.md + backend-handoff.md: record the contract delta
Diffstat (limited to 'src/features/workspaces/store.svelte.ts')
| -rw-r--r-- | src/features/workspaces/store.svelte.ts | 51 |
1 files changed, 50 insertions, 1 deletions
diff --git a/src/features/workspaces/store.svelte.ts b/src/features/workspaces/store.svelte.ts index 22d73b5..57e34b2 100644 --- a/src/features/workspaces/store.svelte.ts +++ b/src/features/workspaces/store.svelte.ts @@ -1,7 +1,7 @@ import type { EnsureWorkspaceRequest } from "@dispatch/transport-contract"; import type { Workspace, WorkspaceEntry } from "@dispatch/wire"; import type { WorkspaceHttp, WorkspaceResult } from "./adapter/http"; -import { applyStarred, sortWorkspaces } from "./logic/view-model"; +import { applyArchived, applyStarred, sortWorkspaces } from "./logic/view-model"; /** * Workspace store — a thin reactive wrapper over the pure HTTP edge. Owns the @@ -16,10 +16,15 @@ export interface WorkspaceStore { * backend's `lastActivityAt`-desc order; this getter re-sorts reactively. */ readonly list: readonly WorkspaceEntry[]; + /** Archived workspaces, for the archived view. Loaded via `refreshArchived()`. */ + readonly archivedList: readonly WorkspaceEntry[]; readonly loading: boolean; + readonly archivedLoading: boolean; readonly error: string | null; /** Refresh the list from the backend. */ refresh(): Promise<void>; + /** Refresh the archived list (`GET /workspaces?archived=true`). */ + refreshArchived(): Promise<void>; /** `PUT /workspaces/:id` (create-on-miss). Returns the workspace or an error. */ ensure(id: string, body?: EnsureWorkspaceRequest): Promise<WorkspaceResult<Workspace>>; /** Rename a workspace (display only; id unchanged). */ @@ -34,25 +39,41 @@ export interface WorkspaceStore { * reverts to the prior value. No full refresh on success (avoids flicker). */ setStarred(id: string, starred: boolean): Promise<WorkspaceResult<Workspace>>; + /** + * Archive or unarchive a workspace. Optimistic: the local `archived` flag + * flips immediately (archiving drops the tile from the home grid); on error + * it reverts to the prior value. A successful archive/unarchive is followed + * by a `refresh()` so the active list reflects the backend (the archived + * workspace leaves/enters the default-filtered list). + */ + setArchived(id: string, archived: boolean): Promise<WorkspaceResult<Workspace>>; /** Delete a workspace (closes its conversations, reassigns to "default"). */ remove(id: string): Promise<WorkspaceResult<{ closedCount: number }>>; } export function createWorkspaceStore(http: WorkspaceHttp): WorkspaceStore { let list = $state<readonly WorkspaceEntry[]>([]); + let archivedList = $state<readonly WorkspaceEntry[]>([]); // Sorted view (starred first, then most-recent) — derived so it recomputes // only when the backing list changes, not on every read. let sorted = $derived(sortWorkspaces(list)); let loading = $state(false); + let archivedLoading = $state(false); let error = $state<string | null>(null); return { get list(): readonly WorkspaceEntry[] { return sorted; }, + get archivedList(): readonly WorkspaceEntry[] { + return archivedList; + }, get loading(): boolean { return loading; }, + get archivedLoading(): boolean { + return archivedLoading; + }, get error(): string | null { return error; }, @@ -69,6 +90,18 @@ export function createWorkspaceStore(http: WorkspaceHttp): WorkspaceStore { } }, + async refreshArchived(): Promise<void> { + archivedLoading = true; + error = null; + try { + archivedList = await http.list("true"); + } catch (err) { + error = err instanceof Error ? err.message : "Failed to load archived workspaces"; + } finally { + archivedLoading = false; + } + }, + async ensure(id, body): Promise<WorkspaceResult<Workspace>> { const result = await http.ensure(id, body); if (result.ok) void this.refresh(); @@ -105,6 +138,22 @@ export function createWorkspaceStore(http: WorkspaceHttp): WorkspaceStore { return result; }, + async setArchived(id, archived): Promise<WorkspaceResult<Workspace>> { + const prev = list.find((w) => w.id === id)?.archived ?? false; + // Optimistic: flip immediately so the tile leaves/enters the home grid. + list = applyArchived(list, id, archived); + const result = archived ? await http.archive(id) : await http.unarchive(id); + if (!result.ok) { + // Revert the optimistic flip on failure. + list = applyArchived(list, id, prev); + return result; + } + // Reconcile with the backend's default (active-only) filter so the + // archived tile is dropped / the unarchived tile is restored. + void this.refresh(); + return result; + }, + async remove(id): Promise<WorkspaceResult<{ closedCount: number }>> { const result = await http.delete(id); if (result.ok) void this.refresh(); |
