11 KiB
Code Generation Plan — Unit 3: frontend-cms-page
Unit Context
Workspace root: K:\Development\Projects\SlpModularCms
Frontend root: frontend/src/
Project type: Brownfield
FD artifacts: aidlc-docs/features/master-cms-module/construction/frontend-cms-page/functional-design/
NFR artifacts: aidlc-docs/features/master-cms-module/construction/frontend-cms-page/nfr-requirements/ + nfr-design/
Dependencies
- Backend API (
CmsInstanceController) implemented in Unit 1 (master-backend) ✅ - Existing frontend:
src/api/types.ts,src/lib/schemas/auth.ts,src/mocks/index.ts,src/pages/CmsPage.tsx(placeholder) - MSW: existing
mocks/index.tspattern with feature folders
Cross-cutting migrations (part of this unit)
InviteUserDialog.tsx: schema →@/lib/schemas/users, error state →useState<string | null>- New schema files:
src/lib/schemas/users.ts,src/lib/schemas/cms.ts
Steps
Step 1 — src/api/types.ts (MODIFY)
Add CMS domain types after existing types:
CmsInstanceStatusunion type ('Available' | 'NotAvailable' | 'Inactive')CmsInstanceinterface (id, name, url, status, disableMessage, lastContactedAt, lastStatusPushedAt, lastIntegrityCheckFailedAt)CreateCmsInstanceRequestinterface (name, url, apiKey)UpdateCmsInstanceStatusRequestinterface (status, disableMessage)UpdateStatusResultinterface (success, slaveContactSuccess)
Step 2 — src/lib/schemas/users.ts (CREATE)
Migrate inviteSchema from InviteUserDialog.tsx to src/lib/schemas/users.ts:
- Export
inviteUserSchema(z.object: email z.string().email(), role z.enum(['Administrator','User'])) - Export
InviteUserFormDatainferred type
Step 3 — src/lib/schemas/cms.ts (CREATE)
addCmsInstanceSchema: name (min 1, max 255), url (z.string().url()), apiKey (min 1)AddCmsInstanceFormDatainferred typesetStatusSchema: status enum +.refine()requiring disableMessage when NotAvailableSetStatusFormDatainferred type
Step 4 — src/lib/schemas/users.test.ts (CREATE)
Tests for migrated inviteUserSchema:
- Valid email + role → passes
- Invalid email → fails
- Missing role → fails
Step 5 — src/lib/schemas/cms.test.ts (CREATE)
Tests for both schemas:
addCmsInstanceSchema: valid data passes; missing name/apiKey fails;z.string().url()—http://valid.urlpasses,no-protocolfailssetStatusSchema: Available without disableMessage passes; NotAvailable without disableMessage fails; NotAvailable with disableMessage passes
Step 6 — src/components/users/InviteUserDialog.tsx (MODIFY)
Two changes (pattern migration):
- Replace inline
inviteSchema+InviteFormDatawith import from@/lib/schemas/users - Replace
inviteUser.errorpassed to<FormErrorBanner>with localuseState<string | null>(null)+onErrorcallback setting generic error message
Step 7 — src/mocks/cms/handlers.ts (CREATE)
In-memory MSW handlers:
let mockCmsInstances: CmsInstance[]with 2 seed entries (one Available, one Inactive)resetMockCmsInstances()exportGET /api/v1/CmsInstances→ return arrayPOST /api/v1/CmsInstances→ create instance (status=Available), push to array, return 201 + instancePUT /api/v1/CmsInstances/:id/status→ update instance in array, returnUpdateStatusResult { success: true, slaveContactSuccess: true }
Step 8 — src/mocks/index.ts (MODIFY)
- Import
cmsHandlersfrom./cms/handlers - Add
...cmsHandlerstohandlersarray - Re-export
cmsHandlersandresetMockCmsInstances
Step 9 — src/api/useCmsInstances.ts (CREATE)
export function useCmsInstances() {
return useQuery<CmsInstance[], Error>({
queryKey: ['cmsInstances'],
queryFn: () => api.get<CmsInstance[]>('/api/v1/CmsInstances'),
staleTime: 30_000,
});
}
Step 10 — src/api/useAddCmsInstance.ts (CREATE)
export function useAddCmsInstance(options?: MutationOptions) {
const queryClient = useQueryClient();
return useMutation<CmsInstance, Error, CreateCmsInstanceRequest>({
mutationFn: (data) => api.post<CmsInstance>('/api/v1/CmsInstances', data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }),
...options,
});
}
Step 11 — src/api/useUpdateCmsInstanceStatus.ts (CREATE)
export function useUpdateCmsInstanceStatus(options?: MutationOptions) {
const queryClient = useQueryClient();
return useMutation<UpdateStatusResult, Error, { id: string } & UpdateCmsInstanceStatusRequest>({
mutationFn: ({ id, ...body }) =>
api.put<UpdateStatusResult>(`/api/v1/CmsInstances/${id}/status`, body),
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }),
...options,
});
}
Step 12 — src/i18n/locales/en/translation.json (MODIFY)
Replace existing "cms": { "title": ..., "description": ... } with full cms.* key tree (from frontend-components.md i18n section).
Step 13 — src/i18n/locales/nl/translation.json (MODIFY)
Replace existing "cms": { "title": ..., "description": ... } with full cms.* key tree in Dutch.
Step 14 — src/components/cms/CmsInstanceList.tsx (CREATE)
Table component:
- Props:
instances: CmsInstance[],onSetStatus: (instance: CmsInstance) => void - Shadcn
Tablewith 6 columns (Name, URL, Status, Last Contact, Disable Message, Actions) - Status badge: green/red/muted per BR-FE-05..07
- Inactive row:
className={instance.status === 'Inactive' ? 'opacity-50' : ''} - Actions:
DropdownMenuwith "Set Status" item data-testidon row (cms-instance-row), badge, and actions trigger
Step 15 — src/components/cms/AddCmsInstanceDialog.tsx (CREATE)
Dialog with react-hook-form + addCmsInstanceSchema:
- Fields: name (text), url (text), apiKey (
PasswordField) useState<string | null>(null)for serverErroronErrorcallback: ProblemDetailsError 409 → specific message, NetworkError →errors.network, else →errors.genericuseEffectonopen→ reset form + serverError + mutationFormErrorBanner,FieldError,data-testidon all fields and submit button
Step 16 — src/components/cms/SetStatusDialog.tsx (CREATE)
Dialog with react-hook-form + setStatusSchema:
- Fields: status (Shadcn
Select), disableMessage (conditional text, only when NotAvailable) useState<string | null>(null)for serverErroronSuccess: close dialog, toast message based onresult.slaveContactSuccess(BR-FE-12/13)onError: set serverError from toast (generic)useEffectonopenorinstancechange → resetdata-testidon select, disableMessage input, submit button
Step 17 — src/pages/CmsPage.tsx (MODIFY)
Replace placeholder with full page:
- State:
addDialogOpen,statusTarget useCmsInstances()hook- Header row: title + "Add CMS" button (data-testid="cms-add-button")
- Loading/error text per NFR-FE-10/11
- Empty state (BR-FE-18) vs
CmsInstanceList AddCmsInstanceDialog+SetStatusDialog- Remove old
data-testid="cms-placeholder"(test file updated in step 18)
Step 18 — src/pages/CmsPage.test.tsx (MODIFY)
Replace existing placeholder-only tests with:
renders list of CMS instances(happy path with seed data)renders empty state when no instances(MSW returns [])opens AddCmsInstanceDialog on button clickadds a CMS instance successfullyredirects unauthenticated users to login(existing — keep)
Step 19 — src/api/useCmsInstances.test.ts (CREATE)
- Returns list on success
- Handles network error
Step 20 — src/api/useAddCmsInstance.test.ts (CREATE)
- Creates instance + invalidates cache on success (HTTP 201)
- Calls onError on HTTP 400
Step 21 — src/api/useUpdateCmsInstanceStatus.test.ts (CREATE)
- Updates status + invalidates cache on success
- Returns UpdateStatusResult with slaveContactSuccess flag
Step 22 — src/components/cms/AddCmsInstanceDialog.test.tsx (CREATE)
- Renders fields and submit button
- Shows FieldError for missing name
- Shows FieldError for invalid URL (non-URL format)
- Shows FormErrorBanner on HTTP 400
- Resets form on dialog close
Step 23 — src/components/cms/SetStatusDialog.test.tsx (CREATE)
- DisableMessage field hidden when status ≠ NotAvailable
- DisableMessage field shown and required when status = NotAvailable
- Form blocks submit when DisableMessage empty + NotAvailable
File Summary
| # | File | Action |
|---|---|---|
| 1 | frontend/src/api/types.ts |
MODIFY — add 5 CMS types |
| 2 | frontend/src/lib/schemas/users.ts |
CREATE — migrated inviteUserSchema |
| 3 | frontend/src/lib/schemas/cms.ts |
CREATE — addCmsInstanceSchema, setStatusSchema |
| 4 | frontend/src/lib/schemas/users.test.ts |
CREATE |
| 5 | frontend/src/lib/schemas/cms.test.ts |
CREATE |
| 6 | frontend/src/components/users/InviteUserDialog.tsx |
MODIFY — schema import + serverError migration |
| 7 | frontend/src/mocks/cms/handlers.ts |
CREATE — MSW handlers |
| 8 | frontend/src/mocks/index.ts |
MODIFY — register cmsHandlers |
| 9 | frontend/src/api/useCmsInstances.ts |
CREATE |
| 10 | frontend/src/api/useAddCmsInstance.ts |
CREATE |
| 11 | frontend/src/api/useUpdateCmsInstanceStatus.ts |
CREATE |
| 12 | frontend/src/i18n/locales/en/translation.json |
MODIFY — cms.* keys |
| 13 | frontend/src/i18n/locales/nl/translation.json |
MODIFY — cms.* keys |
| 14 | frontend/src/components/cms/CmsInstanceList.tsx |
CREATE |
| 15 | frontend/src/components/cms/AddCmsInstanceDialog.tsx |
CREATE |
| 16 | frontend/src/components/cms/SetStatusDialog.tsx |
CREATE |
| 17 | frontend/src/pages/CmsPage.tsx |
MODIFY — replace placeholder |
| 18 | frontend/src/pages/CmsPage.test.tsx |
MODIFY — full test suite |
| 19 | frontend/src/api/useCmsInstances.test.ts |
CREATE |
| 20 | frontend/src/api/useAddCmsInstance.test.ts |
CREATE |
| 21 | frontend/src/api/useUpdateCmsInstanceStatus.test.ts |
CREATE |
| 22 | frontend/src/components/cms/AddCmsInstanceDialog.test.tsx |
CREATE |
| 23 | frontend/src/components/cms/SetStatusDialog.test.tsx |
CREATE |
Total: 16 new files, 7 modified files
Step Completion Tracking
- Step 1 — types.ts
- Step 2 — schemas/users.ts
- Step 3 — schemas/cms.ts
- Step 4 — schemas/users.test.ts
- Step 5 — schemas/cms.test.ts
- Step 6 — InviteUserDialog.tsx migration
- Step 7 — mocks/cms/handlers.ts
- Step 8 — mocks/index.ts
- Step 9 — useCmsInstances.ts
- Step 10 — useAddCmsInstance.ts
- Step 11 — useUpdateCmsInstanceStatus.ts
- Step 12 — en/translation.json
- Step 13 — nl/translation.json
- Step 14 — CmsInstanceList.tsx
- Step 15 — AddCmsInstanceDialog.tsx
- Step 16 — SetStatusDialog.tsx
- Step 17 — CmsPage.tsx
- Step 18 — CmsPage.test.tsx
- Step 19 — useCmsInstances.test.ts
- Step 20 — useAddCmsInstance.test.ts
- Step 21 — useUpdateCmsInstanceStatus.test.ts
- Step 22 — AddCmsInstanceDialog.test.tsx
- Step 23 — SetStatusDialog.test.tsx