Compare commits

...

1 Commits

Author SHA1 Message Date
Philipinho 0b32511dff fix space favorites view 2026-04-14 02:39:22 +01:00
11 changed files with 103 additions and 39 deletions
@@ -14,11 +14,11 @@ import {
} from "../services/favorite-service"; } from "../services/favorite-service";
import { FavoriteType } from "../types/favorite.types"; import { FavoriteType } from "../types/favorite.types";
export function useFavoritesQuery(type?: FavoriteType) { export function useFavoritesQuery(type?: FavoriteType, spaceId?: string) {
return useInfiniteQuery({ return useInfiniteQuery({
queryKey: ["favorites", type], queryKey: ["favorites", type, spaceId],
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) =>
getFavorites({ type, cursor: pageParam, limit: 15 }), getFavorites({ type, spaceId, cursor: pageParam, limit: 15 }),
initialPageParam: undefined as string | undefined, initialPageParam: undefined as string | undefined,
getNextPageParam: (lastPage) => getNextPageParam: (lastPage) =>
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined, lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
@@ -26,10 +26,10 @@ export function useFavoritesQuery(type?: FavoriteType) {
}); });
} }
export function useFavoriteIds(type: FavoriteType): Set<string> { export function useFavoriteIds(type: FavoriteType, spaceId?: string): Set<string> {
const { data } = useQuery({ const { data } = useQuery({
queryKey: ["favorite-ids", type], queryKey: ["favorite-ids", type, spaceId],
queryFn: () => getFavoriteIds(type), queryFn: () => getFavoriteIds(type, spaceId),
refetchOnMount: true, refetchOnMount: true,
}); });
@@ -52,9 +52,9 @@ export function useAddFavoriteMutation() {
onSuccess: (_result, variables) => { onSuccess: (_result, variables) => {
const entityId = getEntityId(variables); const entityId = getEntityId(variables);
if (entityId) { if (entityId) {
queryClient.setQueryData( queryClient.setQueriesData<{ items: string[]; meta: any }>(
["favorite-ids", variables.type], { queryKey: ["favorite-ids", variables.type] },
(old: { items: string[]; meta: any } | undefined) => { (old) => {
if (!old) return old; if (!old) return old;
if (old.items.includes(entityId)) return old; if (old.items.includes(entityId)) return old;
return { ...old, items: [...old.items, entityId] }; return { ...old, items: [...old.items, entityId] };
@@ -76,9 +76,9 @@ export function useRemoveFavoriteMutation() {
onSuccess: (_result, variables) => { onSuccess: (_result, variables) => {
const entityId = getEntityId(variables); const entityId = getEntityId(variables);
if (entityId) { if (entityId) {
queryClient.setQueryData( queryClient.setQueriesData<{ items: string[]; meta: any }>(
["favorite-ids", variables.type], { queryKey: ["favorite-ids", variables.type] },
(old: { items: string[]; meta: any } | undefined) => { (old) => {
if (!old) return old; if (!old) return old;
return { ...old, items: old.items.filter((id) => id !== entityId) }; return { ...old, items: old.items.filter((id) => id !== entityId) };
}, },
@@ -21,13 +21,14 @@ export async function removeFavorite(
await api.post("/favorites/remove", params); await api.post("/favorites/remove", params);
} }
export async function getFavoriteIds(type: FavoriteType): Promise<IPagination<string>> { export async function getFavoriteIds(type: FavoriteType, spaceId?: string): Promise<IPagination<string>> {
const req = await api.post<IPagination<string>>("/favorites/ids", { type }); const req = await api.post<IPagination<string>>("/favorites/ids", { type, spaceId });
return req.data; return req.data;
} }
export async function getFavorites(params?: { export async function getFavorites(params?: {
type?: FavoriteType; type?: FavoriteType;
spaceId?: string;
limit?: number; limit?: number;
cursor?: string; cursor?: string;
}): Promise<IPagination<IFavorite>> { }): Promise<IPagination<IFavorite>> {
@@ -18,7 +18,11 @@ import { getSpaceUrl } from "@/lib/config";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { getInitialsColor } from "@/lib/get-initials-color"; 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 { t } = useTranslation();
const { const {
data, data,
@@ -27,7 +31,7 @@ export default function FavoritesPages() {
hasNextPage, hasNextPage,
fetchNextPage, fetchNextPage,
isFetchingNextPage, isFetchingNextPage,
} = useFavoritesQuery("page"); } = useFavoritesQuery("page", spaceId);
const favorites = data?.pages.flatMap((p) => p.items) ?? []; const favorites = data?.pages.flatMap((p) => p.items) ?? [];
@@ -72,19 +76,21 @@ export default function FavoritesPages() {
</Group> </Group>
</UnstyledButton> </UnstyledButton>
</Table.Td> </Table.Td>
<Table.Td> {!spaceId && (
{fav.space && ( <Table.Td>
<Badge {fav.space && (
color={getInitialsColor(fav.space.name)} <Badge
variant="light" color={getInitialsColor(fav.space.name)}
component={Link} variant="light"
to={getSpaceUrl(fav.space.slug)} component={Link}
style={{ cursor: "pointer" }} to={getSpaceUrl(fav.space.slug)}
> style={{ cursor: "pointer" }}
{fav.space.name} >
</Badge> {fav.space.name}
)} </Badge>
</Table.Td> )}
</Table.Td>
)}
<Table.Td> <Table.Td>
<Text <Text
c="dimmed" c="dimmed"
@@ -145,7 +145,7 @@ function PageActionMenu({ readOnly }: PageActionMenuProps) {
] = useDisclosure(false); ] = useDisclosure(false);
const [pageEditor] = useAtom(pageEditorAtom); const [pageEditor] = useAtom(pageEditorAtom);
const pageUpdatedAt = useTimeAgo(page?.updatedAt); const pageUpdatedAt = useTimeAgo(page?.updatedAt);
const favoriteIds = useFavoriteIds("page"); const favoriteIds = useFavoriteIds("page", page?.spaceId);
const addFavoriteMutation = useAddFavoriteMutation(); const addFavoriteMutation = useAddFavoriteMutation();
const removeFavoriteMutation = useRemoveFavoriteMutation(); const removeFavoriteMutation = useRemoveFavoriteMutation();
const isFavorited = page?.id ? favoriteIds.has(page.id) : false; const isFavorited = page?.id ? favoriteIds.has(page.id) : false;
@@ -509,7 +509,7 @@ function NodeMenu({ node, treeApi, spaceId }: NodeMenuProps) {
copyPageModalOpened, copyPageModalOpened,
{ open: openCopyPageModal, close: closeCopySpaceModal }, { open: openCopyPageModal, close: closeCopySpaceModal },
] = useDisclosure(false); ] = useDisclosure(false);
const favoriteIds = useFavoriteIds("page"); const favoriteIds = useFavoriteIds("page", spaceId);
const addFavorite = useAddFavoriteMutation(); const addFavorite = useAddFavoriteMutation();
const removeFavorite = useRemoveFavoriteMutation(); const removeFavorite = useRemoveFavoriteMutation();
const isFavorited = favoriteIds.has(node.data.id); const isFavorited = favoriteIds.has(node.data.id);
@@ -47,7 +47,7 @@ export default function SpaceHomeTabs() {
{space?.id && <RecentChanges spaceId={space.id} />} {space?.id && <RecentChanges spaceId={space.id} />}
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="favorites"> <Tabs.Panel value="favorites">
<FavoritesPages /> {space?.id && <FavoritesPages spaceId={space.id} />}
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel value="created"> <Tabs.Panel value="created">
{space?.id && <CreatedByMe spaceId={space.id} />} {space?.id && <CreatedByMe spaceId={space.id} />}
@@ -1,8 +1,12 @@
import { IsIn, IsNotEmpty, IsString } from 'class-validator'; import { IsIn, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator';
export class FavoriteIdsDto { export class FavoriteIdsDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@IsIn(['page', 'space', 'template']) @IsIn(['page', 'space', 'template'])
type: 'page' | 'space' | 'template'; type: 'page' | 'space' | 'template';
@IsOptional()
@IsUUID()
spaceId?: string;
} }
@@ -1,8 +1,12 @@
import { IsIn, IsOptional, IsString } from 'class-validator'; import { IsIn, IsOptional, IsString, IsUUID } from 'class-validator';
export class ListFavoritesDto { export class ListFavoritesDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
@IsIn(['page', 'space', 'template']) @IsIn(['page', 'space', 'template'])
type?: 'page' | 'space' | 'template'; type?: 'page' | 'space' | 'template';
@IsOptional()
@IsUUID()
spaceId?: string;
} }
@@ -82,6 +82,7 @@ export class FavoriteController {
user.id, user.id,
workspace.id, workspace.id,
dto.type as FavoriteType, dto.type as FavoriteType,
dto.spaceId,
); );
} }
@@ -98,6 +99,7 @@ export class FavoriteController {
workspace.id, workspace.id,
pagination, pagination,
dto.type as FavoriteType | undefined, dto.type as FavoriteType | undefined,
dto.spaceId,
); );
} }
@@ -20,11 +20,13 @@ export class FavoriteService {
userId: string, userId: string,
workspaceId: string, workspaceId: string,
type: FavoriteType, type: FavoriteType,
spaceId?: string,
) { ) {
const result = await this.favoriteRepo.getFavoriteIds( const result = await this.favoriteRepo.getFavoriteIds(
userId, userId,
workspaceId, workspaceId,
type, type,
spaceId,
); );
if (result.items.length === 0) { if (result.items.length === 0) {
@@ -95,12 +97,14 @@ export class FavoriteService {
workspaceId: string, workspaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
type?: FavoriteType, type?: FavoriteType,
spaceId?: string,
) { ) {
const result = await this.favoriteRepo.findUserFavorites( const result = await this.favoriteRepo.findUserFavorites(
userId, userId,
workspaceId, workspaceId,
pagination, pagination,
type, type,
spaceId,
); );
if (result.items.length === 0) { if (result.items.length === 0) {
@@ -5,7 +5,7 @@ import { InsertableFavorite, Favorite } from '@docmost/db/types/entity.types';
import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options';
import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination'; import { executeWithCursorPagination } from '@docmost/db/pagination/cursor-pagination';
import { jsonObjectFrom } from 'kysely/helpers/postgres'; 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 { DB } from '@docmost/db/types/db';
import { dbOrTx } from '@docmost/db/utils'; import { dbOrTx } from '@docmost/db/utils';
@@ -66,6 +66,7 @@ export class FavoriteRepo {
userId: string, userId: string,
workspaceId: string, workspaceId: string,
type: FavoriteType, type: FavoriteType,
spaceId?: string,
): Promise<{ items: string[]; meta: any }> { ): Promise<{ items: string[]; meta: any }> {
const idColumn = const idColumn =
type === FavoriteType.PAGE type === FavoriteType.PAGE
@@ -74,12 +75,16 @@ export class FavoriteRepo {
? 'spaceId' ? 'spaceId'
: 'templateId'; : 'templateId';
const query = this.db let query = this.db
.selectFrom('favorites') .selectFrom('favorites')
.select(['favorites.id', `favorites.${idColumn} as entityId`]) .select(['favorites.id', `favorites.${idColumn} as entityId`])
.where('userId', '=', userId) .where('favorites.userId', '=', userId)
.where('workspaceId', '=', workspaceId) .where('favorites.workspaceId', '=', workspaceId)
.where('type', '=', type); .where('favorites.type', '=', type);
if (spaceId) {
query = this.applySpaceFilter(query, type, spaceId);
}
const result = await executeWithCursorPagination(query, { const result = await executeWithCursorPagination(query, {
perPage: 250, perPage: 250,
@@ -100,6 +105,7 @@ export class FavoriteRepo {
workspaceId: string, workspaceId: string,
pagination: PaginationOptions, pagination: PaginationOptions,
type?: FavoriteType, type?: FavoriteType,
spaceId?: string,
) { ) {
let query = this.db let query = this.db
.selectFrom('favorites') .selectFrom('favorites')
@@ -111,6 +117,10 @@ export class FavoriteRepo {
query = query.where('favorites.type', '=', type); query = query.where('favorites.type', '=', type);
} }
if (spaceId) {
query = this.applySpaceFilter(query, type, spaceId);
}
if (type === FavoriteType.PAGE || !type) { if (type === FavoriteType.PAGE || !type) {
query = query.select((eb) => this.withPage(eb)); query = query.select((eb) => this.withPage(eb));
} }
@@ -184,6 +194,39 @@ export class FavoriteRepo {
.execute(); .execute();
} }
private applySpaceFilter<Q extends SelectQueryBuilder<any, any, any>>(
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<DB, 'favorites'>) { private withPage(eb: ExpressionBuilder<DB, 'favorites'>) {
return jsonObjectFrom( return jsonObjectFrom(
eb eb