mirror of
https://github.com/docmost/docmost.git
synced 2026-06-11 10:46:54 +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:
@@ -12,6 +12,8 @@ import {
|
||||
IconMarkdown,
|
||||
IconMessage,
|
||||
IconPrinter,
|
||||
IconStar,
|
||||
IconStarFilled,
|
||||
IconTrash,
|
||||
IconWifiOff,
|
||||
} from "@tabler/icons-react";
|
||||
@@ -42,6 +44,11 @@ import { PageStateSegmentedControl } from "@/features/user/components/page-state
|
||||
import MovePageModal from "@/features/page/components/move-page-modal.tsx";
|
||||
import { useTimeAgo } from "@/hooks/use-time-ago.tsx";
|
||||
import { PageShareModal } from "@/ee/page-permission";
|
||||
import {
|
||||
useFavoriteIds,
|
||||
useAddFavoriteMutation,
|
||||
useRemoveFavoriteMutation,
|
||||
} from "@/features/favorite/queries/favorite-query";
|
||||
import {
|
||||
useWatchStatusQuery,
|
||||
useWatchPageMutation,
|
||||
@@ -130,6 +137,10 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
] = useDisclosure(false);
|
||||
const [pageEditor] = useAtom(pageEditorAtom);
|
||||
const pageUpdatedAt = useTimeAgo(page?.updatedAt);
|
||||
const favoriteIds = useFavoriteIds("page");
|
||||
const addFavoriteMutation = useAddFavoriteMutation();
|
||||
const removeFavoriteMutation = useRemoveFavoriteMutation();
|
||||
const isFavorited = page?.id ? favoriteIds.has(page.id) : false;
|
||||
const { data: watchStatus } = useWatchStatusQuery(page?.id);
|
||||
const watchPage = useWatchPageMutation();
|
||||
const unwatchPage = useUnwatchPageMutation();
|
||||
@@ -165,6 +176,16 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
openDeleteModal({ onConfirm: () => tree?.delete(page.id) });
|
||||
};
|
||||
|
||||
const handleToggleFavorite = () => {
|
||||
if (!page?.id) return;
|
||||
const params = { type: "page" as const, pageId: page.id };
|
||||
if (isFavorited) {
|
||||
removeFavoriteMutation.mutate(params);
|
||||
} else {
|
||||
addFavoriteMutation.mutate(params);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
@@ -196,6 +217,19 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
|
||||
{t("Copy as Markdown")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={
|
||||
isFavorited ? (
|
||||
<IconStarFilled size={16} color="var(--mantine-color-yellow-5)" />
|
||||
) : (
|
||||
<IconStar size={16} />
|
||||
)
|
||||
}
|
||||
onClick={handleToggleFavorite}
|
||||
>
|
||||
{isFavorited ? t("Remove from favorites") : t("Add to favorites")}
|
||||
</Menu.Item>
|
||||
|
||||
{watchStatus?.watching ? (
|
||||
<Menu.Item
|
||||
leftSection={<IconEyeOff size={16} />}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
movePage,
|
||||
getPageBreadcrumbs,
|
||||
getRecentChanges,
|
||||
getCreatedByPages,
|
||||
getAllSidebarPages,
|
||||
getDeletedPages,
|
||||
restorePage,
|
||||
@@ -244,7 +245,7 @@ export function useGetSidebarPagesQuery(
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["sidebar-pages", data],
|
||||
enabled: !!data?.pageId || !!data?.spaceId,
|
||||
queryFn: ({ pageParam }) => getSidebarPages({ ...data, cursor: pageParam }),
|
||||
queryFn: ({ pageParam }) => getSidebarPages({ ...data, cursor: pageParam, limit: 100 }),
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.meta?.nextCursor ?? undefined,
|
||||
@@ -255,7 +256,7 @@ export function useGetRootSidebarPagesQuery(data: SidebarPagesParams) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["root-sidebar-pages", data.spaceId],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return getSidebarPages({ spaceId: data.spaceId, cursor: pageParam });
|
||||
return getSidebarPages({ spaceId: data.spaceId, cursor: pageParam, limit: 100 });
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
@@ -285,12 +286,26 @@ export async function fetchAllAncestorChildren(params: SidebarPagesParams) {
|
||||
return buildTree(allItems);
|
||||
}
|
||||
|
||||
export function useRecentChangesQuery(
|
||||
spaceId?: string,
|
||||
): UseQueryResult<IPagination<IPage>, Error> {
|
||||
return useQuery({
|
||||
export function useRecentChangesQuery(spaceId?: string) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["recent-changes", spaceId],
|
||||
queryFn: () => getRecentChanges(spaceId),
|
||||
queryFn: ({ pageParam }) =>
|
||||
getRecentChanges({ spaceId, cursor: pageParam, limit: 15 }),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreatedByQuery(params?: { userId?: string; spaceId?: string }) {
|
||||
const { userId, spaceId } = params ?? {};
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["pages-created-by-user", { userId, spaceId }],
|
||||
queryFn: ({ pageParam }) => getCreatedByPages({ userId, spaceId, cursor: pageParam, limit: 15 }),
|
||||
initialPageParam: undefined as string | undefined,
|
||||
getNextPageParam: (lastPage) =>
|
||||
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
|
||||
refetchOnMount: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export async function getAllSidebarPages(
|
||||
const pageParams: (string | undefined)[] = [];
|
||||
|
||||
do {
|
||||
const req = await api.post("/pages/sidebar-pages", { ...params, cursor });
|
||||
const req = await api.post("/pages/sidebar-pages", { ...params, cursor, limit: 100 });
|
||||
|
||||
const data: IPagination<IPage> = req.data;
|
||||
pages.push(data);
|
||||
@@ -100,9 +100,16 @@ export async function getPageBreadcrumbs(
|
||||
}
|
||||
|
||||
export async function getRecentChanges(
|
||||
spaceId?: string,
|
||||
params?: QueryParams & { spaceId?: string },
|
||||
): Promise<IPagination<IPage>> {
|
||||
const req = await api.post("/pages/recent", { spaceId });
|
||||
const req = await api.post("/pages/recent", params);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getCreatedByPages(
|
||||
params?: QueryParams & { userId?: string; spaceId?: string },
|
||||
): Promise<IPagination<IPage>> {
|
||||
const req = await api.post("/pages/created-by-user", params);
|
||||
return req.data;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ import {
|
||||
IconLink,
|
||||
IconPlus,
|
||||
IconPointFilled,
|
||||
IconStar,
|
||||
IconStarFilled,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
@@ -69,6 +71,7 @@ import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sideb
|
||||
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
|
||||
import CopyPageModal from "../../components/copy-page-modal.tsx";
|
||||
import { duplicatePage } from "../../services/page-service.ts";
|
||||
import { useFavoriteIds, useAddFavoriteMutation, useRemoveFavoriteMutation } from "@/features/favorite/queries/favorite-query";
|
||||
|
||||
interface SpaceTreeProps {
|
||||
spaceId: string;
|
||||
@@ -506,6 +509,10 @@ function NodeMenu({ node, treeApi, spaceId }: NodeMenuProps) {
|
||||
copyPageModalOpened,
|
||||
{ open: openCopyPageModal, close: closeCopySpaceModal },
|
||||
] = useDisclosure(false);
|
||||
const favoriteIds = useFavoriteIds("page");
|
||||
const addFavorite = useAddFavoriteMutation();
|
||||
const removeFavorite = useRemoveFavoriteMutation();
|
||||
const isFavorited = favoriteIds.has(node.data.id);
|
||||
|
||||
const handleCopyLink = () => {
|
||||
const pageUrl =
|
||||
@@ -608,6 +615,21 @@ function NodeMenu({ node, treeApi, spaceId }: NodeMenuProps) {
|
||||
{t("Copy link")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={isFavorited ? <IconStarFilled size={16} /> : <IconStar size={16} />}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isFavorited) {
|
||||
removeFavorite.mutate({ type: "page", pageId: node.data.id });
|
||||
} else {
|
||||
addFavorite.mutate({ type: "page", pageId: node.data.id });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isFavorited ? t("Remove from favorites") : t("Add to favorites")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -68,6 +68,7 @@ export interface SidebarPagesParams {
|
||||
spaceId?: string;
|
||||
pageId?: string;
|
||||
cursor?: string;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
export interface IPageInput {
|
||||
|
||||
Reference in New Issue
Block a user