6e59793643
- F1 [blocking]: share-modal.test.tsx + comment-content-view.test.tsx mocked page-query without usePageMetaQuery → 3 tests threw (ShareModal uses it directly, comment-content-view via MentionContent). Added usePageMetaQuery to both mocks (the space-tree mocks were already fixed; these two were missed). - F2: restored refetchOnMount:true on useGetSpacesQuery — ["spaces"] is invalidated only by same-tab mutations (no socket path), so a cross-actor change (an admin adding/removing THIS user from a space) left the list stale until a hard reload. The other refetchOnMount removals (favorites/watched — per-user, same-tab-only gap) stay removed. - F3: corrected the trash-list + recent-changes KEEP comments — both keys ARE invalidated (trash-list by 3 mutations, recent-changes by page CRUD), but invalidateQueries only marks an UNMOUNTED query stale without refetching, so the mount refetch closes the gap. The old "never invalidated" wording was wrong and risked a maintainer deleting a live invalidation as dead code. - F4: tests for the two load-bearing pure paths — invalidate-on-update-page (the undefined-guard: a title-only event keeps the icon; sibling/unrelated subtrees untouched) and breadcrumb-path-equal (equal chain → true; any id/slugId/name/ icon change or length diff → false; both-null → true). Exported breadcrumbPathEqual for the test. Gate: client tsc 0; the 4 affected/new test files 33 passed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
272 lines
8.0 KiB
TypeScript
272 lines
8.0 KiB
TypeScript
import {
|
|
keepPreviousData,
|
|
useInfiniteQuery,
|
|
useMutation,
|
|
useQuery,
|
|
useQueryClient,
|
|
UseQueryResult,
|
|
} from "@tanstack/react-query";
|
|
import {
|
|
IAddSpaceMember,
|
|
IChangeSpaceMemberRole,
|
|
IRemoveSpaceMember,
|
|
ISpace,
|
|
} from "@/features/space/types/space.types";
|
|
import {
|
|
addSpaceMember,
|
|
changeMemberRole,
|
|
getSpaceById,
|
|
getSpaceMembers,
|
|
getSpaces,
|
|
removeSpaceMember,
|
|
createSpace,
|
|
updateSpace,
|
|
deleteSpace,
|
|
} from "@/features/space/services/space-service.ts";
|
|
import { notifications } from "@mantine/notifications";
|
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
|
import { useTranslation } from "react-i18next";
|
|
import { queryClient } from "@/main.tsx";
|
|
import { getRecentChanges } from "@/features/page/services/page-service.ts";
|
|
import { useEffect } from "react";
|
|
import { validate as isValidUuid } from "uuid";
|
|
|
|
export function useGetSpacesQuery(
|
|
params?: QueryParams,
|
|
): UseQueryResult<IPagination<ISpace>, Error> {
|
|
return useQuery({
|
|
queryKey: ["spaces", params],
|
|
queryFn: () => getSpaces(params),
|
|
placeholderData: keepPreviousData,
|
|
// KEEP refetchOnMount:true (against the global default false): the ["spaces"]
|
|
// key is invalidated only by same-tab mutations (no socket path), so a
|
|
// cross-actor change — an admin adding/removing THIS user from a space — has
|
|
// no local mutation or socket event and would leave the space list stale until
|
|
// a hard reload. The mount refetch is its only cross-actor freshness path.
|
|
refetchOnMount: true,
|
|
});
|
|
}
|
|
|
|
export function useSpaceQuery(spaceId: string): UseQueryResult<ISpace, Error> {
|
|
const query = useQuery({
|
|
queryKey: ["space", spaceId],
|
|
queryFn: () => getSpaceById(spaceId),
|
|
enabled: !!spaceId,
|
|
});
|
|
useEffect(() => {
|
|
if (query.data) {
|
|
if (isValidUuid(spaceId)) {
|
|
queryClient.setQueryData(["space", query.data.slug], query.data);
|
|
} else {
|
|
queryClient.setQueryData(["space", query.data.id], query.data);
|
|
}
|
|
}
|
|
}, [query.data]);
|
|
|
|
return query;
|
|
}
|
|
|
|
export const prefetchSpace = (spaceSlug: string, spaceId?: string) => {
|
|
queryClient.prefetchQuery({
|
|
queryKey: ["space", spaceSlug],
|
|
queryFn: () => getSpaceById(spaceSlug),
|
|
});
|
|
|
|
if (spaceId) {
|
|
// this endpoint only accepts uuid for now
|
|
queryClient.prefetchInfiniteQuery({
|
|
queryKey: ["recent-changes", spaceId],
|
|
queryFn: () => getRecentChanges({ spaceId }),
|
|
initialPageParam: undefined,
|
|
});
|
|
}
|
|
};
|
|
|
|
export function useCreateSpaceMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation<ISpace, Error, Partial<ISpace>>({
|
|
mutationFn: (data) => createSpace(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["spaces"],
|
|
});
|
|
notifications.show({ message: t("Space created successfully") });
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useGetSpaceBySlugQuery(
|
|
spaceId: string,
|
|
): UseQueryResult<ISpace, Error> {
|
|
return useQuery({
|
|
queryKey: ["space", spaceId],
|
|
queryFn: () => getSpaceById(spaceId),
|
|
enabled: !!spaceId,
|
|
staleTime: 5 * 60 * 1000,
|
|
});
|
|
}
|
|
|
|
export function useUpdateSpaceMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation<ISpace, Error, Partial<ISpace>>({
|
|
mutationFn: (data) => updateSpace(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Space updated successfully") });
|
|
|
|
const space = queryClient.getQueryData([
|
|
"space",
|
|
variables.spaceId,
|
|
]) as ISpace;
|
|
if (space) {
|
|
const updatedSpace = { ...space, ...data };
|
|
queryClient.setQueryData(["space", variables.spaceId], updatedSpace);
|
|
queryClient.setQueryData(["space", data.slug], updatedSpace);
|
|
}
|
|
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["spaces"],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeleteSpaceMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation({
|
|
mutationFn: (data: Partial<ISpace>) => deleteSpace(data.id),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Space deleted successfully") });
|
|
|
|
if (variables.slug) {
|
|
queryClient.removeQueries({
|
|
queryKey: ["space", variables.slug],
|
|
exact: true,
|
|
});
|
|
}
|
|
|
|
// Remove space-specific queries
|
|
if (variables.id) {
|
|
queryClient.removeQueries({
|
|
queryKey: ["space", variables.id],
|
|
exact: true,
|
|
});
|
|
|
|
// Invalidate recent changes
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["recent-changes"],
|
|
});
|
|
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["recent-changes", variables.id],
|
|
});
|
|
}
|
|
|
|
// Update spaces list cache
|
|
/* const spaces = queryClient.getQueryData(["spaces"]) as any;
|
|
if (spaces) {
|
|
spaces.items = spaces.items?.filter(
|
|
(space: ISpace) => space.id !== variables.id,
|
|
);
|
|
queryClient.setQueryData(["spaces"], spaces);
|
|
}*/
|
|
|
|
// Invalidate all spaces queries to refresh lists
|
|
queryClient.invalidateQueries({
|
|
predicate: (item) => ["spaces"].includes(item.queryKey[0] as string),
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useSpaceMembersInfiniteQuery(
|
|
spaceId: string,
|
|
query?: string,
|
|
) {
|
|
return useInfiniteQuery({
|
|
queryKey: ["spaceMembers", spaceId, query],
|
|
queryFn: ({ pageParam }) =>
|
|
getSpaceMembers(spaceId, { cursor: pageParam, limit: 50, query }),
|
|
enabled: !!spaceId,
|
|
placeholderData: keepPreviousData,
|
|
initialPageParam: undefined as string | undefined,
|
|
getNextPageParam: (lastPage) =>
|
|
lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined,
|
|
});
|
|
}
|
|
|
|
export function useAddSpaceMemberMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation<void, Error, IAddSpaceMember>({
|
|
mutationFn: (data) => addSpaceMember(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Members added successfully") });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["spaceMembers", variables.spaceId],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRemoveSpaceMemberMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation<void, Error, IRemoveSpaceMember>({
|
|
mutationFn: (data) => removeSpaceMember(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Member removed successfully") });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["spaceMembers", variables.spaceId],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useChangeSpaceMemberRoleMutation() {
|
|
const queryClient = useQueryClient();
|
|
const { t } = useTranslation();
|
|
|
|
return useMutation<void, Error, IChangeSpaceMemberRole>({
|
|
mutationFn: (data) => changeMemberRole(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Member role updated successfully") });
|
|
// due to pagination levels, change in cache instead
|
|
queryClient.refetchQueries({
|
|
queryKey: ["spaceMembers", variables.spaceId],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|