Adds 2 units and docs for unit 3. nfr-requirements plan
This commit is contained in:
+76
@@ -0,0 +1,76 @@
|
||||
# Business Logic Model — Unit 3: frontend-cms-page
|
||||
|
||||
## Component Orchestration
|
||||
|
||||
`CmsPage` is the root orchestrator. It owns all modal state and delegates data operations to hooks.
|
||||
|
||||
```
|
||||
CmsPage
|
||||
├── useCmsInstances() → fetches instance list
|
||||
├── CmsInstanceList → renders table, emits onSetStatus
|
||||
├── AddCmsInstanceDialog → useAddCmsInstance() internally
|
||||
└── SetStatusDialog → useUpdateCmsInstanceStatus() internally
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Flows
|
||||
|
||||
### Flow 1 — Load instances
|
||||
|
||||
```
|
||||
CmsPage mounts
|
||||
→ useCmsInstances fires GET /api/v1/CmsInstances
|
||||
→ isPending: show loading indicator
|
||||
→ isError: show error message
|
||||
→ data=[]: render EmptyState (centered placeholder + Add button)
|
||||
→ data=[...]: render CmsInstanceList
|
||||
```
|
||||
|
||||
### Flow 2 — Add CmsInstance
|
||||
|
||||
```
|
||||
User clicks "Add CMS" button
|
||||
→ addDialogOpen = true → AddCmsInstanceDialog opens
|
||||
User fills Name, URL, ApiKey and submits
|
||||
→ useAddCmsInstance fires POST /api/v1/CmsInstances
|
||||
→ HTTP 201: close dialog, invalidate ['cmsInstances'], toast success
|
||||
→ HTTP 400: show FormBannerError inside dialog (do not close)
|
||||
→ Network error: show FormBannerError inside dialog
|
||||
```
|
||||
|
||||
### Flow 3 — Update instance status
|
||||
|
||||
```
|
||||
User clicks "Set Status" in row actions dropdown
|
||||
→ statusTarget = instance → SetStatusDialog opens
|
||||
User selects status, optionally fills DisableMessage, submits
|
||||
→ useUpdateCmsInstanceStatus fires PUT /api/v1/CmsInstances/{id}/status
|
||||
→ HTTP 200 + slaveContactSuccess=true:
|
||||
close dialog, invalidate ['cmsInstances'],
|
||||
toast "Status updated — cliënt confirmed"
|
||||
→ HTTP 200 + slaveContactSuccess=false:
|
||||
close dialog, invalidate ['cmsInstances'],
|
||||
toast "Status saved — cliënt unreachable"
|
||||
→ HTTP 404: toast error "Instance not found"
|
||||
→ HTTP 400: toast error (generic)
|
||||
→ Network error: toast error (generic)
|
||||
```
|
||||
|
||||
### Flow 4 — DisableMessage conditional visibility
|
||||
|
||||
```
|
||||
SetStatusDialog: status field changes
|
||||
→ status === 'NotAvailable': show DisableMessage field (required)
|
||||
→ status !== 'NotAvailable': hide DisableMessage field, clear value
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Cache Strategy
|
||||
|
||||
| Hook | Query key | Stale time | Invalidated by |
|
||||
|------|-----------|------------|----------------|
|
||||
| `useCmsInstances` | `['cmsInstances']` | 30 000 ms | `useAddCmsInstance.onSuccess`, `useUpdateCmsInstanceStatus.onSuccess` |
|
||||
| `useAddCmsInstance` | — | — | — |
|
||||
| `useUpdateCmsInstanceStatus` | — | — | — |
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
# Business Rules — Unit 3: frontend-cms-page
|
||||
|
||||
## Table Display (Q1: B+C)
|
||||
|
||||
- **BR-FE-01**: The `CmsInstanceList` table displays six columns: Name, URL, Status, Last Contact, DisableMessage, Actions.
|
||||
- **BR-FE-02**: `lastContactedAt` is formatted as a localised date/time string; displays "—" when `null`.
|
||||
- **BR-FE-03**: `disableMessage` displays "—" when `null` or empty.
|
||||
|
||||
## Inactive Row Styling (Q2: A)
|
||||
|
||||
- **BR-FE-04**: A `<TableRow>` whose `instance.status === 'Inactive'` receives `className="opacity-50"`. All cells within that row are visually dimmed as a result.
|
||||
|
||||
## Status Badge Colors
|
||||
|
||||
- **BR-FE-05**: `Available` → green badge (`secondary` or custom green variant).
|
||||
- **BR-FE-06**: `NotAvailable` → red badge (`destructive` variant).
|
||||
- **BR-FE-07**: `Inactive` → muted badge (`outline` variant).
|
||||
|
||||
## SetStatusDialog — DisableMessage (Q3 note, existing unit-of-work)
|
||||
|
||||
- **BR-FE-08**: The DisableMessage field is only rendered when the selected status is `NotAvailable`.
|
||||
- **BR-FE-09**: DisableMessage is required (non-empty) when status is `NotAvailable`. Form submit is blocked if it is empty.
|
||||
- **BR-FE-10**: When status changes away from `NotAvailable`, the DisableMessage field is hidden and its value is reset to `""`.
|
||||
- **BR-FE-11**: The value sent to the API is `null` for all statuses except `NotAvailable`; for `NotAvailable` it is the trimmed string value.
|
||||
|
||||
## UpdateStatusResult Toast (Q3: B, "cliënt" terminology)
|
||||
|
||||
- **BR-FE-12**: On HTTP 200 with `slaveContactSuccess === true`: show toast "Status updated — cliënt confirmed".
|
||||
- **BR-FE-13**: On HTTP 200 with `slaveContactSuccess === false`: show toast "Status saved — cliënt unreachable".
|
||||
- **BR-FE-14**: The word "slave" must not appear in any user-visible text. Use "cliënt" in all UI strings.
|
||||
|
||||
## AddCmsInstanceDialog Error Handling (Q4: A)
|
||||
|
||||
- **BR-FE-15**: On HTTP 400 from `POST /api/v1/CmsInstances`: display a `FormBannerError` at the top of the dialog. The dialog remains open.
|
||||
- **BR-FE-16**: On network error: display a `FormBannerError` at the top of the dialog. The dialog remains open.
|
||||
- **BR-FE-17**: On success (HTTP 201): close the dialog and show a success toast.
|
||||
|
||||
## Empty State (Q5: A)
|
||||
|
||||
- **BR-FE-18**: When `useCmsInstances` returns an empty array, render a centered placeholder instead of the table. The placeholder contains:
|
||||
- A `LayoutGrid` icon (consistent with the existing `CmsPage` placeholder).
|
||||
- A heading: "No CMS instances yet".
|
||||
- A description: "Add your first CMS instance to get started."
|
||||
- An "Add CMS" button that opens `AddCmsInstanceDialog`.
|
||||
- **BR-FE-19**: The "Add CMS" button in the page header is always visible regardless of empty state.
|
||||
|
||||
## Access Control
|
||||
|
||||
- **BR-FE-20**: The `/cms` route is already guarded by `RoleGuard allowedRoles={['Owner']}`. No additional guard logic is needed inside `CmsPage`.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
# Domain Entities — Unit 3: frontend-cms-page
|
||||
|
||||
## TypeScript Types
|
||||
|
||||
### CmsInstanceStatus
|
||||
|
||||
```typescript
|
||||
export type CmsInstanceStatus = 'Available' | 'NotAvailable' | 'Inactive';
|
||||
```
|
||||
|
||||
String union matching `CmsInstanceStatus` enum values serialized by the backend (`Available`, `NotAvailable`, `Inactive`).
|
||||
|
||||
---
|
||||
|
||||
### CmsInstance
|
||||
|
||||
```typescript
|
||||
export interface CmsInstance {
|
||||
id: string; // UUID
|
||||
name: string;
|
||||
url: string;
|
||||
status: CmsInstanceStatus;
|
||||
disableMessage: string | null;
|
||||
lastContactedAt: string | null; // ISO 8601, null if never contacted
|
||||
lastStatusPushedAt: string | null; // ISO 8601
|
||||
lastIntegrityCheckFailedAt: string | null; // ISO 8601
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### CreateCmsInstanceRequest
|
||||
|
||||
```typescript
|
||||
export interface CreateCmsInstanceRequest {
|
||||
name: string;
|
||||
url: string;
|
||||
apiKey: string;
|
||||
}
|
||||
```
|
||||
|
||||
Sent as body to `POST /api/v1/CmsInstances`.
|
||||
|
||||
---
|
||||
|
||||
### UpdateCmsInstanceStatusRequest
|
||||
|
||||
```typescript
|
||||
export interface UpdateCmsInstanceStatusRequest {
|
||||
status: CmsInstanceStatus;
|
||||
disableMessage: string | null;
|
||||
}
|
||||
```
|
||||
|
||||
Sent as body to `PUT /api/v1/CmsInstances/{id}/status`. `disableMessage` is `null` unless `status === 'NotAvailable'`.
|
||||
|
||||
---
|
||||
|
||||
### UpdateStatusResult
|
||||
|
||||
```typescript
|
||||
export interface UpdateStatusResult {
|
||||
success: boolean;
|
||||
slaveContactSuccess: boolean; // true = cliënt was reachable and confirmed
|
||||
}
|
||||
```
|
||||
|
||||
Returned by `PUT /api/v1/CmsInstances/{id}/status`. `slaveContactSuccess` drives the differentiated toast message.
|
||||
|
||||
---
|
||||
|
||||
## File Location
|
||||
|
||||
All types added to `src/api/types.ts`.
|
||||
+214
@@ -0,0 +1,214 @@
|
||||
# Frontend Components — Unit 3: frontend-cms-page
|
||||
|
||||
## Component Hierarchy
|
||||
|
||||
```
|
||||
CmsPage (src/pages/CmsPage.tsx)
|
||||
├── AddCmsInstanceDialog (src/components/cms/AddCmsInstanceDialog.tsx)
|
||||
├── SetStatusDialog (src/components/cms/SetStatusDialog.tsx)
|
||||
└── CmsInstanceList (src/components/cms/CmsInstanceList.tsx)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CmsPage
|
||||
|
||||
**File**: `src/pages/CmsPage.tsx`
|
||||
|
||||
**State**:
|
||||
| State | Type | Initial | Description |
|
||||
|-------|------|---------|-------------|
|
||||
| `addDialogOpen` | `boolean` | `false` | Controls AddCmsInstanceDialog visibility |
|
||||
| `statusTarget` | `CmsInstance \| null` | `null` | Instance passed to SetStatusDialog; null = closed |
|
||||
|
||||
**Hooks**: `useCmsInstances()`
|
||||
|
||||
**Render logic**:
|
||||
```
|
||||
Header row: title + "Add CMS" button (always visible)
|
||||
isPending → loading text
|
||||
isError → error text
|
||||
data === [] → EmptyState (icon + heading + description + Add button)
|
||||
data.length > 0 → CmsInstanceList
|
||||
AddCmsInstanceDialog (controlled by addDialogOpen)
|
||||
SetStatusDialog (controlled by statusTarget !== null)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## CmsInstanceList
|
||||
|
||||
**File**: `src/components/cms/CmsInstanceList.tsx`
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface CmsInstanceListProps {
|
||||
instances: CmsInstance[];
|
||||
onSetStatus: (instance: CmsInstance) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Columns** (BR-FE-01):
|
||||
|
||||
| Column | Source field | Notes |
|
||||
|--------|-------------|-------|
|
||||
| Name | `instance.name` | — |
|
||||
| URL | `instance.url` | — |
|
||||
| Status | `instance.status` | Rendered as badge (BR-FE-05..07) |
|
||||
| Last Contact | `instance.lastContactedAt` | Formatted date; "—" when null (BR-FE-02) |
|
||||
| Disable Message | `instance.disableMessage` | "—" when null (BR-FE-03) |
|
||||
| Actions | — | Dropdown with "Set Status" item |
|
||||
|
||||
**Row styling**: `<TableRow className={instance.status === 'Inactive' ? 'opacity-50' : ''}>` (BR-FE-04)
|
||||
|
||||
**Interactions**: Clicking "Set Status" in the actions dropdown calls `onSetStatus(instance)`.
|
||||
|
||||
---
|
||||
|
||||
## AddCmsInstanceDialog
|
||||
|
||||
**File**: `src/components/cms/AddCmsInstanceDialog.tsx`
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface AddCmsInstanceDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Form fields**:
|
||||
| Field | Type | Validation |
|
||||
|-------|------|-----------|
|
||||
| Name | text | Required, non-empty |
|
||||
| URL | text | Required, valid URL format |
|
||||
| ApiKey | password | Required, non-empty |
|
||||
|
||||
**Hooks**: `useAddCmsInstance()`
|
||||
|
||||
**Submit behaviour**:
|
||||
- Success (HTTP 201): call `onOpenChange(false)`, show success toast
|
||||
- HTTP 400: show `FormBannerError` inside dialog, keep open (BR-FE-15)
|
||||
- Network error: show `FormBannerError` inside dialog, keep open (BR-FE-16)
|
||||
|
||||
---
|
||||
|
||||
## SetStatusDialog
|
||||
|
||||
**File**: `src/components/cms/SetStatusDialog.tsx`
|
||||
|
||||
**Props**:
|
||||
```typescript
|
||||
interface SetStatusDialogProps {
|
||||
instance: CmsInstance | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
```
|
||||
|
||||
**Form fields**:
|
||||
| Field | Type | Condition | Validation |
|
||||
|-------|------|-----------|-----------|
|
||||
| Status | select (`Available` \| `NotAvailable` \| `Inactive`) | always | Required |
|
||||
| DisableMessage | text | only when `status === 'NotAvailable'` | Required when visible |
|
||||
|
||||
**Hooks**: `useUpdateCmsInstanceStatus()`
|
||||
|
||||
**Submit behaviour**:
|
||||
- HTTP 200 + `slaveContactSuccess=true`: close dialog, toast "Status updated — cliënt confirmed" (BR-FE-12)
|
||||
- HTTP 200 + `slaveContactSuccess=false`: close dialog, toast "Status saved — cliënt unreachable" (BR-FE-13)
|
||||
- HTTP 404: toast error, keep open
|
||||
- HTTP 400 / network error: toast error, keep open
|
||||
|
||||
**DisableMessage field logic** (BR-FE-08..11):
|
||||
- Rendered only when selected status is `'NotAvailable'`
|
||||
- On status change away from `'NotAvailable'`: clear field value
|
||||
- Value sent to API: `null` unless status is `'NotAvailable'`
|
||||
|
||||
---
|
||||
|
||||
## Hooks
|
||||
|
||||
### useCmsInstances
|
||||
|
||||
**File**: `src/api/useCmsInstances.ts`
|
||||
|
||||
```typescript
|
||||
useQuery<CmsInstance[], Error>({
|
||||
queryKey: ['cmsInstances'],
|
||||
queryFn: () => api.get<CmsInstance[]>('/api/v1/CmsInstances'),
|
||||
staleTime: 30_000,
|
||||
})
|
||||
```
|
||||
|
||||
### useAddCmsInstance
|
||||
|
||||
**File**: `src/api/useAddCmsInstance.ts`
|
||||
|
||||
```typescript
|
||||
useMutation<CmsInstance, Error, CreateCmsInstanceRequest>({
|
||||
mutationFn: (data) => api.post<CmsInstance>('/api/v1/CmsInstances', data),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }),
|
||||
})
|
||||
```
|
||||
|
||||
### useUpdateCmsInstanceStatus
|
||||
|
||||
**File**: `src/api/useUpdateCmsInstanceStatus.ts`
|
||||
|
||||
```typescript
|
||||
useMutation<UpdateStatusResult, Error, { id: string } & UpdateCmsInstanceStatusRequest>({
|
||||
mutationFn: ({ id, ...body }) =>
|
||||
api.put<UpdateStatusResult>(`/api/v1/CmsInstances/${id}/status`, body),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cmsInstances'] }),
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## i18n Keys (additions to `cms` namespace)
|
||||
|
||||
```json
|
||||
"cms": {
|
||||
"title": "CMS Instances",
|
||||
"addButton": "Add CMS",
|
||||
"emptyState": {
|
||||
"heading": "No CMS instances yet",
|
||||
"description": "Add your first CMS instance to get started."
|
||||
},
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"url": "URL",
|
||||
"status": "Status",
|
||||
"lastContact": "Last Contact",
|
||||
"disableMessage": "Disable Message",
|
||||
"actions": "Actions",
|
||||
"never": "—"
|
||||
},
|
||||
"status": {
|
||||
"Available": "Available",
|
||||
"NotAvailable": "Unavailable",
|
||||
"Inactive": "Inactive"
|
||||
},
|
||||
"actions": {
|
||||
"setStatus": "Set Status"
|
||||
},
|
||||
"add": {
|
||||
"title": "Add CMS Instance",
|
||||
"nameLabel": "Name",
|
||||
"urlLabel": "URL",
|
||||
"apiKeyLabel": "API Key",
|
||||
"submitButton": "Add",
|
||||
"successToast": "CMS instance added successfully"
|
||||
},
|
||||
"setStatus": {
|
||||
"title": "Set Status",
|
||||
"statusLabel": "Status",
|
||||
"disableMessageLabel": "Disable Message",
|
||||
"disableMessagePlaceholder": "Reason for disabling…",
|
||||
"submitButton": "Save",
|
||||
"successContactedToast": "Status updated — cliënt confirmed",
|
||||
"successUnreachableToast": "Status saved — cliënt unreachable"
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user