Tasks: Vue/Onyx Web Frontend¶
Input: Design documents from specs/003-vue-onyx-frontend/
Prerequisites: plan.md ✅ spec.md ✅ research.md ✅ data-model.md ✅ contracts/ ✅ quickstart.md ✅
Tests: Included per Constitution Principle II (NON-NEGOTIABLE — overrides template default). Unit tests cover composables; Playwright E2E tests cover API adapters and user journeys. Tests are written FIRST and MUST FAIL before implementation begins (Red → Green → Refactor).
Organization: Tasks are grouped by user story to enable independent implementation and testing.
Format: [ID] [P?] [Story] Description¶
- [P]: Can run in parallel (different files, no dependencies on incomplete tasks)
- [Story]: Which user story this task belongs to (US1–US4)
- Exact file paths are included in all task descriptions
Phase 1: Setup (Shared Infrastructure)¶
Purpose: Scaffold the ui/ project and add Keycloak to the dev stack. No user story work begins here.
- [X] T001 Scaffold Vite + Vue 3 + TypeScript project in
ui/(runnpm create vite@latest ui -- --template vue-ts, commit initial scaffold) - [X] T002 Install Onyx component library (
@sit-onyx/components), register globally inui/src/main.ts, import Onyx CSS - [X] T003 [P] Configure ESLint (
@vue/eslint-config-typescript) and Prettier inui/eslint.config.tsandui/.prettierrc - [X] T004 [P] Configure Vitest in
ui/vitest.config.tswith@vue/test-utilsand@testing-library/vue - [X] T005 [P] Configure Playwright in
ui/playwright.config.tstargetinghttp://localhost:5173 - [X] T006 Add Keycloak 25.x service and its Postgres DB to
docker-compose.yml; mountui/keycloak/realm-export.jsonvia--import-realmon first boot - [X] T007 Create
ui/keycloak/realm-export.jsonwith realmwrkflw, public OIDC clientwrkflw-ui(PKCE), groupsinitiators/legal-reviewers, and test usersalice(initiators),bob(legal-reviewers),carol(both) — all with passwordpassword - [X] T008 Add
generate:typesnpm script toui/package.jsonusingopenapi-typescript; run it againstspecs/001-document-approval-engine/contracts/openapi.yamland commit initialui/src/api/types.ts - [X] T009 Create
ui/.env.exampledocumentingVITE_API_BASE_URL,VITE_OIDC_AUTHORITY,VITE_OIDC_CLIENT_ID,VITE_API_TIMEOUT_MS - [X] T010 Add
npm run checkscript toui/package.json(lint + typecheck + unit tests + build) to serve as the local validation gate per Constitution Principle VI
Checkpoint: docker compose up -d starts Keycloak at :8180; cd ui && npm run check passes on the empty scaffold.
Phase 2: Foundational (Blocking Prerequisites)¶
Purpose: Auth, API client, router skeleton, nav shell — everything US1–US4 build on. No user story work begins until this phase is complete.
⚠️ CRITICAL: No user story implementation can begin until this phase is complete.
- [X] T011 Implement OIDC setup in
ui/src/auth/oidc.tsusingoidc-client-ts(PKCE flow, silent token renewal,VITE_OIDC_*env vars) - [X] T012 Create
authPinia store inui/src/stores/auth.ts(fields:user: { id, name, groups } | null,isAuthenticated: boolean; actions:login(),logout(),loadUser()) - [X] T013 Implement API HTTP client in
ui/src/api/client.ts(attachesAuthorization: Bearer+ compat headersX-Actor-Id/X-Actor-Groups; handles 401 → silent refresh → retry; 10 s timeout; maps all error statuses per contracts/api-consumption.md) - [X] T014 [P] Implement flows API module
ui/src/api/flows.ts(functions:submitFlow,getFlow; typed against generatedsrc/api/types.ts) - [X] T015 [P] Implement worklist API module
ui/src/api/worklist.ts(functions:getGroupWorklist,getMyTasks) - [X] T016 [P] Implement tasks API module
ui/src/api/tasks.ts(functions:claimTask,releaseTask,submitDecision) - [X] T017 Configure Vue Router 4 in
ui/src/router/index.ts: define placeholder routes/submissions,/submit/:definitionId,/flows/:flowId,/worklist,/tasks/:taskId; add navigation guard that redirects unauthenticated users to OIDC login, preserving the intended route - [X] T018 Implement smart landing logic in
ui/src/router/index.ts: after auth, callgetGroupWorklist(); if result is non-empty redirect to/worklist, else to/submissions(FR-016) - [X] T019 Create
AppNavcomponentui/src/components/AppNav.vueusing Onyx nav primitives; shows "My Submissions" and "My Worklist" sections always, plus rework badge placeholder (FR-015) - [X] T020 Create global error toast handler
ui/src/composables/useErrorHandler.ts; wire it to the API client so all error status codes surface an Onyx toast without duplicating error logic in views
Checkpoint: App loads, redirects to Keycloak login, logs in as alice, lands on /submissions (empty state). Navigation between sections works without page reload.
Phase 3: User Story 1 — Submit a document for approval (Priority: P1) 🎯 MVP¶
Goal: An authenticated initiator selects a workflow definition, fills in a dynamic form, submits, and is taken to a flow-status confirmation view.
Independent Test: Log in as alice, navigate to /submit/document-approval, fill form fields, submit — confirm redirect to /flows/{id} showing flow state PENDING_REVIEW and the submitted form data.
Tests for User Story 1 (Constitution Principle II — write first, must fail before T025)¶
- [X] T021 [P] [US1] Write Vitest unit test for
useFlowscomposable (testsubmitFlowcalls the correct API module function with correct payload and updates reactive state) inui/tests/unit/useFlows.spec.ts - [X] T022 [P] [US1] Write Playwright E2E test for submit-flow happy path: login as
alice, fill form, submit, assert/flows/{id}route andPENDING_REVIEWbadge visible inui/tests/e2e/submit-flow.spec.ts
Implementation for User Story 1¶
- [X] T023 [US1] Create
DynamicFormFieldcomponentui/src/components/DynamicFormField.vue— renders a single field (text,textarea,select,date) from aFieldDefinitionprop using Onyx input components; emits value updates - [X] T024 [US1] Add hardcoded
documentApprovalDefinitionconstant toui/src/api/flows.ts(fields:title: text,description: textarea,priority: select[low/medium/high]) to unblock the form untilGET /definitionsbackend endpoint exists - [X] T025 [US1] Create
SubmitFlowViewui/src/views/SubmitFlowView.vue: renders definition name, mapsFieldDefinition[]toDynamicFormFieldcomponents, collectsformData, callsuseFlows.submitFlow(), navigates to/flows/{id}on success - [X] T026 [US1] Add inline required-field validation to
SubmitFlowView(FR-013): prevent submission when required fields are blank; highlight each empty required field with an Onyx error state - [X] T027 [US1] Implement
useFlowscomposableui/src/composables/useFlows.ts(submitFlow,getFlow,flowsreactive ref, loading/error state) - [X] T028 [US1] Wire
SubmitFlowViewto route/submit/:definitionIdinui/src/router/index.ts
Checkpoint: User Story 1 fully functional. alice can submit a document; flow appears in backend; /flows/{id} shows status. npm run test:e2e -- submit-flow passes.
Phase 4: User Story 2 — Review and decide on a task (Priority: P1)¶
Goal: A reviewer sees all unclaimed group tasks, claims one, reviews the full submitted form data, and approves or rejects (rejection requires a comment).
Independent Test: Log in as bob, open /worklist — pending task from alice's submission is visible with title, stage, submitter, time waiting. Claim it; task moves to "My Tasks". Open task; approve — task disappears from worklist, flow advances.
Tests for User Story 2 (write first, must fail before T033)¶
- [X] T029 [P] [US2] Write Vitest unit test for
useWorklistcomposable (assertsgetGroupWorklistis called on mount and result is stored reactively) inui/tests/unit/useWorklist.spec.ts - [X] T030 [P] [US2] Write Playwright E2E test for claim-and-approve cycle: login as
bob, claim alice's task, approve, assert flow status advances inui/tests/e2e/claim-approve.spec.ts
Implementation for User Story 2¶
- [X] T031 [US2] Implement
useWorklistcomposableui/src/composables/useWorklist.ts(groupTasks,myTasksreactive refs;claimTask,releaseTaskactions; auto-refresh on mount) - [X] T032 [US2] Create
WorklistViewui/src/views/WorklistView.vue: Onyx table of unclaimed group tasks (columns: document title, stage, submitter name, time waiting); claim button per row; section for "My claimed tasks" - [X] T033 [US2] Create
TaskDetailViewui/src/views/TaskDetailView.vue: renders fullformDataas read-only field/value pairs (FR-008a); Approve button + Reject button; reject opens Onyx dialog requiring non-empty comment; callssubmitDecision() - [X] T034 [US2] Wire
WorklistViewto/worklistandTaskDetailViewto/tasks/:taskIdinui/src/router/index.ts - [X] T035 [US2] Handle 409 conflict in
WorklistView(task already claimed by someone else): show "Task is no longer available" Onyx toast and refresh the worklist (edge case from spec)
Checkpoint: User Stories 1 + 2 fully functional end-to-end. Full approve cycle works. npm run test:e2e -- claim-approve passes.
Phase 5: User Story 3 — Track submitted flows (Priority: P2)¶
Goal: A submitter sees all their flows with current states and can drill into any flow to see the full immutable audit history.
Independent Test: Log in as alice, open /submissions — see her submitted flow with state and last-updated. Click flow → /flows/{id} shows every audit event (FLOW_STARTED, TASK_CLAIMED, DECISION_RECORDED) with actor name and timestamp.
Tests for User Story 3 (write first, must fail before T040)¶
- [X] T036 [P] [US3] Write Vitest unit test for
useFlowslist behaviour (assertsflowsref populates from a mocked API response, state labels map correctly) inui/tests/unit/useFlows.spec.ts(extend existing file) - [X] T037 [P] [US3] Write Playwright E2E test for flow tracking: login as
alice, open/submissions, assert flow row visible, click through to/flows/{id}, assert timeline shows FLOW_STARTED and TASK_CREATED events inui/tests/e2e/flow-tracking.spec.ts
Implementation for User Story 3¶
- [X] T038 [US3] Extend
useFlowscomposable to support fetching the submitter's own flows (callGET /flowswith submitter filter or adapt to available query parameter; store result insubmittedFlowsref) - [X] T039 [US3] Create
MySubmissionsViewui/src/views/MySubmissionsView.vue: Onyx table of the user's flows (columns: definition name, current state withFlowStatusBadge, last-updated); wire to/submissionsroute - [X] T040 [US3] Create
FlowDetailViewui/src/views/FlowDetailView.vue: shows flow state, submittedformData(read-only), and a chronological audit timeline (actor name, event type, timestamp) for everyAuditEventinhistory; wire to/flows/:flowIdroute (also used by US1 post-submit redirect)
Checkpoint: User Stories 1–3 all functional. alice can see her flow history after bob approves. npm run test:e2e -- flow-tracking passes.
Phase 6: User Story 4 — Re-submit a reworked document (Priority: P2)¶
Goal: After rejection, the submitter sees an in-app notification, reviews the rejection comment, and re-submits from the flow detail view.
Independent Test: Reject alice's flow as bob with comment "Missing signature". Log in as alice — rework badge visible in nav. Open the flow → rejection comment prominent → click re-submit → new review task appears in bob's worklist.
Tests for User Story 4 (write first, must fail before T044)¶
- [X] T041 [P] [US4] Write Vitest unit test for
notificationsstore (reworkPendingCountincrements when a flow with stateRETURNED_FOR_REWORKis present insubmittedFlows) inui/tests/unit/notifications.spec.ts - [X] T042 [P] [US4] Write Playwright E2E test for full rework cycle: reject as
bob, login asalice, assert rework badge, re-submit, assert new task in bob's worklist inui/tests/e2e/rework-cycle.spec.ts
Implementation for User Story 4¶
- [X] T043 [US4] Implement
notificationsPinia storeui/src/stores/notifications.ts(reworkPendingCountcomputed fromsubmittedFlows;markSeen()action; persisted tosessionStorageso badge clears after user visits the flow) - [X] T044 [US4] Create
ReworkBannercomponentui/src/components/ReworkBanner.vue(Onyx badge on "My Submissions" nav item showingreworkPendingCount; clicking navigates to/submissions) - [X] T045 [US4] Wire
ReworkBannerintoAppNavand wirenotificationsstore to load on every login - [X] T046 [US4] Add re-submit action to
FlowDetailView— renders a "Re-submit for review" button and the rejection comment prominently whenflow.state === 'RETURNED_FOR_REWORK'; callssubmitFlow()with samedefinitionIdand pre-populatedformData; navigates to new flow on success (FR-011)
Checkpoint: Full lifecycle works end-to-end: submit → review → reject → rework notification → re-submit → review again. npm run test:e2e -- rework-cycle passes.
Phase 7: Polish & Cross-Cutting Concerns¶
Purpose: Accessibility, edge-case hardening, local dev experience.
- [X] T047 [P] Audit all interactive Onyx components in views for keyboard reachability (Tab order, Enter/Space activation) and descriptive
aria-label/aria-describedbyattributes (SC-006, WCAG 2.1 AA) - [X] T048 [P] Add
FlowStatusBadgecomponentui/src/components/FlowStatusBadge.vue(Onyx chip/badge mapping eachFlowStateto a colour and human label; used inMySubmissionsViewandFlowDetailView) - [X] T049 Add session-expiry recovery: if OIDC silent refresh fails during form fill, preserve form data in
sessionStorage, redirect to login, restore data on return (edge case from spec) - [X] T050 Verify
npm run check(lint + typecheck + vitest + build) passes clean with zero type errors after all phases - [X] T051 Validate
quickstart.mdagainst actual local setup: rundocker compose up -d,npm install,npm run dev, log in with each test user — confirm all flows work as documented - [X] T052 Update
AGENTS.mdwithui/project entry: tech stack, local commands (npm run dev,npm run check,npm run test:e2e), test user credentials
Dependencies & Execution Order¶
Phase Dependencies¶
- Phase 1 (Setup): No dependencies — start immediately
- Phase 2 (Foundation): Depends on Phase 1 complete — BLOCKS all user stories
- Phase 3 (US1): Depends on Phase 2 complete
- Phase 4 (US2): Depends on Phase 2 complete; US2 E2E tests depend on a submitted flow (US1 runtime dependency, not code dependency)
- Phase 5 (US3): Depends on Phase 2 complete; shares
useFlowswith US1 - Phase 6 (US4): Depends on US1 (submit) and US2 (reject path) being complete
- Phase 7 (Polish): Depends on all user story phases complete
Within Each User Story¶
- Write tests FIRST — verify they FAIL (Red)
- Implement composables / stores
- Implement components / views
- Wire routes
- Verify tests now PASS (Green)
- Refactor if needed (Refactor)
Parallel Opportunities (within Phase 2)¶
All of T014, T015, T016 can run in parallel (different files, no inter-dependencies).
Parallel Opportunities (within each US phase)¶
- US1: T021 and T022 can run in parallel; T023 and T024 can run in parallel
- US2: T029 and T030 can run in parallel
- US3: T036 and T037 can run in parallel
- US4: T041 and T042 can run in parallel
Parallel Example: User Story 2¶
# Write tests in parallel:
Task T029: "Write Vitest unit test for useWorklist in ui/tests/unit/useWorklist.spec.ts"
Task T030: "Write Playwright E2E test for claim-approve cycle in ui/tests/e2e/claim-approve.spec.ts"
# Then implement in parallel:
Task T031: "Implement useWorklist composable in ui/src/composables/useWorklist.ts"
# (T032, T033 depend on T031 completing first)
Implementation Strategy¶
MVP (User Story 1 + 2 Only)¶
- Complete Phase 1: Setup
- Complete Phase 2: Foundation (blocks all stories)
- Complete Phase 3: US1 — submit flow
- Complete Phase 4: US2 — claim and decide
- STOP and VALIDATE: Full approval cycle works end-to-end with real Keycloak + backend
- Demo / deploy
Incremental Delivery¶
- Setup + Foundation → navigable skeleton with auth
- Add US1 → submitters can initiate flows (MVP for submitters)
- Add US2 → reviewers can act (MVP for full approval cycle)
- Add US3 → submitters can track history
- Add US4 → full rework loop closes
- Polish → production-ready quality
Notes¶
- [P] tasks operate on different files with no incomplete-task dependencies — safe to parallelise
- Constitution Principle II is non-negotiable: tests are written before implementation in every phase; they must fail before the implementation tasks begin
- The
GET /definitionsbackend endpoint is missing (noted in contracts/api-consumption.md); T024 provides a hardcoded fallback that is removed once the endpoint ships - The backend auth header migration (Bearer token → JWT claims extraction) is a backend concern tracked separately;
client.tssends both headers during the transition period - Commit after each phase checkpoint; push before marking phase complete (Constitution Principle VI)