Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

08 — Form Specifications (Feature Flags Module)

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 the VALIDATION_ERROR (400) envelope with per-field details (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.

#FieldLabelRequiredType/KeyboardValidation (server)Client UX
1keyKeytext, 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)
2enabledEnabledAppSwitch boolean@IsBoolean() (update-feature-flag.dto.ts:10-11)default OFF in create mode; "off until you flip it"
3labelLabelAppTextField@IsOptional() @IsString() (update-feature-flag.dto.ts:14-16)hint "Human-readable name shown to staff"; used in list row title
4descriptionDescriptionAppTextField 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)
5moduleModuleAppTextField + 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.upsert persists only {tenantId, key, enabled, label} (feature-flag.repository.ts:37-42) — description and module are 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 $set is extended (OQ-3); the form is built so enabling them is a one-line change.
  • A description/module present 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 → map error.details[].message to fields (http-exception.filter.ts:103-107); empty details → 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

FieldLabelRequiredValidationUX
(selection)N selected flags≥ 1 itemmulti-select rows in list (long-press / checkbox); count in header
stateSet tobooleanSegmentedButton [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 returned keys.
  • 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: .next through fields, last .done; Enter submits; Esc cancels.
  • Error copy: from message for business 4xx; codes for the rest; never render raw 5xx.

Client-side error priority

  1. 400 VALIDATION → fields / row mapping.
  2. 401 UNAUTHENTICATED → silent refresh; fail → sessionExpired.
  3. 404 RESOURCE_NOT_FOUND → "flag removed" (detail/delete).
  4. 403 PERMISSION_DENIED → hide affordance (target; not enforced today — OQ-5).
  5. 429 RATE_LIMITED → countdown (api tier 100/min, rate-limit.constants.ts:6 — rare for admin screens; treat generically).
  6. 5xx → AppErrorState + requestId.