"Temporary notes" with a death timer: created via a dedicated hourglass button
in the space-tree header, a note auto-moves to Trash after a configurable X
hours (default 24) unless explicitly made permanent ("structure or die").
Reuses existing mechanisms, mirroring is_template and the trash-cleanup job:
- New nullable column pages.temporary_expires_at (NULL = permanent; non-NULL =
frozen deadline) + partial index for the sweep; workspace column
temporary_note_hours (default via DEFAULT_TEMPORARY_NOTE_HOURS = 24).
- create-page DTO `temporary` flag; the deadline is frozen at creation so later
setting changes never reschedule existing notes.
- POST /pages/toggle-temporary (mirror of toggle-template): arm/clear the timer,
CASL-guarded via validateCanEdit, cross-workspace NotFound defense-in-depth.
- TemporaryNoteCleanupService: hourly @Interval sweep that soft-deletes expired
notes through the exact PageRepo.removePage path (recursive over children,
emits PAGE_SOFT_DELETED), attributed to the creator; idempotent via
deletedAt IS NULL filters.
- restorePage clears temporary_expires_at so a restored note can't be re-trashed.
- Workspace setting temporary_note_hours (audit-tracked) + a hours editor in
workspace General settings.
- Client: second create button, orange tree icon, tree + page-header menu toggle
("Make temporary"/"Make permanent"), an open-note banner with a rescue action,
and en/ru i18n.
Tests (unit): toggle-temporary controller (toggle/explicit/permission/cross-ws +
DTO validation), cleanup-job sweep (selection filters, per-note removePage,
error isolation), and a migration up/down sanity. Server tsc, client tsc -b,
and the page+workspace jest suites are green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
30 lines
784 B
TypeScript
30 lines
784 B
TypeScript
import api from "@/lib/api-client";
|
|
import type {
|
|
PageTemplateLookup,
|
|
ToggleTemplateResponse,
|
|
ToggleTemporaryResponse,
|
|
} from "../types/page-embed.types";
|
|
|
|
export async function lookupTemplate(params: {
|
|
sourcePageIds: string[];
|
|
}): Promise<{ items: PageTemplateLookup[] }> {
|
|
const r = await api.post("/pages/template/lookup", params);
|
|
return r.data;
|
|
}
|
|
|
|
export async function toggleTemplate(params: {
|
|
pageId: string;
|
|
isTemplate?: boolean;
|
|
}): Promise<ToggleTemplateResponse> {
|
|
const r = await api.post("/pages/toggle-template", params);
|
|
return r.data;
|
|
}
|
|
|
|
export async function toggleTemporary(params: {
|
|
pageId: string;
|
|
temporary?: boolean;
|
|
}): Promise<ToggleTemporaryResponse> {
|
|
const r = await api.post("/pages/toggle-temporary", params);
|
|
return r.data;
|
|
}
|