feat: guard CommandBar busy actions (AEG-V16-019)
Keep action order while preventing disabled or busy actions from emitting a command. Preserve test evidence; status remains IN_PROGRESS pending predecessor acceptance.
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
# AEG-V16-019 — CommandBar
|
||||
|
||||
## Scope
|
||||
|
||||
- **WBS / Requirement / UI / Test:** AEG-V16-019 / REQ-V16-FEC-03 / UI-V16-FEC-03 / T-V16-FEC-03
|
||||
- **Classification:** shared FE component vertical slice; no API, policy, data, or provider contract change.
|
||||
|
||||
## Source / Assumption / Unknown / Decision Required
|
||||
|
||||
- **Source:** WBS Master, `KsCommandBar.vue`, `KsButton.vue`, and v4 native/Prime button adapters.
|
||||
- **Assumption:** callers own command idempotency keys; this component prevents a local disabled/busy click from becoming a second UI event but cannot replace server-side idempotency.
|
||||
- **Unknown:** predecessor AEG-V16-018 remains pending formal UX/a11y acceptance evidence.
|
||||
- **Decision Required:** no new action ordering policy is introduced. Callers supply the approved action array order.
|
||||
|
||||
## Acceptance mapping
|
||||
|
||||
| Acceptance requirement | Implementation evidence |
|
||||
| --- | --- |
|
||||
| action order | the component renders the supplied readonly array in order. |
|
||||
| disabled/busy | execution is blocked in the command boundary for either state; busy is exposed to assistive technology. |
|
||||
| reproducibility | unit test covers order, event emission, disabled, and busy paths. |
|
||||
|
||||
## Execution evidence — 2026-08-08
|
||||
|
||||
- `frontend: pnpm typecheck`: passed.
|
||||
- `frontend: pnpm test -- --run src/shared/ui/components/tests/KsCommandBar.spec.ts`: 1 file / 1 test passed.
|
||||
@@ -10,6 +10,7 @@ AEG-V16-017,S6,Cross,FieldShell 표준,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-017
|
||||
AEG-V16-016,S0,VS-00,Vendor boundary fitness,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-016_VENDOR_BOUNDARY_SLICE_NOTE.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts","FE Lead","2026-08-08: Removed stale fixed WBS row-count assertion; validator now verifies WBS ID integrity and reports vendor imports outside the approved adapter boundary. Actual evidence: python tools/validate_v16.py PASS=1 WARN=2 FAIL=0; targeted adapter tests 4/4 PASS; frontend typecheck PASS. COMPLETED is blocked because dependency AEG-V16-015 has no approved acceptance evidence in the tracker."
|
||||
AEG-V16-015,S0,VS-00,Adapter rollback runbook,IN_PROGRESS,TBD,"docs/CURRENT/ui-provider-switch.md","FE Lead","2026-08-08: Created startup-only provider switch and rollback runbook with fail-closed configuration, immutable-artifact rollback, and append-only evidence record. Visual/a11y/performance rehearsal evidence is not present and is explicitly required before completion; dependency AEG-V16-014 is also not evidenced in the tracker."
|
||||
AEG-V16-018,S6,Cross,DataContextHeader,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-018_DATA_CONTEXT_HEADER_SLICE_NOTE.md; frontend/src/shared/ui/components/KsDataContextHeader.vue; frontend/src/shared/ui/components/tests/KsDataContextHeader.spec.ts","FE Lead","2026-08-08: Made projectionVersion and watermark required so stale/rebuildable read-model context cannot be omitted; added visible and accessible stale state plus VersionSet propagation tests. Actual evidence: targeted Vitest 2/2 PASS, frontend typecheck PASS, production build PASS. Build emitted unrelated tracked .js drift, excluded from this Slice. COMPLETED is blocked pending predecessor AEG-V16-017 acceptance and UX/a11y evidence."
|
||||
AEG-V16-019,S6,Cross,CommandBar,IN_PROGRESS,TBD,"docs/CURRENT/AEG-V16-019_COMMAND_BAR_SLICE_NOTE.md; frontend/src/shared/ui/components/KsCommandBar.vue; frontend/src/shared/ui/components/tests/KsCommandBar.spec.ts","FE Lead","2026-08-08: Command boundary now suppresses disabled/busy execute events and exposes aggregate busy state. Actual evidence: targeted Vitest 1/1 PASS; frontend typecheck PASS. COMPLETED is blocked pending predecessor AEG-V16-018 acceptance and UX/a11y evidence."
|
||||
AEG-X-008,S0,Cross,OpenAPI artifact 고도화,COMPLETED,2026-08-04,.gitea/workflows/openapi-gate.yml + docs/api/openapi.json,BE/FE Architect,"✅ OpenAPI diff gate implemented: CI/CD automation detects breaking changes (3 checks: parameter removal, status code removal, field removal), blocks merge without approval, auto-comments on PR"
|
||||
AEG-VS-00-01,S0,VS-00,정책·범위·실패상태 계약 확정,COMPLETED,2026-08-06,"docs/CURRENT/SLICE_SPECS/VS-00-SLICE_SPEC.md + commit e7913db",PM/Architect,"✅ SLICE_SPEC produced: VS-00-SLICE_SPEC.md (state transitions, RBAC, governance gates, DQ rules, compliance). Commit e7913db. 249/253 tests PASS."
|
||||
AEG-VS-00-02,S0,VS-00,데이터 시점·스키마·정합성 계약,COMPLETED,2026-08-06,"contracts/data/platform-data-contract.v1.json + commit e7913db",Data Architect/DBA,"✅ DATA_CONTRACT v1.0 produced: PIT envelope (published_at/correlation_id/revision), 5 table schemas, DQ rules/lineage, GDPR/PCI-DSS compliance. JSON schema + validation. 249/253 tests PASS."
|
||||
|
||||
|
@@ -1,8 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
import KsButton from './KsButton.vue'
|
||||
export interface CommandBarAction { id: string; label: string; severity?: 'primary'|'secondary'|'success'|'info'|'warning'|'danger'; disabled?: boolean; busy?: boolean }
|
||||
defineProps<{ actions: readonly CommandBarAction[]; ariaLabel?: string }>()
|
||||
const props = defineProps<{ actions: readonly CommandBarAction[]; ariaLabel?: string }>()
|
||||
const emit = defineEmits<{ execute: [actionId: string] }>()
|
||||
|
||||
function execute(action: CommandBarAction): void {
|
||||
if (action.disabled || action.busy) return
|
||||
emit('execute', action.id)
|
||||
}
|
||||
</script>
|
||||
<template><nav class="ks-command-bar" :aria-label="ariaLabel ?? 'Page actions'"><KsButton v-for="action in actions" :key="action.id" :label="action.label" :severity="action.severity" :disabled="action.disabled" :loading="action.busy" @click="emit('execute', action.id)" /></nav></template>
|
||||
<template><nav class="ks-command-bar" :aria-label="ariaLabel ?? 'Page actions'" :aria-busy="actions.some(action => action.busy) || undefined"><KsButton v-for="action in actions" :key="action.id" :label="action.label" :severity="action.severity" :disabled="action.disabled" :loading="action.busy" @click="execute(action)" /></nav></template>
|
||||
<style scoped>.ks-command-bar{display:flex;gap:var(--ks-space-2);flex-wrap:wrap;justify-content:flex-end}</style>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import KsCommandBar from '../KsCommandBar.vue'
|
||||
|
||||
const actions = [
|
||||
{ id: 'save', label: 'Save' },
|
||||
{ id: 'review', label: 'Review', disabled: true },
|
||||
{ id: 'publish', label: 'Publish', busy: true }
|
||||
] as const
|
||||
|
||||
describe('KsCommandBar', () => {
|
||||
it('preserves supplied action order and executes only enabled, idle actions', async () => {
|
||||
const wrapper = mount(KsCommandBar, {
|
||||
props: { actions, ariaLabel: 'Decision actions' },
|
||||
global: { stubs: { KsButton: { name: 'KsButton', props: ['label'], template: '<button>{{ label }}</button>' } } }
|
||||
})
|
||||
|
||||
expect(wrapper.findAll('button').map(button => button.text())).toEqual(['Save', 'Review', 'Publish'])
|
||||
const buttons = wrapper.findAllComponents({ name: 'KsButton' })
|
||||
buttons[0].vm.$emit('click')
|
||||
buttons[1].vm.$emit('click')
|
||||
buttons[2].vm.$emit('click')
|
||||
await wrapper.vm.$nextTick()
|
||||
|
||||
expect(wrapper.emitted('execute')).toEqual([['save']])
|
||||
expect(wrapper.get('nav').attributes()).toMatchObject({ 'aria-label': 'Decision actions', 'aria-busy': 'true' })
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user