mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
feat: watch space (#2096)
This commit is contained in:
@@ -677,6 +677,8 @@
|
||||
"<bold>{{name}}</bold> updated a page": "<bold>{{name}}</bold> updated a page",
|
||||
"Watch page": "Watch page",
|
||||
"Stop watching": "Stop watching",
|
||||
"Watch space": "Watch space",
|
||||
"Stop watching space": "Stop watching space",
|
||||
"Email notifications": "Email notifications",
|
||||
"Page updates": "Page updates",
|
||||
"Get notified when pages you watch are updated.": "Receive notifications when the pages you watch are updated.",
|
||||
@@ -690,6 +692,8 @@
|
||||
"Get notified when your comment is resolved.": "Receive a notification when your comment is resolved.",
|
||||
"You are now watching this page": "You’re now watching this page",
|
||||
"You are no longer watching this page": "You’re no longer watching this page",
|
||||
"You are now watching this space": "You’re now watching this space",
|
||||
"You are no longer watching this space": "You’re no longer watching this space",
|
||||
"Direct": "Direct",
|
||||
"Updates": "Updates",
|
||||
"Today": "Today",
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
import {
|
||||
IconArrowDown,
|
||||
IconDots,
|
||||
IconEye,
|
||||
IconEyeOff,
|
||||
IconFileExport,
|
||||
IconHome,
|
||||
IconPlus,
|
||||
@@ -16,6 +18,11 @@ import {
|
||||
IconSettings,
|
||||
IconTrash,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
useSpaceWatchStatusQuery,
|
||||
useWatchSpaceMutation,
|
||||
useUnwatchSpaceMutation,
|
||||
} from "@/features/space/queries/space-watcher-query.ts";
|
||||
import classes from "./space-sidebar.module.css";
|
||||
import React from "react";
|
||||
import { useAtom } from "jotai";
|
||||
@@ -160,13 +167,20 @@ export function SpaceSidebar() {
|
||||
{t("Pages")}
|
||||
</Text>
|
||||
|
||||
{spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Page,
|
||||
) && (
|
||||
<Group gap="xs">
|
||||
<SpaceMenu spaceId={space.id} onSpaceSettings={openSettings} />
|
||||
<Group gap="xs">
|
||||
<SpaceMenu
|
||||
spaceId={space.id}
|
||||
canManagePages={spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Page,
|
||||
)}
|
||||
onSpaceSettings={openSettings}
|
||||
/>
|
||||
|
||||
{spaceAbility.can(
|
||||
SpaceCaslAction.Manage,
|
||||
SpaceCaslSubject.Page,
|
||||
) && (
|
||||
<Tooltip label={t("Create page")} withArrow position="right">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
@@ -177,8 +191,8 @@ export function SpaceSidebar() {
|
||||
<IconPlus />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
)}
|
||||
)}
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
<div className={classes.pages}>
|
||||
@@ -204,9 +218,14 @@ export function SpaceSidebar() {
|
||||
|
||||
interface SpaceMenuProps {
|
||||
spaceId: string;
|
||||
canManagePages: boolean;
|
||||
onSpaceSettings: () => void;
|
||||
}
|
||||
function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
function SpaceMenu({
|
||||
spaceId,
|
||||
canManagePages,
|
||||
onSpaceSettings,
|
||||
}: SpaceMenuProps) {
|
||||
const { t } = useTranslation();
|
||||
const { spaceSlug } = useParams();
|
||||
const [importOpened, { open: openImportModal, close: closeImportModal }] =
|
||||
@@ -214,15 +233,24 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
const [exportOpened, { open: openExportModal, close: closeExportModal }] =
|
||||
useDisclosure(false);
|
||||
|
||||
const { data: watchStatus } = useSpaceWatchStatusQuery(spaceId);
|
||||
const watchMutation = useWatchSpaceMutation();
|
||||
const unwatchMutation = useUnwatchSpaceMutation();
|
||||
const isWatching = watchStatus?.watching ?? false;
|
||||
|
||||
const handleToggleWatch = () => {
|
||||
if (isWatching) {
|
||||
unwatchMutation.mutate(spaceId);
|
||||
} else {
|
||||
watchMutation.mutate(spaceId);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu width={200} shadow="md" withArrow>
|
||||
<Menu.Target>
|
||||
<Tooltip
|
||||
label={t("Import pages & space settings")}
|
||||
withArrow
|
||||
position="top"
|
||||
>
|
||||
<Tooltip label={t("Space menu")} withArrow position="top">
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size={18}
|
||||
@@ -235,50 +263,69 @@ function SpaceMenu({ spaceId, onSpaceSettings }: SpaceMenuProps) {
|
||||
|
||||
<Menu.Dropdown>
|
||||
<Menu.Item
|
||||
onClick={openImportModal}
|
||||
leftSection={<IconArrowDown size={16} />}
|
||||
onClick={handleToggleWatch}
|
||||
leftSection={
|
||||
isWatching ? <IconEyeOff size={16} /> : <IconEye size={16} />
|
||||
}
|
||||
>
|
||||
{t("Import pages")}
|
||||
{isWatching ? t("Stop watching space") : t("Watch space")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={openExportModal}
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
>
|
||||
{t("Export space")}
|
||||
</Menu.Item>
|
||||
{canManagePages && (
|
||||
<>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Divider />
|
||||
<Menu.Item
|
||||
onClick={openImportModal}
|
||||
leftSection={<IconArrowDown size={16} />}
|
||||
>
|
||||
{t("Import pages")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
onClick={onSpaceSettings}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
{t("Space settings")}
|
||||
</Menu.Item>
|
||||
<Menu.Item
|
||||
onClick={openExportModal}
|
||||
leftSection={<IconFileExport size={16} />}
|
||||
>
|
||||
{t("Export space")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={`/s/${spaceSlug}/trash`}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
>
|
||||
{t("Trash")}
|
||||
</Menu.Item>
|
||||
<Menu.Divider />
|
||||
|
||||
<Menu.Item
|
||||
onClick={onSpaceSettings}
|
||||
leftSection={<IconSettings size={16} />}
|
||||
>
|
||||
{t("Space settings")}
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item
|
||||
component={Link}
|
||||
to={`/s/${spaceSlug}/trash`}
|
||||
leftSection={<IconTrash size={16} />}
|
||||
>
|
||||
{t("Trash")}
|
||||
</Menu.Item>
|
||||
</>
|
||||
)}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
|
||||
<PageImportModal
|
||||
spaceId={spaceId}
|
||||
open={importOpened}
|
||||
onClose={closeImportModal}
|
||||
/>
|
||||
{canManagePages && (
|
||||
<>
|
||||
<PageImportModal
|
||||
spaceId={spaceId}
|
||||
open={importOpened}
|
||||
onClose={closeImportModal}
|
||||
/>
|
||||
|
||||
<ExportModal
|
||||
type="space"
|
||||
id={spaceId}
|
||||
open={exportOpened}
|
||||
onClose={closeExportModal}
|
||||
/>
|
||||
<ExportModal
|
||||
type="space"
|
||||
id={spaceId}
|
||||
open={exportOpened}
|
||||
onClose={closeExportModal}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
watchSpace,
|
||||
unwatchSpace,
|
||||
getSpaceWatchStatus,
|
||||
} from "@/features/space/services/space-watcher-service";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
const SPACE_WATCHER_KEY = "space-watcher";
|
||||
|
||||
export function useSpaceWatchStatusQuery(spaceId: string) {
|
||||
return useQuery({
|
||||
queryKey: [SPACE_WATCHER_KEY, spaceId],
|
||||
queryFn: () => getSpaceWatchStatus(spaceId),
|
||||
enabled: !!spaceId,
|
||||
staleTime: 60_000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useWatchSpaceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
return useMutation({
|
||||
mutationFn: (spaceId: string) => watchSpace(spaceId),
|
||||
onSuccess: (_data, spaceId) => {
|
||||
queryClient.setQueryData([SPACE_WATCHER_KEY, spaceId], {
|
||||
watching: true,
|
||||
});
|
||||
notifications.show({ message: t("You are now watching this space") });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUnwatchSpaceMutation() {
|
||||
const queryClient = useQueryClient();
|
||||
const { t } = useTranslation();
|
||||
return useMutation({
|
||||
mutationFn: (spaceId: string) => unwatchSpace(spaceId),
|
||||
onSuccess: (_data, spaceId) => {
|
||||
queryClient.setQueryData([SPACE_WATCHER_KEY, spaceId], {
|
||||
watching: false,
|
||||
});
|
||||
notifications.show({
|
||||
message: t("You are no longer watching this space"),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import api from "@/lib/api-client";
|
||||
|
||||
export async function watchSpace(
|
||||
spaceId: string,
|
||||
): Promise<{ watching: boolean }> {
|
||||
const req = await api.post<{ watching: boolean }>("/spaces/watch", {
|
||||
spaceId,
|
||||
});
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function unwatchSpace(
|
||||
spaceId: string,
|
||||
): Promise<{ watching: boolean }> {
|
||||
const req = await api.post<{ watching: boolean }>("/spaces/unwatch", {
|
||||
spaceId,
|
||||
});
|
||||
return req.data;
|
||||
}
|
||||
|
||||
export async function getSpaceWatchStatus(
|
||||
spaceId: string,
|
||||
): Promise<{ watching: boolean }> {
|
||||
const req = await api.post<{ watching: boolean }>("/spaces/watch-status", {
|
||||
spaceId,
|
||||
});
|
||||
return req.data;
|
||||
}
|
||||
Reference in New Issue
Block a user