From cc00e77dfba1403da26ea1015964ae8307985178 Mon Sep 17 00:00:00 2001 From: Philip Okugbe <16838612+Philipinho@users.noreply.github.com> Date: Tue, 14 Apr 2026 02:58:24 +0100 Subject: [PATCH] fix: space overview favorites (#2110) --- .../favorite/queries/favorite-query.ts | 24 ++++----- .../favorite/services/favorite-service.ts | 5 +- .../home/components/favorites-pages.tsx | 36 +++++++------ .../components/header/page-header-menu.tsx | 2 +- .../page/tree/components/space-tree.tsx | 2 +- .../space/components/space-home-tabs.tsx | 2 +- .../src/core/favorite/dto/favorite-ids.dto.ts | 6 ++- .../core/favorite/dto/list-favorites.dto.ts | 6 ++- .../src/core/favorite/favorite.controller.ts | 2 + .../favorite/services/favorite.service.ts | 4 ++ .../database/repos/favorite/favorite.repo.ts | 53 +++++++++++++++++-- 11 files changed, 103 insertions(+), 39 deletions(-) diff --git a/apps/client/src/features/favorite/queries/favorite-query.ts b/apps/client/src/features/favorite/queries/favorite-query.ts index 72d71166..886f2f5b 100644 --- a/apps/client/src/features/favorite/queries/favorite-query.ts +++ b/apps/client/src/features/favorite/queries/favorite-query.ts @@ -14,11 +14,11 @@ import { } from "../services/favorite-service"; import { FavoriteType } from "../types/favorite.types"; -export function useFavoritesQuery(type?: FavoriteType) { +export function useFavoritesQuery(type?: FavoriteType, spaceId?: string) { return useInfiniteQuery({ - queryKey: ["favorites", type], + queryKey: ["favorites", type, spaceId], queryFn: ({ pageParam }) => - getFavorites({ type, cursor: pageParam, limit: 15 }), + getFavorites({ type, spaceId, cursor: pageParam, limit: 15 }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined, @@ -26,10 +26,10 @@ export function useFavoritesQuery(type?: FavoriteType) { }); } -export function useFavoriteIds(type: FavoriteType): Set { +export function useFavoriteIds(type: FavoriteType, spaceId?: string): Set { const { data } = useQuery({ - queryKey: ["favorite-ids", type], - queryFn: () => getFavoriteIds(type), + queryKey: ["favorite-ids", type, spaceId], + queryFn: () => getFavoriteIds(type, spaceId), refetchOnMount: true, }); @@ -52,9 +52,9 @@ export function useAddFavoriteMutation() { onSuccess: (_result, variables) => { const entityId = getEntityId(variables); if (entityId) { - queryClient.setQueryData( - ["favorite-ids", variables.type], - (old: { items: string[]; meta: any } | undefined) => { + queryClient.setQueriesData<{ items: string[]; meta: any }>( + { queryKey: ["favorite-ids", variables.type] }, + (old) => { if (!old) return old; if (old.items.includes(entityId)) return old; return { ...old, items: [...old.items, entityId] }; @@ -76,9 +76,9 @@ export function useRemoveFavoriteMutation() { onSuccess: (_result, variables) => { const entityId = getEntityId(variables); if (entityId) { - queryClient.setQueryData( - ["favorite-ids", variables.type], - (old: { items: string[]; meta: any } | undefined) => { + queryClient.setQueriesData<{ items: string[]; meta: any }>( + { queryKey: ["favorite-ids", variables.type] }, + (old) => { if (!old) return old; return { ...old, items: old.items.filter((id) => id !== entityId) }; }, diff --git a/apps/client/src/features/favorite/services/favorite-service.ts b/apps/client/src/features/favorite/services/favorite-service.ts index 1fd2da30..dbc68492 100644 --- a/apps/client/src/features/favorite/services/favorite-service.ts +++ b/apps/client/src/features/favorite/services/favorite-service.ts @@ -21,13 +21,14 @@ 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 }); +export async function getFavoriteIds(type: FavoriteType, spaceId?: string): Promise> { + const req = await api.post>("/favorites/ids", { type, spaceId }); return req.data; } export async function getFavorites(params?: { type?: FavoriteType; + spaceId?: string; limit?: number; cursor?: string; }): Promise> { diff --git a/apps/client/src/features/home/components/favorites-pages.tsx b/apps/client/src/features/home/components/favorites-pages.tsx index c4cb252d..eb87216e 100644 --- a/apps/client/src/features/home/components/favorites-pages.tsx +++ b/apps/client/src/features/home/components/favorites-pages.tsx @@ -18,7 +18,11 @@ import { getSpaceUrl } from "@/lib/config"; import { useTranslation } from "react-i18next"; import { getInitialsColor } from "@/lib/get-initials-color"; -export default function FavoritesPages() { +interface Props { + spaceId?: string; +} + +export default function FavoritesPages({ spaceId }: Props) { const { t } = useTranslation(); const { data, @@ -27,7 +31,7 @@ export default function FavoritesPages() { hasNextPage, fetchNextPage, isFetchingNextPage, - } = useFavoritesQuery("page"); + } = useFavoritesQuery("page", spaceId); const favorites = data?.pages.flatMap((p) => p.items) ?? []; @@ -72,19 +76,21 @@ export default function FavoritesPages() { - - {fav.space && ( - - {fav.space.name} - - )} - + {!spaceId && ( + + {fav.space && ( + + {fav.space.name} + + )} + + )} } - + {space?.id && } {space?.id && } diff --git a/apps/server/src/core/favorite/dto/favorite-ids.dto.ts b/apps/server/src/core/favorite/dto/favorite-ids.dto.ts index 1809024e..bc5b1ebb 100644 --- a/apps/server/src/core/favorite/dto/favorite-ids.dto.ts +++ b/apps/server/src/core/favorite/dto/favorite-ids.dto.ts @@ -1,8 +1,12 @@ -import { IsIn, IsNotEmpty, IsString } from 'class-validator'; +import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; export class FavoriteIdsDto { @IsString() @IsNotEmpty() @IsIn(['page', 'space', 'template']) type: 'page' | 'space' | 'template'; + + @IsOptional() + @IsUUID() + spaceId?: string; } diff --git a/apps/server/src/core/favorite/dto/list-favorites.dto.ts b/apps/server/src/core/favorite/dto/list-favorites.dto.ts index f480f5ac..f32c7aac 100644 --- a/apps/server/src/core/favorite/dto/list-favorites.dto.ts +++ b/apps/server/src/core/favorite/dto/list-favorites.dto.ts @@ -1,8 +1,12 @@ -import { IsIn, IsOptional, IsString } from 'class-validator'; +import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; export class ListFavoritesDto { @IsOptional() @IsString() @IsIn(['page', 'space', 'template']) type?: 'page' | 'space' | 'template'; + + @IsOptional() + @IsUUID() + spaceId?: string; } diff --git a/apps/server/src/core/favorite/favorite.controller.ts b/apps/server/src/core/favorite/favorite.controller.ts index 0cd56b08..47e63aac 100644 --- a/apps/server/src/core/favorite/favorite.controller.ts +++ b/apps/server/src/core/favorite/favorite.controller.ts @@ -82,6 +82,7 @@ export class FavoriteController { user.id, workspace.id, dto.type as FavoriteType, + dto.spaceId, ); } @@ -98,6 +99,7 @@ export class FavoriteController { workspace.id, pagination, dto.type as FavoriteType | undefined, + dto.spaceId, ); } diff --git a/apps/server/src/core/favorite/services/favorite.service.ts b/apps/server/src/core/favorite/services/favorite.service.ts index 1f72e92a..79902e64 100644 --- a/apps/server/src/core/favorite/services/favorite.service.ts +++ b/apps/server/src/core/favorite/services/favorite.service.ts @@ -20,11 +20,13 @@ export class FavoriteService { userId: string, workspaceId: string, type: FavoriteType, + spaceId?: string, ) { const result = await this.favoriteRepo.getFavoriteIds( userId, workspaceId, type, + spaceId, ); if (result.items.length === 0) { @@ -95,12 +97,14 @@ export class FavoriteService { workspaceId: string, pagination: PaginationOptions, type?: FavoriteType, + spaceId?: string, ) { const result = await this.favoriteRepo.findUserFavorites( userId, workspaceId, pagination, type, + spaceId, ); if (result.items.length === 0) { diff --git a/apps/server/src/database/repos/favorite/favorite.repo.ts b/apps/server/src/database/repos/favorite/favorite.repo.ts index 35826225..54b21135 100644 --- a/apps/server/src/database/repos/favorite/favorite.repo.ts +++ b/apps/server/src/database/repos/favorite/favorite.repo.ts @@ -5,7 +5,7 @@ import { InsertableFavorite, Favorite } from '@docmost/db/types/entity.types'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; -import { ExpressionBuilder, sql } from 'kysely'; +import { ExpressionBuilder, SelectQueryBuilder, sql } from 'kysely'; import { DB } from '@docmost/db/types/db'; import { dbOrTx } from '@docmost/db/utils'; @@ -66,6 +66,7 @@ export class FavoriteRepo { userId: string, workspaceId: string, type: FavoriteType, + spaceId?: string, ): Promise<{ items: string[]; meta: any }> { const idColumn = type === FavoriteType.PAGE @@ -74,12 +75,16 @@ export class FavoriteRepo { ? 'spaceId' : 'templateId'; - const query = this.db + let query = this.db .selectFrom('favorites') .select(['favorites.id', `favorites.${idColumn} as entityId`]) - .where('userId', '=', userId) - .where('workspaceId', '=', workspaceId) - .where('type', '=', type); + .where('favorites.userId', '=', userId) + .where('favorites.workspaceId', '=', workspaceId) + .where('favorites.type', '=', type); + + if (spaceId) { + query = this.applySpaceFilter(query, type, spaceId); + } const result = await executeWithCursorPagination(query, { perPage: 250, @@ -100,6 +105,7 @@ export class FavoriteRepo { workspaceId: string, pagination: PaginationOptions, type?: FavoriteType, + spaceId?: string, ) { let query = this.db .selectFrom('favorites') @@ -111,6 +117,10 @@ export class FavoriteRepo { query = query.where('favorites.type', '=', type); } + if (spaceId) { + query = this.applySpaceFilter(query, type, spaceId); + } + if (type === FavoriteType.PAGE || !type) { query = query.select((eb) => this.withPage(eb)); } @@ -184,6 +194,39 @@ export class FavoriteRepo { .execute(); } + private applySpaceFilter>( + query: Q, + type: FavoriteType | undefined, + spaceId: string, + ): Q { + if (type === FavoriteType.PAGE) { + return query.where((eb: any) => + eb.exists( + eb + .selectFrom('pages') + .select(sql`1`.as('one')) + .whereRef('pages.id', '=', 'favorites.pageId') + .where('pages.spaceId', '=', spaceId), + ), + ) as Q; + } + if (type === FavoriteType.SPACE) { + return query.where('favorites.spaceId' as any, '=', spaceId) as Q; + } + if (type === FavoriteType.TEMPLATE) { + return query.where((eb: any) => + eb.exists( + eb + .selectFrom('templates') + .select(sql`1`.as('one')) + .whereRef('templates.id', '=', 'favorites.templateId') + .where('templates.spaceId', '=', spaceId), + ), + ) as Q; + } + return query; + } + private withPage(eb: ExpressionBuilder) { return jsonObjectFrom( eb