feat(SPA): schema list and the sidebar Content group

This commit is contained in:
Florian Sylvain committed 2026-10-19 19:34:45 +02:00
1 parent 73a75d5cb9
commit 3c67cd2314
34 files changed
+2181 -18

No files matched your search

+32 -6
View File
@@ -418,6 +418,10 @@ const settleMs = 400
const diffThreshold = 24
const maxDiffPercent = 10
const shellApi: Record<string, ApiMock> = {
'GET /v1/content-type': { status: 200, body: [] },
}
function budgetFor(cap: DiffException['maxPercent'] | undefined, viewport: Viewport) {
if (cap === undefined) {
return maxDiffPercent
@@ -595,6 +599,15 @@ function groupSectionRows(markdown: string, group: string) {
return [...section.matchAll(/^\| \d+ \| ([^|]+?) \|/gm)].map((match) => match[1]!.trim())
}
const boardGroupLabels: Record<string, Record<string, string>> = {
schema: {
'Schema list': '1 Schema list',
'Schema builder': '2 Schema builder',
'Review and delete': '3 Review and delete',
'Field types': '4 Field types',
},
}
const referenceGroups: Record<string, string> = {
'integration-api-keys': 'API keys',
}
@@ -614,7 +627,9 @@ async function readScreen(name: string): Promise<Screen> {
if (!reference) {
throw new Error(`docs/design/screens/${name}.md has no visual reference line`)
}
const referenceGroups = [...reference[2]!.matchAll(/"([^"]+)"/g)].map((match) => match[1]!)
const referenceGroups = [...reference[2]!.matchAll(/"([^"]+)"/g)].map(
(match) => boardGroupLabels[name]?.[match[1]!] ?? match[1]!,
)
const numberedRows = [...markdown.matchAll(/^\| \d+ \| ([^|]+?) \|/gm)].map((match) =>
match[1]!.trim(),
)
@@ -813,7 +828,14 @@ async function captureDesign(
const title = headers.find(
(div) => div.querySelector('span')?.textContent?.trim() === state,
)
const block = title?.parentElement
const wideBlock = section
? [...section.querySelectorAll<HTMLElement>('div')].find(
(div) =>
div.style.width === '1037px' &&
div.querySelector('span > .sc-interp')?.textContent?.trim() === state,
)
: undefined
const block = title ? title.parentElement : wideBlock
const target = block
? [...block.querySelectorAll<HTMLElement>('div')].find(
(div) => div.style.width === `${width}px`,
@@ -890,6 +912,7 @@ async function captureApp(
})
await serveIconsLocally(context)
const unmocked = new Set<string>()
const fallback = new Set<string>()
const calls = new Map<string, number>()
await context.route('**/v1/**', async (route) => {
const request = route.request()
@@ -897,7 +920,10 @@ async function captureApp(
const key = `${request.method()} ${url.pathname}`
const call = (calls.get(key) ?? 0) + 1
calls.set(key, call)
const mock = setup.api?.[key]
const mock = setup.api?.[key] ?? shellApi[key]
if (mock && !setup.api?.[key]) {
fallback.add(key)
}
if (!mock) {
unmocked.add(key)
await route.fulfill({ status: 404, body: '' })
@@ -932,7 +958,7 @@ async function captureApp(
await page.waitForTimeout(settleMs)
await page.screenshot({ path, animations: 'disabled' })
await context.close()
return [...unmocked]
return { unmocked: [...unmocked], fallback: [...fallback] }
}
async function diff(browser: Browser, designPath: string, appPath: string, diffPath: string) {
@@ -1053,7 +1079,7 @@ async function main() {
const appPath = join(directory, `app-${suffix}`)
const diffPath = join(directory, `diff-${suffix}`)
const unmocked = await captureApp(
const { unmocked, fallback } = await captureApp(
browser,
app.origin,
setup,
@@ -1080,7 +1106,7 @@ async function main() {
? ` [exception ${describeCap(setup.exception.maxPercent)} approved by ${setup.exception.approvedBy}: ${setup.exception.reason}]`
: ''
lines.push(
`${problems.length ? 'FAIL' : 'PASS'} ${state} · ${viewport} · ${theme}: ${percent.toFixed(2)}% pixels differ → ${relative(repoRoot, directory)}${problems.length ? ` (${problems.join('; ')})` : ''}${exception}`,
`${problems.length ? 'FAIL' : 'PASS'} ${state} · ${viewport} · ${theme}: ${percent.toFixed(2)}% pixels differ → ${relative(repoRoot, directory)}${problems.length ? ` (${problems.join('; ')})` : ''}${exception}${fallback.length ? ` [shellApi fallback: ${fallback.join(', ')}]` : ''}`,
)
}
await board.context().close()
+66
View File
@@ -6445,8 +6445,74 @@ 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,
}))
const schemaBuiltinTypes = [
['articles', 'Articles', 5],
['authors', 'Authors', 5],
['sites', 'Sites', 8],
['media', 'Media', 6],
['users', 'Users', 6],
].map(([key, name, fieldCount]) => ({
key,
name,
fields: Array.from({ length: fieldCount as number }, (_, field) => ({ key: `field_${field}` })),
}))
function schemaSession(role: string): ApiMock {
return {
status: 200,
body: { ...(verifiedSession.body as object), display_name: 'Marta Alves', role },
}
}
function schemaApi(overrides: Record<string, ApiMock> = {}): Record<string, ApiMock> {
return {
'GET /v1/auth/me': schemaSession('owner'),
'GET /v1/content-type': { status: 200, body: schemaCustomTypes },
'GET /v1/builtin-type': { status: 200, body: schemaBuiltinTypes },
...overrides,
}
}
const schemaStates: Record<string, VisualState> = {
Populated: { path: '/schema', api: schemaApi() },
Loading: {
path: '/schema',
api: schemaApi({ 'GET /v1/content-type': { status: 200, hang: true } }),
},
'Empty: no custom type yet': {
path: '/schema',
api: schemaApi({ 'GET /v1/content-type': { status: 200, body: [] } }),
},
Error: {
path: '/schema',
api: schemaApi({ 'GET /v1/content-type': { status: 500 } }),
},
'Forbidden (403)': {
path: '/schema',
api: schemaApi({
'GET /v1/auth/me': schemaSession('user'),
}),
},
'Built-in type: read-only view': { path: '/schema', skip: 'pending' },
}
export const visualStates: Record<string, Record<string, VisualState>> = {
sites: sitesStates,
schema: schemaStates,
authors: authorsStates,
activity: activityStates,
profile: {