mirror of
https://github.com/azaion/ui.git
synced 2026-06-21 08:21:11 +00:00
admin v2: implement design from ui_design/v2/plugin/admin.html
- Design system: v2 CSS variables (surface-0/1/2, border-hair, accent-amber/cyan/red/green/blue)
and utility classes (.btn, .inp, .pill, .chip, .bracket, .panel, .seg, .swatch,
.type-sq, .grid-bg, .ibtn, .checkbox, .tab); v1 az-* names aliased to v2 vars
so other pages still render. Google Fonts (IBM Plex Sans + JetBrains Mono)
loaded via <link> in index.html <head> to avoid FOUT.
- Header rebuilt to v2: amber wordmark + // divider, amber-bordered flight pill
with cyan live dot, tab-style nav with amber underline on active, LINK status
pill, cog + sign-out icon buttons.
- AdminPage rewritten to 3-column layout (340 / flex / 280):
- Detection Classes: search + ADD button, table with #/Name/Hex/Ops columns,
name-only inline edit with ringed swatch, sibling-row error alert.
- AI Recognition Engine + GPS Device Link panels with corner-bracket borders,
number steppers, segmented protocol control, dashed telemetry footers.
Hooks (useAiSettings, useGpsSettings) seed factory defaults so the UI is
interactive when GET fails (no backend).
- Default Aircrafts: P/C/F type chips, isDefault star toggle, + ADD AIRCRAFT
modal with model/type/resolution/maxMinutes/default fields.
- Co-located components: Modal (backdrop + ESC + body-scroll-lock),
NumberStepper (▲▼ with clamp on click but not on typing), ClassEditRow.
- Types: Aircraft extended with FixedWing + optional resolution/maxMinutes;
new AiRecognitionSettings/Telemetry, GpsDeviceSettings/Telemetry, GpsProtocol.
- Endpoints: /api/admin/ai-settings, /api/admin/gps-settings (+ /ping, /reconnect).
POST /api/flights/aircrafts (plural REST collection).
- MSW: stateful admin-settings handler with resetAdminSettingsSeed() wired into
tests/setup.ts. Aircraft seed expanded to 6 entries matching the mockup.
- i18n: full admin.{classes,aiEngine,gpsDevice,aircrafts} key sets in en+ua;
nav.dataset shortened to "Dataset"; obsolete users-management keys removed.
- Tests: new AdminPage AI/GPS/aircraft test suites; admin_class_edit selectors
updated for the name-only inline editor and the modal-based add flow.
This commit is contained in:
@@ -105,14 +105,12 @@ describe('AZ-512 / AdminPage — inline detection-class edit', () => {
|
||||
// Act
|
||||
await clickEdit('1')
|
||||
|
||||
// Assert — form is visible inside row 1.
|
||||
// Assert — name input is visible inside row 1 (v2 minimal edit:
|
||||
// only the name is editable inline; shortName/color/maxSizeM are
|
||||
// preserved in form state and sent on save).
|
||||
const row1 = getRow('1')
|
||||
const nameInput = within(row1).getByDisplayValue('class-a') as HTMLInputElement
|
||||
expect(nameInput).toBeInTheDocument()
|
||||
const shortInput = within(row1).getByDisplayValue('a') as HTMLInputElement
|
||||
expect(shortInput).toBeInTheDocument()
|
||||
const maxSize = within(row1).getByDisplayValue('7') as HTMLInputElement
|
||||
expect(maxSize).toBeInTheDocument()
|
||||
|
||||
// Assert — row 2 stays read-only: the row still shows the plain text name.
|
||||
const row2 = getRow('2')
|
||||
@@ -246,31 +244,17 @@ describe('AZ-512 / AdminPage — inline detection-class edit', () => {
|
||||
// Act
|
||||
await userEvent.click(within(row1).getByRole('button', { name: /^save$|^зберегти$/i }))
|
||||
|
||||
// Assert — no PATCH; error alert rendered.
|
||||
// Assert — no PATCH; error alert rendered (v2 renders the alert in
|
||||
// a sibling tr below the edit row, not inside row1 itself).
|
||||
expect(patchCalls.length).toBe(0)
|
||||
const alert = within(row1).getByRole('alert')
|
||||
const alert = screen.getByRole('alert')
|
||||
expect(alert.textContent ?? '').toMatch(/name is required|назва обов/i)
|
||||
})
|
||||
|
||||
it('non-positive maxSizeM → no PATCH; maxSizeMustBePositive error visible', async () => {
|
||||
// Arrange
|
||||
const patchCalls = capturePatchCalls()
|
||||
renderWithProviders(<AdminPage />)
|
||||
await screen.findByText('class-a')
|
||||
await clickEdit('1')
|
||||
const row1 = getRow('1')
|
||||
const maxInput = within(row1).getByDisplayValue('7') as HTMLInputElement
|
||||
await userEvent.clear(maxInput)
|
||||
await userEvent.type(maxInput, '0')
|
||||
|
||||
// Act
|
||||
await userEvent.click(within(row1).getByRole('button', { name: /^save$|^зберегти$/i }))
|
||||
|
||||
// Assert — no PATCH; error alert rendered.
|
||||
expect(patchCalls.length).toBe(0)
|
||||
const alert = within(row1).getByRole('alert')
|
||||
expect(alert.textContent ?? '').toMatch(/positive|додатнім/i)
|
||||
})
|
||||
// The maxSizeM field is no longer editable inline in v2 (mockup shows
|
||||
// name-only). The original "non-positive maxSizeM" validation test is
|
||||
// removed — the constraint is now enforced by a separate edit-class
|
||||
// flow (not yet built) rather than inline.
|
||||
})
|
||||
|
||||
describe('AC-6: backend error is surfaced inline', () => {
|
||||
@@ -299,10 +283,11 @@ describe('AZ-512 / AdminPage — inline detection-class edit', () => {
|
||||
// Act
|
||||
await userEvent.click(within(row1).getByRole('button', { name: /^save$|^зберегти$/i }))
|
||||
|
||||
// Assert — PATCH happened, error rendered, form still open, no alert().
|
||||
// Assert — PATCH happened, error rendered (in a sibling tr), form
|
||||
// still open, no alert().
|
||||
await waitFor(() => expect(patchCount).toBe(1))
|
||||
const row1After = getRow('1')
|
||||
const alert = await within(row1After).findByRole('alert')
|
||||
const alert = await screen.findByRole('alert')
|
||||
expect(alert.textContent ?? '').toMatch(/update failed|не вдалося оновити/i)
|
||||
expect(within(row1After).getByDisplayValue('will-fail')).toBeInTheDocument()
|
||||
expect(alertCalls).toBe(0)
|
||||
@@ -317,7 +302,7 @@ describe('AZ-512 / AdminPage — inline detection-class edit', () => {
|
||||
// Arrange — capture POST; second GET returns 3 classes.
|
||||
const postCalls: { body: unknown }[] = []
|
||||
let getCount = 0
|
||||
const NEW_CLASS: DetectionClass = { id: 3, name: 'fresh', shortName: '', color: '#FF0000', maxSizeM: 7, photoMode: 0 }
|
||||
const NEW_CLASS: DetectionClass = { id: 3, name: 'fresh', shortName: '', color: '#FF9D3D', maxSizeM: 7, photoMode: 0 }
|
||||
server.use(
|
||||
http.post('/api/admin/classes', async ({ request }) => {
|
||||
postCalls.push({ body: await request.json() })
|
||||
@@ -332,13 +317,15 @@ describe('AZ-512 / AdminPage — inline detection-class edit', () => {
|
||||
renderWithProviders(<AdminPage />)
|
||||
await screen.findByText('class-a')
|
||||
|
||||
// Act — scope to the classes table panel (both the class-add row and
|
||||
// the user-add row use placeholder="Name" + a `+` button; disambiguate
|
||||
// by walking up from the class-a cell to the enclosing panel).
|
||||
const classesPanel = (getRow('1').closest('table') as HTMLElement).parentElement as HTMLElement
|
||||
const addNameInput = within(classesPanel).getByPlaceholderText('Name') as HTMLInputElement
|
||||
await userEvent.type(addNameInput, 'fresh')
|
||||
await userEvent.click(within(classesPanel).getByRole('button', { name: '+' }))
|
||||
// Act — v2 layout: click the top "+ ADD" button to open an inline
|
||||
// add-row at the top of the table, type the name, click the save
|
||||
// (cyan checkmark, aria-label "Save") icon button.
|
||||
const classesPanel = getRow('1').closest('aside') as HTMLElement
|
||||
await userEvent.click(within(classesPanel).getByRole('button', { name: /^\+ add$|^\+ додати$/i }))
|
||||
const addRow = within(classesPanel).getByText('+', { selector: 'td' }).closest('tr') as HTMLElement
|
||||
const nameInput = within(addRow).getByPlaceholderText('Name') as HTMLInputElement
|
||||
await userEvent.type(nameInput, 'fresh')
|
||||
await userEvent.click(within(addRow).getByRole('button', { name: /^save$|^зберегти$/i }))
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(postCalls.length).toBe(1))
|
||||
|
||||
Reference in New Issue
Block a user