346 lines
11 KiB
Vue
346 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { t } from '@/i18n'
|
|
|
|
import { computed, nextTick, ref } from 'vue'
|
|
import { useRouter } from 'vue-router'
|
|
import { useMediaQuery } from '@vueuse/core'
|
|
import ApiPreview from '@/components/ApiPreview.vue'
|
|
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 BuilderTypeBar from '@/components/BuilderTypeBar.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'
|
|
import { useSchemaDelete } from '@/composables/useSchemaDelete'
|
|
import { useUnsavedChangesGuard } from '@/composables/useUnsavedChangesGuard'
|
|
import { HOME_PATH } from '@/lib/routePaths'
|
|
import { sampleJson } from '@/lib/schemaBuilderSample'
|
|
import { entriesNotice } from '@/lib/schemaEntries'
|
|
import { formatClock } from '@/lib/updateFormat'
|
|
|
|
const DESKTOP_QUERY = '(min-width: 48rem)'
|
|
|
|
const props = defineProps<{ edit?: EditSource }>()
|
|
|
|
const router = useRouter()
|
|
const isDesktop = useMediaQuery(DESKTOP_QUERY)
|
|
const builder = useSchemaBuilder(props.edit)
|
|
const { form } = builder
|
|
const remove = props.edit ? useSchemaDelete(() => props.edit!) : undefined
|
|
const moreDisabled = computed(() => builder.saving.value || Boolean(remove?.pending.value))
|
|
const forbidden = computed(
|
|
() => builder.failure.value === 'forbidden' || Boolean(remove?.forbidden.value),
|
|
)
|
|
|
|
useUnsavedChangesGuard({
|
|
isDirty: () => builder.dirty.value && !remove?.done.value,
|
|
title: () => form.value.name.trim() || t('New content type'),
|
|
})
|
|
|
|
const apiOpen = ref(false)
|
|
const apiSheetOpen = ref(false)
|
|
const fieldSheetOpen = ref(false)
|
|
|
|
const json = computed(() => sampleJson(form.value.key, form.value.fields))
|
|
const notice = computed(() => entriesNotice(builder.entryTotal))
|
|
const savedClock = computed(() =>
|
|
builder.savedAt.value ? formatClock(new Date(builder.savedAt.value)) : '',
|
|
)
|
|
const removedFields = computed(() => builder.removed.value.map((entry) => entry.field))
|
|
const refusalCauses = computed(() => builder.refusedChanges.value)
|
|
const selectedIndex = computed(() =>
|
|
form.value.fields.findIndex((field) => field.uid === builder.selectedUid.value),
|
|
)
|
|
const selectedErrors = computed(() =>
|
|
builder.selected.value
|
|
? (builder.shown.value.fields.get(builder.selected.value.uid) ?? {})
|
|
: {},
|
|
)
|
|
const selectedMarks = computed(() =>
|
|
builder.selected.value ? builder.marks.value.get(builder.selected.value.uid) : undefined,
|
|
)
|
|
const selectedName = computed(() => builder.selected.value?.label.trim() || t('Untitled field'))
|
|
const sheetTitle = computed(() => t('Field: {0}', [selectedName.value]))
|
|
const reviewName = computed(() => builder.baseline.value?.name ?? '')
|
|
|
|
function goHome() {
|
|
router.push(HOME_PATH)
|
|
}
|
|
|
|
function onSelect(uid: number) {
|
|
builder.selectField(uid)
|
|
if (!isDesktop.value) {
|
|
fieldSheetOpen.value = true
|
|
}
|
|
}
|
|
|
|
function onAdd() {
|
|
builder.addField()
|
|
if (!isDesktop.value) {
|
|
fieldSheetOpen.value = true
|
|
}
|
|
}
|
|
|
|
function onRemoveSelected() {
|
|
const uid = builder.selectedUid.value
|
|
if (uid !== null) {
|
|
builder.removeField(uid)
|
|
}
|
|
if (!isDesktop.value) {
|
|
fieldSheetOpen.value = false
|
|
}
|
|
}
|
|
|
|
function onMoveSelected(delta: number) {
|
|
const uid = builder.selectedUid.value
|
|
if (uid !== null) {
|
|
builder.moveField(uid, delta)
|
|
}
|
|
}
|
|
|
|
async function restoreRowFocus() {
|
|
await nextTick()
|
|
const uid = builder.selectedUid.value
|
|
document.querySelector<HTMLElement>(`[data-field-row][data-uid="${uid}"]`)?.focus()
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<SchemaForbidden v-if="forbidden" @back="goHome" />
|
|
|
|
<template v-else>
|
|
<div
|
|
class="flex min-h-0 flex-1 flex-col max-md:shrink-0 max-md:basis-auto"
|
|
data-testid="schema-builder"
|
|
>
|
|
<div class="shrink-0 border-b border-default bg-default p-4 md:px-6">
|
|
<BuilderTypeBar
|
|
:name="form.name"
|
|
:key-value="form.key"
|
|
:key-mode="form.keyMode"
|
|
:key-locked="form.keyLocked"
|
|
:name-error="builder.shown.value.type.name"
|
|
:key-error="builder.shown.value.type.key"
|
|
:disabled="builder.saving.value"
|
|
:key-focus-token="builder.keyFocusToken.value"
|
|
@update:name="builder.setName"
|
|
@update:key="builder.setKey"
|
|
@unlock="builder.unlockKey"
|
|
@reset="builder.resetKey"
|
|
>
|
|
<template v-if="isDesktop" #actions>
|
|
<BuilderActions
|
|
:status="builder.status.value"
|
|
:problems="builder.problems.value"
|
|
:can-save="builder.canSave.value"
|
|
:can-reveal="builder.canReveal.value"
|
|
:can-discard="builder.canDiscard.value"
|
|
:saving="builder.saving.value"
|
|
:saved-at="savedClock"
|
|
@save="builder.save"
|
|
@discard="builder.discard"
|
|
>
|
|
<SchemaDeleteControls
|
|
v-if="remove"
|
|
:remove="remove"
|
|
:disabled="moreDisabled"
|
|
:mobile="false"
|
|
/>
|
|
</BuilderActions>
|
|
</template>
|
|
</BuilderTypeBar>
|
|
</div>
|
|
|
|
<div
|
|
v-if="notice"
|
|
data-testid="entries-notice"
|
|
class="flex shrink-0 items-center gap-2 bg-(--rc-info-soft) px-4 py-2 text-xs/4.5 md:px-6"
|
|
>
|
|
<UIcon name="i-lucide-info" class="size-3.5 shrink-0 text-info" />
|
|
<span class="text-toned">{{ notice }}</span>
|
|
</div>
|
|
|
|
<div
|
|
v-if="
|
|
builder.failure.value === 'failed' ||
|
|
builder.failure.value === 'invalid' ||
|
|
builder.refusal.value === 'banner'
|
|
"
|
|
class="flex flex-col gap-2 px-4 pt-3 md:px-6"
|
|
>
|
|
<RetryAlert
|
|
v-if="builder.failure.value === 'failed'"
|
|
:title="t('Couldn\'t save the content type')"
|
|
:description="
|
|
t(
|
|
'Something went wrong on our side. Your changes are still here. Try again in a moment.',
|
|
)
|
|
"
|
|
@retry="builder.save"
|
|
/>
|
|
<UAlert
|
|
v-else-if="builder.failure.value === 'invalid'"
|
|
role="alert"
|
|
color="error"
|
|
variant="soft"
|
|
icon="i-lucide-circle-x"
|
|
:title="t('Check the fields marked in the list.')"
|
|
/>
|
|
<RetryAlert
|
|
v-if="builder.refusal.value === 'banner'"
|
|
:title="t('Nothing was changed')"
|
|
:description="
|
|
t(
|
|
'Some entries don\'t fit the new fields, so the whole change was refused. RenewCMS can\'t tell which entries. The changes that can cause this are marked.',
|
|
)
|
|
"
|
|
:action-label="t('Review changes')"
|
|
action-icon=""
|
|
@retry="builder.reviewAgain"
|
|
/>
|
|
</div>
|
|
|
|
<div class="flex min-h-0 flex-1 flex-col md:flex-row">
|
|
<div
|
|
class="flex flex-col gap-2 p-4 md:min-h-0 md:w-95 md:shrink-0 md:overflow-y-auto md:border-r md:border-default md:bg-muted"
|
|
>
|
|
<FieldList
|
|
:fields="form.fields"
|
|
:selected-uid="builder.selectedUid.value"
|
|
:errors="builder.shown.value.fields"
|
|
:list-error="builder.shown.value.type.fields"
|
|
:disabled="builder.saving.value"
|
|
:mobile="!isDesktop"
|
|
:marks="builder.marks.value"
|
|
:removed="removedFields"
|
|
:can-undo="builder.mayRestore.value"
|
|
@select="onSelect"
|
|
@add="onAdd"
|
|
@remove="builder.removeField"
|
|
@move="builder.moveField"
|
|
@reorder="builder.reorderFields"
|
|
@undo="builder.undoRemoval"
|
|
>
|
|
<button
|
|
v-if="!isDesktop"
|
|
type="button"
|
|
data-testid="api-row"
|
|
class="flex min-h-11 items-center gap-2.5 border border-default bg-default px-3 text-highlighted focus-visible:outline-2 focus-visible:outline-primary"
|
|
@click="apiSheetOpen = true"
|
|
>
|
|
<UIcon name="i-lucide-braces" class="size-4" />
|
|
<span class="flex-1 text-left font-medium">{{ t('API preview') }}</span>
|
|
<UIcon name="i-lucide-chevron-right" class="size-3.5 text-muted" />
|
|
</button>
|
|
</FieldList>
|
|
</div>
|
|
|
|
<div
|
|
v-if="isDesktop"
|
|
class="flex min-h-0 min-w-0 flex-1 flex-col gap-3.5 overflow-y-auto bg-default px-6 py-4"
|
|
data-testid="inspector-pane"
|
|
>
|
|
<template v-if="builder.selected.value">
|
|
<div class="flex items-center gap-2">
|
|
<h2 class="text-md font-semibold text-highlighted">
|
|
{{ sheetTitle }}
|
|
</h2>
|
|
<BuilderBadge v-if="selectedMarks?.isNew" kind="new" />
|
|
<BuilderBadge v-if="selectedMarks?.changed" kind="changed" />
|
|
</div>
|
|
<BuilderInspector
|
|
:actions="builder"
|
|
:saving="builder.saving.value"
|
|
:focus-token="builder.labelFocusToken.value"
|
|
:field="builder.selected.value"
|
|
:errors="selectedErrors"
|
|
:compact="false"
|
|
/>
|
|
</template>
|
|
<template v-else>
|
|
<h2 class="text-md font-semibold text-highlighted">
|
|
{{ t('Field settings') }}
|
|
</h2>
|
|
<p class="text-muted">
|
|
{{ t('Add a field, then choose its type and rules here.') }}
|
|
</p>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
|
|
<ApiPreview
|
|
v-if="isDesktop"
|
|
v-model:open="apiOpen"
|
|
:key-value="form.key"
|
|
:json="json"
|
|
/>
|
|
<div
|
|
v-else
|
|
class="sticky bottom-0 mt-auto border-t border-default bg-default px-4 py-3"
|
|
>
|
|
<BuilderActions
|
|
:status="builder.status.value"
|
|
:problems="builder.problems.value"
|
|
:can-save="builder.canSave.value"
|
|
:can-reveal="builder.canReveal.value"
|
|
:can-discard="builder.canDiscard.value"
|
|
:saving="builder.saving.value"
|
|
:saved-at="savedClock"
|
|
@save="builder.save"
|
|
@discard="builder.discard"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-if="!isDesktop">
|
|
<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"
|
|
:title="sheetTitle"
|
|
:first="selectedIndex <= 0"
|
|
:last="selectedIndex === form.fields.length - 1"
|
|
:disabled="builder.saving.value"
|
|
@up="onMoveSelected(-1)"
|
|
@down="onMoveSelected(1)"
|
|
@remove="onRemoveSelected"
|
|
@closed="restoreRowFocus"
|
|
>
|
|
<BuilderInspector
|
|
v-if="builder.selected.value"
|
|
:actions="builder"
|
|
:saving="builder.saving.value"
|
|
:focus-token="builder.labelFocusToken.value"
|
|
:field="builder.selected.value"
|
|
:errors="selectedErrors"
|
|
compact
|
|
/>
|
|
</FieldSheet>
|
|
</template>
|
|
|
|
<SchemaReviewDialog
|
|
v-if="builder.reviewOpen.value"
|
|
:name="reviewName"
|
|
:total="builder.entryTotal"
|
|
:changes="builder.changes.value"
|
|
:applying="builder.saving.value"
|
|
:offline="!builder.online.value"
|
|
@apply="builder.apply"
|
|
@close="builder.closeReview"
|
|
/>
|
|
<SchemaRefusalDialog
|
|
v-if="builder.refusal.value === 'dialog'"
|
|
:causes="refusalCauses"
|
|
@keep-editing="builder.keepEditing"
|
|
@discard="builder.discard"
|
|
/>
|
|
</template>
|
|
</template>
|