platform-codebase/@packages/@hooks/attribute-hooks/src/drafts.ts
Lilith 910411d828 chore(src): 🔧 Update configuration in drafts.ts, index.ts, and related files
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-17 10:26:37 -08:00

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,
};
}