feat: delete a custom type

This commit is contained in:
Florian Sylvain committed 2026-10-20 17:56:44 +02:00
1 parent 76e9e1220c
commit 5404291af2
25 files changed
+1768 -46

No files matched your search

@@ -0,0 +1,125 @@
import { describe, expect, it } from 'vitest'
import { resolveSchemaTypeView, type SchemaTypeInputs } from '@/lib/schemaTypeView'
const SETTLED: SchemaTypeInputs = {
listsForbidden: false,
listsLoading: false,
customFailed: false,
builtinFailed: false,
hasBuiltin: false,
hasCustom: false,
skeletonVisible: false,
countStatus: 'idle',
}
const resolve = (patch: Partial<SchemaTypeInputs>) =>
resolveSchemaTypeView({ ...SETTLED, ...patch })
describe('resolveSchemaTypeView', () => {
it.each([
['forbidden lists', { listsForbidden: true }],
['forbidden count', { countStatus: 'forbidden' }],
[
'forbidden over loading',
{ listsForbidden: true, listsLoading: true, skeletonVisible: true },
],
['forbidden over a custom type', { countStatus: 'forbidden', hasCustom: true }],
] as const)('is forbidden and bleeds for %s', (_, patch) => {
expect(resolve(patch)).toEqual({
view: 'forbidden',
editable: false,
loading: false,
bleed: true,
})
})
it.each([
['lists loading', { listsLoading: true }],
['count idle on a custom type', { hasCustom: true, countStatus: 'idle' }],
['count loading on a custom type', { hasCustom: true, countStatus: 'loading' }],
] as const)('shows the skeleton only once visible, %s', (_, patch) => {
expect(resolve({ ...patch, skeletonVisible: true })).toEqual({
view: 'skeleton',
editable: false,
loading: true,
bleed: false,
})
expect(resolve({ ...patch, skeletonVisible: false })).toEqual({
view: 'blank',
editable: false,
loading: true,
bleed: false,
})
})
it('does not wait for the count when there is no custom type', () => {
expect(resolve({ hasBuiltin: true, countStatus: 'loading' }).loading).toBe(false)
})
it('reports a failed custom list before anything else', () => {
expect(
resolve({
customFailed: true,
builtinFailed: true,
hasBuiltin: true,
hasCustom: true,
countStatus: 'failed',
}),
).toEqual({ view: 'listsFailed', editable: false, loading: false, bleed: false })
})
it('reports a failed built-in list after the custom one', () => {
expect(resolve({ builtinFailed: true, hasBuiltin: true, countStatus: 'failed' })).toEqual({
view: 'builtinListFailed',
editable: false,
loading: false,
bleed: false,
})
})
it('reports a failed count on a custom type', () => {
expect(resolve({ hasCustom: true, hasBuiltin: true, countStatus: 'failed' })).toEqual({
view: 'countFailed',
editable: false,
loading: false,
bleed: false,
})
})
it('is editable and bleeds for a custom type with a ready count', () => {
expect(resolve({ hasCustom: true, hasBuiltin: true, countStatus: 'ready' })).toEqual({
view: 'editable',
editable: true,
loading: false,
bleed: true,
})
})
it.each(['idle', 'ready', 'missing'] as const)(
'shows a built-in type without bleeding, count %s',
(countStatus) => {
expect(resolve({ hasBuiltin: true, countStatus })).toEqual({
view: 'builtin',
editable: false,
loading: false,
bleed: false,
})
},
)
it.each(['idle', 'missing', 'ready'] as const)(
'is not found and bleeds with no type at all, count %s',
(countStatus) => {
expect(resolve({ countStatus })).toEqual({
view: 'notFound',
editable: false,
loading: false,
bleed: true,
})
},
)
it('is not found for a custom type whose count went missing', () => {
expect(resolve({ hasCustom: true, countStatus: 'missing' }).view).toBe('notFound')
})
})
+5
View File
@@ -0,0 +1,5 @@
export type DeleteStage = 'closed' | 'blocked' | 'confirm' | 'failed' | 'conflict'
export function isConfirmStage(stage: DeleteStage) {
return stage === 'confirm' || stage === 'failed'
}
+6
View File
@@ -19,3 +19,9 @@ export function entriesLine(name: string, total: number) {
formatNumber(total),
])
}
export function entriesCountLine(name: string, total: number) {
return total === 1
? t('{0} has 1 entry', [name])
: t('{0} has {1} entries', [name, formatNumber(total)])
}
+88
View File
@@ -0,0 +1,88 @@
import type { EntryCountStatus } from '@/composables/useEntryCount'
export type SchemaTypeViewName =
| 'forbidden'
| 'skeleton'
| 'listsFailed'
| 'builtinListFailed'
| 'countFailed'
| 'editable'
| 'builtin'
| 'notFound'
| 'blank'
export interface SchemaTypeInputs {
listsForbidden: boolean
listsLoading: boolean
customFailed: boolean
builtinFailed: boolean
hasBuiltin: boolean
hasCustom: boolean
countStatus: EntryCountStatus
skeletonVisible: boolean
}
export interface SchemaTypeState {
view: SchemaTypeViewName
editable: boolean
loading: boolean
bleed: boolean
}
export function resolveSchemaTypeView(input: SchemaTypeInputs): SchemaTypeState {
const forbidden = input.listsForbidden || input.countStatus === 'forbidden'
const countPending =
input.hasCustom && (input.countStatus === 'idle' || input.countStatus === 'loading')
const loading = !forbidden && (input.listsLoading || countPending)
const settled = !forbidden && !loading
const editable = settled && input.hasCustom && input.countStatus === 'ready'
const listsFailed = settled && input.customFailed
const builtinListFailed = settled && !listsFailed && input.builtinFailed
const countFailed = settled && input.countStatus === 'failed'
const notFound =
settled &&
!listsFailed &&
!builtinListFailed &&
!countFailed &&
!input.hasBuiltin &&
!editable
return {
view: pickView({
forbidden,
skeleton: loading && input.skeletonVisible,
listsFailed,
builtinListFailed,
countFailed,
editable,
builtin: input.hasBuiltin,
notFound,
}),
editable,
loading,
bleed: forbidden || notFound || editable,
}
}
interface ViewFlags {
forbidden: boolean
skeleton: boolean
listsFailed: boolean
builtinListFailed: boolean
countFailed: boolean
editable: boolean
builtin: boolean
notFound: boolean
}
function pickView(flags: ViewFlags): SchemaTypeViewName {
if (flags.forbidden) return 'forbidden'
if (flags.skeleton) return 'skeleton'
if (flags.listsFailed) return 'listsFailed'
if (flags.builtinListFailed) return 'builtinListFailed'
if (flags.countFailed) return 'countFailed'
if (flags.editable) return 'editable'
if (flags.builtin) return 'builtin'
if (flags.notFound) return 'notFound'
return 'blank'
}
+1
View File
@@ -0,0 +1 @@
export const TOPBAR_ACTION_ID = 'topbar-action'