feat: entry editor at /content/:key/new and /content/:key/:id
This commit is contained in:
1 parent
91d916fb74
commit
fba20f83c5
94 files changed
+7116
-423
No files matched your search
@@ -0,0 +1,162 @@
|
||||
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 { 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)
|
||||
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) {
|
||||
form.setValue(key, value)
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in new issue
Block a user