mirror of
https://github.com/docmost/docmost.git
synced 2026-05-07 06:23:06 +08:00
fix space favorites view
This commit is contained in:
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user