"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>
44 lines
1.1 KiB
TypeScript
44 lines
1.1 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import { notifications } from "@mantine/notifications";
|
|
import {
|
|
toggleTemplate,
|
|
toggleTemporary,
|
|
} from "@/features/page-embed/services/page-embed-api";
|
|
import type {
|
|
ToggleTemplateResponse,
|
|
ToggleTemporaryResponse,
|
|
} from "@/features/page-embed/types/page-embed.types";
|
|
|
|
export function useToggleTemplateMutation() {
|
|
return useMutation<
|
|
ToggleTemplateResponse,
|
|
Error,
|
|
{ pageId: string; isTemplate?: boolean }
|
|
>({
|
|
mutationFn: (data) => toggleTemplate(data),
|
|
onError: (err: any) => {
|
|
notifications.show({
|
|
message: err?.response?.data?.message || "Failed to update template",
|
|
color: "red",
|
|
});
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useToggleTemporaryMutation() {
|
|
return useMutation<
|
|
ToggleTemporaryResponse,
|
|
Error,
|
|
{ pageId: string; temporary?: boolean }
|
|
>({
|
|
mutationFn: (data) => toggleTemporary(data),
|
|
onError: (err: any) => {
|
|
notifications.show({
|
|
message:
|
|
err?.response?.data?.message || "Failed to update temporary note",
|
|
color: "red",
|
|
});
|
|
},
|
|
});
|
|
}
|