From 4056bd01046fcf8c5b4501a5c2eb491b2e1f5502 Mon Sep 17 00:00:00 2001
From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com>
Date: Mon, 13 Apr 2026 23:34:40 +0100
Subject: [PATCH] feat: enhancements (#2107)
* refactor
* fix
* update packages
---
apps/client/package.json | 2 +-
.../ee/ai-chat/components/ai-chat-layout.tsx | 25 ++-
.../favorite/queries/favorite-query.ts | 56 ++++--
.../favorite/services/favorite-service.ts | 5 +
.../components/sidebar/space-sidebar.tsx | 45 ++++-
.../spaces-page/all-spaces-list.tsx | 53 ++++-
.../space/queries/space-watcher-query.ts | 29 +++
.../space/services/space-watcher-service.ts | 6 +
apps/server/package.json | 10 +-
.../src/core/favorite/dto/favorite-ids.dto.ts | 8 +
.../src/core/favorite/favorite.controller.ts | 15 ++
.../favorite/services/favorite.service.ts | 34 ++++
.../core/watcher/space-watcher.controller.ts | 9 +
.../src/core/watcher/watcher.service.ts | 24 ++-
.../database/repos/favorite/favorite.repo.ts | 33 ++++
.../database/repos/watcher/watcher.repo.ts | 16 ++
package.json | 10 +-
pnpm-lock.yaml | 187 ++++++++----------
18 files changed, 412 insertions(+), 155 deletions(-)
create mode 100644 apps/server/src/core/favorite/dto/favorite-ids.dto.ts
diff --git a/apps/client/package.json b/apps/client/package.json
index 150a9389..f7a15626 100644
--- a/apps/client/package.json
+++ b/apps/client/package.json
@@ -25,7 +25,7 @@
"@tabler/icons-react": "^3.40.0",
"@tanstack/react-query": "5.90.17",
"alfaaz": "^1.1.0",
- "axios": "1.13.6",
+ "axios": "1.15.0",
"blueimp-load-image": "^5.16.0",
"clsx": "^2.1.1",
"emoji-mart": "^5.6.0",
diff --git a/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx b/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx
index f0fe3035..884e4f40 100644
--- a/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx
+++ b/apps/client/src/ee/ai-chat/components/ai-chat-layout.tsx
@@ -55,7 +55,7 @@ export default function AiChatLayout() {
navigate(location.pathname, { replace: true, state: null });
}, [chatId, location, navigate, sendMessage]);
- const hasMessages = messages.length > 0 || isStreaming;
+ const hasMessages = messages.length > 0 || isStreaming || !!chatId;
// While the redirect effect is running (or if the user is still on this
// component for any reason) never render the chat UI for a forbidden chat.
@@ -65,18 +65,6 @@ export default function AiChatLayout() {
return (
- {error && (
-
- {error}
-
- )}
-
{hasMessages ? (
<>
+ {error && (
+
+ {error}
+
+ )}
{
- const { data } = useQuery>({
+ const { data } = useQuery({
queryKey: ["favorite-ids", type],
- queryFn: () => getFavorites({ type, limit: 50 }),
+ queryFn: () => getFavoriteIds(type),
refetchOnMount: true,
});
- const ids = new Set();
- if (data?.items) {
- for (const fav of data.items) {
- let id: string | undefined;
- if (type === "page") id = fav.pageId;
- else if (type === "space") id = fav.spaceId;
- else if (type === "template") id = fav.templateId;
- if (id) ids.add(id);
- }
- }
- return ids;
+ const items = data?.items;
+ return useMemo(() => new Set(items ?? []), [items]);
+}
+
+function getEntityId(variables: ToggleFavoriteParams): string | undefined {
+ if (variables.type === "page") return variables.pageId;
+ if (variables.type === "space") return variables.spaceId;
+ if (variables.type === "template") return variables.templateId;
+ return undefined;
}
export function useAddFavoriteMutation() {
@@ -51,9 +50,17 @@ export function useAddFavoriteMutation() {
return useMutation({
mutationFn: (data) => addFavorite(data),
onSuccess: (_result, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["favorite-ids", variables.type],
- });
+ const entityId = getEntityId(variables);
+ if (entityId) {
+ queryClient.setQueryData(
+ ["favorite-ids", variables.type],
+ (old: { items: string[]; meta: any } | undefined) => {
+ if (!old) return old;
+ if (old.items.includes(entityId)) return old;
+ return { ...old, items: [...old.items, entityId] };
+ },
+ );
+ }
queryClient.invalidateQueries({
queryKey: ["favorites", variables.type],
});
@@ -67,9 +74,16 @@ export function useRemoveFavoriteMutation() {
return useMutation({
mutationFn: (data) => removeFavorite(data),
onSuccess: (_result, variables) => {
- queryClient.invalidateQueries({
- queryKey: ["favorite-ids", variables.type],
- });
+ const entityId = getEntityId(variables);
+ if (entityId) {
+ queryClient.setQueryData(
+ ["favorite-ids", variables.type],
+ (old: { items: string[]; meta: any } | undefined) => {
+ if (!old) return old;
+ return { ...old, items: old.items.filter((id) => id !== entityId) };
+ },
+ );
+ }
queryClient.invalidateQueries({
queryKey: ["favorites", variables.type],
});
diff --git a/apps/client/src/features/favorite/services/favorite-service.ts b/apps/client/src/features/favorite/services/favorite-service.ts
index 73174c25..1fd2da30 100644
--- a/apps/client/src/features/favorite/services/favorite-service.ts
+++ b/apps/client/src/features/favorite/services/favorite-service.ts
@@ -21,6 +21,11 @@ export async function removeFavorite(
await api.post("/favorites/remove", params);
}
+export async function getFavoriteIds(type: FavoriteType): Promise> {
+ const req = await api.post>("/favorites/ids", { type });
+ return req.data;
+}
+
export async function getFavorites(params?: {
type?: FavoriteType;
limit?: number;
diff --git a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
index 7bb1c88e..0ad0094e 100644
--- a/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
+++ b/apps/client/src/features/space/components/sidebar/space-sidebar.tsx
@@ -16,6 +16,8 @@ import {
IconPlus,
IconSearch,
IconSettings,
+ IconStar,
+ IconStarFilled,
IconTrash,
} from "@tabler/icons-react";
import {
@@ -43,6 +45,11 @@ import PageImportModal from "@/features/page/components/page-import-modal.tsx";
import { useTranslation } from "react-i18next";
import { SwitchSpace } from "./switch-space";
import ExportModal from "@/components/common/export-modal";
+import {
+ useFavoriteIds,
+ useAddFavoriteMutation,
+ useRemoveFavoriteMutation,
+} from "@/features/favorite/queries/favorite-query";
import { mobileSidebarAtom } from "@/components/layouts/global/hooks/atoms/sidebar-atom.ts";
import { useToggleSidebar } from "@/components/layouts/global/hooks/hooks/use-toggle-sidebar.ts";
import { searchSpotlight } from "@/features/search/constants";
@@ -56,7 +63,6 @@ export function SpaceSidebar() {
const [mobileSidebarOpened] = useAtom(mobileSidebarAtom);
const toggleMobileSidebar = useToggleSidebar(mobileSidebarAtom);
-
const { spaceSlug } = useParams();
const { data: space } = useGetSpaceBySlugQuery(spaceSlug);
@@ -82,7 +88,12 @@ export function SpaceSidebar() {
marginBottom: 3,
}}
>
-
+
{
+ const params = { type: "space" as const, spaceId };
+ if (isFavorited) {
+ removeFavoriteMutation.mutate(params);
+ } else {
+ addFavoriteMutation.mutate(params);
+ }
+ };
+
const handleToggleWatch = () => {
if (isWatching) {
unwatchMutation.mutate(spaceId);
@@ -265,6 +290,22 @@ function SpaceMenu({
+
+ ) : (
+
+ )
+ }
+ >
+ {isFavorited ? t("Remove from favorites") : t("Add to favorites")}
+
+
; size?: number }) {
+ const { t } = useTranslation();
+ const watchMutation = useWatchSpaceMutation();
+ const unwatchMutation = useUnwatchSpaceMutation();
+ const isWatching = watchedIds.has(spaceId);
+ const isPending = watchMutation.isPending || unwatchMutation.isPending;
+
+ const handleToggle = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (isWatching) {
+ unwatchMutation.mutate(spaceId);
+ } else {
+ watchMutation.mutate(spaceId);
+ }
+ };
+
+ return (
+
+
+ {isWatching ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
interface AllSpacesListProps {
spaces: any[];
onSearch: (query: string) => void;
@@ -44,6 +89,7 @@ export default function AllSpacesList({
onPrev,
}: AllSpacesListProps) {
const { t } = useTranslation();
+ const watchedIds = useWatchedSpaceIds();
const [settingsOpened, { open: openSettings, close: closeSettings }] =
useDisclosure(false);
const [selectedSpaceId, setSelectedSpaceId] = useState(null);
@@ -65,7 +111,7 @@ export default function AllSpacesList({
{t("Space")}
{t("Members")}
-
+
@@ -117,8 +163,9 @@ export default function AllSpacesList({
-
+
+