164 lines
4.1 KiB
TypeScript
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>
|