refac: DELETE /v1/user/{id}?expected_role_id=N, user.remove audit event, 409 target_role_not_found, GetAll returns its error, Own-scope visibility filter moved into repos, JSON tags removed from use-case types

This commit is contained in:
Florian Sylvain committed 2026-10-29 16:28:25 +01:00
1 parent 5d6f378fce
commit b39a41be88
124 files changed
+8816 -523

No files matched your search

+172 -4
View File
@@ -45,8 +45,64 @@ const PAGINATION_STATES = [
]
const schemaBoard = 'RenewCMS Screens Schema.dc.html'
const rolesBoard = 'RenewCMS Screens Roles.dc.html'
const extraSources: Record<string, Source[]> = {
roles: [
{
board: rolesBoard,
group: '12 Toasts',
states: [
'Role changed',
'Role edited elsewhere (409)',
'Role deleted elsewhere',
"Role not changed: member's role changed first (409)",
"Ownership not transferred: target's role changed first (409)",
"User not removed: member's role changed first (409)",
],
alias: {},
},
{
board: rolesBoard,
group: '13 Inline errors',
states: [
'Role editor: save failed (500)',
'Change role: failed (500)',
'Delete role: target role deleted',
'Delete role: role gained members',
'Delete role: move failed (500)',
'Invite: invalid email',
'Invite: address already has an account (409)',
'Invite: role above yours (403)',
'Invite: send failed (500)',
],
alias: {},
},
{
board: rolesBoard,
group: '14 Read = Own',
states: [
'Role editor: Read set to Own',
'Articles list: Read = Own',
'Articles list: no drafts of their own',
'Entry list: Read = Own',
'Entry list: no entries of their own',
],
alias: {},
},
{
board: rolesBoard,
group: '15 409 conflicts',
states: [
'Stale save: banner',
'Stale save: review differences',
'Stale save: edits kept after reload',
"Change role: member's role changed first (409)",
"Transfer ownership: target's role changed first (409)",
],
alias: {},
},
],
schema: [
{
board: schemaBoard,
@@ -131,7 +187,7 @@ const extraSources: Record<string, Source[]> = {
{
board: schemaBoard,
group: '7 Relations',
states: ['Relation in the entry form'],
states: ['Relation in the entry form', 'Relation picker: loading more'],
alias: {},
},
{
@@ -342,6 +398,12 @@ const extraSources: Record<string, Source[]> = {
},
],
profile: [
{
board: rolesBoard,
group: '9 Profile',
states: ['Role card: custom role', 'Role card: Owner'],
alias: {},
},
{
board: officeBoard,
group: 'Profile: author profile',
@@ -765,10 +827,25 @@ function referenceWithoutGroup(name: string, markdown: string) {
return group && board ? [board[0], board[1]!, `"${group}"`] : null
}
function rolesScreen(): Screen {
const sources = extraSources.roles!
const [first] = sources
return {
name: 'roles',
board: first!.board,
group: first!.group,
states: sources.flatMap((source) => source.states),
sources,
}
}
async function readScreen(name: string): Promise<Screen> {
if (name === 'image-variants') {
return variantsScreen()
}
if (name === 'roles') {
return rolesScreen()
}
const markdown = await readFile(join(designRoot, 'screens', `${name}.md`), 'utf8')
const reference = VISUAL_REFERENCE.exec(markdown) ?? referenceWithoutGroup(name, markdown)
if (!reference) {
@@ -955,12 +1032,78 @@ async function openBoard(browser: Browser, origin: string, screen: Screen, theme
await serveIconsLocally(context)
const page = await context.newPage()
await page.goto(`${origin}/${encodeURIComponent(screen.board)}`)
await page.evaluate(() => document.fonts.ready)
await page.reload()
await page.locator(`section[data-screen-label="${screen.group}"]`).waitFor()
await page.getByRole('radio', { name: theme === 'dark' ? 'Dark' : 'Light' }).click()
await page.evaluate(() => document.fonts.ready)
return page
}
const REALIGNED_BOARD_STATES = new Set(['9j. Navigation: scroll-spy on Content types'])
const SCROLL_REALIGN_MAX_DRIFT = 60
const BOARD_SCROLL_OFFSETS = {
desktop: [32],
mobile: [64],
}
async function realignBoardScroll(frame: ReturnType<Page['locator']>, viewport: Viewport) {
await frame.evaluate(
(root, { offsets, maxDrift }) => {
const anchors = [...root.querySelectorAll<HTMLElement>('[data-anchor]')].filter(
(anchor) => !anchor.dataset.anchor?.startsWith('ep-'),
)
const first = anchors[0]
if (!first) {
return
}
let scroller: HTMLElement | null = first.parentElement
while (scroller && !['auto', 'scroll'].includes(getComputedStyle(scroller).overflowY)) {
scroller = scroller.parentElement
}
if (!scroller || scroller.scrollTop === 0) {
return
}
const offsetTopWithin = (element: HTMLElement, within: HTMLElement) => {
const parent = element.offsetParent as HTMLElement | null
return element.offsetTop + (parent && parent !== within ? parent.offsetTop : 0)
}
let best: number | null = null
for (const anchor of anchors) {
for (const offset of offsets) {
const target = Math.max(0, offsetTopWithin(anchor, scroller) - offset)
const drift = Math.abs(target - scroller.scrollTop)
if (
drift <= maxDrift &&
(best === null || drift < Math.abs(best - scroller.scrollTop))
) {
best = target
}
}
}
if (best !== null) {
scroller.scrollTop = best
}
},
{ offsets: BOARD_SCROLL_OFFSETS[viewport], maxDrift: SCROLL_REALIGN_MAX_DRIFT },
)
}
const STABLE_SAMPLES = 3
const STABLE_INTERVAL_MS = 200
const STABLE_MAX_POLLS = 40
async function waitForStableFrame(board: Page, frame: ReturnType<Page['locator']>) {
let previous: Buffer | null = null
let steady = 0
for (let poll = 0; poll < STABLE_MAX_POLLS && steady < STABLE_SAMPLES; poll++) {
const shot = await frame.screenshot({ animations: 'disabled' })
steady = previous !== null && shot.equals(previous) ? steady + 1 : 0
previous = shot
await board.waitForTimeout(STABLE_INTERVAL_MS)
}
}
async function captureDesign(
board: Page,
screen: Screen,
@@ -969,10 +1112,14 @@ async function captureDesign(
path: string,
) {
const found = await board.evaluate(
({ group, state, width, height }) => {
({ group, state, width, height, widths }) => {
document
.querySelectorAll('[data-visual-check]')
.forEach((element) => element.removeAttribute('data-visual-check'))
document.querySelectorAll<HTMLElement>('[data-visual-hidden]').forEach((element) => {
element.style.visibility = ''
element.removeAttribute('data-visual-hidden')
})
const section = [...document.querySelectorAll('section[data-screen-label]')].find(
(element) => element.getAttribute('data-screen-label') === group,
)
@@ -1007,9 +1154,26 @@ async function captureDesign(
container.style.height = `${height}px`
target.style.zoom = '1'
target.dataset.visualCheck = 'frame'
const frameWidths = new Set(widths.map((value) => `${value}px`))
for (const other of section!.querySelectorAll<HTMLElement>('div')) {
if (
other !== target &&
frameWidths.has(other.style.width) &&
!other.contains(target) &&
!target.contains(other)
) {
other.dataset.visualHidden = 'true'
other.style.visibility = 'hidden'
}
}
return true
},
{ group: screen.group, state, ...frames[viewport] },
{
group: screen.group,
state,
...frames[viewport],
widths: [frames.desktop.width, frames.mobile.width],
},
)
if (!found) {
throw new Error(`State "${state}" not found in group "${screen.group}" of ${screen.board}`)
@@ -1037,6 +1201,10 @@ async function captureDesign(
)
})
await board.waitForTimeout(settleMs)
if (REALIGNED_BOARD_STATES.has(state)) {
await realignBoardScroll(frame, viewport)
}
await waitForStableFrame(board, frame)
await frame.screenshot({ path, animations: 'disabled' })
}
@@ -1064,7 +1232,7 @@ async function captureApp(
viewport: { width, height },
colorScheme: theme,
reducedMotion: 'reduce',
...(setup.now ? { timezoneId: 'UTC' } : {}),
...(setup.now || setup.timezone ? { timezoneId: setup.timezone ?? 'UTC' } : {}),
})
await serveIconsLocally(context)
const unmocked = new Set<string>()