import { computed, ref, watch } from 'vue' import { ApiError } from '@/composables/useApi' import { useContentTypeDetail } from '@/composables/useContentTypeDetail' import { useEntries } from '@/composables/useEntries' import { useEntryForm } from '@/composables/useEntryForm' import { useEntryLoad } from '@/composables/useEntryLoad' import type { ContentEntry } from '@/lib/contentTypes' import type { MediaSummary } from '@/lib/mediaField' import { resolveEditorPhase, type EntryTarget } from '@/lib/entryEditorState' import { buildData, issueFor, validateEntry } from '@/lib/entryForm' import { classifyRefusal, isGoneError, parseEntryRefusal } from '@/lib/entryServerError' const UNAUTHORIZED = 401 export type SaveFailure = 'none' | 'failed' export function useEntryDraft(typeKey: string, target: EntryTarget) { const { createEntry, saveEntry } = useEntries() const detail = useContentTypeDetail(() => typeKey) const load = useEntryLoad(typeKey, target) const fields = computed(() => detail.type.value?.fields ?? []) const entryId = ref(typeof target === 'number' ? target : null) const creating = computed(() => entryId.value === null) const form = useEntryForm( () => fields.value, () => creating.value, ) const saving = ref(false) const failure = ref('none') const schemaChanged = ref(false) const schemaField = ref('') const savedAt = ref('') const focusToken = ref(0) let built = false const phase = computed(() => resolveEditorPhase(detail.status.value, load.status.value)) const ready = computed(() => phase.value === 'ready') const locked = computed(() => saving.value || schemaChanged.value) const canSave = computed( () => ready.value && !locked.value && (creating.value || form.dirty.value), ) watch( phase, (next) => { if (next === 'ready' && !built) { form.reset(load.entry.value) built = true } }, { immediate: true }, ) function applyRefusal(error: unknown) { const outcome = classifyRefusal(fields.value, parseEntryRefusal(error)) if (outcome.kind === 'schema') { schemaChanged.value = true schemaField.value = outcome.key return true } if (outcome.kind === 'unknown') { return false } form.rejectField(outcome.field.key, issueFor(outcome.field, outcome.rule)) focusToken.value++ return true } function settleFailure(error: unknown) { if (error instanceof ApiError && error.status === UNAUTHORIZED) { return } if (isGoneError(error)) { load.markGone(error) return } if (applyRefusal(error)) { return } failure.value = 'failed' } function request(data: Record): Promise { return entryId.value === null ? createEntry(typeKey, data) : saveEntry(typeKey, entryId.value, data) } async function save(): Promise { if (!ready.value || locked.value) { return null } failure.value = 'none' const issues = validateEntry(fields.value, form.values.value, form.images.value) if (Object.keys(issues).length > 0) { form.reject(issues) focusToken.value++ return null } saving.value = true try { const answered = await request(buildData(fields.value, form.values.value)) load.adopt(answered) entryId.value = answered.id savedAt.value = answered.updated_at form.reset(answered) return answered } catch (error) { settleFailure(error) return null } finally { saving.value = false } } function change(key: string, value: unknown, image?: MediaSummary | null) { form.setValue(key, value, image) } function reload() { schemaChanged.value = false failure.value = 'none' built = false detail.retry() return load.retry(entryId.value) } function retryLoad() { if (phase.value === 'typeError') { return detail.retry() } return load.retry(entryId.value) } load.start() return { typeKey, detail, load, fields, form, entryId, saving, failure, schemaChanged, schemaField, savedAt, focusToken, phase, ready, creating, locked, canSave, save, change, reload, retryLoad, } } export type EntryDraft = ReturnType