ba94def3c8
On narrow screens the temporary-note banner squeezed its text into a one-word-per-line ladder and overflowing words slid under the subtle "Move to trash" button. Two layout causes, both fixed here (layout-only; no handler/logic/i18n changes): - The text Group had `flex: 1` (= basis 0), so the outer `wrap="wrap"` never wrapped the buttons to a second row — it crushed the text instead. Give it a non-zero basis (`flex: 1 1 16rem`) so the wrap engages on narrow containers. - Mirror DeletedPageBanner's adaptive actions: labeled Buttons visibleFrom="sm", icon-only ActionIcon + Tooltip + aria-label hiddenFrom="sm" (same handlers, loading flags, and t() keys). This also fixes the ru locale, whose long labels no longer render on mobile. The sibling DeletedPageBanner already uses this pattern; adding the second button in #273/#277 didn't carry the adaptive part over. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
161 lines
5.5 KiB
TypeScript
161 lines
5.5 KiB
TypeScript
import {
|
|
ActionIcon,
|
|
Button,
|
|
Group,
|
|
Paper,
|
|
Text,
|
|
Tooltip,
|
|
} from "@mantine/core";
|
|
import { IconClockHour4, IconTrash } from "@tabler/icons-react";
|
|
import { useState } from "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 { useTreeMutation } from "@/features/page/tree/hooks/use-tree-mutation.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();
|
|
// Reuse the exact soft-delete path the tree/header menus use: optimistic
|
|
// tree removal, the "Page moved to trash" undo-toast, the deletedAt cache
|
|
// stamp, and the redirect to space home (which unmounts this banner).
|
|
const { handleDelete: trashPage } = useTreeMutation(page?.spaceId ?? "");
|
|
const [isDeleting, setIsDeleting] = useState(false);
|
|
|
|
// 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 handleTrashNow = async () => {
|
|
// No confirm modal by convention — the undo-toast is the safety net.
|
|
setIsDeleting(true);
|
|
try {
|
|
await trashPage(page.id);
|
|
} finally {
|
|
setIsDeleting(false);
|
|
}
|
|
};
|
|
|
|
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">
|
|
{/* A non-zero flex-basis lets the outer wrap="wrap" drop the buttons to
|
|
their own row on narrow screens; flex:1 (basis 0) never wraps and
|
|
instead crushes the text into a one-word-per-line ladder. */}
|
|
<Group
|
|
gap="xs"
|
|
wrap="nowrap"
|
|
style={{ flex: "1 1 16rem", 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 && (
|
|
<>
|
|
{/* Desktop: full labeled buttons. */}
|
|
<Group gap="xs" wrap="nowrap" visibleFrom="sm">
|
|
<Button
|
|
size="xs"
|
|
variant="subtle"
|
|
color="red"
|
|
leftSection={<IconTrash size={16} />}
|
|
onClick={handleTrashNow}
|
|
loading={isDeleting}
|
|
>
|
|
{t("Move to trash")}
|
|
</Button>
|
|
<Button
|
|
size="xs"
|
|
variant="light"
|
|
color="orange"
|
|
leftSection={<IconClockHour4 size={16} />}
|
|
onClick={handleMakePermanent}
|
|
loading={toggleTemporary.isPending}
|
|
>
|
|
{t("Make permanent")}
|
|
</Button>
|
|
</Group>
|
|
{/* Mobile: icon-only actions so they never overflow the narrow row. */}
|
|
<Group gap="xs" wrap="nowrap" hiddenFrom="sm">
|
|
<Tooltip label={t("Move to trash")} withArrow>
|
|
<ActionIcon
|
|
size="lg"
|
|
variant="subtle"
|
|
color="red"
|
|
onClick={handleTrashNow}
|
|
loading={isDeleting}
|
|
aria-label={t("Move to trash")}
|
|
>
|
|
<IconTrash size={18} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
<Tooltip label={t("Make permanent")} withArrow>
|
|
<ActionIcon
|
|
size="lg"
|
|
variant="light"
|
|
color="orange"
|
|
onClick={handleMakePermanent}
|
|
loading={toggleTemporary.isPending}
|
|
aria-label={t("Make permanent")}
|
|
>
|
|
<IconClockHour4 size={18} />
|
|
</ActionIcon>
|
|
</Tooltip>
|
|
</Group>
|
|
</>
|
|
)}
|
|
</Group>
|
|
</Paper>
|
|
);
|
|
}
|