refac: duplicates, crap, dry etc. on last features commits
This commit is contained in:
1 parent
fba20f83c5
commit
89051b3f1f
12 files changed
+260
-153
No files matched your search
@@ -6,8 +6,26 @@ import type { FieldErrors } from '@/lib/schemaBuilderValidation'
|
|||||||
|
|
||||||
type Builder = ReturnType<typeof useSchemaBuilder>
|
type Builder = ReturnType<typeof useSchemaBuilder>
|
||||||
|
|
||||||
|
type InspectorActions = Pick<
|
||||||
|
Builder,
|
||||||
|
| 'setLabel'
|
||||||
|
| 'setFieldKey'
|
||||||
|
| 'unlockFieldKey'
|
||||||
|
| 'resetFieldKey'
|
||||||
|
| 'setFieldType'
|
||||||
|
| 'patchField'
|
||||||
|
| 'setLimit'
|
||||||
|
| 'setDefault'
|
||||||
|
| 'addChoice'
|
||||||
|
| 'removeChoice'
|
||||||
|
| 'renameChoice'
|
||||||
|
| 'reorderChoices'
|
||||||
|
>
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
builder: Builder
|
actions: InspectorActions
|
||||||
|
saving: boolean
|
||||||
|
focusToken: number
|
||||||
field: BuilderField
|
field: BuilderField
|
||||||
errors: FieldErrors
|
errors: FieldErrors
|
||||||
compact: boolean
|
compact: boolean
|
||||||
@@ -20,22 +38,22 @@ const uid = () => props.field.uid
|
|||||||
<FieldInspector
|
<FieldInspector
|
||||||
:field="field"
|
:field="field"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
:disabled="builder.saving.value"
|
:disabled="saving"
|
||||||
:compact="compact"
|
:compact="compact"
|
||||||
:focus-token="builder.labelFocusToken.value"
|
:focus-token="focusToken"
|
||||||
@label="builder.setLabel(uid(), $event)"
|
@label="actions.setLabel(uid(), $event)"
|
||||||
@key="builder.setFieldKey(uid(), $event)"
|
@key="actions.setFieldKey(uid(), $event)"
|
||||||
@unlock="builder.unlockFieldKey(uid())"
|
@unlock="actions.unlockFieldKey(uid())"
|
||||||
@reset="builder.resetFieldKey(uid())"
|
@reset="actions.resetFieldKey(uid())"
|
||||||
@type="builder.setFieldType(uid(), $event)"
|
@type="actions.setFieldType(uid(), $event)"
|
||||||
@required="builder.patchField(uid(), { required: $event })"
|
@required="actions.patchField(uid(), { required: $event })"
|
||||||
@unique="builder.patchField(uid(), { unique: $event })"
|
@unique="actions.patchField(uid(), { unique: $event })"
|
||||||
@limit="(which, value) => builder.setLimit(uid(), which, value)"
|
@limit="(which, value) => actions.setLimit(uid(), which, value)"
|
||||||
@default="builder.setDefault(uid(), $event)"
|
@default="actions.setDefault(uid(), $event)"
|
||||||
@default-on="builder.patchField(uid(), { defaultOn: $event })"
|
@default-on="actions.patchField(uid(), { defaultOn: $event })"
|
||||||
@add-choice="builder.addChoice(uid(), $event)"
|
@add-choice="actions.addChoice(uid(), $event)"
|
||||||
@remove-choice="builder.removeChoice(uid(), $event)"
|
@remove-choice="actions.removeChoice(uid(), $event)"
|
||||||
@rename-choice="(index, text) => builder.renameChoice(uid(), index, text)"
|
@rename-choice="(index, text) => actions.renameChoice(uid(), index, text)"
|
||||||
@reorder-choices="builder.reorderChoices(uid(), $event)"
|
@reorder-choices="actions.reorderChoices(uid(), $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
import { t, translate } from '@/i18n'
|
import { t, translate } from '@/i18n'
|
||||||
|
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { useContentTypesStore } from '@/stores/contentTypes'
|
|
||||||
import { useSessionStore } from '@/stores/session'
|
import { useSessionStore } from '@/stores/session'
|
||||||
import { useUpdateStore } from '@/stores/update'
|
import { useUpdateStore } from '@/stores/update'
|
||||||
import { useApi } from '@/composables/useApi'
|
import { useContentTypesPreload, useShellLogout } from '@/composables/useShellSession'
|
||||||
import { useSidebarCollapsed } from '@/composables/useSidebarCollapsed'
|
import { useSidebarCollapsed } from '@/composables/useSidebarCollapsed'
|
||||||
import AppBoot from '@/components/AppBoot.vue'
|
import AppBoot from '@/components/AppBoot.vue'
|
||||||
import AccountMenuSheet from '@/components/AccountMenuSheet.vue'
|
import AccountMenuSheet from '@/components/AccountMenuSheet.vue'
|
||||||
@@ -68,27 +67,15 @@ const bodyUi = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
|
||||||
const session = useSessionStore()
|
const session = useSessionStore()
|
||||||
const update = useUpdateStore()
|
const update = useUpdateStore()
|
||||||
const api = useApi()
|
const { loggingOut, logout } = useShellLogout()
|
||||||
const contentTypes = useContentTypesStore()
|
|
||||||
|
|
||||||
const wantsBuiltin = computed(
|
const wantsBuiltin = computed(
|
||||||
() => session.canManageContent && route.meta.needsBuiltinTypes === true,
|
() => session.canManageContent && route.meta.needsBuiltinTypes === true,
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(
|
useContentTypesPreload(() => wantsBuiltin.value)
|
||||||
[() => session.status === 'ready' && session.session?.verified === true, wantsBuiltin],
|
|
||||||
([verified, builtin], [wasVerified]) => {
|
|
||||||
if (verified) {
|
|
||||||
contentTypes.load({ builtin })
|
|
||||||
} else if (wasVerified) {
|
|
||||||
contentTypes.reset()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ immediate: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
const publishingChildren = computed(() => [
|
const publishingChildren = computed(() => [
|
||||||
{ label: t('Publishing'), type: 'label' as const },
|
{ label: t('Publishing'), type: 'label' as const },
|
||||||
@@ -237,7 +224,6 @@ const navUi = computed(() => ({
|
|||||||
childLink: 'h-8 max-lg:h-11 gap-2.5 py-0 pr-2.5 pl-3 text-sm',
|
childLink: 'h-8 max-lg:h-11 gap-2.5 py-0 pr-2.5 pl-3 text-sm',
|
||||||
childLinkIcon: 'size-3.5',
|
childLinkIcon: 'size-3.5',
|
||||||
}))
|
}))
|
||||||
const loggingOut = ref(false)
|
|
||||||
const sidebarOpen = ref(false)
|
const sidebarOpen = ref(false)
|
||||||
const accountSheetOpen = ref(false)
|
const accountSheetOpen = ref(false)
|
||||||
const NAVBAR_TOGGLE_SELECTOR = '[data-slot="toggle"]'
|
const NAVBAR_TOGGLE_SELECTOR = '[data-slot="toggle"]'
|
||||||
@@ -272,17 +258,6 @@ const showBoot = computed(() => session.status === 'checking' && !props.renderWh
|
|||||||
function onRetry() {
|
function onRetry() {
|
||||||
session.retry()
|
session.retry()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onLogout() {
|
|
||||||
loggingOut.value = true
|
|
||||||
try {
|
|
||||||
await api.request('/v1/auth/logout', { method: 'POST' })
|
|
||||||
} finally {
|
|
||||||
loggingOut.value = false
|
|
||||||
}
|
|
||||||
session.clear()
|
|
||||||
await router.push('/login')
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -335,7 +310,7 @@ async function onLogout() {
|
|||||||
:email="session.session?.email"
|
:email="session.session?.email"
|
||||||
:collapsed="sidebarCollapsed"
|
:collapsed="sidebarCollapsed"
|
||||||
:sheet-open="accountSheetOpen"
|
:sheet-open="accountSheetOpen"
|
||||||
@logout="onLogout"
|
@logout="logout"
|
||||||
@open-sheet="openAccountSheet"
|
@open-sheet="openAccountSheet"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -438,7 +413,7 @@ async function onLogout() {
|
|||||||
:username="session.session?.username ?? ''"
|
:username="session.session?.username ?? ''"
|
||||||
:display-name="session.session?.displayName"
|
:display-name="session.session?.displayName"
|
||||||
:email="session.session?.email"
|
:email="session.session?.email"
|
||||||
@logout="onLogout"
|
@logout="logout"
|
||||||
/>
|
/>
|
||||||
</UDashboardGroup>
|
</UDashboardGroup>
|
||||||
</template>
|
</template>
|
||||||
@@ -2,9 +2,18 @@
|
|||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
import BuilderStatusChip from '@/components/BuilderStatusChip.vue'
|
import BuilderStatusChip from '@/components/BuilderStatusChip.vue'
|
||||||
import type { EntryEditorPage } from '@/composables/useEntryEditorPage'
|
import type { EntryChipStatus } from '@/composables/useEntryEditorPage'
|
||||||
|
|
||||||
defineProps<{ page: EntryEditorPage; bar?: boolean }>()
|
defineProps<{
|
||||||
|
chip: EntryChipStatus
|
||||||
|
savedClock: string
|
||||||
|
creating: boolean
|
||||||
|
saving: boolean
|
||||||
|
canSave: boolean
|
||||||
|
bar?: boolean
|
||||||
|
}>()
|
||||||
|
|
||||||
|
defineEmits<{ remove: []; cancel: [] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -13,20 +22,16 @@ defineProps<{ page: EntryEditorPage; bar?: boolean }>()
|
|||||||
:class="bar ? 'w-full gap-2' : 'gap-3'"
|
:class="bar ? 'w-full gap-2' : 'gap-3'"
|
||||||
:data-testid="bar ? 'entry-bar' : 'entry-actions'"
|
:data-testid="bar ? 'entry-bar' : 'entry-actions'"
|
||||||
>
|
>
|
||||||
<BuilderStatusChip
|
<BuilderStatusChip v-if="!bar" :status="chip" :saved-at="savedClock" />
|
||||||
v-if="!bar"
|
|
||||||
:status="page.chip.value"
|
|
||||||
:saved-at="page.savedClock.value"
|
|
||||||
/>
|
|
||||||
<UButton
|
<UButton
|
||||||
v-if="!bar && !page.creating.value"
|
v-if="!bar && !creating"
|
||||||
type="button"
|
type="button"
|
||||||
color="error"
|
color="error"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
icon="i-lucide-trash-2"
|
icon="i-lucide-trash-2"
|
||||||
:disabled="page.saving.value"
|
:disabled="saving"
|
||||||
class="h-8 px-3"
|
class="h-8 px-3"
|
||||||
@click="page.removal.open"
|
@click="$emit('remove')"
|
||||||
>
|
>
|
||||||
{{ t('Delete') }}
|
{{ t('Delete') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
@@ -34,25 +39,22 @@ defineProps<{ page: EntryEditorPage; bar?: boolean }>()
|
|||||||
type="button"
|
type="button"
|
||||||
color="neutral"
|
color="neutral"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
:disabled="page.saving.value"
|
:disabled="saving"
|
||||||
class="h-11 border border-accented bg-default px-3 text-highlighted ring-0 md:h-8"
|
class="h-11 border border-accented bg-default px-3 text-highlighted ring-0 md:h-8"
|
||||||
:class="bar ? 'flex-1 justify-center' : ''"
|
:class="bar ? 'flex-1 justify-center' : ''"
|
||||||
@click="page.cancel"
|
@click="$emit('cancel')"
|
||||||
>
|
>
|
||||||
{{ t('Cancel') }}
|
{{ t('Cancel') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
<UButton
|
<UButton
|
||||||
type="submit"
|
type="submit"
|
||||||
form="entry-form"
|
form="entry-form"
|
||||||
:disabled="!page.canSave.value"
|
:disabled="!canSave"
|
||||||
:loading="page.saving.value"
|
:loading="saving"
|
||||||
class="h-11 px-4 md:h-8"
|
class="h-11 px-4 md:h-8"
|
||||||
:class="[
|
:class="[bar ? 'flex-1 justify-center' : '', saving ? 'disabled:opacity-100' : '']"
|
||||||
bar ? 'flex-1 justify-center' : '',
|
|
||||||
page.saving.value ? 'disabled:opacity-100' : '',
|
|
||||||
]"
|
|
||||||
>
|
>
|
||||||
{{ page.saving.value ? t('Saving…') : t('Save') }}
|
{{ saving ? t('Saving…') : t('Save') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
|
|
||||||
|
import { computed } from 'vue'
|
||||||
import BuilderStatusChip from '@/components/BuilderStatusChip.vue'
|
import BuilderStatusChip from '@/components/BuilderStatusChip.vue'
|
||||||
import ContentState from '@/components/ContentState.vue'
|
import ContentState from '@/components/ContentState.vue'
|
||||||
import DirectoryLoadError from '@/components/DirectoryLoadError.vue'
|
import DirectoryLoadError from '@/components/DirectoryLoadError.vue'
|
||||||
@@ -28,6 +29,14 @@ function loadErrorLine() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const actionProps = computed(() => ({
|
||||||
|
chip: props.page.chip.value,
|
||||||
|
savedClock: props.page.savedClock.value,
|
||||||
|
creating: props.page.creating.value,
|
||||||
|
saving: props.page.saving.value,
|
||||||
|
canSave: props.page.canSave.value,
|
||||||
|
}))
|
||||||
|
|
||||||
function goHome() {
|
function goHome() {
|
||||||
router.push(HOME_PATH)
|
router.push(HOME_PATH)
|
||||||
}
|
}
|
||||||
@@ -65,7 +74,12 @@ function backToList() {
|
|||||||
<p class="text-muted">{{ page.subtitle.value }}</p>
|
<p class="text-muted">{{ page.subtitle.value }}</p>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
<EntryActions v-if="isDesktop && phase === 'ready'" :page="page" />
|
<EntryActions
|
||||||
|
v-if="isDesktop && phase === 'ready'"
|
||||||
|
v-bind="actionProps"
|
||||||
|
@remove="page.removal.open"
|
||||||
|
@cancel="page.cancel"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@@ -83,8 +97,32 @@ function backToList() {
|
|||||||
/>
|
/>
|
||||||
<EntryEditorSkeleton v-else-if="phase === 'loading'" />
|
<EntryEditorSkeleton v-else-if="phase === 'loading'" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<EntryNotices :page="page" />
|
<EntryNotices
|
||||||
<EntryForm :page="page" />
|
:failure="page.failure.value"
|
||||||
|
:schema-changed="page.schemaChanged.value"
|
||||||
|
:schema-field="page.schemaField.value"
|
||||||
|
:issues="page.form.issues.value"
|
||||||
|
:fields="page.fields.value"
|
||||||
|
:type-name="page.typeName.value"
|
||||||
|
@retry="page.submit"
|
||||||
|
@reload="page.reload"
|
||||||
|
/>
|
||||||
|
<EntryForm
|
||||||
|
:fields="page.fields.value"
|
||||||
|
:values="page.form.values.value"
|
||||||
|
:issues="page.form.issues.value"
|
||||||
|
:first-issue="page.form.firstIssue.value"
|
||||||
|
:focus-token="page.focusToken.value"
|
||||||
|
:locked="page.locked.value"
|
||||||
|
:saving="page.saving.value"
|
||||||
|
:creating="page.creating.value"
|
||||||
|
:is-default="page.form.isDefault"
|
||||||
|
:entry-line="page.entryLine.value"
|
||||||
|
:created-line="page.createdLine.value"
|
||||||
|
:updated-line="page.updatedLine.value"
|
||||||
|
@update="page.change"
|
||||||
|
@submit="page.submit"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,7 +130,7 @@ function backToList() {
|
|||||||
v-if="!isDesktop && phase === 'ready'"
|
v-if="!isDesktop && phase === 'ready'"
|
||||||
class="sticky bottom-0 -mx-4 mt-auto flex border-t border-default bg-default px-4 py-3"
|
class="sticky bottom-0 -mx-4 mt-auto flex border-t border-default bg-default px-4 py-3"
|
||||||
>
|
>
|
||||||
<EntryActions :page="page" bar />
|
<EntryActions v-bind="actionProps" bar @remove="page.removal.open" @cancel="page.cancel" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<EntryDeleteDialog
|
<EntryDeleteDialog
|
||||||
|
|||||||
@@ -3,21 +3,35 @@ import { t } from '@/i18n'
|
|||||||
|
|
||||||
import { computed, nextTick, watch } from 'vue'
|
import { computed, nextTick, watch } from 'vue'
|
||||||
import EntryField from '@/components/EntryField.vue'
|
import EntryField from '@/components/EntryField.vue'
|
||||||
import type { EntryEditorPage } from '@/composables/useEntryEditorPage'
|
import type { BuiltinField } from '@/lib/contentTypes'
|
||||||
|
import type { EntryIssues, EntryValues } from '@/lib/entryForm'
|
||||||
import { groupFields } from '@/lib/entryLayout'
|
import { groupFields } from '@/lib/entryLayout'
|
||||||
|
|
||||||
const FOCUSABLE =
|
const FOCUSABLE =
|
||||||
'input:not([type="hidden"]), textarea, [role="spinbutton"], [role="switch"], [role="combobox"]'
|
'input:not([type="hidden"]), textarea, [role="spinbutton"], [role="switch"], [role="combobox"]'
|
||||||
|
|
||||||
const props = defineProps<{ page: EntryEditorPage }>()
|
const props = defineProps<{
|
||||||
|
fields: BuiltinField[]
|
||||||
|
values: EntryValues
|
||||||
|
issues: EntryIssues
|
||||||
|
firstIssue: string | null
|
||||||
|
focusToken: number
|
||||||
|
locked: boolean
|
||||||
|
saving: boolean
|
||||||
|
creating: boolean
|
||||||
|
isDefault: (field: BuiltinField) => boolean
|
||||||
|
entryLine: string
|
||||||
|
createdLine: string
|
||||||
|
updatedLine: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const { form, fields, locked, focusToken } = props.page
|
const emit = defineEmits<{ update: [key: string, value: unknown]; submit: [] }>()
|
||||||
const rows = computed(() => groupFields(fields.value))
|
|
||||||
const dimmed = computed(() => locked.value)
|
const rows = computed(() => groupFields(props.fields))
|
||||||
|
|
||||||
async function focusFirstIssue() {
|
async function focusFirstIssue() {
|
||||||
await nextTick()
|
await nextTick()
|
||||||
const key = form.firstIssue.value
|
const key = props.firstIssue
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -29,7 +43,7 @@ async function focusFirstIssue() {
|
|||||||
;(wrapper ?? control)?.scrollIntoView?.({ block: 'center' })
|
;(wrapper ?? control)?.scrollIntoView?.({ block: 'center' })
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(focusToken, focusFirstIssue)
|
watch(() => props.focusToken, focusFirstIssue)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -38,9 +52,9 @@ watch(focusToken, focusFirstIssue)
|
|||||||
novalidate
|
novalidate
|
||||||
data-testid="entry-form"
|
data-testid="entry-form"
|
||||||
class="flex max-w-190 flex-col gap-5 border border-default bg-default p-4 transition-opacity md:p-6"
|
class="flex max-w-190 flex-col gap-5 border border-default bg-default p-4 transition-opacity md:p-6"
|
||||||
:class="dimmed ? 'opacity-70' : ''"
|
:class="locked ? 'opacity-70' : ''"
|
||||||
:aria-busy="page.saving.value ? 'true' : undefined"
|
:aria-busy="saving ? 'true' : undefined"
|
||||||
@submit.prevent="page.submit"
|
@submit.prevent="emit('submit')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="row in rows"
|
v-for="row in rows"
|
||||||
@@ -52,11 +66,11 @@ watch(focusToken, focusFirstIssue)
|
|||||||
v-for="field in row"
|
v-for="field in row"
|
||||||
:key="field.key"
|
:key="field.key"
|
||||||
:field="field"
|
:field="field"
|
||||||
:value="form.values.value[field.key]"
|
:value="values[field.key]"
|
||||||
:issue="form.issues.value[field.key]"
|
:issue="issues[field.key]"
|
||||||
:is-default="form.isDefault(field)"
|
:is-default="isDefault(field)"
|
||||||
:disabled="locked"
|
:disabled="locked"
|
||||||
@update="page.change"
|
@update="(key, value) => emit('update', key, value)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,11 +79,11 @@ watch(focusToken, focusFirstIssue)
|
|||||||
class="grid grid-cols-1 gap-x-4 gap-y-1.5 border-t border-default pt-4 text-xs/4.5 md:grid-cols-[auto_1fr]"
|
class="grid grid-cols-1 gap-x-4 gap-y-1.5 border-t border-default pt-4 text-xs/4.5 md:grid-cols-[auto_1fr]"
|
||||||
>
|
>
|
||||||
<dt class="text-muted">{{ t('Entry') }}</dt>
|
<dt class="text-muted">{{ t('Entry') }}</dt>
|
||||||
<dd :class="page.creating.value ? '' : 'font-mono'">{{ page.entryLine.value }}</dd>
|
<dd :class="creating ? '' : 'font-mono'">{{ entryLine }}</dd>
|
||||||
<dt class="text-muted">{{ t('Created by') }}</dt>
|
<dt class="text-muted">{{ t('Created by') }}</dt>
|
||||||
<dd>{{ page.createdLine.value }}</dd>
|
<dd>{{ createdLine }}</dd>
|
||||||
<dt class="text-muted">{{ t('Updated by') }}</dt>
|
<dt class="text-muted">{{ t('Updated by') }}</dt>
|
||||||
<dd>{{ page.updatedLine.value }}</dd>
|
<dd>{{ updatedLine }}</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
@@ -3,15 +3,24 @@ import { t } from '@/i18n'
|
|||||||
|
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import RetryAlert from '@/components/RetryAlert.vue'
|
import RetryAlert from '@/components/RetryAlert.vue'
|
||||||
import type { EntryEditorPage } from '@/composables/useEntryEditorPage'
|
import type { SaveFailure } from '@/composables/useEntryDraft'
|
||||||
|
import type { BuiltinField } from '@/lib/contentTypes'
|
||||||
|
import type { EntryIssues } from '@/lib/entryForm'
|
||||||
import { schemaChangedLine, uniqueAlert } from '@/lib/entryMessages'
|
import { schemaChangedLine, uniqueAlert } from '@/lib/entryMessages'
|
||||||
|
|
||||||
const props = defineProps<{ page: EntryEditorPage }>()
|
const props = defineProps<{
|
||||||
|
failure: SaveFailure
|
||||||
|
schemaChanged: boolean
|
||||||
|
schemaField: string
|
||||||
|
issues: EntryIssues
|
||||||
|
fields: BuiltinField[]
|
||||||
|
typeName: string
|
||||||
|
}>()
|
||||||
|
|
||||||
const { failure, schemaChanged, schemaField, form, fields, typeName } = props.page
|
defineEmits<{ retry: []; reload: [] }>()
|
||||||
|
|
||||||
const uniqueField = computed(() =>
|
const uniqueField = computed(() =>
|
||||||
fields.value.find((field) => form.issues.value[field.key]?.rule === 'unique'),
|
props.fields.find((field) => props.issues[field.key]?.rule === 'unique'),
|
||||||
)
|
)
|
||||||
const alertUi = {
|
const alertUi = {
|
||||||
root: 'items-start gap-2.5 px-3.5 py-3',
|
root: 'items-start gap-2.5 px-3.5 py-3',
|
||||||
@@ -29,7 +38,7 @@ const alertUi = {
|
|||||||
'Something went wrong on our side. Your changes are still here. Try again in a moment.',
|
'Something went wrong on our side. Your changes are still here. Try again in a moment.',
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
@retry="page.submit"
|
@retry="$emit('retry')"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<UAlert
|
<UAlert
|
||||||
@@ -61,7 +70,7 @@ const alertUi = {
|
|||||||
icon="i-lucide-rotate-cw"
|
icon="i-lucide-rotate-cw"
|
||||||
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-8"
|
class="h-11 gap-1.5 border border-accented bg-default px-2.5 text-xs text-highlighted ring-0 md:h-8"
|
||||||
:ui="{ leadingIcon: 'size-3.5' }"
|
:ui="{ leadingIcon: 'size-3.5' }"
|
||||||
@click="page.reload"
|
@click="$emit('reload')"
|
||||||
>
|
>
|
||||||
{{ t('Reload form') }}
|
{{ t('Reload form') }}
|
||||||
</UButton>
|
</UButton>
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ const height = computed(() => (props.touch ? 'h-11' : 'h-8'))
|
|||||||
trailingIcon: 'size-3.5 text-muted',
|
trailingIcon: 'size-3.5 text-muted',
|
||||||
content: 'w-40 rounded-none',
|
content: 'w-40 rounded-none',
|
||||||
itemTrailingIcon: 'hidden',
|
itemTrailingIcon: 'hidden',
|
||||||
item: `${height} rounded-none data-[state=checked]:bg-(--rc-sand) data-[state=checked]:font-semibold data-[state=checked]:text-(--rc-on-sand)`,
|
item: `${height} items-center rounded-none py-0 data-[state=checked]:bg-(--rc-sand) data-[state=checked]:font-semibold data-[state=checked]:text-(--rc-on-sand)`,
|
||||||
}"
|
}"
|
||||||
@update:model-value="emit('update:size', $event as number)"
|
@update:model-value="emit('update:size', $event as number)"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -256,7 +256,9 @@ async function restoreRowFocus() {
|
|||||||
<BuilderBadge v-if="selectedMarks?.changed" kind="changed" />
|
<BuilderBadge v-if="selectedMarks?.changed" kind="changed" />
|
||||||
</div>
|
</div>
|
||||||
<BuilderInspector
|
<BuilderInspector
|
||||||
:builder="builder"
|
:actions="builder"
|
||||||
|
:saving="builder.saving.value"
|
||||||
|
:focus-token="builder.labelFocusToken.value"
|
||||||
:field="builder.selected.value"
|
:field="builder.selected.value"
|
||||||
:errors="selectedErrors"
|
:errors="selectedErrors"
|
||||||
:compact="false"
|
:compact="false"
|
||||||
@@ -313,7 +315,9 @@ async function restoreRowFocus() {
|
|||||||
>
|
>
|
||||||
<BuilderInspector
|
<BuilderInspector
|
||||||
v-if="builder.selected.value"
|
v-if="builder.selected.value"
|
||||||
:builder="builder"
|
:actions="builder"
|
||||||
|
:saving="builder.saving.value"
|
||||||
|
:focus-token="builder.labelFocusToken.value"
|
||||||
:field="builder.selected.value"
|
:field="builder.selected.value"
|
||||||
:errors="selectedErrors"
|
:errors="selectedErrors"
|
||||||
compact
|
compact
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { ref, type Ref } from 'vue'
|
||||||
|
import { useLocalizedToast } from '@/i18n/presentation'
|
||||||
|
|
||||||
|
interface ConfirmedDeleteOptions {
|
||||||
|
toastId: string
|
||||||
|
toastTitle: () => string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConfirmedDelete<Stage extends string>(options: ConfirmedDeleteOptions) {
|
||||||
|
const toast = useLocalizedToast()
|
||||||
|
|
||||||
|
const stage = ref('closed') as Ref<Stage | 'closed'>
|
||||||
|
const pending = ref(false)
|
||||||
|
const done = ref(false)
|
||||||
|
|
||||||
|
function announce(description: () => string) {
|
||||||
|
toast.add(() => ({
|
||||||
|
id: options.toastId,
|
||||||
|
color: 'success',
|
||||||
|
icon: 'i-lucide-circle-check',
|
||||||
|
title: options.toastTitle(),
|
||||||
|
description: description(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
async function leave(description: () => string, navigate: () => Promise<unknown>) {
|
||||||
|
done.value = true
|
||||||
|
stage.value = 'closed'
|
||||||
|
announce(description)
|
||||||
|
const arrived = await navigate().then(
|
||||||
|
(failure) => !failure,
|
||||||
|
() => false,
|
||||||
|
)
|
||||||
|
if (!arrived) {
|
||||||
|
done.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { stage, pending, done, leave }
|
||||||
|
}
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { ref } from 'vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useLocalizedToast } from '@/i18n/presentation'
|
|
||||||
import { ApiError } from '@/composables/useApi'
|
import { ApiError } from '@/composables/useApi'
|
||||||
|
import { useConfirmedDelete } from '@/composables/useConfirmedDelete'
|
||||||
import { useEntries } from '@/composables/useEntries'
|
import { useEntries } from '@/composables/useEntries'
|
||||||
import type { EntryDraft } from '@/composables/useEntryDraft'
|
import type { EntryDraft } from '@/composables/useEntryDraft'
|
||||||
import { isGoneError } from '@/lib/entryServerError'
|
import { isGoneError } from '@/lib/entryServerError'
|
||||||
@@ -15,12 +14,12 @@ export type RemovalStage = 'closed' | 'confirm' | 'failed'
|
|||||||
|
|
||||||
export function useEntryRemoval(editor: EntryDraft, typeName: () => string, title: () => string) {
|
export function useEntryRemoval(editor: EntryDraft, typeName: () => string, title: () => string) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const toast = useLocalizedToast()
|
|
||||||
const { deleteEntry } = useEntries()
|
const { deleteEntry } = useEntries()
|
||||||
|
|
||||||
const stage = ref<RemovalStage>('closed')
|
const { stage, pending, done, leave } = useConfirmedDelete<RemovalStage>({
|
||||||
const pending = ref(false)
|
toastId: TOAST_ID,
|
||||||
const done = ref(false)
|
toastTitle: () => t('Entry deleted'),
|
||||||
|
})
|
||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
if (!pending.value && !editor.saving.value && editor.entryId.value !== null) {
|
if (!pending.value && !editor.saving.value && editor.entryId.value !== null) {
|
||||||
@@ -34,16 +33,6 @@ export function useEntryRemoval(editor: EntryDraft, typeName: () => string, titl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function announce(deletedTitle: string) {
|
|
||||||
toast.add(() => ({
|
|
||||||
id: TOAST_ID,
|
|
||||||
color: 'success',
|
|
||||||
icon: 'i-lucide-circle-check',
|
|
||||||
title: t('Entry deleted'),
|
|
||||||
description: t('"{0}" was deleted from {1}.', [deletedTitle, typeName()]),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
function refuse(error: unknown) {
|
function refuse(error: unknown) {
|
||||||
if (error instanceof ApiError && error.status === UNAUTHORIZED) {
|
if (error instanceof ApiError && error.status === UNAUTHORIZED) {
|
||||||
return
|
return
|
||||||
@@ -72,16 +61,10 @@ export function useEntryRemoval(editor: EntryDraft, typeName: () => string, titl
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pending.value = false
|
pending.value = false
|
||||||
done.value = true
|
await leave(
|
||||||
stage.value = 'closed'
|
() => t('"{0}" was deleted from {1}.', [deletedTitle, typeName()]),
|
||||||
announce(deletedTitle)
|
() => router.replace(contentPath(editor.typeKey)),
|
||||||
const arrived = await router.replace(contentPath(editor.typeKey)).then(
|
|
||||||
(failure) => !failure,
|
|
||||||
() => false,
|
|
||||||
)
|
)
|
||||||
if (!arrived) {
|
|
||||||
done.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return { stage, pending, done, open, close, confirm }
|
return { stage, pending, done, open, close, confirm }
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import { computed, ref } from 'vue'
|
|||||||
import { useOnline } from '@vueuse/core'
|
import { useOnline } from '@vueuse/core'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useLocalizedToast } from '@/i18n/presentation'
|
|
||||||
import { ApiError } from '@/composables/useApi'
|
import { ApiError } from '@/composables/useApi'
|
||||||
import { useContentTypes, type ContentType } from '@/composables/useContentTypes'
|
import { useConfirmedDelete } from '@/composables/useConfirmedDelete'
|
||||||
|
import { useContentTypes } from '@/composables/useContentTypes'
|
||||||
import type { EditSource } from '@/composables/useSchemaBuilder'
|
import type { EditSource } from '@/composables/useSchemaBuilder'
|
||||||
import { contentLink } from '@/lib/contentNav'
|
import { contentLink } from '@/lib/contentNav'
|
||||||
import { isConfirmStage, type DeleteStage } from '@/lib/schemaDeleteStage'
|
import { isConfirmStage, type DeleteStage } from '@/lib/schemaDeleteStage'
|
||||||
@@ -17,13 +17,13 @@ const TOAST_ID = 'content-type-deleted'
|
|||||||
export function useSchemaDelete(source: () => EditSource) {
|
export function useSchemaDelete(source: () => EditSource) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const store = useContentTypesStore()
|
const store = useContentTypesStore()
|
||||||
const toast = useLocalizedToast()
|
|
||||||
const online = useOnline()
|
const online = useOnline()
|
||||||
|
|
||||||
const stage = ref<DeleteStage>('closed')
|
const { stage, pending, done, leave } = useConfirmedDelete<DeleteStage>({
|
||||||
const pending = ref(false)
|
toastId: TOAST_ID,
|
||||||
|
toastTitle: () => t('Content type deleted'),
|
||||||
|
})
|
||||||
const forbidden = ref(false)
|
const forbidden = ref(false)
|
||||||
const done = ref(false)
|
|
||||||
const offline = computed(() => !online.value)
|
const offline = computed(() => !online.value)
|
||||||
const type = computed(() => source().type)
|
const type = computed(() => source().type)
|
||||||
const total = computed(() => source().total)
|
const total = computed(() => source().total)
|
||||||
@@ -46,30 +46,6 @@ export function useSchemaDelete(source: () => EditSource) {
|
|||||||
await router.push(contentLink(type.value.key)).catch(() => undefined)
|
await router.push(contentLink(type.value.key)).catch(() => undefined)
|
||||||
}
|
}
|
||||||
|
|
||||||
function announceDeleted(name: string) {
|
|
||||||
toast.add(() => ({
|
|
||||||
id: TOAST_ID,
|
|
||||||
color: 'success',
|
|
||||||
icon: 'i-lucide-circle-check',
|
|
||||||
title: t('Content type deleted'),
|
|
||||||
description: t('{0} was deleted.', [name]),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async function leave(deleted: ContentType) {
|
|
||||||
done.value = true
|
|
||||||
stage.value = 'closed'
|
|
||||||
announceDeleted(deleted.name)
|
|
||||||
const arrived = await router.push(SCHEMA_PATH).then(
|
|
||||||
(failure) => !failure,
|
|
||||||
() => false,
|
|
||||||
)
|
|
||||||
if (!arrived) {
|
|
||||||
done.value = false
|
|
||||||
}
|
|
||||||
store.drop(deleted.key)
|
|
||||||
}
|
|
||||||
|
|
||||||
function refuse(error: unknown, key: string) {
|
function refuse(error: unknown, key: string) {
|
||||||
const status = error instanceof ApiError ? error.status : 0
|
const status = error instanceof ApiError ? error.status : 0
|
||||||
const body = error instanceof ApiError ? error.body : undefined
|
const body = error instanceof ApiError ? error.body : undefined
|
||||||
@@ -104,7 +80,11 @@ export function useSchemaDelete(source: () => EditSource) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
pending.value = false
|
pending.value = false
|
||||||
await leave(deleted)
|
await leave(
|
||||||
|
() => t('{0} was deleted.', [deleted.name]),
|
||||||
|
() => router.push(SCHEMA_PATH),
|
||||||
|
)
|
||||||
|
store.drop(deleted.key)
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { useApi } from '@/composables/useApi'
|
||||||
|
import { useContentTypesStore } from '@/stores/contentTypes'
|
||||||
|
import { useSessionStore } from '@/stores/session'
|
||||||
|
|
||||||
|
const LOGIN_PATH = '/login'
|
||||||
|
|
||||||
|
export function useContentTypesPreload(wantsBuiltin: () => boolean) {
|
||||||
|
const session = useSessionStore()
|
||||||
|
const contentTypes = useContentTypesStore()
|
||||||
|
|
||||||
|
watch(
|
||||||
|
[() => session.status === 'ready' && session.session?.verified === true, wantsBuiltin],
|
||||||
|
([verified, builtin], [wasVerified]) => {
|
||||||
|
if (verified) {
|
||||||
|
contentTypes.load({ builtin })
|
||||||
|
} else if (wasVerified) {
|
||||||
|
contentTypes.reset()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useShellLogout() {
|
||||||
|
const router = useRouter()
|
||||||
|
const session = useSessionStore()
|
||||||
|
const api = useApi()
|
||||||
|
const loggingOut = ref(false)
|
||||||
|
|
||||||
|
async function logout() {
|
||||||
|
loggingOut.value = true
|
||||||
|
try {
|
||||||
|
await api.request('/v1/auth/logout', { method: 'POST' })
|
||||||
|
} finally {
|
||||||
|
loggingOut.value = false
|
||||||
|
}
|
||||||
|
session.clear()
|
||||||
|
await router.push(LOGIN_PATH)
|
||||||
|
}
|
||||||
|
|
||||||
|
return { loggingOut, logout }
|
||||||
|
}
|
||||||
Reference in new issue
Block a user