feat(activity): update summary cards to toggles and introduce export menu

This commit is contained in:
Florian Sylvain committed 2026-10-12 05:13:25 +02:00
1 parent 73f4b539fe
commit 03b39e64f2
30 files changed
+2376 -238

No files matched your search

+48 -2
View File
@@ -41,8 +41,17 @@ export const EXPORT_PATH = '/v1/activity/export'
export const USERS_PATH = '/v1/user/'
export const KEYS_PATH = '/v1/api-key/'
export const CSV_HEADER = 'when,actor,action,target,outcome,source,ip'
export const CSV_WITH_ROWS = `${CSV_HEADER}\n2026-10-06,Marta Alves,Published,Spring release,success,web,203.0.113.24\n2026-10-05,Ines Duarte,Deleted,old-banner.png,success,web,203.0.113.12\n`
export const CSV_HEADER_ONLY = `${CSV_HEADER}\n`
export function csvBlob(text: string) {
return new Blob([text], { type: 'text/csv' })
}
export const harness = {
calls: [] as RecordedCall[],
signals: [] as (AbortSignal | undefined)[],
wrapper: null as VueWrapper | null,
}
@@ -65,12 +74,13 @@ export async function mountActivity(options: MountOptions = {}) {
const api: ActivityApiMock = {
list: () => pageOf(activityItems),
summary: () => summaryDefault,
exportFile: () => new Blob(['when,actor'], { type: 'text/csv' }),
exportFile: () => csvBlob(CSV_WITH_ROWS),
users: () => [],
keys: () => [],
...options,
}
harness.calls = []
harness.signals = []
counters.clear()
setViewportWidth(desktop ? DESKTOP_WIDTH : MOBILE_WIDTH)
setActivePinia(createPinia())
@@ -105,7 +115,8 @@ export async function mountActivity(options: MountOptions = {}) {
throw new Error(`Unexpected request: ${path}`)
}
})
const requestBlob = vi.fn(async (path: string) => {
const requestBlob = vi.fn(async (path: string, options?: RequestInit) => {
harness.signals.push(options?.signal ?? undefined)
const url = record(path)
return respond(api.exportFile, url.searchParams, url.pathname) as Blob
})
@@ -204,3 +215,38 @@ export async function expectIcon(root: Element | null | undefined, name: string,
await new Promise((resolve) => setTimeout(resolve, 50))
expect(hasIcon(root, name)).toBe(false)
}
export function exportButton() {
return findButton('Export') ?? findButton('Exporter')
}
export function menuItems() {
return Array.from(document.body.querySelectorAll<HTMLElement>('[role="menuitem"]'))
}
export function menuItem(label: string) {
return menuItems().find((item) => textOf(item).startsWith(label))
}
export async function openExportMenu() {
await click(exportButton())
await vi.waitFor(() => expect(menuItems().length).toBe(2))
}
export function dialogElement() {
return document.body.querySelector<HTMLElement>('[role="dialog"]')
}
export function dialogButton(label: string) {
return Array.from(dialogElement()?.querySelectorAll<HTMLButtonElement>('button') ?? []).find(
(button) => textOf(button) === label || button.getAttribute('aria-label') === label,
)
}
export function dialogButtons() {
return Array.from(
dialogElement()?.querySelectorAll<HTMLButtonElement>(
'footer button, [data-slot="footer"] button',
) ?? [],
).map(textOf)
}