refactor(web): split queries.ts into api/queries/ domain modules behind a barrel (#65)

This commit is contained in:
2026-06-08 21:35:02 +02:00
parent 704b159d48
commit d8d8035850
8 changed files with 591 additions and 556 deletions
-556
View File
@@ -1,556 +0,0 @@
import { keepPreviousData, useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "./client";
import { keys, type ObjectListParams } from "./query-keys";
import type { components } from "./schema";
import { HttpError, FieldRejection, InUseError, VisibilityError } from "./errors";
export { HttpError, FieldRejection, InUseError, VisibilityError } from "./errors";
export type { ObjectListParams } from "./query-keys";
type UserView = components["schemas"]["UserView"];
type LoginRequest = components["schemas"]["LoginRequest"];
export function useMe() {
return useQuery({
queryKey: keys.me(),
queryFn: async (): Promise<UserView | null> => {
const { data, response } = await api.GET("/api/admin/me");
if (response.status === 401) return null;
if (!data) throw new Error("failed to load session");
return data;
},
retry: false,
});
}
export function useObjectsPage(params: ObjectListParams) {
return useQuery({
queryKey: keys.objectsPage(params),
placeholderData: keepPreviousData,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/objects", {
params: {
query: {
limit: params.limit,
offset: params.offset,
sort: params.sort,
order: params.order,
visibility: params.visibility,
q: params.q,
},
},
});
if (error || !data) throw new Error("failed to load objects");
return data;
},
});
}
export function useObject(id: string) {
return useQuery({
queryKey: keys.object(id),
queryFn: async () => {
const { data, response } = await api.GET("/api/admin/objects/{id}", {
params: { path: { id } },
});
if (response.status === 404) return null;
if (!data) throw new Error("failed to load object");
return data;
},
// A 404 resolves to null rather than erroring, so don't retry it.
retry: false,
});
}
export function useFieldDefinitions() {
return useQuery({
queryKey: keys.fieldDefinitions(),
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/field-definitions");
if (error || !data) throw new Error("failed to load field definitions");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useLogin() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: LoginRequest) => {
const { response } = await api.POST("/api/admin/login", { body });
if (response.status !== 204) {
throw new Error(response.status === 401 ? "invalid" : "network");
}
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.me() }),
meta: { suppressErrorToast: true },
});
}
export function useLogout() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => {
await api.POST("/api/admin/logout");
},
onSuccess: () => qc.setQueryData(keys.me(), null),
});
}
type ObjectCreateRequest = components["schemas"]["ObjectCreateRequest"];
type ObjectUpdateRequest = components["schemas"]["ObjectUpdateRequest"];
export function useTerms(vocabularyId: string | null | undefined) {
return useQuery({
queryKey: keys.terms(vocabularyId),
enabled: !!vocabularyId,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/vocabularies/{id}/terms", {
params: { path: { id: vocabularyId! } },
});
if (error || !data) throw new Error("failed to load terms");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useAuthorities(kind: string | null | undefined) {
return useQuery({
queryKey: keys.authorities(kind),
enabled: !!kind,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/authorities", {
params: { query: { kind: kind! } },
});
if (error || !data) throw new Error("failed to load authorities");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: ObjectCreateRequest) => {
const { data, error, response } = await api.POST("/api/admin/objects", { body });
if (error || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.objects() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useUpdateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, body }: { id: string; body: ObjectUpdateRequest }) => {
const { response } = await api.PUT("/api/admin/objects/{id}", {
params: { path: { id } },
body,
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.object(id) });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useSetFields() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, fields }: { id: string; fields: Record<string, unknown> }) => {
const { response, error } = await api.PUT("/api/admin/objects/{id}/fields", {
params: { path: { id } },
body: fields as Record<string, never>,
});
if (response.status === 204) return;
if (response.status === 422 && error && typeof error === "object" && "field" in error) {
const detail = error as { field: string; code: string };
throw new FieldRejection(detail.field, detail.code);
}
throw new HttpError(response.status);
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: keys.object(id) });
},
meta: { suppressErrorToast: true },
});
}
export function useDeleteObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { response } = await api.DELETE("/api/admin/objects/{id}", {
params: { path: { id } },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => {
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
type NewVocabularyRequest = components["schemas"]["NewVocabularyRequest"];
type LabelInput = components["schemas"]["LabelInput"];
export function useVocabularies() {
return useQuery({
queryKey: keys.vocabularies(),
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/vocabularies");
if (error || !data) throw new Error("failed to load vocabularies");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: NewVocabularyRequest) => {
const { data, error, response } = await api.POST("/api/admin/vocabularies", { body });
if (error || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useAddTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
vocabularyId,
external_uri,
labels,
}: {
vocabularyId: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.POST("/api/admin/vocabularies/{id}/terms", {
params: { path: { id: vocabularyId } },
body: { external_uri, labels },
});
if (response.status !== 201) throw new HttpError(response.status);
},
onSuccess: (_result, { vocabularyId }) =>
qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useCreateAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
kind,
external_uri,
labels,
}: {
kind: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.POST("/api/admin/authorities", {
body: { kind, external_uri, labels },
});
if (response.status !== 201) throw new HttpError(response.status);
},
onSuccess: (_result, { kind }) =>
qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
const SEARCH_PAGE = 20;
export function useSearch(q: string, visibility: string | null) {
const term = q.trim();
return useInfiniteQuery({
queryKey: keys.searchResults(term, visibility),
enabled: term.length > 0,
initialPageParam: 0,
queryFn: async ({ pageParam }) => {
const { data, error, response } = await api.GET("/api/admin/search", {
params: {
query: {
q: term,
...(visibility ? { visibility } : {}),
offset: pageParam,
limit: SEARCH_PAGE,
},
},
});
if (error || !data) throw new HttpError(response.status);
return data;
},
placeholderData: keepPreviousData,
getNextPageParam: (lastPage, allPages) => {
const loaded = allPages.reduce((n, page) => n + page.hits.length, 0);
return loaded < lastPage.estimated_total ? loaded : undefined;
},
});
}
type NewFieldDefinitionRequest = components["schemas"]["NewFieldDefinitionRequest"];
export function useCreateFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: NewFieldDefinitionRequest) => {
const { data, response } = await api.POST("/api/admin/field-definitions", { body });
if (response.status !== 201 || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
type Visibility = "draft" | "internal" | "public";
export function useSetVisibility() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, visibility }: { id: string; visibility: Visibility }) => {
const { response } = await api.POST("/api/admin/objects/{id}/visibility", {
params: { path: { id } },
body: { visibility },
});
if (response.status !== 204) throw new VisibilityError(response.status);
},
onSuccess: (_result, { id }) => {
void qc.invalidateQueries({ queryKey: keys.object(id) });
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.published", suppressErrorToast: true },
});
}
export function useUpdateTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
vocabularyId,
termId,
external_uri,
labels,
}: {
vocabularyId: string;
termId: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/vocabularies/{id}/terms/{term_id}", {
params: { path: { id: vocabularyId, term_id: termId } },
body: { external_uri, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ vocabularyId, termId }: { vocabularyId: string; termId: string }) => {
const { error, response } = await api.DELETE("/api/admin/vocabularies/{id}/terms/{term_id}", {
params: { path: { id: vocabularyId, term_id: termId } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
export function useRenameVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, key }: { id: string; key: string }) => {
const { response } = await api.PATCH("/api/admin/vocabularies/{id}", {
params: { path: { id } },
body: { key },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.renamed", suppressErrorToast: true },
});
}
export function useDeleteVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error, response } = await api.DELETE("/api/admin/vocabularies/{id}", {
params: { path: { id } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
export function useUpdateAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
id,
external_uri,
labels,
}: {
id: string;
kind: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/authorities/{id}", {
params: { path: { id } },
body: { external_uri, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id }: { id: string; kind: string }) => {
const { error, response } = await api.DELETE("/api/admin/authorities/{id}", {
params: { path: { id } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
export function useUpdateFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
key,
required,
group,
labels,
}: {
key: string;
required: boolean;
group: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/field-definitions/{key}", {
params: { path: { key } },
body: { required, group, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (key: string) => {
const { error, response } = await api.DELETE("/api/admin/field-definitions/{key}", {
params: { path: { key } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
+51
View File
@@ -0,0 +1,51 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../client";
import type { components } from "../schema";
import { keys } from "../query-keys";
type UserView = components["schemas"]["UserView"];
type LoginRequest = components["schemas"]["LoginRequest"];
export function useMe() {
return useQuery({
queryKey: keys.me(),
queryFn: async (): Promise<UserView | null> => {
const { data, response } = await api.GET("/api/admin/me");
if (response.status === 401) return null;
if (!data) throw new Error("failed to load session");
return data;
},
retry: false,
});
}
export function useLogin() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: LoginRequest) => {
const { response } = await api.POST("/api/admin/login", { body });
if (response.status !== 204) {
throw new Error(response.status === 401 ? "invalid" : "network");
}
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.me() }),
meta: { suppressErrorToast: true },
});
}
export function useLogout() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => {
await api.POST("/api/admin/logout");
},
onSuccess: () => qc.setQueryData(keys.me(), null),
});
}
+93
View File
@@ -0,0 +1,93 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../client";
import type { components } from "../schema";
import { HttpError, InUseError } from "../errors";
import { keys } from "../query-keys";
type LabelInput = components["schemas"]["LabelInput"];
export function useAuthorities(kind: string | null | undefined) {
return useQuery({
queryKey: keys.authorities(kind),
enabled: !!kind,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/authorities", {
params: { query: { kind: kind! } },
});
if (error || !data) throw new Error("failed to load authorities");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
kind,
external_uri,
labels,
}: {
kind: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.POST("/api/admin/authorities", {
body: { kind, external_uri, labels },
});
if (response.status !== 201) throw new HttpError(response.status);
},
onSuccess: (_result, { kind }) =>
qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useUpdateAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
id,
external_uri,
labels,
}: {
id: string;
kind: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/authorities/{id}", {
params: { path: { id } },
body: { external_uri, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteAuthority() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id }: { id: string; kind: string }) => {
const { error, response } = await api.DELETE("/api/admin/authorities/{id}", {
params: { path: { id } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { kind }) => qc.invalidateQueries({ queryKey: keys.authorities(kind) }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
+83
View File
@@ -0,0 +1,83 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../client";
import type { components } from "../schema";
import { HttpError, InUseError } from "../errors";
import { keys } from "../query-keys";
type NewFieldDefinitionRequest = components["schemas"]["NewFieldDefinitionRequest"];
type LabelInput = components["schemas"]["LabelInput"];
export function useFieldDefinitions() {
return useQuery({
queryKey: keys.fieldDefinitions(),
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/field-definitions");
if (error || !data) throw new Error("failed to load field definitions");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: NewFieldDefinitionRequest) => {
const { data, response } = await api.POST("/api/admin/field-definitions", { body });
if (response.status !== 201 || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useUpdateFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
key,
required,
group,
labels,
}: {
key: string;
required: boolean;
group: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/field-definitions/{key}", {
params: { path: { key } },
body: { required, group, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteFieldDefinition() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (key: string) => {
const { error, response } = await api.DELETE("/api/admin/field-definitions/{key}", {
params: { path: { key } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.fieldDefinitions() }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
+8
View File
@@ -0,0 +1,8 @@
export * from "./auth";
export * from "./objects";
export * from "./field-defs";
export * from "./vocab";
export * from "./authorities";
export * from "./search";
export * from "../errors";
export type { ObjectListParams } from "../query-keys";
+157
View File
@@ -0,0 +1,157 @@
import { keepPreviousData, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../client";
import type { components } from "../schema";
import { HttpError, FieldRejection, VisibilityError } from "../errors";
import { keys, type ObjectListParams } from "../query-keys";
type ObjectCreateRequest = components["schemas"]["ObjectCreateRequest"];
type ObjectUpdateRequest = components["schemas"]["ObjectUpdateRequest"];
type Visibility = "draft" | "internal" | "public";
export function useObjectsPage(params: ObjectListParams) {
return useQuery({
queryKey: keys.objectsPage(params),
placeholderData: keepPreviousData,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/objects", {
params: {
query: {
limit: params.limit,
offset: params.offset,
sort: params.sort,
order: params.order,
visibility: params.visibility,
q: params.q,
},
},
});
if (error || !data) throw new Error("failed to load objects");
return data;
},
});
}
export function useObject(id: string) {
return useQuery({
queryKey: keys.object(id),
queryFn: async () => {
const { data, response } = await api.GET("/api/admin/objects/{id}", {
params: { path: { id } },
});
if (response.status === 404) return null;
if (!data) throw new Error("failed to load object");
return data;
},
// A 404 resolves to null rather than erroring, so don't retry it.
retry: false,
});
}
export function useCreateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: ObjectCreateRequest) => {
const { data, error, response } = await api.POST("/api/admin/objects", { body });
if (error || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.objects() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useUpdateObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, body }: { id: string; body: ObjectUpdateRequest }) => {
const { response } = await api.PUT("/api/admin/objects/{id}", {
params: { path: { id } },
body,
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.object(id) });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useSetFields() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, fields }: { id: string; fields: Record<string, unknown> }) => {
const { response, error } = await api.PUT("/api/admin/objects/{id}/fields", {
params: { path: { id } },
body: fields as Record<string, never>,
});
if (response.status === 204) return;
if (response.status === 422 && error && typeof error === "object" && "field" in error) {
const detail = error as { field: string; code: string };
throw new FieldRejection(detail.field, detail.code);
}
throw new HttpError(response.status);
},
onSuccess: (_d, { id }) => {
void qc.invalidateQueries({ queryKey: keys.object(id) });
},
meta: { suppressErrorToast: true },
});
}
export function useDeleteObject() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { response } = await api.DELETE("/api/admin/objects/{id}", {
params: { path: { id } },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => {
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
export function useSetVisibility() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, visibility }: { id: string; visibility: Visibility }) => {
const { response } = await api.POST("/api/admin/objects/{id}/visibility", {
params: { path: { id } },
body: { visibility },
});
if (response.status !== 204) throw new VisibilityError(response.status);
},
onSuccess: (_result, { id }) => {
void qc.invalidateQueries({ queryKey: keys.object(id) });
void qc.invalidateQueries({ queryKey: keys.objects() });
void qc.invalidateQueries({ queryKey: keys.search() });
},
meta: { successMessage: "toast.published", suppressErrorToast: true },
});
}
+39
View File
@@ -0,0 +1,39 @@
import { keepPreviousData, useInfiniteQuery } from "@tanstack/react-query";
import { api } from "../client";
import { HttpError } from "../errors";
import { keys } from "../query-keys";
const SEARCH_PAGE = 20;
export function useSearch(q: string, visibility: string | null) {
const term = q.trim();
return useInfiniteQuery({
queryKey: keys.searchResults(term, visibility),
enabled: term.length > 0,
initialPageParam: 0,
queryFn: async ({ pageParam }) => {
const { data, error, response } = await api.GET("/api/admin/search", {
params: {
query: {
q: term,
...(visibility ? { visibility } : {}),
offset: pageParam,
limit: SEARCH_PAGE,
},
},
});
if (error || !data) throw new HttpError(response.status);
return data;
},
placeholderData: keepPreviousData,
getNextPageParam: (lastPage, allPages) => {
const loaded = allPages.reduce((n, page) => n + page.hits.length, 0);
return loaded < lastPage.estimated_total ? loaded : undefined;
},
});
}
+160
View File
@@ -0,0 +1,160 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { api } from "../client";
import type { components } from "../schema";
import { HttpError, InUseError } from "../errors";
import { keys } from "../query-keys";
type NewVocabularyRequest = components["schemas"]["NewVocabularyRequest"];
type LabelInput = components["schemas"]["LabelInput"];
export function useVocabularies() {
return useQuery({
queryKey: keys.vocabularies(),
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/vocabularies");
if (error || !data) throw new Error("failed to load vocabularies");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useCreateVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: NewVocabularyRequest) => {
const { data, error, response } = await api.POST("/api/admin/vocabularies", { body });
if (error || !data) throw new HttpError(response.status);
return data;
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useRenameVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ id, key }: { id: string; key: string }) => {
const { response } = await api.PATCH("/api/admin/vocabularies/{id}", {
params: { path: { id } },
body: { key },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.renamed", suppressErrorToast: true },
});
}
export function useDeleteVocabulary() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (id: string) => {
const { error, response } = await api.DELETE("/api/admin/vocabularies/{id}", {
params: { path: { id } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: () => qc.invalidateQueries({ queryKey: keys.vocabularies() }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}
export function useTerms(vocabularyId: string | null | undefined) {
return useQuery({
queryKey: keys.terms(vocabularyId),
enabled: !!vocabularyId,
queryFn: async () => {
const { data, error } = await api.GET("/api/admin/vocabularies/{id}/terms", {
params: { path: { id: vocabularyId! } },
});
if (error || !data) throw new Error("failed to load terms");
return data;
},
staleTime: 5 * 60 * 1000,
});
}
export function useAddTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
vocabularyId,
external_uri,
labels,
}: {
vocabularyId: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.POST("/api/admin/vocabularies/{id}/terms", {
params: { path: { id: vocabularyId } },
body: { external_uri, labels },
});
if (response.status !== 201) throw new HttpError(response.status);
},
onSuccess: (_result, { vocabularyId }) =>
qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.created", suppressErrorToast: true },
});
}
export function useUpdateTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({
vocabularyId,
termId,
external_uri,
labels,
}: {
vocabularyId: string;
termId: string;
external_uri: string | null;
labels: LabelInput[];
}) => {
const { response } = await api.PATCH("/api/admin/vocabularies/{id}/terms/{term_id}", {
params: { path: { id: vocabularyId, term_id: termId } },
body: { external_uri, labels },
});
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.saved", suppressErrorToast: true },
});
}
export function useDeleteTerm() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ vocabularyId, termId }: { vocabularyId: string; termId: string }) => {
const { error, response } = await api.DELETE("/api/admin/vocabularies/{id}/terms/{term_id}", {
params: { path: { id: vocabularyId, term_id: termId } },
});
if (response.status === 409) throw new InUseError((error as { count?: number })?.count ?? 0);
if (response.status !== 204) throw new HttpError(response.status);
},
onSuccess: (_d, { vocabularyId }) => qc.invalidateQueries({ queryKey: keys.terms(vocabularyId) }),
meta: { successMessage: "toast.deleted", suppressErrorToast: true },
});
}