08 — Form Specifications (Feature Flags Module)
- 1. Flag Editor (create + edit) —
PUT /feature-flags - 2. Bulk Update Sheet —
PUT /feature-flags/bulk - 3. Delete Flag —
DELETE /feature-flags/:key - 4. Org-level flag map —
PATCH /organizations/:id/feature-flags - Form-level rules (all)
- Client-side error priority
The flag editor and bulk sheet, field-by-field. Validation mirrors class-validator decorators exactly from
src/modules/feature-flags/dto/update-feature-flag.dto.ts; errors follow theVALIDATION_ERROR(400) envelope with per-fielddetails(http-exception.filter.ts:103-107). Client validates inline; server 400 shadows client.
1. Flag Editor (create + edit) — PUT /feature-flags
DTO source: update-feature-flag.dto.ts:4-26.
| # | Field | Label | Required | Type/Keyboard | Validation (server) | Client UX |
|---|---|---|---|---|---|---|
| 1 | key | Key | ✓ | text, mono, lowercase-hinted | @IsString() (update-feature-flag.dto.ts:6-7) | create mode: editable, placeholder "e.g. channels.whatsapp", helper "Unique per school. Use dots to group (module.name)". edit mode: read-only (upsert matches on key; changing it = new flag) |
| 2 | enabled | Enabled | ✓ | AppSwitch boolean | @IsBoolean() (update-feature-flag.dto.ts:10-11) | default OFF in create mode; "off until you flip it" |
| 3 | label | Label | — | AppTextField | @IsOptional() @IsString() (update-feature-flag.dto.ts:14-16) | hint "Human-readable name shown to staff"; used in list row title |
| 4 | description | Description | — | AppTextField multiline (3 rows) | @IsOptional() @IsString() (update-feature-flag.dto.ts:19-21) | not persisted by server upsert — warning banner; read-only-ish behavior (see below) |
| 5 | module | Module | — | AppTextField + suggestion chips | @IsOptional() @IsString() (update-feature-flag.dto.ts:24-26) | suggestions from loaded data; not persisted by server upsert — see below |
Server write behavior (must be surfaced):
FeatureFlagRepository.upsertpersists only{tenantId, key, enabled, label}(feature-flag.repository.ts:37-42) —descriptionandmoduleare accepted by the DTO but dropped on every write, including first insert.- Impact: the editor's description/module fields can never be saved via the API today. The
form shows
AppBanner(warning)in edit mode: "Description & module can't be saved yet — server persists only enabled & label (feature-flag.repository.ts:40)." Save button persists the rest. These fields will become writable when the repository$setis extended (OQ-3); the form is built so enabling them is a one-line change. - A
description/modulepresent in the doc (e.g., seeded later —(planned)IMPLEMENTATION_PLAN.md:291-307) renders read-only in edit mode until then.
Submit → loading → server:
- 200 → server returns the flag doc (
{new:true},feature-flag.repository.ts:41) → pop to detail (refresh) + snackbar "Flag saved". - 400
VALIDATION_ERROR→ maperror.details[].messageto fields (http-exception.filter.ts:103-107); emptydetails→ generic 400 banner. - 404 on edit → flag deleted meanwhile → banner "This flag was removed — reopening list".
- 5xx (incl. E11000 duplicate on re-create after soft-delete, OQ-4) →
AppErrorState-style snackbar with requestId; form retained.
Client-side pre-validation (UX only; server is authority):
- key: non-empty, trimmed; suggest
^[a-z][a-z0-9._-]{1,63}$(proposed)(server accepts any string — no pattern,update-feature-flag.dto.ts:5-7); warn on spaces. - enabled: boolean, no client gate.
- label/description/module: optional, trimmed.
- Double-submit blocked while saving (00-shared/08 §6).
2. Bulk Update Sheet — PUT /feature-flags/bulk
| Field | Label | Required | Validation | UX |
|---|---|---|---|---|
| (selection) | N selected flags | ✓ | ≥ 1 item | multi-select rows in list (long-press / checkbox); count in header |
state | Set to | ✓ | boolean | SegmentedButton [Enable all] [Disable all] |
label (shared) | Label (all) | — | @IsOptional() @IsString() | applied to every selected key; optional |
Body: JSON array of UpdateFeatureFlagDto (feature-flags.controller.ts:50).
Each item: {key, enabled, label?} (only these persist — same repository, same caveat).
Server behavior:
- 400 → whole-body validation failure: class-validator reports array-item errors
(
details[].message) → map each message back to its item by order; highlight offending rows. - 200 → array of server docs, applied sequentially (
feature-flags.service.ts:40-44) — order preserved; client derives per-row success by matching returnedkeys. - 5xx mid-loop → partial apply; error envelope returned; no rollback. Sheet shows "Applied: k of n" and "Retry failed (n−k)" — retry resends only missing keys (client-tracked; safe because upsert is idempotent per key).
Result presentation: rows with ✓ (secondary) / ✕ + reason (error); dismiss →
list refetch (Refresh).
3. Delete Flag — DELETE /feature-flags/:key
No body. Confirm dialog (destructive) → DELETE /feature-flags/:key
(feature-flags.controller.ts:54-58). Server: 404 if key missing (feature-flags.service.ts:47-52)
→ treat as already-gone; 200 → soft delete, row removed. Copy warns about re-create
constraint (OQ-4). Server-confirm only — no optimistic removal.
4. Org-level flag map — PATCH /organizations/:id/feature-flags
Cross-surface form (context) — see 12_API_Mapping.md §Org. Body = plain Record<string, boolean> (organizations.controller.ts:86) — full replace ($set: {'metadata.featureFlags': flags}, organizations.service.ts:149-151). Client must load the
current map (GET /organizations/:id/feature-flags, organizations.service.ts:139-142) and
merge locally before PATCH — any key omitted from the body is lost. Validate boolean
values; no DTO server-side (no class-validator) — send only booleans.
Form-level rules (all)
- Double-submit: disabled while pending.
- Optimistic: toggle-only (list/detail switches) is optimistic with rollback (00-shared/06 §3.5); editor save, bulk apply, and delete are server-confirm.
- Undo: delete has no undo (soft delete, but no restore endpoint; re-create blocked — OQ-4); toggles have no undo snackbar (rollback = flip again).
- Abandonment: dirty editor → discard-confirm dialog (Esc/back); injected key (deep link) preserved until saved.
- Keyboard:
.nextthrough fields, last.done;Entersubmits;Esccancels. - Error copy: from
messagefor business 4xx; codes for the rest; never render raw 5xx.
Client-side error priority
- 400 VALIDATION → fields / row mapping.
- 401 UNAUTHENTICATED → silent refresh; fail → sessionExpired.
- 404 RESOURCE_NOT_FOUND → "flag removed" (detail/delete).
- 403 PERMISSION_DENIED → hide affordance (target; not enforced today — OQ-5).
- 429 RATE_LIMITED → countdown (api tier 100/min,
rate-limit.constants.ts:6— rare for admin screens; treat generically). - 5xx → AppErrorState + requestId.