Support restoring archived LLM Wiki spaces

This commit is contained in:
Dotta committed 2026-05-09 23:41:04 -05:00
1 parent dc015bea9a
commit 7bbc705413
3 files changed
+69 -4

No files matched your search

@@ -2829,7 +2829,7 @@ function SpaceRowMenu({
const handleArchive = async () => {
if (!companyId || isDefault || busy) return;
if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages remain on disk; you can restore from settings.`)) {
if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages remain on disk; you can restore later through the plugin API or by un-archiving from the database.`)) {
return;
}
setBusy(true);
@@ -6768,7 +6768,7 @@ function SpaceEditCard({
async function archive() {
if (!companyId || isDefault || isOnlySpace || archiveBusy) return;
if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages stay on disk; you can restore by un-archiving from the database.`)) {
if (typeof window !== "undefined" && !window.confirm(`Archive ${space.displayName}? Pages stay on disk; you can restore later through the plugin API or by un-archiving from the database.`)) {
return;
}
setArchiveBusy(true);
+21 -2
View File
@@ -1367,6 +1367,25 @@ export async function resolveSpace(ctx: PluginContext, input: SpaceInput): Promi
return wikiSpaceFromRow(rows[0]);
}
async function resolveSpaceAnyStatus(ctx: PluginContext, input: SpaceInput): Promise<WikiSpace> {
const wikiId = normalizeWikiId(input.wikiId);
const slug = normalizeSpaceSlug(input.spaceSlug);
if (slug === DEFAULT_SPACE_SLUG) {
return ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId });
}
const rows = await ctx.db.query<WikiSpaceRow>(
`SELECT id, company_id, wiki_id, slug, display_name, space_type, folder_mode, root_folder_key,
path_prefix, configured_root_path, access_scope, owner_user_id, owner_agent_id, team_key,
settings, status, created_at::text AS created_at, updated_at::text AS updated_at
FROM ${spaceTable(ctx)}
WHERE company_id = $1 AND wiki_id = $2 AND slug = $3
LIMIT 1`,
[input.companyId, wikiId, slug],
);
if (!rows[0]) throw new Error(`LLM Wiki space not found: ${slug}`);
return wikiSpaceFromRow(rows[0]);
}
export async function listSpaces(ctx: PluginContext, input: { companyId: string; wikiId?: string | null }): Promise<{ spaces: WikiSpace[] }> {
const wikiId = normalizeWikiId(input.wikiId);
await ensureDefaultSpace(ctx, { companyId: input.companyId, wikiId });
@@ -1440,12 +1459,12 @@ export async function createSpace(ctx: PluginContext, input: CreateSpaceInput):
}
export async function updateSpace(ctx: PluginContext, input: UpdateSpaceInput): Promise<{ status: "ok"; space: WikiSpace }> {
const space = await resolveSpace(ctx, input);
const nextDisplayName = stringField(input.displayName);
const nextStatus = input.status ?? null;
if (nextStatus !== null && nextStatus !== "active" && nextStatus !== "archived") {
throw new Error("LLM Wiki space status must be active or archived.");
}
const space = nextStatus === "active" ? await resolveSpaceAnyStatus(ctx, input) : await resolveSpace(ctx, input);
const nextDisplayName = stringField(input.displayName);
if (space.slug === DEFAULT_SPACE_SLUG && nextStatus === "archived") {
throw new Error("The default LLM Wiki space cannot be archived.");
}
@@ -2867,6 +2867,52 @@ Duplicate headings receive stable suffixes.
expect(archived).toBe(true);
});
it("restores an archived non-default space through update-space", async () => {
const harness = createTestHarness({ manifest });
const spaceRow = wikiSpaceRow({
id: "77777777-7777-4777-8777-7777777777b2",
slug: "qa-space",
displayName: "QA Space",
pathPrefix: "spaces/qa-space",
status: "archived",
});
let restored = false;
const originalQuery = harness.ctx.db.query.bind(harness.ctx.db);
harness.ctx.db.query = async <T,>(sql: string, params?: unknown[]) => {
harness.dbQueries.push({ sql, params });
if (sql.includes("wiki_spaces") && params?.[2] === "qa-space") {
if (!restored && sql.includes("status <> 'archived'")) return [] as T[];
return [{
...spaceRow,
status: restored ? "active" : "archived",
}] as T[];
}
return originalQuery<T>(sql, params);
};
const originalExecute = harness.ctx.db.execute.bind(harness.ctx.db);
harness.ctx.db.execute = async (sql: string, params?: unknown[]) => {
if (sql.includes("UPDATE") && sql.includes("wiki_spaces") && params?.includes("active")) restored = true;
return originalExecute(sql, params);
};
await plugin.definition.setup(harness.ctx);
const updated = await harness.performAction<{
status: string;
space: { slug: string; status: string };
}>("update-space", {
companyId: COMPANY_ID,
spaceSlug: "qa-space",
status: "active",
});
expect(updated.status).toBe("ok");
expect(updated.space).toMatchObject({
slug: "qa-space",
status: "active",
});
expect(restored).toBe(true);
});
it("rejects unknown space statuses through update-space", async () => {
const harness = createTestHarness({ manifest });
await plugin.definition.setup(harness.ctx);