refac: split useSchemaBuilder & SchemaDeleteControls
This commit is contained in:
1 parent
5404291af2
commit
b4cbcb0395
4 files changed
+354
-240
No files matched your search
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { t } from '@/i18n'
|
||||
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
import { computed, nextTick, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useMediaQuery } from '@vueuse/core'
|
||||
import ApiPreview from '@/components/ApiPreview.vue'
|
||||
@@ -9,13 +9,12 @@ import ApiPreviewSheet from '@/components/ApiPreviewSheet.vue'
|
||||
import BuilderActions from '@/components/BuilderActions.vue'
|
||||
import BuilderBadge from '@/components/BuilderBadge.vue'
|
||||
import BuilderInspector from '@/components/BuilderInspector.vue'
|
||||
import BuilderMoreMenu from '@/components/BuilderMoreMenu.vue'
|
||||
import BuilderTypeBar from '@/components/BuilderTypeBar.vue'
|
||||
import DeleteTypeDialog from '@/components/DeleteTypeDialog.vue'
|
||||
import FieldList from '@/components/FieldList.vue'
|
||||
import FieldSheet from '@/components/FieldSheet.vue'
|
||||
import RetryAlert from '@/components/RetryAlert.vue'
|
||||
import SchemaForbidden from '@/components/SchemaForbidden.vue'
|
||||
import SchemaDeleteControls from '@/components/SchemaDeleteControls.vue'
|
||||
import SchemaRefusalDialog from '@/components/SchemaRefusalDialog.vue'
|
||||
import SchemaReviewDialog from '@/components/SchemaReviewDialog.vue'
|
||||
import { useSchemaBuilder, type EditSource } from '@/composables/useSchemaBuilder'
|
||||
@@ -24,7 +23,6 @@ import { useUnsavedChangesGuard } from '@/composables/useUnsavedChangesGuard'
|
||||
import { HOME_PATH } from '@/lib/routePaths'
|
||||
import { sampleJson } from '@/lib/schemaBuilderSample'
|
||||
import { entriesNotice } from '@/lib/schemaEntries'
|
||||
import { TOPBAR_ACTION_ID } from '@/lib/uiSlots'
|
||||
import { formatClock } from '@/lib/updateFormat'
|
||||
|
||||
const DESKTOP_QUERY = '(min-width: 48rem)'
|
||||
@@ -36,7 +34,6 @@ const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
||||
const builder = useSchemaBuilder(props.edit)
|
||||
const { form } = builder
|
||||
const remove = props.edit ? useSchemaDelete(() => props.edit!) : undefined
|
||||
const moreMenu = ref<InstanceType<typeof BuilderMoreMenu> | null>(null)
|
||||
const moreDisabled = computed(() => builder.saving.value || Boolean(remove?.pending.value))
|
||||
const forbidden = computed(
|
||||
() => builder.failure.value === 'forbidden' || Boolean(remove?.forbidden.value),
|
||||
@@ -73,16 +70,6 @@ const selectedName = computed(() => builder.selected.value?.label.trim() || t('U
|
||||
const sheetTitle = computed(() => t('Field: {0}', [selectedName.value]))
|
||||
const reviewName = computed(() => builder.baseline.value?.name ?? '')
|
||||
|
||||
watch(
|
||||
() => remove?.stage.value,
|
||||
async (stage) => {
|
||||
if (stage === 'closed') {
|
||||
await nextTick()
|
||||
moreMenu.value?.focus()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function goHome() {
|
||||
router.push(HOME_PATH)
|
||||
}
|
||||
@@ -160,11 +147,11 @@ async function restoreRowFocus() {
|
||||
@save="builder.save"
|
||||
@discard="builder.discard"
|
||||
>
|
||||
<BuilderMoreMenu
|
||||
<SchemaDeleteControls
|
||||
v-if="remove"
|
||||
ref="moreMenu"
|
||||
:remove="remove"
|
||||
:disabled="moreDisabled"
|
||||
@delete="remove.open"
|
||||
:mobile="false"
|
||||
/>
|
||||
</BuilderActions>
|
||||
</template>
|
||||
@@ -311,9 +298,7 @@ async function restoreRowFocus() {
|
||||
</div>
|
||||
|
||||
<template v-if="!isDesktop">
|
||||
<Teleport v-if="remove" :to="`#${TOPBAR_ACTION_ID}`" defer>
|
||||
<BuilderMoreMenu ref="moreMenu" :disabled="moreDisabled" @delete="remove.open" />
|
||||
</Teleport>
|
||||
<SchemaDeleteControls v-if="remove" :remove="remove" :disabled="moreDisabled" mobile />
|
||||
<ApiPreviewSheet v-model:open="apiSheetOpen" :key-value="form.key" :json="json" />
|
||||
<FieldSheet
|
||||
v-model:open="fieldSheetOpen"
|
||||
@@ -346,18 +331,6 @@ async function restoreRowFocus() {
|
||||
@apply="builder.apply"
|
||||
@close="builder.closeReview"
|
||||
/>
|
||||
<DeleteTypeDialog
|
||||
v-if="remove && remove.stage.value !== 'closed'"
|
||||
:stage="remove.stage.value"
|
||||
:name="remove.type.value.name"
|
||||
:key-value="remove.type.value.key"
|
||||
:total="remove.total.value"
|
||||
:pending="remove.pending.value"
|
||||
:offline="remove.offline.value"
|
||||
@confirm="remove.confirm"
|
||||
@close="remove.close"
|
||||
@view-entries="remove.viewEntries"
|
||||
/>
|
||||
<SchemaRefusalDialog
|
||||
v-if="builder.refusal.value === 'dialog'"
|
||||
:causes="refusalCauses"
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref, watch } from 'vue'
|
||||
import BuilderMoreMenu from '@/components/BuilderMoreMenu.vue'
|
||||
import DeleteTypeDialog from '@/components/DeleteTypeDialog.vue'
|
||||
import type { useSchemaDelete } from '@/composables/useSchemaDelete'
|
||||
import { TOPBAR_ACTION_ID } from '@/lib/uiSlots'
|
||||
|
||||
const props = defineProps<{
|
||||
remove: ReturnType<typeof useSchemaDelete>
|
||||
disabled: boolean
|
||||
mobile: boolean
|
||||
}>()
|
||||
|
||||
const moreMenu = ref<InstanceType<typeof BuilderMoreMenu> | null>(null)
|
||||
|
||||
watch(
|
||||
() => props.remove.stage.value,
|
||||
async (stage) => {
|
||||
if (stage === 'closed') {
|
||||
await nextTick()
|
||||
moreMenu.value?.focus()
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Teleport v-if="mobile" :to="`#${TOPBAR_ACTION_ID}`" defer>
|
||||
<BuilderMoreMenu ref="moreMenu" :disabled="disabled" @delete="remove.open" />
|
||||
</Teleport>
|
||||
<BuilderMoreMenu v-else ref="moreMenu" :disabled="disabled" @delete="remove.open" />
|
||||
<DeleteTypeDialog
|
||||
v-if="remove.stage.value !== 'closed'"
|
||||
:stage="remove.stage.value"
|
||||
:name="remove.type.value.name"
|
||||
:key-value="remove.type.value.key"
|
||||
:total="remove.total.value"
|
||||
:pending="remove.pending.value"
|
||||
:offline="remove.offline.value"
|
||||
@confirm="remove.confirm"
|
||||
@close="remove.close"
|
||||
@view-entries="remove.viewEntries"
|
||||
/>
|
||||
</template>
|
||||
@@ -1,12 +1,8 @@
|
||||
import { computed, ref } from 'vue'
|
||||
import { useOnline } from '@vueuse/core'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t } from '@/i18n'
|
||||
import { useLocalizedToast } from '@/i18n/presentation'
|
||||
import { ApiError } from '@/composables/useApi'
|
||||
import { useContentTypes, type ContentType } from '@/composables/useContentTypes'
|
||||
import type { ContentType } from '@/composables/useContentTypes'
|
||||
import { useSchemaSave } from '@/composables/useSchemaSave'
|
||||
import { moveItem } from '@/lib/arrayMove'
|
||||
import { SCHEMA_PATH } from '@/lib/routePaths'
|
||||
import { deriveFieldKey, deriveTypeKey } from '@/lib/schemaBuilderKeys'
|
||||
import {
|
||||
BUILDER_LIMITS,
|
||||
@@ -24,10 +20,8 @@ import {
|
||||
diffFields,
|
||||
flaggedChanges,
|
||||
isFieldChanged,
|
||||
type FieldChange,
|
||||
} from '@/lib/schemaBuilderDiff'
|
||||
import { baselinePayload, formFromType } from '@/lib/schemaBuilderLoad'
|
||||
import { buildPayload, buildUpdatePayload } from '@/lib/schemaBuilderPayload'
|
||||
import { reservedKeys, restoreIndex, type RemovedField } from '@/lib/schemaBuilderRemoval'
|
||||
import { FIELD_ERROR_TOUCH, fieldTouchKey, typeTouchKey } from '@/lib/schemaBuilderTouch'
|
||||
import type { FieldTouch, TypeTouch } from '@/lib/schemaBuilderTouch'
|
||||
@@ -38,17 +32,8 @@ import {
|
||||
type FieldErrors,
|
||||
type FormErrors,
|
||||
} from '@/lib/schemaBuilderValidation'
|
||||
import { useContentTypesStore } from '@/stores/contentTypes'
|
||||
|
||||
const KEY_TAKEN_BODY = 'content_type_key_taken'
|
||||
const INVALID_KEY_BODY = 'invalid_content_type_key'
|
||||
const INVALID_FIELD_BODY = 'invalid_field'
|
||||
const INCOMPATIBLE_BODY = 'schema_incompatible'
|
||||
const TOAST_ID = 'content-type-saved'
|
||||
|
||||
export type SaveFailure = 'none' | 'failed' | 'invalid' | 'forbidden'
|
||||
export type SaveState = 'editing' | 'saving' | 'saved' | 'failed'
|
||||
export type RefusalState = 'none' | 'dialog' | 'banner'
|
||||
export type { SaveFailure, SaveState, RefusalState } from '@/composables/useSchemaSave'
|
||||
|
||||
export interface EditSource {
|
||||
type: ContentType
|
||||
@@ -73,9 +58,6 @@ function signature(form: BuilderForm) {
|
||||
}
|
||||
|
||||
export function useSchemaBuilder(source?: EditSource) {
|
||||
const router = useRouter()
|
||||
const store = useContentTypesStore()
|
||||
const toast = useLocalizedToast()
|
||||
const online = useOnline()
|
||||
let nextUid = 1
|
||||
const newUid = () => nextUid++
|
||||
@@ -85,23 +67,14 @@ export function useSchemaBuilder(source?: EditSource) {
|
||||
const entryTotal = source?.total ?? 0
|
||||
const form = ref<BuilderForm>(source ? formFromType(source.type, newUid) : blankForm(newUid()))
|
||||
const removed = ref<RemovedField[]>([])
|
||||
const reviewOpen = ref(false)
|
||||
const refusal = ref<RefusalState>('none')
|
||||
const refusedUids = ref<number[]>([])
|
||||
const refusedChanges = ref<FieldChange[]>([])
|
||||
const savedAt = ref('')
|
||||
const nameServerError = ref('')
|
||||
let initialSignature = signature(form.value)
|
||||
const selectedUid = ref<number | null>(form.value.fields[0]!.uid)
|
||||
const touched = ref(new Set<string>())
|
||||
const submitted = ref(false)
|
||||
const saveState = ref<SaveState>('editing')
|
||||
const failure = ref<SaveFailure>('none')
|
||||
const keyServerError = ref('')
|
||||
const keyFocusToken = ref(0)
|
||||
const labelFocusToken = ref(0)
|
||||
|
||||
const saving = computed(() => saveState.value === 'saving')
|
||||
const dirty = computed(
|
||||
() => saveState.value !== 'saved' && signature(form.value) !== initialSignature,
|
||||
)
|
||||
@@ -111,6 +84,38 @@ export function useSchemaBuilder(source?: EditSource) {
|
||||
const savedSignatures = computed(() => baselineSignatures(baselineFields.value))
|
||||
const changes = computed(() => diffFields(baselineFields.value, form.value.fields))
|
||||
const needsReview = computed(() => editing && entryTotal > 0 && changes.value.length > 0)
|
||||
const {
|
||||
saveState,
|
||||
saving,
|
||||
failure,
|
||||
reviewOpen,
|
||||
refusal,
|
||||
refusedUids,
|
||||
refusedChanges,
|
||||
savedAt,
|
||||
keyFocusToken,
|
||||
resetSaveOutcome,
|
||||
save,
|
||||
apply,
|
||||
reviewAgain,
|
||||
closeReview,
|
||||
keepEditing,
|
||||
} = useSchemaSave({
|
||||
editing,
|
||||
form,
|
||||
baseline,
|
||||
selectedUid,
|
||||
submitted,
|
||||
keyServerError,
|
||||
nameServerError,
|
||||
changes,
|
||||
needsReview,
|
||||
canSave: () => canSave.value,
|
||||
canReveal: () => canReveal.value,
|
||||
newUid,
|
||||
touch,
|
||||
resetEditState,
|
||||
})
|
||||
const flaggedUids = computed(
|
||||
() => new Set(flaggedChanges(changes.value).map((change) => change.uid)),
|
||||
)
|
||||
@@ -442,12 +447,8 @@ export function useSchemaBuilder(source?: EditSource) {
|
||||
submitted.value = false
|
||||
keyServerError.value = ''
|
||||
nameServerError.value = ''
|
||||
failure.value = 'none'
|
||||
removed.value = []
|
||||
reviewOpen.value = false
|
||||
refusal.value = 'none'
|
||||
refusedUids.value = []
|
||||
refusedChanges.value = []
|
||||
resetSaveOutcome()
|
||||
}
|
||||
|
||||
function discard() {
|
||||
@@ -456,178 +457,6 @@ export function useSchemaBuilder(source?: EditSource) {
|
||||
saveState.value = 'editing'
|
||||
}
|
||||
|
||||
function onKeyTaken() {
|
||||
form.value.keyMode = 'manual'
|
||||
keyServerError.value = t(
|
||||
'The key "{0}" is already used by another content type. Choose a different key.',
|
||||
[form.value.key],
|
||||
)
|
||||
keyFocusToken.value++
|
||||
}
|
||||
|
||||
function onInvalidKey() {
|
||||
form.value.keyMode = 'manual'
|
||||
keyServerError.value = t('Use 2 to 40 lowercase letters, digits or hyphens.')
|
||||
keyFocusToken.value++
|
||||
}
|
||||
|
||||
function onIncompatible() {
|
||||
const flagged = flaggedChanges(changes.value)
|
||||
refusedChanges.value = flagged
|
||||
refusedUids.value = flagged.flatMap((change) => (change.uid === null ? [] : [change.uid]))
|
||||
refusal.value = 'dialog'
|
||||
saveState.value = 'editing'
|
||||
}
|
||||
|
||||
function onInvalidName() {
|
||||
nameServerError.value = t('Give the content type a name.')
|
||||
touch('name')
|
||||
}
|
||||
|
||||
function onMissing() {
|
||||
store.drop(form.value.key)
|
||||
}
|
||||
|
||||
function onRefused(error: unknown) {
|
||||
reviewOpen.value = false
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
return
|
||||
}
|
||||
const body = error instanceof ApiError ? error.body : undefined
|
||||
if (
|
||||
editing &&
|
||||
error instanceof ApiError &&
|
||||
error.status === 409 &&
|
||||
body === INCOMPATIBLE_BODY
|
||||
) {
|
||||
onIncompatible()
|
||||
} else if (editing && error instanceof ApiError && error.status === 404) {
|
||||
onMissing()
|
||||
} else if (error instanceof ApiError && error.status === 409 && body === KEY_TAKEN_BODY) {
|
||||
onKeyTaken()
|
||||
} else if (error instanceof ApiError && error.status === 400 && body === INVALID_KEY_BODY) {
|
||||
onInvalidKey()
|
||||
} else if (editing && error instanceof ApiError && error.status === 400) {
|
||||
saveState.value = 'editing'
|
||||
onInvalidName()
|
||||
} else if (
|
||||
error instanceof ApiError &&
|
||||
error.status === 422 &&
|
||||
body === INVALID_FIELD_BODY
|
||||
) {
|
||||
failure.value = 'invalid'
|
||||
} else if (error instanceof ApiError && error.status === 403) {
|
||||
failure.value = 'forbidden'
|
||||
} else {
|
||||
failure.value = 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
function announceSaved(name: string, count: number) {
|
||||
toast.add(() => ({
|
||||
id: TOAST_ID,
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check',
|
||||
title: t('Content type saved'),
|
||||
description:
|
||||
count === 1
|
||||
? t('{0} now has 1 field.', [name])
|
||||
: t('{0} now has {1} fields.', [name, count]),
|
||||
}))
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!canSave.value && !canReveal.value) {
|
||||
return
|
||||
}
|
||||
submitted.value = true
|
||||
if (!canSave.value) {
|
||||
return
|
||||
}
|
||||
if (needsReview.value) {
|
||||
reviewOpen.value = true
|
||||
return
|
||||
}
|
||||
await apply()
|
||||
}
|
||||
|
||||
function reviewAgain() {
|
||||
if (needsReview.value) {
|
||||
reviewOpen.value = true
|
||||
return
|
||||
}
|
||||
refusal.value = 'none'
|
||||
}
|
||||
|
||||
function closeReview() {
|
||||
if (!saving.value) {
|
||||
reviewOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function keepEditing() {
|
||||
if (refusal.value === 'dialog') {
|
||||
refusal.value = 'banner'
|
||||
}
|
||||
}
|
||||
|
||||
function settleSaved(answered: ContentType) {
|
||||
store.replace(answered)
|
||||
baseline.value = answered
|
||||
form.value = formFromType(
|
||||
answered,
|
||||
newUid,
|
||||
form.value.fields.map((field) => field.uid),
|
||||
)
|
||||
const keep = selectedUid.value
|
||||
resetEditState()
|
||||
if (keep !== null && form.value.fields.some((field) => field.uid === keep)) {
|
||||
selectedUid.value = keep
|
||||
}
|
||||
savedAt.value = answered.updated_at
|
||||
saveState.value = 'saved'
|
||||
announceSaved(answered.name, answered.fields.length)
|
||||
}
|
||||
|
||||
async function apply() {
|
||||
if (saving.value) {
|
||||
return
|
||||
}
|
||||
failure.value = 'none'
|
||||
saveState.value = 'saving'
|
||||
if (!baseline.value) {
|
||||
await create()
|
||||
return
|
||||
}
|
||||
let answered: ContentType
|
||||
try {
|
||||
answered = await useContentTypes().updateContentType(
|
||||
baseline.value.key,
|
||||
buildUpdatePayload(form.value, baseline.value.icon),
|
||||
)
|
||||
} catch (error) {
|
||||
saveState.value = 'failed'
|
||||
onRefused(error)
|
||||
return
|
||||
}
|
||||
settleSaved(answered)
|
||||
}
|
||||
|
||||
async function create() {
|
||||
let created: ContentType
|
||||
try {
|
||||
created = await useContentTypes().createContentType(buildPayload(form.value))
|
||||
} catch (error) {
|
||||
saveState.value = 'failed'
|
||||
onRefused(error)
|
||||
return
|
||||
}
|
||||
saveState.value = 'saved'
|
||||
store.add(created)
|
||||
announceSaved(created.name, created.fields.length)
|
||||
await router.replace(SCHEMA_PATH).catch(() => undefined)
|
||||
}
|
||||
|
||||
return {
|
||||
form,
|
||||
selectedUid,
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
import { computed, ref, type Ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { t } from '@/i18n'
|
||||
import { useLocalizedToast } from '@/i18n/presentation'
|
||||
import { ApiError } from '@/composables/useApi'
|
||||
import { useContentTypes, type ContentType } from '@/composables/useContentTypes'
|
||||
import { SCHEMA_PATH } from '@/lib/routePaths'
|
||||
import type { BuilderForm } from '@/lib/schemaBuilderModel'
|
||||
import { flaggedChanges, type FieldChange } from '@/lib/schemaBuilderDiff'
|
||||
import { formFromType } from '@/lib/schemaBuilderLoad'
|
||||
import { buildPayload, buildUpdatePayload } from '@/lib/schemaBuilderPayload'
|
||||
import type { TypeTouch } from '@/lib/schemaBuilderTouch'
|
||||
import { useContentTypesStore } from '@/stores/contentTypes'
|
||||
|
||||
const KEY_TAKEN_BODY = 'content_type_key_taken'
|
||||
const INVALID_KEY_BODY = 'invalid_content_type_key'
|
||||
const INVALID_FIELD_BODY = 'invalid_field'
|
||||
const INCOMPATIBLE_BODY = 'schema_incompatible'
|
||||
const TOAST_ID = 'content-type-saved'
|
||||
|
||||
export type SaveFailure = 'none' | 'failed' | 'invalid' | 'forbidden'
|
||||
export type SaveState = 'editing' | 'saving' | 'saved' | 'failed'
|
||||
export type RefusalState = 'none' | 'dialog' | 'banner'
|
||||
|
||||
export interface SchemaSaveContext {
|
||||
editing: boolean
|
||||
form: Ref<BuilderForm>
|
||||
baseline: Ref<ContentType | null>
|
||||
selectedUid: Ref<number | null>
|
||||
submitted: Ref<boolean>
|
||||
keyServerError: Ref<string>
|
||||
nameServerError: Ref<string>
|
||||
changes: Readonly<Ref<FieldChange[]>>
|
||||
needsReview: Readonly<Ref<boolean>>
|
||||
canSave: () => boolean
|
||||
canReveal: () => boolean
|
||||
newUid: () => number
|
||||
touch: (name: TypeTouch) => void
|
||||
resetEditState: () => void
|
||||
}
|
||||
|
||||
export function useSchemaSave(context: SchemaSaveContext) {
|
||||
const router = useRouter()
|
||||
const store = useContentTypesStore()
|
||||
const toast = useLocalizedToast()
|
||||
const {
|
||||
editing,
|
||||
form,
|
||||
baseline,
|
||||
selectedUid,
|
||||
submitted,
|
||||
keyServerError,
|
||||
nameServerError,
|
||||
changes,
|
||||
needsReview,
|
||||
newUid,
|
||||
touch,
|
||||
} = context
|
||||
|
||||
const saveState = ref<SaveState>('editing')
|
||||
const failure = ref<SaveFailure>('none')
|
||||
const reviewOpen = ref(false)
|
||||
const refusal = ref<RefusalState>('none')
|
||||
const refusedUids = ref<number[]>([])
|
||||
const refusedChanges = ref<FieldChange[]>([])
|
||||
const savedAt = ref('')
|
||||
const keyFocusToken = ref(0)
|
||||
|
||||
const saving = computed(() => saveState.value === 'saving')
|
||||
|
||||
function resetSaveOutcome() {
|
||||
failure.value = 'none'
|
||||
reviewOpen.value = false
|
||||
refusal.value = 'none'
|
||||
refusedUids.value = []
|
||||
refusedChanges.value = []
|
||||
}
|
||||
|
||||
function onKeyTaken() {
|
||||
form.value.keyMode = 'manual'
|
||||
keyServerError.value = t(
|
||||
'The key "{0}" is already used by another content type. Choose a different key.',
|
||||
[form.value.key],
|
||||
)
|
||||
keyFocusToken.value++
|
||||
}
|
||||
|
||||
function onInvalidKey() {
|
||||
form.value.keyMode = 'manual'
|
||||
keyServerError.value = t('Use 2 to 40 lowercase letters, digits or hyphens.')
|
||||
keyFocusToken.value++
|
||||
}
|
||||
|
||||
function onIncompatible() {
|
||||
const flagged = flaggedChanges(changes.value)
|
||||
refusedChanges.value = flagged
|
||||
refusedUids.value = flagged.flatMap((change) => (change.uid === null ? [] : [change.uid]))
|
||||
refusal.value = 'dialog'
|
||||
saveState.value = 'editing'
|
||||
}
|
||||
|
||||
function onInvalidName() {
|
||||
nameServerError.value = t('Give the content type a name.')
|
||||
touch('name')
|
||||
}
|
||||
|
||||
function onMissing() {
|
||||
store.drop(form.value.key)
|
||||
}
|
||||
|
||||
function onRefused(error: unknown) {
|
||||
reviewOpen.value = false
|
||||
if (error instanceof ApiError && error.status === 401) {
|
||||
return
|
||||
}
|
||||
const body = error instanceof ApiError ? error.body : undefined
|
||||
if (
|
||||
editing &&
|
||||
error instanceof ApiError &&
|
||||
error.status === 409 &&
|
||||
body === INCOMPATIBLE_BODY
|
||||
) {
|
||||
onIncompatible()
|
||||
} else if (editing && error instanceof ApiError && error.status === 404) {
|
||||
onMissing()
|
||||
} else if (error instanceof ApiError && error.status === 409 && body === KEY_TAKEN_BODY) {
|
||||
onKeyTaken()
|
||||
} else if (error instanceof ApiError && error.status === 400 && body === INVALID_KEY_BODY) {
|
||||
onInvalidKey()
|
||||
} else if (editing && error instanceof ApiError && error.status === 400) {
|
||||
saveState.value = 'editing'
|
||||
onInvalidName()
|
||||
} else if (
|
||||
error instanceof ApiError &&
|
||||
error.status === 422 &&
|
||||
body === INVALID_FIELD_BODY
|
||||
) {
|
||||
failure.value = 'invalid'
|
||||
} else if (error instanceof ApiError && error.status === 403) {
|
||||
failure.value = 'forbidden'
|
||||
} else {
|
||||
failure.value = 'failed'
|
||||
}
|
||||
}
|
||||
|
||||
function announceSaved(name: string, count: number) {
|
||||
toast.add(() => ({
|
||||
id: TOAST_ID,
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check',
|
||||
title: t('Content type saved'),
|
||||
description:
|
||||
count === 1
|
||||
? t('{0} now has 1 field.', [name])
|
||||
: t('{0} now has {1} fields.', [name, count]),
|
||||
}))
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (!context.canSave() && !context.canReveal()) {
|
||||
return
|
||||
}
|
||||
submitted.value = true
|
||||
if (!context.canSave()) {
|
||||
return
|
||||
}
|
||||
if (needsReview.value) {
|
||||
reviewOpen.value = true
|
||||
return
|
||||
}
|
||||
await apply()
|
||||
}
|
||||
|
||||
function reviewAgain() {
|
||||
if (needsReview.value) {
|
||||
reviewOpen.value = true
|
||||
return
|
||||
}
|
||||
refusal.value = 'none'
|
||||
}
|
||||
|
||||
function closeReview() {
|
||||
if (!saving.value) {
|
||||
reviewOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function keepEditing() {
|
||||
if (refusal.value === 'dialog') {
|
||||
refusal.value = 'banner'
|
||||
}
|
||||
}
|
||||
|
||||
function settleSaved(answered: ContentType) {
|
||||
store.replace(answered)
|
||||
baseline.value = answered
|
||||
form.value = formFromType(
|
||||
answered,
|
||||
newUid,
|
||||
form.value.fields.map((field) => field.uid),
|
||||
)
|
||||
const keep = selectedUid.value
|
||||
context.resetEditState()
|
||||
if (keep !== null && form.value.fields.some((field) => field.uid === keep)) {
|
||||
selectedUid.value = keep
|
||||
}
|
||||
savedAt.value = answered.updated_at
|
||||
saveState.value = 'saved'
|
||||
announceSaved(answered.name, answered.fields.length)
|
||||
}
|
||||
|
||||
async function apply() {
|
||||
if (saving.value) {
|
||||
return
|
||||
}
|
||||
failure.value = 'none'
|
||||
saveState.value = 'saving'
|
||||
if (!baseline.value) {
|
||||
await create()
|
||||
return
|
||||
}
|
||||
let answered: ContentType
|
||||
try {
|
||||
answered = await useContentTypes().updateContentType(
|
||||
baseline.value.key,
|
||||
buildUpdatePayload(form.value, baseline.value.icon),
|
||||
)
|
||||
} catch (error) {
|
||||
saveState.value = 'failed'
|
||||
onRefused(error)
|
||||
return
|
||||
}
|
||||
settleSaved(answered)
|
||||
}
|
||||
|
||||
async function create() {
|
||||
let created: ContentType
|
||||
try {
|
||||
created = await useContentTypes().createContentType(buildPayload(form.value))
|
||||
} catch (error) {
|
||||
saveState.value = 'failed'
|
||||
onRefused(error)
|
||||
return
|
||||
}
|
||||
saveState.value = 'saved'
|
||||
store.add(created)
|
||||
announceSaved(created.name, created.fields.length)
|
||||
await router.replace(SCHEMA_PATH).catch(() => undefined)
|
||||
}
|
||||
|
||||
return {
|
||||
saveState,
|
||||
saving,
|
||||
failure,
|
||||
reviewOpen,
|
||||
refusal,
|
||||
refusedUids,
|
||||
refusedChanges,
|
||||
savedAt,
|
||||
keyFocusToken,
|
||||
resetSaveOutcome,
|
||||
save,
|
||||
apply,
|
||||
reviewAgain,
|
||||
closeReview,
|
||||
keepEditing,
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user