Files
gitmost/apps/client/src/features/workspace/queries/workspace-query.ts
claude code agent 227 9e1d057878 fix(qa): resolve QA-pass issues #122–#134
Batch of fixes from the automated QA pass on develop. Each was reproduced and
then verified fixed live (browser/curl); logic-bearing fixes have unit tests.

Functional bugs:
- #122 collab-token was capped by the anonymous public-share-AI throttler (5/min);
  skip all non-AUTH named throttlers on this auth-guarded, client-cached route.
- #123 editor onAuthenticationFailed threw `jwtDecode(undefined)` and never
  reconnected; read the token via a ref, guard the decode (incl. missing exp),
  and refetch+reconnect on any auth failure.
- #124 a slash command containing a space ("/Heading 1") inserted literal text;
  enable allowSpaces and close the menu when the query matches no items.
- #125 space slug auto-gen produced uppercase initials for multi-word names;
  computeSpaceSlug now yields a lowercase alphanumeric slug.
- #126 AI chat window position/size now persisted (atomWithStorage) across reload;
  also fixes a latent ResizeObserver-attach bug on first open.
- #127 workspace name update accepted URLs; add @NoUrls (parity with setup).
- #132 icon-columns 4/5 passed calc() into SVG width/height attrs (console spam);
  size via style. share-for-page query returns null instead of undefined.
- #134 "Reindex now" counter looked stuck: reindex runs async; the client now
  polls coverage (bounded) so the counter climbs live; misleading server comment
  reworded.

UX / consistency:
- #128 add success toasts to favorite/label/avatar/member-(de)activate.
- #129 "1 result found" pluralization; hide the single-option Type filter.
- #130 replace raw Zod strings with friendly messages (name/password/group).
- #131 unify "Untitled" casing in tree/breadcrumb/tab; stop force-uppercasing
  space-name chips; fix confirm-dialog labels (Cancel / Remove), invite
  placeholder typo, Export/Move-to-space labels.
- #133 disable profile Save when clean; toast on unsupported avatar image;
  style the invalid-invitation page with a CTA; hide Share for read-only users;
  align the dictation "not configured" message; "Go to login page" typo.

Tests: computeSpaceSlug, workspace-name NoUrls DTO, share-query null
normalization, slash getSuggestionItems empty-close.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 20:47:40 +03:00

249 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,
refetchOnMount: true,
});
}