mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
empty states
This commit is contained in:
@@ -357,6 +357,9 @@
|
|||||||
"Multiple": "Multiple",
|
"Multiple": "Multiple",
|
||||||
"Turn into": "Turn into",
|
"Turn into": "Turn into",
|
||||||
"Text align": "Text align",
|
"Text align": "Text align",
|
||||||
|
"This page may have been deleted, moved, or you may not have access.": "This page may have been deleted, moved, or you may not have access.",
|
||||||
|
"Go to homepage": "Go to homepage",
|
||||||
|
"Pages you create will show up here.": "Pages you create will show up here.",
|
||||||
"Heading {{level}}": "Heading {{level}}",
|
"Heading {{level}}": "Heading {{level}}",
|
||||||
"Toggle title": "Toggle title",
|
"Toggle title": "Toggle title",
|
||||||
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
"Write anything. Enter \"/\" for commands": "Write anything. Enter \"/\" for commands",
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import PageListSkeleton from "@/components/ui/page-list-skeleton.tsx";
|
|||||||
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
import { buildPageUrl } from "@/features/page/page.utils.ts";
|
||||||
import { formattedDate } from "@/lib/time.ts";
|
import { formattedDate } from "@/lib/time.ts";
|
||||||
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
import { useRecentChangesQuery } from "@/features/page/queries/page-query.ts";
|
||||||
import { IconFileDescription } from "@tabler/icons-react";
|
import { IconFileDescription, IconFiles } from "@tabler/icons-react";
|
||||||
|
import { EmptyState } from "@/components/ui/empty-state.tsx";
|
||||||
import { getSpaceUrl } from "@/lib/config.ts";
|
import { getSpaceUrl } from "@/lib/config.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { getInitialsColor } from "@/lib/get-initials-color.ts";
|
import { getInitialsColor } from "@/lib/get-initials-color.ts";
|
||||||
@@ -85,8 +86,10 @@ export default function RecentChanges({ spaceId }: Props) {
|
|||||||
</Table>
|
</Table>
|
||||||
</Table.ScrollContainer>
|
</Table.ScrollContainer>
|
||||||
) : (
|
) : (
|
||||||
<Text size="md" ta="center">
|
<EmptyState
|
||||||
{t("No pages yet")}
|
icon={IconFiles}
|
||||||
</Text>
|
title={t("No pages yet")}
|
||||||
|
description={t("Pages you create will show up here.")}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 60px 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import { Stack, Text } from "@mantine/core";
|
||||||
|
import { type TablerIcon } from "@tabler/icons-react";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
import classes from "./empty-state.module.css";
|
||||||
|
|
||||||
|
type EmptyStateProps = {
|
||||||
|
icon: TablerIcon;
|
||||||
|
title: string;
|
||||||
|
description?: string;
|
||||||
|
action?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EmptyState({ icon: Icon, title, description, action }: EmptyStateProps) {
|
||||||
|
return (
|
||||||
|
<div className={classes.root}>
|
||||||
|
<Stack align="center" gap="xs">
|
||||||
|
<Icon size={40} stroke={1.5} color="var(--mantine-color-dimmed)" />
|
||||||
|
<Text size="lg" fw={500}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{description && (
|
||||||
|
<Text size="sm" c="dimmed" maw={350}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{action}
|
||||||
|
</Stack>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import classes from "@/features/page/tree/styles/tree.module.css";
|
import classes from "@/features/page/tree/styles/tree.module.css";
|
||||||
import { ActionIcon, Box, Menu, rem } from "@mantine/core";
|
import { ActionIcon, Box, Menu, rem, Text } from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconArrowRight,
|
IconArrowRight,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
@@ -82,6 +82,7 @@ interface SpaceTreeProps {
|
|||||||
const openTreeNodesAtom = atom<OpenMap>({});
|
const openTreeNodesAtom = atom<OpenMap>({});
|
||||||
|
|
||||||
export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
const { pageSlug } = useParams();
|
const { pageSlug } = useParams();
|
||||||
const { data, setData, controllers } =
|
const { data, setData, controllers } =
|
||||||
useTreeMutation<TreeApi<SpaceTreeNode>>(spaceId);
|
useTreeMutation<TreeApi<SpaceTreeNode>>(spaceId);
|
||||||
@@ -231,11 +232,18 @@ export default function SpaceTree({ spaceId, readOnly }: SpaceTreeProps) {
|
|||||||
};
|
};
|
||||||
}, [setTreeApi]);
|
}, [setTreeApi]);
|
||||||
|
|
||||||
|
const filteredData = data.filter((node) => node?.spaceId === spaceId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={mergedRef} className={classes.treeContainer}>
|
<div ref={mergedRef} className={classes.treeContainer}>
|
||||||
|
{isDataLoaded && filteredData.length === 0 && (
|
||||||
|
<Text size="xs" c="dimmed" py="xs" px="sm">
|
||||||
|
{t("No pages yet")}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
{isRootReady && rootElement.current && (
|
{isRootReady && rootElement.current && (
|
||||||
<Tree
|
<Tree
|
||||||
data={data.filter((node) => node?.spaceId === spaceId)}
|
data={filteredData}
|
||||||
disableDrag={readOnly}
|
disableDrag={readOnly}
|
||||||
disableDrop={readOnly}
|
disableDrop={readOnly}
|
||||||
disableEdit={readOnly}
|
disableEdit={readOnly}
|
||||||
|
|||||||
@@ -13,6 +13,10 @@ import {
|
|||||||
} from "@/features/space/permissions/permissions.type.ts";
|
} from "@/features/space/permissions/permissions.type.ts";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
import { EmptyState } from "@/components/ui/empty-state.tsx";
|
||||||
|
import { IconFileOff } from "@tabler/icons-react";
|
||||||
|
import { Button } from "@mantine/core";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
const MemoizedFullEditor = React.memo(FullEditor);
|
const MemoizedFullEditor = React.memo(FullEditor);
|
||||||
const MemoizedPageHeader = React.memo(PageHeader);
|
const MemoizedPageHeader = React.memo(PageHeader);
|
||||||
@@ -39,9 +43,27 @@ export default function Page() {
|
|||||||
|
|
||||||
if (isError || !page) {
|
if (isError || !page) {
|
||||||
if ([401, 403, 404].includes(error?.["status"])) {
|
if ([401, 403, 404].includes(error?.["status"])) {
|
||||||
return <div>{t("Page not found")}</div>;
|
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 <div>{t("Error fetching page data.")}</div>;
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<EmptyState
|
||||||
|
icon={IconFileOff}
|
||||||
|
title={t("Error fetching page data.")}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!space) {
|
if (!space) {
|
||||||
|
|||||||
Reference in New Issue
Block a user