Must-fix: - CHANGELOG: add [Unreleased]/Added entry for temporary notes (#201). - temporary-note-cleanup: re-check temporary_expires_at at deletion time so a concurrent "Make permanent" (sets it NULL) between the batch SELECT and the per-row removePage wins the race and the note is not trashed. Add unit tests for the make-permanent and already-trashed race windows. Non-blocking review items: - temporary-note-cleanup: cap the sweep batch (LIMIT 500) so a large backlog is not loaded into memory; remainder drains on the next hourly run. - client: extract duplicated post-toggle cache sync into syncTemporaryExpiresInCache() shared by the header menu and the banner. - Remove the tautological migration spec that mocked the whole Kysely builder. - Tests: cover create() frozen temporaryExpiresAt (workspace override + NULL default fallback + non-temporary skips lookup) and restorePage disarming the timer (temporaryExpiresAt: null). Deferred (forward-looking, non-blocking): extract PageService.computeTemporaryExpiresAt() to dedupe the deadline formula and drop the @InjectKysely from PageTemplateController; replace migration unit test with a real Postgres up/down integration test. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
88 lines
3.0 KiB
TypeScript
88 lines
3.0 KiB
TypeScript
import { Button, Group, Paper, Text } from "@mantine/core";
|
|
import { IconClockHour4 } from "@tabler/icons-react";
|
|
import { Trans, useTranslation } from "react-i18next";
|
|
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
|
|
import { usePageQuery } from "@/features/page/queries/page-query.ts";
|
|
import {
|
|
useToggleTemporaryMutation,
|
|
syncTemporaryExpiresInCache,
|
|
} from "@/features/page-embed/queries/page-embed-query.ts";
|
|
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
|
|
import { useSpaceAbility } from "@/features/space/permissions/use-space-ability.ts";
|
|
import {
|
|
SpaceCaslAction,
|
|
SpaceCaslSubject,
|
|
} from "@/features/space/permissions/permissions.type.ts";
|
|
|
|
type TemporaryNoteBannerProps = {
|
|
slugId: string;
|
|
};
|
|
|
|
/**
|
|
* Banner shown on an open temporary note ("structure or die"). Mirrors
|
|
* DeletedPageBanner: it reads the page from the shared query cache and offers
|
|
* the explicit rescue action — "Make permanent". Children ride along to trash
|
|
* with the note, which is noted in the copy.
|
|
*/
|
|
export function TemporaryNoteBanner({ slugId }: TemporaryNoteBannerProps) {
|
|
const { t } = useTranslation();
|
|
const { data: page } = usePageQuery({ pageId: slugId });
|
|
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
|
|
const spaceAbility = useSpaceAbility(space?.membership?.permissions);
|
|
const expiresTimeAgo = useTimeAgo(page?.temporaryExpiresAt);
|
|
const toggleTemporary = useToggleTemporaryMutation();
|
|
|
|
// Don't show on a note that is already in trash; the deleted-page banner
|
|
// owns that state.
|
|
if (!page?.temporaryExpiresAt || page?.deletedAt) return null;
|
|
|
|
const canEdit = spaceAbility.can(SpaceCaslAction.Edit, SpaceCaslSubject.Page);
|
|
|
|
const handleMakePermanent = async () => {
|
|
try {
|
|
const res = await toggleTemporary.mutateAsync({
|
|
pageId: page.id,
|
|
temporary: false,
|
|
});
|
|
syncTemporaryExpiresInCache(page, res.temporaryExpiresAt);
|
|
} catch {
|
|
// mutation surfaces the error via notifications
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Paper radius="sm" mb="md" px="md" py="xs" bg="orange.0">
|
|
<Group justify="space-between" wrap="wrap" gap="sm">
|
|
<Group gap="xs" wrap="nowrap" style={{ flex: 1, minWidth: 0 }}>
|
|
<IconClockHour4
|
|
size={18}
|
|
stroke={1.5}
|
|
style={{
|
|
flexShrink: 0,
|
|
color: "var(--mantine-color-orange-7)",
|
|
}}
|
|
/>
|
|
<Text size="sm">
|
|
<Trans
|
|
i18nKey="This temporary note moves to trash {{time}} (with its sub-pages) unless made permanent."
|
|
values={{ time: expiresTimeAgo }}
|
|
/>
|
|
</Text>
|
|
</Group>
|
|
{canEdit && (
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
color="orange"
|
|
leftSection={<IconClockHour4 size={16} />}
|
|
onClick={handleMakePermanent}
|
|
loading={toggleTemporary.isPending}
|
|
>
|
|
{t("Make permanent")}
|
|
</Button>
|
|
)}
|
|
</Group>
|
|
</Paper>
|
|
);
|
|
}
|