feat: edit an existing custom type

This commit is contained in:
Florian Sylvain committed 2026-10-20 04:42:50 +02:00
1 parent 083cc9e23e
commit 8e344b2500
57 files changed
+5648 -425

No files matched your search

+6
View File
@@ -71,6 +71,12 @@ const extraSources: Record<string, Source[]> = {
],
alias: {},
},
{
board: schemaBoard,
group: '3 Review and delete',
states: ['Review before saving', 'Applying', 'Refused (schema_incompatible)'],
alias: {},
},
],
sites: [
{
+348 -30
View File
@@ -6445,20 +6445,6 @@ const editorSeoStates: Record<string, VisualState> = {
},
}
const schemaCustomTypes = [
['events', 'Events', 7, '2026-10-15T09:30:00Z'],
['recipes', 'Recipes', 4, '2026-10-02T09:30:00Z'],
['press-mentions', 'Press mentions', 5, '2026-09-18T09:30:00Z'],
].map(([key, name, fieldCount, updatedAt], index) => ({
id: index + 1,
key,
name,
icon: 'files',
fields: Array.from({ length: fieldCount as number }, (_, field) => ({ key: `field_${field}` })),
created_at: '2026-09-01T09:30:00Z',
updated_at: updatedAt,
}))
function schemaField(label: string, key: string, type: string, rest: object = {}) {
return {
key,
@@ -6474,6 +6460,56 @@ function schemaField(label: string, key: string, type: string, rest: object = {}
}
}
const schemaEventsFields = [
schemaField('Title', 'title', 'text', {
required: true,
unique: true,
max: 80,
options: [],
}),
schemaField('Starts on', 'starts_on', 'date', {
required: true,
options: [],
}),
schemaField('Ends on', 'ends_on', 'date', { options: [] }),
schemaField('Online', 'online', 'boolean', { default: false, options: [] }),
schemaField('Capacity', 'capacity', 'number', {
min: 1,
max: 5000,
options: [],
}),
schemaField('Category', 'category', 'select', {
options: ['Talk', 'Workshop', 'Meetup'],
}),
schemaField('Description', 'description', 'richtext', { options: [] }),
]
const schemaRecipesFields = [
schemaField('Title', 'title', 'text', { required: true, options: [] }),
schemaField('Servings', 'servings', 'number', { min: 1, options: [] }),
schemaField('Vegetarian', 'vegetarian', 'boolean', { options: [] }),
schemaField('Method', 'method', 'richtext', { options: [] }),
]
const schemaCustomTypes = [
['events', 'Events', schemaEventsFields, '2026-10-15T09:30:00Z'],
['recipes', 'Recipes', schemaRecipesFields, '2026-10-02T09:30:00Z'],
[
'press-mentions',
'Press mentions',
Array.from({ length: 5 }, (_, field) => ({ key: `field_${field}` })),
'2026-09-18T09:30:00Z',
],
].map(([key, name, fields, updatedAt], index) => ({
id: index + 1,
key,
name,
icon: 'files',
fields,
created_at: '2026-09-01T09:30:00Z',
updated_at: updatedAt,
}))
const schemaArticleFields = [
schemaField('Title', 'title', 'text', { required: true, max: 200 }),
schemaField('Slug', 'slug', 'text', { required: true, unique: true }),
@@ -6502,7 +6538,11 @@ const schemaBuiltinTypes = [
function schemaSession(role: string): ApiMock {
return {
status: 200,
body: { ...(verifiedSession.body as object), display_name: 'Marta Alves', role },
body: {
...(verifiedSession.body as object),
display_name: 'Marta Alves',
role,
},
}
}
@@ -6511,16 +6551,119 @@ function schemaApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiM
'GET /v1/auth/me': schemaSession('owner'),
'GET /v1/content-type': { status: 200, body: schemaCustomTypes },
'GET /v1/builtin-type': { status: 200, body: schemaBuiltinTypes },
'GET /v1/content-type/events/entry': {
status: 200,
body: schemaEntryPage(37),
},
'GET /v1/content-type/recipes/entry': {
status: 200,
body: schemaEntryPage(0),
},
...overrides,
}
}
function schemaEntryPage(total: number) {
return { items: [], total, page: 1, size: 1 }
}
function schemaPut(response: ApiMock): Record<string, ApiMock> {
return { 'PUT /v1/content-type/events': response }
}
async function schemaSettle(page: Page) {
for (let pass = 0; pass < 2; pass++) {
await page.evaluate(() => {
document.querySelectorAll('*').forEach((element) => {
element.scrollTop = 0
})
})
await page.waitForTimeout(100)
}
await page.mouse.move(0, 0)
}
async function schemaFocusRow(page: Page, label: string) {
await schemaOpenRow(page, label)
await schemaCloseSheet(page)
}
async function schemaAddField(page: Page, label: string) {
await page.getByRole('button', { name: 'Add field' }).dispatchEvent('click')
await page.locator('input[name="label"]').fill(label)
}
async function schemaAddChoice(page: Page, text: string) {
const input = page.getByPlaceholder('Add a choice')
await input.fill(text)
await input.press('Enter')
}
async function schemaUnsavedEdits(page: Page) {
await schemaOpenRow(page, 'Capacity')
await page.locator('input[name="max"]').fill('2000')
await schemaCloseSheet(page)
await schemaAddField(page, 'Speaker')
await schemaCloseSheet(page)
await schemaAddField(page, 'Room')
await page.locator('label:has([role="radio"][value="select"])').click()
await page.getByRole('switch', { name: 'Required' }).click()
await schemaAddChoice(page, 'Main hall')
await schemaAddChoice(page, 'Studio')
await page.locator('[data-testid="field-inspector"] button[role="combobox"]').click()
await page.getByRole('option', { name: 'Main hall' }).click()
await schemaCloseSheet(page)
await schemaOpenRow(page, 'Description')
await page.getByRole('button', { name: 'Remove', exact: true }).click()
await page.getByText('Description will be removed').waitFor()
if (!schemaIsMobile(page)) {
await schemaOpenRow(page, 'Capacity')
}
await schemaSettle(page)
}
async function schemaApplyEdits(page: Page) {
await schemaUnsavedEdits(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
await page.getByRole('alertdialog').waitFor()
await page.mouse.move(0, 0)
}
async function schemaApply(page: Page) {
await schemaUnsavedEdits(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
await page.getByRole('button', { name: 'Apply changes' }).click()
}
async function schemaFieldErrors(page: Page) {
await schemaOpenRow(page, 'Ends on')
await page.locator('input[name="label"]').fill('')
await schemaCloseSheet(page)
await schemaOpenRow(page, 'Capacity')
await page.locator('input[name="min"]').fill('6000')
await schemaCloseSheet(page)
await schemaCategoryChoices(page)
if (!schemaIsMobile(page)) {
await schemaOpenRow(page, 'Capacity')
} else {
await schemaCloseSheet(page)
}
await schemaSettle(page)
}
async function schemaCategoryChoices(page: Page) {
await schemaOpenRow(page, 'Category')
await page.locator('[data-testid="choice-row"] input').nth(2).fill('')
await schemaAddChoice(page, 'Talk')
await page.getByText("Choices can't be blank.").first().waitFor()
}
function schemaIsMobile(page: Page) {
return (page.viewportSize()?.width ?? 0) < 768
}
async function schemaOpenRow(page: Page, label: string) {
await page.locator('[data-field-row]', { hasText: label }).first().click()
await page.locator('[data-field-row]', { hasText: label }).first().dispatchEvent('click')
}
async function schemaCloseSheet(page: Page) {
@@ -6550,12 +6693,38 @@ const schemaStates: Record<string, VisualState> = {
'GET /v1/auth/me': schemaSession('user'),
}),
},
'Built-in type: read-only view': { path: '/schema/articles', api: schemaApi() },
'Built-in type: read-only view': {
path: '/schema/articles',
api: schemaApi(),
},
'New type': { path: '/schema/new', api: schemaApi() },
'Editing an existing type': { path: '/schema/events', skip: 'pending' },
'Locked key: tooltip': { path: '/schema/events', skip: 'pending' },
'Unsaved changes': { path: '/schema/events', skip: 'pending' },
'Field errors': { path: '/schema/events', skip: 'pending' },
'Editing an existing type': { path: '/schema/events', api: schemaApi() },
'Locked key: tooltip': {
path: '/schema/events',
api: schemaApi(),
steps: async (page) => {
await schemaFocusRow(page, 'Starts on')
if (!schemaIsMobile(page)) {
await page.locator('input[name="key"]').hover()
await page.getByText('The key is used in URLs and the API').first().waitFor()
}
},
},
'Unsaved changes': {
path: '/schema/events',
api: schemaApi(),
steps: schemaUnsavedEdits,
},
'Field errors': {
exception: {
maxPercent: { desktop: 10, mobile: 11 },
reason: 'the board list header shrinks when the list overflows and shifts rows, the app scrolls instead',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(),
steps: schemaFieldErrors,
},
'Type-level errors': {
path: '/schema/new',
api: schemaApi(),
@@ -6571,16 +6740,105 @@ const schemaStates: Record<string, VisualState> = {
},
},
'Field key error': { path: '/schema/new', skip: 'pending' },
'Choice list editor': { path: '/schema/events', skip: 'pending' },
'Keyboard reorder': { path: '/schema/events', skip: 'pending' },
'API preview open': { path: '/schema/events', skip: 'pending' },
Saving: { path: '/schema/events', skip: 'pending' },
Saved: { path: '/schema/events', skip: 'pending' },
'Save failed': { path: '/schema/events', skip: 'pending' },
'Choice list editor': {
exception: {
maxPercent: { desktop: 10, mobile: 25 },
reason: 'the accepted spec requires a Move up, Move down and Remove row in the mobile field sheet that the board does not draw',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(),
steps: async (page) => {
await schemaOpenRow(page, 'Category')
await schemaAddChoice(page, 'x')
await page.locator('[data-testid="choice-row"] input').nth(3).fill('')
await schemaAddChoice(page, 'Talk')
await page.getByText('Two choices are the same.').first().waitFor()
await schemaSettle(page)
},
},
'Keyboard reorder': {
exception: {
maxPercent: { desktop: 15, mobile: 14 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame, and the board list header shrinks when the list overflows',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(),
steps: async (page) => {
await schemaFocusRow(page, 'Ends on')
await page.getByRole('button', { name: 'Move Ends on' }).focus()
await page.keyboard.press('Space')
await page.keyboard.press('ArrowUp')
await page.keyboard.press('Space')
await page.getByText('Unsaved changes').first().waitFor()
await page.getByRole('button', { name: 'Move Ends on' }).focus()
await page.keyboard.press('Space')
await page.getByText('Moving Ends on. Position 2 of 7.').first().waitFor()
},
},
'API preview open': {
exception: {
maxPercent: { desktop: 11, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(),
steps: async (page) => {
if (schemaIsMobile(page)) {
await page.locator('[data-testid="api-row"]').click()
await page.getByRole('dialog').waitFor()
} else {
await page.locator('[data-testid="api-preview-toggle"]').click()
await page.locator('[data-testid="api-json"]').waitFor()
}
await schemaSettle(page)
},
},
Saving: {
exception: {
maxPercent: { desktop: 27, mobile: 17 },
reason: 'the board draws Saving with 37 entries and no review, the app always reviews first so the setup has no entries and no info bar; the design board draws a miniature of the board and overlays in the desktop frame',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi({
'GET /v1/content-type/events/entry': {
status: 200,
body: schemaEntryPage(0),
},
...schemaPut({ status: 200, hang: true }),
}),
steps: async (page) => {
await schemaUnsavedEdits(page)
await page.getByRole('button', { name: 'Save', exact: true }).click()
await page.getByText('Saving…').first().waitFor()
await schemaSettle(page)
},
},
Saved: { path: '/schema/events', skip: 'clock-sensitive' },
'Save failed': {
exception: {
maxPercent: { desktop: 12, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame, and the board list header shrinks when the list overflows',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(schemaPut({ status: 500 })),
steps: async (page) => {
await schemaApply(page)
await page.getByText("Couldn't save the content type").waitFor()
await schemaSettle(page)
},
},
'Key already taken': {
path: '/schema/new',
api: schemaApi({
'POST /v1/content-type': { status: 409, body: 'content_type_key_taken' },
'POST /v1/content-type': {
status: 409,
body: 'content_type_key_taken',
},
}),
steps: async (page) => {
await page.getByPlaceholder('For example, Events').fill('Events')
@@ -6599,8 +6857,68 @@ const schemaStates: Record<string, VisualState> = {
await page.mouse.move(0, 0)
},
},
'Schema incompatible': { path: '/schema/events', skip: 'pending' },
Offline: { path: '/schema/events', skip: 'pending' },
'Schema incompatible': {
exception: {
maxPercent: { desktop: 13, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame, and the board list header shrinks when the list overflows',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(schemaPut({ status: 409, body: 'schema_incompatible' })),
steps: async (page) => {
await schemaApply(page)
await page.getByRole('button', { name: 'Back to editing' }).click()
await page.getByRole('button', { name: 'Review changes' }).waitFor()
await schemaSettle(page)
},
},
Offline: {
path: '/schema/events',
api: schemaApi(),
steps: async (page) => {
await schemaUnsavedEdits(page)
await goOffline(page)
await schemaSettle(page)
},
},
'Review before saving': {
exception: {
maxPercent: { desktop: 15, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame, and the board list header shrinks when the list overflows',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(),
steps: schemaApplyEdits,
},
Applying: {
exception: {
maxPercent: { desktop: 42, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame and the overlay of another viewport',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(schemaPut({ status: 200, hang: true })),
steps: async (page) => {
await schemaApply(page)
await page.getByRole('button', { name: 'Applying…' }).waitFor()
await schemaSettle(page)
},
},
'Refused (schema_incompatible)': {
exception: {
maxPercent: { desktop: 43, mobile: 10 },
reason: 'the design board draws a miniature of the board and overlays in the desktop frame and the overlay of another viewport',
approvedBy: 'user',
},
path: '/schema/events',
api: schemaApi(schemaPut({ status: 409, body: 'schema_incompatible' })),
steps: async (page) => {
await schemaApply(page)
await page.getByText('Your content type is as it was').waitFor()
await schemaSettle(page)
},
},
}
export const visualStates: Record<string, Record<string, VisualState>> = {