Files
docmost/apps/client/src/pages/page/page.tsx
T
Peter Tripp 932c1ad5b7 Better trash (#2190)
* Better trash

I recently lost a bunch of time editing and searching for pages that were actually in the Trash. Docmost intentionally tries to not link to Trashed pages, but the url of that Trashed page and any inbound links still work.  This makes it clearer when a page you are interacting with is in the Trash.

- /trash
  - Refactored banner into `trash-banner.tsx`
  - Refactored "Restore" modal into `use-restore-page-modal.tsx`
- Page (when isDeleted)
  - Add: `trash-banner.tsx`
  - Add breadcrumbs: `Parent / Child / Page (Deleted)`
  - Change: Deleted Pages are read-only
  - Replace "Move to Trash" with "Restore" in page menu (invokes `use-restore-page-modal`)

I tried very hard to keep this simple and re-use existing translation strings wherever possible.

* cleanup

---------

Co-authored-by: Philipinho <16838612+Philipinho@users.noreply.github.com>
2026-05-14 14:41:10 +01:00

119 lines
3.4 KiB
TypeScript

import { useParams } from "react-router-dom";
import { usePageQuery } from "@/features/page/queries/page-query";
import { FullEditor } from "@/features/editor/full-editor";
import HistoryModal from "@/features/page-history/components/history-modal";
import { Helmet } from "react-helmet-async";
import PageHeader from "@/features/page/components/header/page-header.tsx";
import { extractPageSlugId } from "@/lib";
import { useGetSpaceBySlugQuery } from "@/features/space/queries/space-query.ts";
import { useTranslation } from "react-i18next";
import React from "react";
import { EmptyState } from "@/components/ui/empty-state.tsx";
import { IconAlertTriangle, IconFileOff } from "@tabler/icons-react";
import { Button } from "@mantine/core";
import { Link } from "react-router-dom";
import { ErrorBoundary } from "react-error-boundary";
const MemoizedFullEditor = React.memo(FullEditor);
const MemoizedPageHeader = React.memo(PageHeader);
const MemoizedHistoryModal = React.memo(HistoryModal);
export default function Page() {
const { t } = useTranslation();
const { pageSlug } = useParams();
return (
<ErrorBoundary
resetKeys={[pageSlug]}
fallbackRender={({ resetErrorBoundary }) => (
<EmptyState
icon={IconAlertTriangle}
title={t("Failed to load page. An error occurred.")}
action={
<Button variant="default" size="sm" mt="xs" onClick={resetErrorBoundary}>
{t("Try again")}
</Button>
}
/>
)}
>
<PageContent pageSlug={pageSlug} />
</ErrorBoundary>
);
}
function PageContent({ pageSlug }: { pageSlug: string | undefined }) {
const { t } = useTranslation();
const {
data: page,
isLoading,
isError,
error,
} = usePageQuery({ pageId: extractPageSlugId(pageSlug) });
const { data: space } = useGetSpaceBySlugQuery(page?.space?.slug);
const canEdit = !page?.deletedAt && (page?.permissions?.canEdit ?? false);
const canComment =
canEdit ||
(space?.settings?.comments?.allowViewerComments === true);
if (isLoading) {
return <></>;
}
if (isError || !page) {
if ([401, 403, 404].includes(error?.["status"])) {
return (
<EmptyState
icon={IconFileOff}
title={t("Page not found")}
description={t(
"This page may have been deleted, moved, or you may not have access.",
)}
action={
<Button component={Link} to="/home" variant="default" size="sm" mt="xs">
{t("Go to homepage")}
</Button>
}
/>
);
}
return (
<EmptyState
icon={IconFileOff}
title={t("Error fetching page data.")}
/>
);
}
if (!space) {
return <></>;
}
return (
page && (
<div>
<Helmet>
<title>{`${page?.icon || ""} ${page?.title || t("untitled")}`}</title>
</Helmet>
<MemoizedPageHeader readOnly={!canEdit} />
<MemoizedFullEditor
key={page.id}
pageId={page.id}
title={page.title}
content={page.content}
slugId={page.slugId}
spaceSlug={page?.space?.slug}
editable={canEdit}
creator={page.creator}
contributors={page.contributors}
canComment={canComment}
/>
<MemoizedHistoryModal pageId={page.id} />
</div>
)
);
}