feat: edit an existing custom type
This commit is contained in:
1 parent
083cc9e23e
commit
8e344b2500
57 files changed
+5648
-425
No files matched your search
+348
-30
@@ -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>> = {
|
||||
|
||||
Reference in new issue
Block a user