Files
RenewCMS/web/src/composables/useEntryDraft.ts
T

164 lines
4.1 KiB
TypeScript

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<number | null>(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<SaveFailure>('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<string, unknown>): Promise<ContentEntry> {
return entryId.value === null
? createEntry(typeKey, data)
: saveEntry(typeKey, entryId.value, data)
}
async function save(): Promise<ContentEntry | null> {
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<typeof useEntryDraft>