feat: built-in type read-only view,s chema list, states, spec files
This commit is contained in:
1 parent
3c67cd2314
commit
f514146fed
24 files changed
+1220
-88
No files matched your search
@@ -0,0 +1,56 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { BuiltinField } from '@/composables/useContentTypes'
|
||||
import { fieldIcon, fieldRules, fieldTypeLabel } from '@/lib/schemaFields'
|
||||
|
||||
function field(overrides: Partial<BuiltinField>): BuiltinField {
|
||||
return {
|
||||
key: 'f',
|
||||
label: 'F',
|
||||
type: 'text',
|
||||
required: false,
|
||||
unique: false,
|
||||
default: null,
|
||||
min: null,
|
||||
max: null,
|
||||
options: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('field types', () => {
|
||||
it.each([
|
||||
['text', 'Text', 'i-lucide-type'],
|
||||
['richtext', 'Rich text', 'i-lucide-pilcrow'],
|
||||
['number', 'Number', 'i-lucide-hash'],
|
||||
['boolean', 'Yes or no', 'i-lucide-toggle-left'],
|
||||
['date', 'Date', 'i-lucide-calendar'],
|
||||
['select', 'Choice', 'i-lucide-list'],
|
||||
])('%s reads %s with %s', (type, label, icon) => {
|
||||
expect(fieldTypeLabel(type)).toBe(label)
|
||||
expect(fieldIcon(type)).toBe(icon)
|
||||
})
|
||||
})
|
||||
|
||||
describe('field rules', () => {
|
||||
it.each([
|
||||
[{ type: 'text', required: true, max: 200 }, 'Required · up to 200 characters'],
|
||||
[{ type: 'text', required: true, unique: true }, 'Required · unique'],
|
||||
[{ type: 'richtext', max: 10000 }, 'Up to 10,000 characters'],
|
||||
[{ type: 'boolean', default: false }, 'Off by default'],
|
||||
[{ type: 'boolean', default: true }, 'On by default'],
|
||||
[{ type: 'date' }, ''],
|
||||
[{ type: 'text', min: 3, max: 80 }, '3 to 80 characters'],
|
||||
[{ type: 'text', min: 3 }, 'At least 3 characters'],
|
||||
[{ type: 'number', min: 1, max: 5000 }, 'Between 1 and 5000'],
|
||||
[{ type: 'number', min: 1 }, 'At least 1'],
|
||||
[{ type: 'number', max: 5000 }, 'At most 5000'],
|
||||
[{ type: 'select', options: ['a', 'b', 'c'] }, '3 choices'],
|
||||
[{ type: 'text', default: 'Draft' }, 'Default: Draft'],
|
||||
])('%j reads %s', (settings, rules) => {
|
||||
expect(fieldRules(field(settings as Partial<BuiltinField>))).toBe(rules)
|
||||
})
|
||||
|
||||
it('reads a missing setting as absent', () => {
|
||||
expect(fieldRules({ key: 'k', label: 'K', type: 'date' } as BuiltinField)).toBe('')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { t, formatNumber } from '@/i18n'
|
||||
import type { BuiltinField } from '@/composables/useContentTypes'
|
||||
|
||||
const RULE_SEPARATOR = ' · '
|
||||
|
||||
const FIELD_ICONS: Record<string, string> = {
|
||||
text: 'i-lucide-type',
|
||||
richtext: 'i-lucide-pilcrow',
|
||||
number: 'i-lucide-hash',
|
||||
boolean: 'i-lucide-toggle-left',
|
||||
date: 'i-lucide-calendar',
|
||||
select: 'i-lucide-list',
|
||||
}
|
||||
|
||||
export function fieldIcon(type: string) {
|
||||
return FIELD_ICONS[type] ?? FIELD_ICONS.text!
|
||||
}
|
||||
|
||||
export function fieldTypeLabel(type: string) {
|
||||
switch (type) {
|
||||
case 'richtext':
|
||||
return t('Rich text')
|
||||
case 'number':
|
||||
return t('Number')
|
||||
case 'boolean':
|
||||
return t('Yes or no')
|
||||
case 'date':
|
||||
return t('Date')
|
||||
case 'select':
|
||||
return t('Choice')
|
||||
default:
|
||||
return t('Text')
|
||||
}
|
||||
}
|
||||
|
||||
function present<T>(value: T | null | undefined): value is T {
|
||||
return value !== null && value !== undefined
|
||||
}
|
||||
|
||||
function lengthRule(min: number | null | undefined, max: number | null | undefined) {
|
||||
if (present(min) && present(max)) {
|
||||
return t('{0} to {1} characters', [formatNumber(min), formatNumber(max)])
|
||||
}
|
||||
if (present(max)) {
|
||||
return t('up to {0} characters', [formatNumber(max)])
|
||||
}
|
||||
if (present(min)) {
|
||||
return t('at least {0} characters', [formatNumber(min)])
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function rangeRule(min: number | null | undefined, max: number | null | undefined) {
|
||||
if (present(min) && present(max)) {
|
||||
return t('between {0} and {1}', [min, max])
|
||||
}
|
||||
if (present(max)) {
|
||||
return t('at most {0}', [max])
|
||||
}
|
||||
if (present(min)) {
|
||||
return t('at least {0}', [min])
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
function defaultRule(field: BuiltinField) {
|
||||
if (field.type === 'boolean') {
|
||||
return field.default === true
|
||||
? t('on by default')
|
||||
: field.default === false
|
||||
? t('off by default')
|
||||
: ''
|
||||
}
|
||||
return present(field.default) && field.default !== ''
|
||||
? t('default: {0}', [String(field.default)])
|
||||
: ''
|
||||
}
|
||||
|
||||
function constraintRule(field: BuiltinField) {
|
||||
if (field.type === 'text' || field.type === 'richtext') {
|
||||
return lengthRule(field.min, field.max)
|
||||
}
|
||||
if (field.type === 'number') {
|
||||
return rangeRule(field.min, field.max)
|
||||
}
|
||||
if (field.type === 'select' && Array.isArray(field.options) && field.options.length > 0) {
|
||||
return t('{0} choices', [field.options.length])
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
export function fieldRules(field: BuiltinField) {
|
||||
const parts = [
|
||||
field.required ? t('required') : '',
|
||||
constraintRule(field),
|
||||
field.unique ? t('unique') : '',
|
||||
defaultRule(field),
|
||||
].filter(Boolean)
|
||||
if (parts.length === 0) {
|
||||
return ''
|
||||
}
|
||||
const joined = parts.join(RULE_SEPARATOR)
|
||||
return joined.charAt(0).toUpperCase() + joined.slice(1)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { t, formatNumber } from '@/i18n'
|
||||
import type { BuiltinType, ContentType } from '@/composables/useContentTypes'
|
||||
import { formatAbsoluteDateFromIso } from '@/lib/relativeDate'
|
||||
|
||||
export const EM_DASH = '—'
|
||||
export const SCHEMA_TABLE_QUERY = '(min-width: 40rem)'
|
||||
|
||||
export interface SchemaRow {
|
||||
id: string
|
||||
@@ -11,6 +11,7 @@ export interface SchemaRow {
|
||||
fieldCount: number
|
||||
updated: string
|
||||
builtin: boolean
|
||||
to?: string
|
||||
}
|
||||
|
||||
export function customRow(type: ContentType): SchemaRow {
|
||||
@@ -30,8 +31,9 @@ export function builtinRow(type: BuiltinType): SchemaRow {
|
||||
name: type.name,
|
||||
key: type.key,
|
||||
fieldCount: type.fields.length,
|
||||
updated: EM_DASH,
|
||||
updated: t('Managed by RenewCMS'),
|
||||
builtin: true,
|
||||
to: `/schema/${type.key}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user