feat: watch space (#2096)

This commit is contained in:
Philip Okugbe
2026-04-09 00:37:51 +01:00
committed by GitHub
parent 4966f9b152
commit da9b43681e
12 changed files with 449 additions and 75 deletions
@@ -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": "Youre now watching this page",
"You are no longer watching this page": "Youre no longer watching this page",
"You are now watching this space": "Youre now watching this space",
"You are no longer watching this space": "Youre 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;
}