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>
249 lines
6.3 KiB
TypeScript
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,
|
|
});
|
|
}
|