feat: entry editor at /content/:key/new and /content/:key/:id
This commit is contained in:
1 parent
91d916fb74
commit
fba20f83c5
94 files changed
+7116
-423
No files matched your search
+204
-4
@@ -7095,7 +7095,7 @@ const contentRows = [
|
||||
by,
|
||||
]) => ({
|
||||
id,
|
||||
content_type_id: 1,
|
||||
content_type: 'events',
|
||||
data: {
|
||||
title,
|
||||
starts_on: startsOn,
|
||||
@@ -7155,10 +7155,9 @@ function contentEntries(url: URL) {
|
||||
total: 37,
|
||||
page: 1,
|
||||
size: 5,
|
||||
sort: 'updated_at:desc',
|
||||
}
|
||||
}
|
||||
return { items: contentRows, total: 7, page: 1, size: 25, sort: 'updated_at:desc' }
|
||||
return { items: contentRows, total: 7, page: 1, size: 25 }
|
||||
}
|
||||
|
||||
function contentApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
|
||||
@@ -7205,7 +7204,7 @@ const contentStates: Record<string, VisualState> = {
|
||||
api: contentApi({
|
||||
'GET /v1/content-type/events/entry': {
|
||||
status: 200,
|
||||
body: { items: [], total: 0, page: 1, size: 25, sort: 'updated_at:desc' },
|
||||
body: { items: [], total: 0, page: 1, size: 25 },
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -7226,10 +7225,211 @@ const contentStates: Record<string, VisualState> = {
|
||||
},
|
||||
}
|
||||
|
||||
const entryDescription =
|
||||
'Two days of hands-on work with **lime putty** and old stone.\n\n- Bring work clothes\n- Lunch is included'
|
||||
|
||||
function entryRecord(data: Record<string, unknown> = {}, rest: Record<string, unknown> = {}) {
|
||||
return {
|
||||
id: 42,
|
||||
content_type: 'events',
|
||||
data: {
|
||||
title: 'Lime plaster workshop',
|
||||
starts_on: '2026-11-21',
|
||||
ends_on: '2026-11-22',
|
||||
online: false,
|
||||
capacity: 18,
|
||||
category: 'Workshop',
|
||||
description: entryDescription,
|
||||
...data,
|
||||
},
|
||||
created_by: 4,
|
||||
updated_by: 3,
|
||||
created_at: '2026-10-12T09:14:00',
|
||||
updated_at: '2026-10-15T11:02:00',
|
||||
...rest,
|
||||
}
|
||||
}
|
||||
|
||||
function entryApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
|
||||
return {
|
||||
'GET /v1/auth/me': schemaSession('owner'),
|
||||
'GET /v1/content-type': { status: 200, body: schemaCustomTypes },
|
||||
'GET /v1/content-type/events': { status: 200, body: contentEventsType },
|
||||
'GET /v1/content-type/events/entry/42': { status: 200, body: entryRecord() },
|
||||
'GET /v1/user/lookup': { status: 200, body: contentUsers },
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
const entrySaved = entryRecord({}, { updated_by: 3, updated_at: '2026-10-19T11:02:00' })
|
||||
|
||||
function entryField(page: Page, key: string) {
|
||||
return page.locator(`[data-entry-field="${key}"]`)
|
||||
}
|
||||
|
||||
async function entryAppend(page: Page, key: string, text: string) {
|
||||
const input = entryField(page, key).locator('input, textarea').first()
|
||||
await input.click()
|
||||
await input.press('End')
|
||||
await input.pressSequentially(text)
|
||||
}
|
||||
|
||||
async function entrySave(page: Page) {
|
||||
await page.getByRole('button', { name: /^Save/ }).click()
|
||||
}
|
||||
|
||||
async function entryCapacityEdit(page: Page) {
|
||||
const input = entryField(page, 'capacity').getByRole('spinbutton')
|
||||
await input.fill('18')
|
||||
await input.press('Tab')
|
||||
}
|
||||
|
||||
const entryLowCapacity = entryApi({
|
||||
'GET /v1/content-type/events/entry/42': { status: 200, body: entryRecord({ capacity: 1 }) },
|
||||
})
|
||||
|
||||
function entryEdited(overrides: Record<string, ApiMock>): Record<string, ApiMock> {
|
||||
return { ...entryLowCapacity, ...overrides }
|
||||
}
|
||||
|
||||
const entryStates: Record<string, VisualState> = {
|
||||
'New entry': { path: '/content/events/new', api: entryApi() },
|
||||
Editing: { path: '/content/events/42', api: entryApi() },
|
||||
'Unsaved changes': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi(),
|
||||
steps: async (page) => {
|
||||
await entryAppend(page, 'title', ', two days')
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
'Markdown preview': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi(),
|
||||
steps: async (page) => {
|
||||
await page.getByRole('tab', { name: 'Preview' }).click()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
Saving: {
|
||||
path: '/content/events/42',
|
||||
api: entryApi({ 'PUT /v1/content-type/events/entry/42': { status: 200, hang: true } }),
|
||||
steps: async (page) => {
|
||||
await entryAppend(page, 'title', ', two days')
|
||||
await entrySave(page)
|
||||
await page.getByText('Saving…').first().waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
Saved: {
|
||||
path: '/content/events/42',
|
||||
api: entryEdited({
|
||||
'PUT /v1/content-type/events/entry/42': { status: 200, body: entrySaved },
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await entryCapacityEdit(page)
|
||||
await entrySave(page)
|
||||
await page.getByText('Entry saved').waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Save failed': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi({ 'PUT /v1/content-type/events/entry/42': { status: 500 } }),
|
||||
steps: async (page) => {
|
||||
await entryAppend(page, 'title', ', two days')
|
||||
await entrySave(page)
|
||||
await page.getByText("Couldn't save the entry").waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Required fields empty': {
|
||||
path: '/content/events/new',
|
||||
api: entryApi(),
|
||||
steps: async (page) => {
|
||||
await entrySave(page)
|
||||
await page.getByText('Fill in Title.').waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Rule errors': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi({
|
||||
'GET /v1/content-type/events/entry/42': {
|
||||
status: 200,
|
||||
body: entryRecord({
|
||||
title: 'Lime plaster workshop, two days of hands-on work with the whole team on site and lunc',
|
||||
ends_on: '31/11/2026',
|
||||
capacity: 6000,
|
||||
category: 'Seminar',
|
||||
}),
|
||||
},
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await entryAppend(page, 'title', 'h')
|
||||
await entrySave(page)
|
||||
await page.getByText('Title can be up to 80 characters.').waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Duplicate unique value (409)': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi({
|
||||
'PUT /v1/content-type/events/entry/42': {
|
||||
status: 409,
|
||||
body: 'invalid_entry: title: unique',
|
||||
},
|
||||
}),
|
||||
steps: async (page) => {
|
||||
const input = entryField(page, 'title').locator('input')
|
||||
await input.fill('Heat pumps: what the numbers say')
|
||||
await entrySave(page)
|
||||
await page.getByText('Another entry already uses this Title.').first().waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
},
|
||||
'Entry not found': {
|
||||
path: '/content/events/999',
|
||||
api: entryApi({
|
||||
'GET /v1/content-type/events/entry/999': { status: 404, body: 'entry_not_found' },
|
||||
}),
|
||||
},
|
||||
'Schema changed since load': {
|
||||
path: '/content/events/42',
|
||||
api: entryEdited({
|
||||
'PUT /v1/content-type/events/entry/42': {
|
||||
status: 422,
|
||||
body: 'invalid_entry: room: required',
|
||||
},
|
||||
}),
|
||||
steps: async (page) => {
|
||||
await entryCapacityEdit(page)
|
||||
await entrySave(page)
|
||||
await page.getByText('The fields of Events changed').waitFor()
|
||||
await schemaSettle(page)
|
||||
},
|
||||
exception: {
|
||||
maxPercent: { desktop: 16, mobile: 13 },
|
||||
reason: 'The light board screenshot of this state contains a stray miniature of the delete dialog state, about 12.6 % of the frame on its own',
|
||||
approvedBy: 'user',
|
||||
},
|
||||
},
|
||||
'Delete entry: confirm': {
|
||||
path: '/content/events/42',
|
||||
api: entryApi(),
|
||||
steps: async (page) => {
|
||||
await page.getByRole('button', { name: 'Delete', exact: true }).click()
|
||||
await page.getByRole('alertdialog').waitFor()
|
||||
await page.mouse.move(0, 0)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export const visualStates: Record<string, Record<string, VisualState>> = {
|
||||
sites: sitesStates,
|
||||
schema: schemaStates,
|
||||
content: contentStates,
|
||||
entry: entryStates,
|
||||
authors: authorsStates,
|
||||
activity: activityStates,
|
||||
profile: {
|
||||
|
||||
Reference in new issue
Block a user