refac: duplicates, crap, dry etc. on last features commits

This commit is contained in:
Florian Sylvain committed 2026-10-21 00:52:14 +02:00
1 parent fba20f83c5
commit 89051b3f1f
12 files changed
+260 -153

No files matched your search

+35 -17
View File
@@ -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>
+6 -31
View File
@@ -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>
+21 -19
View File
@@ -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>
+42 -4
View File
@@ -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
+31 -17
View File
@@ -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>
+15 -6
View File
@@ -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>
+1 -1
View File
@@ -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)"
/> />
+6 -2
View File
@@ -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
+40
View File
@@ -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 }
}
+8 -25
View File
@@ -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 }
+11 -31
View File
@@ -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 {
+44
View File
@@ -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 }
}