mirror of
https://github.com/docmost/docmost.git
synced 2026-05-17 23:14:07 +08:00
feat: favorites (#2103)
* feat: favorites and templates(ee) * rename migrations * fix sidebar * cleanup tabs * fix * turn off templates * cleanup * uuid validation
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
import { atomWithStorage } from "jotai/utils";
|
||||
|
||||
export const homeTabAtom = atomWithStorage<string>("home-tab", "recent");
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
Text,
|
||||
Group,
|
||||
UnstyledButton,
|
||||
Badge,
|
||||
Table,
|
||||
ActionIcon,
|
||||
Button,
|
||||
} from "@mantine/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageListSkeleton from "@/components/ui/page-list-skeleton";
|
||||
import { buildPageUrl } from "@/features/page/page.utils";
|
||||
import { formattedDate } from "@/lib/time";
|
||||
import { useCreatedByQuery } from "@/features/page/queries/page-query";
|
||||
import { IconFileDescription, IconFiles } from "@tabler/icons-react";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { getSpaceUrl } from "@/lib/config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getInitialsColor } from "@/lib/get-initials-color";
|
||||
|
||||
type Props = {
|
||||
spaceId?: string;
|
||||
};
|
||||
|
||||
export default function CreatedByMe({ spaceId }: Props) {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useCreatedByQuery({ spaceId });
|
||||
|
||||
const pages = data?.pages.flatMap((p) => p.items) ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>{t("Failed to fetch pages")}</Text>;
|
||||
}
|
||||
|
||||
return pages.length > 0 ? (
|
||||
<>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{pages.map((page) => (
|
||||
<Table.Tr key={page.id}>
|
||||
<Table.Td>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={buildPageUrl(
|
||||
page?.space.slug,
|
||||
page.slugId,
|
||||
page.title,
|
||||
)}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
{page.icon || (
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
size={18}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
{!spaceId && (
|
||||
<Table.Td>
|
||||
<Badge
|
||||
color={getInitialsColor(page?.space.name)}
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(page?.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{page?.space.name}
|
||||
</Badge>
|
||||
</Table.Td>
|
||||
)}
|
||||
<Table.Td>
|
||||
<Text
|
||||
c="dimmed"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
size="xs"
|
||||
fw={500}
|
||||
>
|
||||
{formattedDate(page.createdAt)}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
mb="xl"
|
||||
onClick={() => fetchNextPage()}
|
||||
loading={isFetchingNextPage}
|
||||
>
|
||||
{t("Load more")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={IconFiles}
|
||||
title={t("No pages yet")}
|
||||
description={t("Pages you create will show up here.")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Text,
|
||||
Group,
|
||||
UnstyledButton,
|
||||
Badge,
|
||||
Table,
|
||||
ActionIcon,
|
||||
Button,
|
||||
} from "@mantine/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import PageListSkeleton from "@/components/ui/page-list-skeleton";
|
||||
import { buildPageUrl } from "@/features/page/page.utils";
|
||||
import { formattedDate } from "@/lib/time";
|
||||
import { useFavoritesQuery } from "@/features/favorite/queries/favorite-query";
|
||||
import { IconFileDescription, IconStar } from "@tabler/icons-react";
|
||||
import { EmptyState } from "@/components/ui/empty-state";
|
||||
import { getSpaceUrl } from "@/lib/config";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getInitialsColor } from "@/lib/get-initials-color";
|
||||
|
||||
export default function FavoritesPages() {
|
||||
const { t } = useTranslation();
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
isError,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useFavoritesQuery("page");
|
||||
|
||||
const favorites = data?.pages.flatMap((p) => p.items) ?? [];
|
||||
|
||||
if (isLoading) {
|
||||
return <PageListSkeleton />;
|
||||
}
|
||||
|
||||
if (isError) {
|
||||
return <Text>{t("Failed to fetch starred pages")}</Text>;
|
||||
}
|
||||
|
||||
return favorites.length > 0 ? (
|
||||
<>
|
||||
<Table.ScrollContainer minWidth={500}>
|
||||
<Table highlightOnHover verticalSpacing="sm">
|
||||
<Table.Tbody>
|
||||
{favorites.map((fav) =>
|
||||
fav.page ? (
|
||||
<Table.Tr key={fav.id}>
|
||||
<Table.Td>
|
||||
<UnstyledButton
|
||||
component={Link}
|
||||
to={buildPageUrl(
|
||||
fav.space?.slug,
|
||||
fav.page.slugId,
|
||||
fav.page.title,
|
||||
)}
|
||||
>
|
||||
<Group wrap="nowrap">
|
||||
{fav.page.icon || (
|
||||
<ActionIcon
|
||||
variant="transparent"
|
||||
color="gray"
|
||||
size={18}
|
||||
>
|
||||
<IconFileDescription size={18} />
|
||||
</ActionIcon>
|
||||
)}
|
||||
<Text fw={500} size="md" lineClamp={1}>
|
||||
{fav.page.title || t("Untitled")}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
{fav.space && (
|
||||
<Badge
|
||||
color={getInitialsColor(fav.space.name)}
|
||||
variant="light"
|
||||
component={Link}
|
||||
to={getSpaceUrl(fav.space.slug)}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
{fav.space.name}
|
||||
</Badge>
|
||||
)}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<Text
|
||||
c="dimmed"
|
||||
style={{ whiteSpace: "nowrap" }}
|
||||
size="xs"
|
||||
fw={500}
|
||||
>
|
||||
{formattedDate(new Date(fav.createdAt))}
|
||||
</Text>
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
) : null,
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Table.ScrollContainer>
|
||||
{hasNextPage && (
|
||||
<Button
|
||||
variant="subtle"
|
||||
fullWidth
|
||||
mt="sm"
|
||||
mb="xl"
|
||||
onClick={() => fetchNextPage()}
|
||||
loading={isFetchingNextPage}
|
||||
>
|
||||
{t("Load more")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<EmptyState
|
||||
icon={IconStar}
|
||||
title={t("No favorites yet")}
|
||||
description={t("Pages you star will show up here.")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +1,40 @@
|
||||
import { Text, Tabs, Space } from "@mantine/core";
|
||||
import { IconClockHour3 } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes.tsx";
|
||||
import { IconClockHour3, IconStar, IconUser } from "@tabler/icons-react";
|
||||
import RecentChanges from "@/components/common/recent-changes";
|
||||
import FavoritesPages from "./favorites-pages";
|
||||
import CreatedByMe from "./created-by-me";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useAtom } from "jotai";
|
||||
import { homeTabAtom } from "@/features/home/atoms/home-tab-atom";
|
||||
|
||||
export default function HomeTabs() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useAtom(homeTabAtom);
|
||||
|
||||
return (
|
||||
<Tabs defaultValue="recent">
|
||||
<Tabs.List>
|
||||
<Tabs
|
||||
color="dark"
|
||||
value={activeTab}
|
||||
onChange={(value) => {
|
||||
if (value) setActiveTab(value);
|
||||
}}
|
||||
>
|
||||
<Tabs.List style={{ flexWrap: "nowrap", overflowX: "auto" }}>
|
||||
<Tabs.Tab value="recent" leftSection={<IconClockHour3 size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Recently updated")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="favorites" leftSection={<IconStar size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Favorites")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
<Tabs.Tab value="created" leftSection={<IconUser size={18} />}>
|
||||
<Text size="sm" fw={500}>
|
||||
{t("Created by me")}
|
||||
</Text>
|
||||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
|
||||
<Space my="md" />
|
||||
@@ -21,6 +42,12 @@ export default function HomeTabs() {
|
||||
<Tabs.Panel value="recent">
|
||||
<RecentChanges />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="favorites">
|
||||
<FavoritesPages />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="created">
|
||||
<CreatedByMe />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user