08 — Form Specifications (Communication Module)
- 1. Form map
- 2. Audience picker — value semantics per type
- 3. Submit model
- 4. Draft semantics
- 5. Client validation (mirrors class-validator)
- 6. Sub-states
- 7. a11y
- 8. Priority & expiry —
(planned)design intent
The Compose Announcement form is the only form in this module (thread compose is a lightweight input bar, deferred). Field rules are verbatim from
create-announcement.dto.ts; anything beyond it is(planned)and does not exist on the backend today.
1. Form map
| Field | Required | Control | Source rule |
|---|---|---|---|
| Title | ✓ | AppTextField single-line | IsString, MinLength(1) (create-announcement.dto.ts:30-33); trimmed on save (announcement.schema.ts:33-34) |
| Body | ✓ | AppTextField multiline | IsString, MinLength(1) (create-announcement.dto.ts:35-38); trimmed (announcement.schema.ts:36-37) |
| Audience type | ✓ | AudiencePicker choice chips | IsEnum(AudienceType) (create-announcement.dto.ts:14-16); enum all|role|grade|section|custom (announcement.schema.ts:7-13) |
| Audience value | conditional | picker / chips / none | IsOptional, IsString({ each: true }), string | string[] (create-announcement.dto.ts:24-26) |
| Attachments | ✗ | chip list + picker | IsOptional, IsArray, IsString({each}) (create-announcement.dto.ts:45-49) |
| Priority | — | not a backend field | (planned) — absent from announcement.schema.ts |
| Expiry | — | not a backend field | (planned) — absent from announcement.schema.ts |
2. Audience picker — value semantics per type
| Type | Value UI | Payload value | Resolution note |
|---|---|---|---|
all | none (hint "Everyone in your school") | omitted | resolves [] (announcement.service.ts:144-146) — broadcast marker |
role | dropdown of roles (RBAC constants) | "teacher" | org members roles: value, status: 'active' → userIds (announcement.service.ts:114-119) |
grade | picker fed by academics (grade names) | "Grade 10" | gradeRepo.findOne({name}) → students (:120-125) — name, not id |
section | picker fed by academics (section names) | "10-A" | sectionRepo.findOne({name}) → students (:126-135) — name, not id |
custom | user search + chips | ["u1","u2"] | direct ObjectId map (:136-143) |
Typo/unknown grade or section name ⇒ backend silently resolves
[](announcement.service.ts:122,129). Client should validate against live pickers and preview the recipient count(planned).
3. Submit model
| Action | Requests | Result |
|---|---|---|
| Save draft | POST /announcements | published: false, createdBy from token (announcement.service.ts:31-37); emits AnnouncementCreated (:39-51) |
| Publish | POST /announcements → POST /announcements/:id/publish | targetUserIds resolved, published: true, publishedAt (:68-93); emits AnnouncementPublished (:80-91) |
Two-step sequencing: publish button performs save-then-publish; if the save fails, nothing publishes; if publish fails (404/network), draft is kept and error surfaced with "Retry publish".
4. Draft semantics
- Draft = document with
published: false(announcement.schema.ts:45-46). - Drafts are never returned specially —
GET /announcementsincludes them in the full list (announcement.service.ts:56-60); client shows them only on "Mine"/author views. - No update route — an existing draft cannot be edited, only recreated.
(planned). AnnouncementCreatedfires on draft save, so downstream channels may receive draft-level events (announcement.service.ts:39-51) — relevant if the client shows "created" toast.
5. Client validation (mirrors class-validator)
| Field | Client rule | Failure UX |
|---|---|---|
| Title | non-empty after trim, ≤ 120 chars (product limit (proposed)) | inline error, focus |
| Body | non-empty after trim | inline error, focus |
| Audience type | one of 5 enum values | radio/chips always select one |
| Audience value | required unless all; strings only | inline error "Choose who sees this" |
| Attachments | URL strings; size/type pre-check client-side only (proposed) | chip-level error |
6. Sub-states
- idle → dirty → saving draft (button spinner, fields disabled) → saved (snackbar "Draft saved", stay on form) → publish (loading, full-screen spinner on second request) → success (pop to feed/mine).
- Network failure: banner + retry; no offline persistence — form state kept in
memory
(forward-looking: local draft store). - Double-tap protection on Publish (idempotent server-side, but avoid duplicate POSTs).
7. a11y
Labels linked to fields; audience chips grouped as radio group (Semantics container);
"0 recipients" warning as live region (planned); keyboard: title .text, body
multiline with toolbar row (attachment, priority (planned)).
8. Priority & expiry — (planned) design intent
When backend adds priority (e.g. normal|high + pinned) and expiry (auto-hide date)
fields — flagged in IMPLEMENTATION_PLAN.md roadmap (docs/IMPLEMENTATION_PLAN.md:231
templates/preferences cluster; none for announcements specifically) — the form gains:
- priority segmented control (normal/high) — high rows pinned on feed
- expiry date picker (
showDatePicker, min = today+1) — expired items filtered client-side and flagged "Expired" Until then these controls MUST NOT render (no schema fields,announcement.schema.ts:31-53).