feat: add workspace management editor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { beforeEach, expect, test } from "vitest";
|
||||
import type { CanonicalWorkspace } from "../api/workspaces";
|
||||
import { workspaceDrafts, workspacePreferences } from "./drafts";
|
||||
import { workspaceDeletionDrafts, workspaceDrafts, workspacePreferences } from "./drafts";
|
||||
|
||||
const workspace: CanonicalWorkspace = {
|
||||
workspace: { schema_version: 2, id: "psd-clinical", name: "PSD Clinical", language: "en" },
|
||||
@@ -42,6 +42,23 @@ test("reloads a canonical workspace draft and discards it by workspace ID", () =
|
||||
expect(workspaceDrafts.load("psd-clinical")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("persists a deletion draft without retaining a workspace definition", () => {
|
||||
workspaceDeletionDrafts.save({
|
||||
id: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
baseBlob: "b".repeat(40),
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(workspaceDeletionDrafts.load("psd-clinical")).toEqual({
|
||||
id: "psd-clinical",
|
||||
baseCommit: "a".repeat(40),
|
||||
baseBlob: "b".repeat(40),
|
||||
updatedAt: "2026-08-04T10:00:00.000Z",
|
||||
});
|
||||
expect(localStorage.getItem("thothii.workspace-registry.v1.delete.psd-clinical")).not.toContain("PSD Clinical");
|
||||
});
|
||||
|
||||
test("keeps valid canonical diagnostic configuration", () => {
|
||||
const configured = {
|
||||
...workspace,
|
||||
|
||||
@@ -9,6 +9,14 @@ export interface WorkspaceDraft {
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
/** A publishable deletion intent; it deliberately carries no workspace body. */
|
||||
export interface WorkspaceDeletionDraft {
|
||||
id: string;
|
||||
baseCommit: string;
|
||||
baseBlob: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export const WORKSPACE_SUMMARY_ERROR = "Could not load workspace registry. Please retry.";
|
||||
export const WORKSPACE_POLICY_ERROR = "Could not load selected workspace policy. Please retry.";
|
||||
|
||||
@@ -114,6 +122,7 @@ export const workspacePolicyGate = {
|
||||
|
||||
const PREFIX = "thothii.workspace-registry.v1";
|
||||
const DRAFT_PREFIX = `${PREFIX}.draft.`;
|
||||
const DELETE_DRAFT_PREFIX = `${PREFIX}.delete.`;
|
||||
|
||||
function storage(): Storage | undefined {
|
||||
try { return window.localStorage; } catch { return undefined; }
|
||||
@@ -342,6 +351,25 @@ function key(id: string): string {
|
||||
return `${DRAFT_PREFIX}${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
function deletionKey(id: string): string {
|
||||
return `${DELETE_DRAFT_PREFIX}${encodeURIComponent(id)}`;
|
||||
}
|
||||
|
||||
function normalizeDeletion(value: unknown): WorkspaceDeletionDraft | undefined {
|
||||
const source = exactRecord(value, ["id", "baseCommit", "baseBlob", "updatedAt"]);
|
||||
const id = workspaceId(source?.id);
|
||||
const baseCommit = typeof source?.baseCommit === "string" && /^[0-9a-f]{40}$/.test(source.baseCommit)
|
||||
? source.baseCommit
|
||||
: undefined;
|
||||
const baseBlob = typeof source?.baseBlob === "string" && /^[0-9a-f]{40}$/.test(source.baseBlob)
|
||||
? source.baseBlob
|
||||
: undefined;
|
||||
const updatedAt = typeof source?.updatedAt === "string" && Number.isFinite(Date.parse(source.updatedAt))
|
||||
? source.updatedAt
|
||||
: undefined;
|
||||
return id && baseCommit && baseBlob && updatedAt ? { id, baseCommit, baseBlob, updatedAt } : undefined;
|
||||
}
|
||||
|
||||
/** Browser-only workspace drafts. Saving or editing one never calls the server. */
|
||||
export const workspaceDrafts = {
|
||||
load(id: string): WorkspaceDraft | undefined {
|
||||
@@ -363,3 +391,25 @@ export const workspaceDrafts = {
|
||||
try { storage()?.removeItem(key(id)); } catch { /* storage is optional */ }
|
||||
},
|
||||
};
|
||||
|
||||
/** Browser-only deletion drafts. Task 10 alone may publish one. */
|
||||
export const workspaceDeletionDrafts = {
|
||||
load(id: string): WorkspaceDeletionDraft | undefined {
|
||||
try {
|
||||
const raw = storage()?.getItem(deletionKey(id));
|
||||
return raw ? normalizeDeletion(JSON.parse(raw)) : undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
},
|
||||
|
||||
save(draft: WorkspaceDeletionDraft): void {
|
||||
const safe = normalizeDeletion(draft);
|
||||
if (!safe) return;
|
||||
try { storage()?.setItem(deletionKey(safe.id), JSON.stringify(safe)); } catch { /* storage is optional */ }
|
||||
},
|
||||
|
||||
discard(id: string): void {
|
||||
try { storage()?.removeItem(deletionKey(id)); } catch { /* storage is optional */ }
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user