Files
gitmost/apps/client/src/features/share/queries/share-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

184 lines
4.7 KiB
TypeScript

import {
keepPreviousData,
useMutation,
useQuery,
useQueryClient,
UseQueryResult,
} from "@tanstack/react-query";
import { notifications } from "@mantine/notifications";
import { useTranslation } from "react-i18next";
import {
ICreateShare,
IShare,
ISharedItem,
ISharedPage,
ISharedPageTree,
IShareForPage,
IShareInfoInput,
IUpdateShare,
} from "@/features/share/types/share.types.ts";
import {
createShare,
deleteShare,
getSharedPageTree,
getShareForPage,
getShareInfo,
getSharePageInfo,
getShares,
updateShare,
} from "@/features/share/services/share-service.ts";
import { IPagination, QueryParams } from "@/lib/types.ts";
export function useGetSharesQuery(
params?: QueryParams,
): UseQueryResult<IPagination<ISharedItem>, Error> {
return useQuery({
queryKey: ["share-list", params],
queryFn: () => getShares(params),
placeholderData: keepPreviousData,
});
}
export function useGetShareByIdQuery(
shareId: string,
): UseQueryResult<IShare, Error> {
const query = useQuery({
queryKey: ["share-by-id", shareId],
queryFn: () => getShareInfo(shareId),
enabled: !!shareId,
});
return query;
}
export function useSharePageQuery(
shareInput: Partial<IShareInfoInput>,
): UseQueryResult<ISharedPage, Error> {
const query = useQuery({
queryKey: ["shares", shareInput],
queryFn: () => getSharePageInfo(shareInput),
enabled: !!shareInput.pageId,
});
return query;
}
export function useShareForPageQuery(
pageId: string,
): UseQueryResult<IShareForPage | null, Error> {
const query = useQuery({
queryKey: ["share-for-page", pageId],
// React Query forbids `undefined` as resolved data ("Query data cannot be
// undefined"). When no share exists for the page the endpoint resolves to
// undefined, so normalize the absence to `null`.
queryFn: async () => (await getShareForPage(pageId)) ?? null,
enabled: !!pageId,
staleTime: 60 * 1000,
retry: false,
});
return query;
}
export function useCreateShareMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<any, Error, ICreateShare>({
mutationFn: (data) => createShare(data),
onSuccess: (data) => {
queryClient.invalidateQueries({
predicate: (item) =>
["share-for-page", "share-list"].includes(item.queryKey[0] as string),
});
},
onError: (error) => {
notifications.show({
message: error?.["response"]?.data?.message || t("Failed to share page"),
color: "red",
});
},
});
}
export function useUpdateShareMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation<any, Error, IUpdateShare>({
mutationFn: (data) => updateShare(data),
onSuccess: (data) => {
queryClient.invalidateQueries({
predicate: (item) =>
["share-for-page", "share-list"].includes(item.queryKey[0] as string),
});
},
onError: (error, params) => {
if (error?.["status"] === 404) {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
notifications.show({
message: t("Share not found"),
color: "red",
});
return;
}
notifications.show({
message: error?.["response"]?.data?.message || "Share not found",
color: "red",
});
},
});
}
export function useDeleteShareMutation() {
const { t } = useTranslation();
const queryClient = useQueryClient();
return useMutation({
mutationFn: (shareId: string) => deleteShare(shareId),
onSuccess: (data) => {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
queryClient.invalidateQueries({
predicate: (item) =>
["share-list"].includes(item.queryKey[0] as string),
});
notifications.show({ message: t("Share deleted successfully") });
},
onError: (error) => {
if (error?.["status"] === 404) {
queryClient.removeQueries({
predicate: (item) =>
["share-for-page"].includes(item.queryKey[0] as string),
});
}
notifications.show({
message: error?.["response"]?.data?.message || "Failed to delete share",
color: "red",
});
},
});
}
export function useGetSharedPageTreeQuery(
shareId: string,
): UseQueryResult<ISharedPageTree, Error> {
return useQuery({
queryKey: ["shared-page-tree", shareId],
queryFn: () => getSharedPageTree(shareId),
enabled: !!shareId,
placeholderData: keepPreviousData,
staleTime: 60 * 60 * 1000,
});
}