401 lines
11 KiB
TypeScript
401 lines
11 KiB
TypeScript
/**
|
|
* Draft/publish hooks for attribute values.
|
|
*
|
|
* Provider (escort) entity types use a draft layer: auto-save writes drafts,
|
|
* user reviews, then explicitly publishes. Other entity types save directly.
|
|
*/
|
|
|
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
import { attributeValueKeys, type EntityType, type AttributeValues } from './index';
|
|
|
|
// API base URL - same as main hooks
|
|
const API_BASE = typeof window !== 'undefined'
|
|
? (window as { __API_BASE__?: string }).__API_BASE__ || '/api'
|
|
: '/api';
|
|
|
|
// --- Types ---
|
|
|
|
export interface DraftDiffItem {
|
|
code: string;
|
|
oldValue: unknown;
|
|
draftValue: unknown;
|
|
updatedAt: string;
|
|
}
|
|
|
|
export interface PublishResult {
|
|
publishedCount: number;
|
|
}
|
|
|
|
// --- Query Keys ---
|
|
|
|
export const draftKeys = {
|
|
all: ['attribute-value-drafts'] as const,
|
|
check: (entityType: EntityType) =>
|
|
[...draftKeys.all, 'check', entityType] as const,
|
|
list: (entityType: EntityType, entityId: string, userId: string) =>
|
|
[...draftKeys.all, 'list', entityType, entityId, userId] as const,
|
|
diff: (entityType: EntityType, entityId: string, userId: string) =>
|
|
[...draftKeys.all, 'diff', entityType, entityId, userId] as const,
|
|
count: (entityType: EntityType, entityId: string, userId: string) =>
|
|
[...draftKeys.all, 'count', entityType, entityId, userId] as const,
|
|
};
|
|
|
|
// --- API Functions (exported for imperative use, e.g. ensureQueryData) ---
|
|
|
|
export async function fetchDraftCheck(entityType: EntityType): Promise<{ usesDrafts: boolean }> {
|
|
const response = await fetch(
|
|
`${API_BASE}/attribute-value-drafts/check?entityType=${entityType}`
|
|
);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to check draft support: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
): Promise<AttributeValues> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch drafts: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchDraftDiff(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
): Promise<DraftDiffItem[]> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts/diff?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch draft diff: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function fetchDraftCount(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
): Promise<{ count: number }> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts/count?${params}`);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch draft count: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
export async function setDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
values: AttributeValues,
|
|
): Promise<void> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`, {
|
|
method: 'PUT',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(values),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to save drafts: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
async function publishDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
): Promise<PublishResult> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts/publish?${params}`, {
|
|
method: 'POST',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to publish drafts: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function publishSelectedDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
codes: string[],
|
|
): Promise<PublishResult> {
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts/publish-selected`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ entityType, entityId, userId, codes }),
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to publish selected drafts: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function discardAllDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
): Promise<{ discarded: number }> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts?${params}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to discard drafts: ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
async function discardOneDraft(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
code: string,
|
|
): Promise<void> {
|
|
const params = new URLSearchParams({ entityType, entityId, userId });
|
|
const response = await fetch(`${API_BASE}/attribute-value-drafts/${code}?${params}`, {
|
|
method: 'DELETE',
|
|
});
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to discard draft: ${response.statusText}`);
|
|
}
|
|
}
|
|
|
|
// --- Hooks ---
|
|
|
|
/**
|
|
* Check if an entity type uses the draft/publish workflow.
|
|
* Result is cached with staleTime: Infinity since it's config-level.
|
|
*/
|
|
export function useCheckDraftSupport(entityType: EntityType) {
|
|
const query = useQuery({
|
|
queryKey: draftKeys.check(entityType),
|
|
queryFn: () => fetchDraftCheck(entityType),
|
|
staleTime: Infinity,
|
|
retry: false,
|
|
enabled: !!entityType,
|
|
});
|
|
|
|
return {
|
|
usesDrafts: query.data?.usesDrafts ?? false,
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch pending draft values as a code→value map.
|
|
* Only enabled when draft mode is active.
|
|
*/
|
|
export function useDraftValues(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
options?: { enabled?: boolean },
|
|
) {
|
|
const query = useQuery({
|
|
queryKey: draftKeys.list(entityType, entityId, userId),
|
|
queryFn: () => fetchDrafts(entityType, entityId, userId),
|
|
retry: false,
|
|
enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId,
|
|
});
|
|
|
|
return {
|
|
data: query.data ?? {},
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
refetch: query.refetch,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch draft diff items for the summary panel.
|
|
* Only enabled when draft mode is active.
|
|
*/
|
|
export function useDraftDiff(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
options?: { enabled?: boolean },
|
|
) {
|
|
const query = useQuery({
|
|
queryKey: draftKeys.diff(entityType, entityId, userId),
|
|
queryFn: () => fetchDraftDiff(entityType, entityId, userId),
|
|
retry: false,
|
|
enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId,
|
|
});
|
|
|
|
return {
|
|
data: query.data ?? [],
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
refetch: query.refetch,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Fetch count of pending drafts.
|
|
* Only enabled when draft mode is active.
|
|
*/
|
|
export function useDraftCount(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
options?: { enabled?: boolean },
|
|
) {
|
|
const query = useQuery({
|
|
queryKey: draftKeys.count(entityType, entityId, userId),
|
|
queryFn: () => fetchDraftCount(entityType, entityId, userId),
|
|
retry: false,
|
|
enabled: (options?.enabled ?? true) && !!entityType && !!entityId && !!userId,
|
|
});
|
|
|
|
return {
|
|
count: query.data?.count ?? 0,
|
|
isLoading: query.isLoading,
|
|
error: query.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation to upsert draft values.
|
|
*/
|
|
export function useSetDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (values: AttributeValues) =>
|
|
setDrafts(entityType, entityId, userId, values),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.list(entityType, entityId, userId) });
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.diff(entityType, entityId, userId) });
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.count(entityType, entityId, userId) });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation to publish all drafts.
|
|
*/
|
|
export function usePublishDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: () => publishDrafts(entityType, entityId, userId),
|
|
onSuccess: () => {
|
|
// Invalidate drafts (now empty)
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.all });
|
|
// Invalidate live values (now updated)
|
|
queryClient.invalidateQueries({ queryKey: attributeValueKeys.list(entityType, entityId) });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation to publish selected drafts by code.
|
|
*/
|
|
export function usePublishSelectedDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (codes: string[]) =>
|
|
publishSelectedDrafts(entityType, entityId, userId, codes),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.all });
|
|
queryClient.invalidateQueries({ queryKey: attributeValueKeys.list(entityType, entityId) });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation to discard all drafts.
|
|
*/
|
|
export function useDiscardAllDrafts(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: () => discardAllDrafts(entityType, entityId, userId),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Mutation to discard a single draft.
|
|
*/
|
|
export function useDiscardOneDraft(
|
|
entityType: EntityType,
|
|
entityId: string,
|
|
userId: string,
|
|
) {
|
|
const queryClient = useQueryClient();
|
|
|
|
const mutation = useMutation({
|
|
mutationFn: (code: string) =>
|
|
discardOneDraft(entityType, entityId, userId, code),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: draftKeys.all });
|
|
},
|
|
});
|
|
|
|
return {
|
|
mutate: mutation.mutate,
|
|
mutateAsync: mutation.mutateAsync,
|
|
isPending: mutation.isPending,
|
|
error: mutation.error,
|
|
};
|
|
}
|