1bcc96685e
Outside the editor the UI did background work on every tree event, socket reconnect, and navigation. Tree infra (virtualization/memo/O(N) utils) was already good — the cost was in the subscriptions and duplicates around it. Client-only; behavior 1:1. - Setter-only atom subscriptions → useSetAtom: space-tree-row, use-tree-mutation, use-tree-socket no longer subscribe every visible row to the WHOLE treeDataAtom value (a tree event re-rendered all ~20-30 rows, bypassing the DocTreeRow memo). space-tree-node-menu / mention-list read the tree imperatively (store.get) in their handlers only. breadcrumb.tsx uses a selectAtom slice (ancestor chain + field equality) instead of the whole-tree subscription. - Socket handler cleanup (BUG): use-tree-socket + use-query-subscription now socket.off() their named handlers on cleanup (were accumulating listeners on every reconnect → duplicated invalidations/tree-walks). Mirrors use-notification-socket. - Field-update tree path: invalidateOnUpdatePage does a pointwise patch of the cached embed subtrees instead of a blanket invalidatePageTree() (refetch storm); structural events keep the blanket invalidate. - usePageMetaQuery: a content-less select slice for the 13 peripheral subscribers that read only title/permissions/id, so they stop re-rendering every ~3s while typing / on every collab page.updated (page.tsx keeps the full query for content). - page.tsx: skeleton + placeholderData keepPreviousData (no blank flash on nav). - Removed refetchOnMount:true where socket/mutation invalidation already keeps the cache fresh (favorite/space/space-watcher/workspace). KEPT it on the 3 queries with NO other freshness path (trash-list, created-by, recent-changes) — the global default is refetchOnMount:false, so those overrides are load-bearing. - Small: resize mousemove/up attached only while dragging; per-row emoji-picker keydown gated on `opened`; AiChatWindow queries enabled only when the window is open. Gate: client tsc 0, client vitest page+websocket 200 passed (+editor suites), build ok. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
248 lines
6.3 KiB
TypeScript
248 lines
6.3 KiB
TypeScript
import {
|
|
keepPreviousData,
|
|
useMutation,
|
|
useQuery,
|
|
useQueryClient,
|
|
UseQueryResult,
|
|
} from "@tanstack/react-query";
|
|
import {
|
|
changeMemberRole,
|
|
getInvitationById,
|
|
getPendingInvitations,
|
|
getWorkspaceMembers,
|
|
createInvitation,
|
|
resendInvitation,
|
|
revokeInvitation,
|
|
getWorkspace,
|
|
getWorkspacePublicData,
|
|
getAppVersion,
|
|
deleteWorkspaceMember,
|
|
deactivateWorkspaceMember,
|
|
activateWorkspaceMember,
|
|
} from "@/features/workspace/services/workspace-service";
|
|
import { IPagination, QueryParams } from "@/lib/types.ts";
|
|
import { notifications } from "@mantine/notifications";
|
|
import {
|
|
ICreateInvite,
|
|
IInvitation,
|
|
IPublicWorkspace,
|
|
IVersion,
|
|
IWorkspace,
|
|
} from "@/features/workspace/types/workspace.types.ts";
|
|
import { IUser } from "@/features/user/types/user.types.ts";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
export function useWorkspaceQuery(): UseQueryResult<IWorkspace, Error> {
|
|
return useQuery({
|
|
queryKey: ["workspace"],
|
|
queryFn: () => getWorkspace(),
|
|
});
|
|
}
|
|
|
|
export function useWorkspacePublicDataQuery(): UseQueryResult<
|
|
IPublicWorkspace,
|
|
Error
|
|
> {
|
|
return useQuery({
|
|
queryKey: ["workspace-public"],
|
|
queryFn: () => getWorkspacePublicData(),
|
|
});
|
|
}
|
|
|
|
export function useWorkspaceMembersQuery(
|
|
params?: QueryParams,
|
|
): UseQueryResult<IPagination<IUser>, Error> {
|
|
return useQuery({
|
|
queryKey: ["workspaceMembers", params],
|
|
queryFn: () => getWorkspaceMembers(params),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
}
|
|
|
|
export function useDeleteWorkspaceMemberMutation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<
|
|
void,
|
|
Error,
|
|
{
|
|
userId: string;
|
|
}
|
|
>({
|
|
mutationFn: (data) => deleteWorkspaceMember(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: "Member deleted successfully" });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["workspaceMembers"],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useDeactivateWorkspaceMemberMutation() {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<
|
|
void,
|
|
Error,
|
|
{
|
|
userId: string;
|
|
}
|
|
>({
|
|
mutationFn: (data) => deactivateWorkspaceMember(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["workspaceMembers"],
|
|
});
|
|
// Notify on success so the action gives visible feedback (issue #128)
|
|
notifications.show({ message: t("Member deactivated") });
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useActivateWorkspaceMemberMutation() {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<
|
|
void,
|
|
Error,
|
|
{
|
|
userId: string;
|
|
}
|
|
>({
|
|
mutationFn: (data) => activateWorkspaceMember(data),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["workspaceMembers"],
|
|
});
|
|
// Notify on success so the action gives visible feedback (issue #128)
|
|
notifications.show({ message: t("Member activated") });
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useChangeMemberRoleMutation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<any, Error, any>({
|
|
mutationFn: (data) => changeMemberRole(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: "Member role updated successfully" });
|
|
queryClient.refetchQueries({
|
|
queryKey: ["workspaceMembers"],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useWorkspaceInvitationsQuery(
|
|
params?: QueryParams,
|
|
): UseQueryResult<IPagination<IInvitation>, Error> {
|
|
return useQuery({
|
|
queryKey: ["invitations", params],
|
|
queryFn: () => getPendingInvitations(params),
|
|
placeholderData: keepPreviousData,
|
|
});
|
|
}
|
|
|
|
export function useCreateInvitationMutation() {
|
|
const { t } = useTranslation();
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<void, Error, ICreateInvite>({
|
|
mutationFn: (data) => createInvitation(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: t("Invitation sent") });
|
|
queryClient.refetchQueries({
|
|
queryKey: ["invitations"],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useResendInvitationMutation() {
|
|
return useMutation<
|
|
void,
|
|
Error,
|
|
{
|
|
invitationId: string;
|
|
}
|
|
>({
|
|
mutationFn: (data) => resendInvitation(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: "Invitation resent" });
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useRevokeInvitationMutation() {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation<
|
|
void,
|
|
Error,
|
|
{
|
|
invitationId: string;
|
|
}
|
|
>({
|
|
mutationFn: (data) => revokeInvitation(data),
|
|
onSuccess: (data, variables) => {
|
|
notifications.show({ message: "Invitation revoked" });
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["invitations"],
|
|
});
|
|
},
|
|
onError: (error) => {
|
|
const errorMessage = error["response"]?.data?.message;
|
|
notifications.show({ message: errorMessage, color: "red" });
|
|
},
|
|
});
|
|
}
|
|
|
|
export function useGetInvitationQuery(
|
|
invitationId: string,
|
|
): UseQueryResult<IInvitation, Error> {
|
|
return useQuery({
|
|
queryKey: ["invitations", invitationId],
|
|
queryFn: () => getInvitationById({ invitationId }),
|
|
enabled: !!invitationId,
|
|
});
|
|
}
|
|
|
|
export function useAppVersion(
|
|
isEnabled: boolean,
|
|
): UseQueryResult<IVersion, Error> {
|
|
return useQuery({
|
|
queryKey: ["version"],
|
|
queryFn: () => getAppVersion(),
|
|
staleTime: 60 * 60 * 1000, // 1 hr
|
|
enabled: isEnabled,
|
|
});
|
|
}
|