Publishing & Versioning UX¶
Addendum to the Question Management UI Specification. Covers the entity model, publishing workflow, draft/published state representation, validation, and publish button placement across Design, Assign, and Preview views.
Table of Contents¶
- Entity Model
- State Lifecycle
- Publishing Workflow
- Design View -- Draft & Published States
- Assign View -- Two Visual Dimensions
- Preview View -- Draft vs Published
- Publish Button Placement
- Pre-Publish Validation
- Cross-Question Validation in Design View
- PQS as Implicit vs Explicit Concept
- Resolved Decisions
- Remaining Open Questions
- Prototype Design Decisions Addendum
- Autosave Failure and Offline Resilience
- Annotator Re-Answer Experience
1. Entity Model¶
Overview¶
The domain model separates draft questions (design-time, fully mutable, embedded in the Project aggregate) from published annotation questions (versioned, structurally frozen, in their own aggregate/collection). The publish ceremony promotes drafts to published entities.
See implementation-plan.md Phase 1 for the complete data model with entity/value object classifications, mutability rules, and MongoDB collection layout.
Key Entities¶
| Type | Classification | Where | Lifecycle |
|---|---|---|---|
| DraftQuestion | Entity (within Project aggregate) | pmProject.DraftQuestions[] |
Fully mutable. Deletable. Created in Design view. Destroyed on publish (promoted to AQ). |
| AnnotationQuestion | Entity (own aggregate) | pmAnnotationQuestion collection |
Created on first publish. Structural props frozen. Cannot be deleted. Content changes via draft field. |
| ProjectQuestionSet | Entity (singleton on Project) | pmProject.ProjectQuestionSet |
One per project. Contains mutable draft + append-only immutable versions. Internal only -- never referenced in UI. |
| StageQuestionSet | Entity (singleton on Stage) | Stage.StageQuestionSet |
One per stage. Contains mutable draft + append-only immutable versions. Internal only -- never referenced in UI. |
UI terminology rule: The terms PQS, SQS, "Project Question Set", "Stage Question Set", and "question set version" must NEVER appear in user-facing UI text. Admins think in terms of "the current questions" vs "your changes", "publishing to a stage", and temporal labels like "Update 3 -- Mar 28, 14:32" when identification is needed. These internal entity names are valid in domain model documentation and code, but not in any UI copy, tooltips, labels, or dialogs.
Key Value Objects¶
| Type | Immutable? | Purpose |
|---|---|---|
| AQVersion | Yes (has reference ID for annotations) | Immutable content snapshot. Created on publish. |
| DraftContent | No (mutable, autosaved) | Pending content changes on a published AQ. Null when clean. |
| PQSVersion | Yes (has reference ID) | Immutable snapshot of ordered question references. |
| SQSVersion | Yes (has reference ID) | Immutable snapshot of stage assignment (unordered set + PQS version ref). |
| QuestionReference | Yes | Typed reference in draft PQS: {questionId, type: "draft" or "published"} |
| QuestionRef | Yes | Reference in published PQS: {questionId, versionId} |
| DraftPQS | No (mutable) | Current ordered arrangement of all project questions. |
| DraftSQS | No (mutable) | Current unordered set of questions assigned to a stage. |
Ordering¶
- Root question ordering: Encoded in the draft PQS / PQS version's ordered array.
- Child question ordering: Derived by filtering the PQS array to children of a given parent (using
parentQuestionIdon each question). The subset's order in the PQS array defines child ordering. - Stage question ordering: Derived from the PQS ordering filtered by the SQS question set.
- No ordering stored on individual questions or on the SQS.
Autosave¶
"Autosave" means the client debounces changes and sends them to the server automatically. The server persists:
- Draft question changes: Written to
DraftQuestionentities on the Project (for unpublished questions) - Published question content changes: Written to the
draftfield on theAnnotationQuestiondocument inpmAnnotationQuestion - Assignment changes: Written to
StageQuestionSet.drafton the Stage - Ordering changes: Written to
ProjectQuestionSet.drafton the Project
This is the PR2398 pendingChanges mechanism with an automatic trigger. All draft data is server-side — nothing is stored only in the browser.
2. State Lifecycle¶
Question States¶
Each annotation question can be in one of these states:
| State | Description | Visual Indicator |
|---|---|---|
| Draft (new) | Never published. Only exists as a draft. | "Draft" label |
| Published (clean) | Published and no pending changes. | No indicator (clean = default state) |
| Published (with unpublished changes) | Published, but has unpublished edits on top. | "Published with changes" indicator |
Note: Version numbers are NOT shown on question nodes. Only "Draft" (never published) or "Published with changes" (published, with unpublished changes) indicators appear. Version history with dates/timestamps is available in the properties panel and version history dialog.
Autosave Model¶
There is no manual "Save" action. All draft changes are autosaved (debounced, every few seconds of inactivity). The only deliberate action is Publish. This follows the Figma/Google Docs model.
Friction Model¶
The system uses graduated friction that matches the stakes of each action:
| Phase | Friction Level | User Experience |
|---|---|---|
| Editing drafts (Design view) | Zero -- autosave, no confirmation | Feels like Figma/Google Docs |
| Assigning questions (Assign view) | Low -- checkbox toggles, autosaved | Lightweight configuration |
| Publishing (Assign view ceremony) | Intentional -- review dialog, change reasons, confirmation | Matches the stakes: creating immutable versions |
Publishing only happens when assigning questions to a stage, which is the moment the admin is committing to 'this is what annotators will see.' That is the right place for ceremony.
Draft safety net — two layers:
Layer 1: Undo/redo stack (session-level, client-side)
- Operates at the whole question management session level, not per-question
- Every autosaved draft change is an operation in a single undo stack (edits, additions, deletions, reorders, assignment changes)
- Cannot undo past a publish boundary -- published versions are immutable. The undo stack resets after a stage publish.
- Lost when the browser session ends
- Autosave interaction: Undo/redo cancels any pending autosave debounce and restarts the timer with the new state. This prevents stale intermediate states from reaching the server during rapid undo/redo sequences. Only the settled state (after the user stops pressing undo/redo for the debounce period) is autosaved. The server always receives consistent state.
Layer 2: Draft snapshots (server-side, persistent, self-service recovery)
Periodic snapshots of the entire draft state (draft PQS ordering + all draft question contents), stored on the Project. Provides Google Docs-style version history for drafts.
What gets snapshotted: Only draft state -- DraftQuestion contents and published AQ draft fields. Published AQ versions are already permanent and don't need snapshotting.
Deduplication: If a question hasn't changed since the last snapshot, the entry references the previous snapshot rather than storing a copy. Only changed questions store full content.
GFS (Grandfather-Father-Son) retention:
- Son: Every 15 minutes of active editing -- kept for 24 hours
- Father: One per day -- kept for 7 days
- Grandfather: One per week -- kept for 30 days
- Hard cap on total snapshots per project (e.g., 50)
Snapshot triggers: Every N minutes of active editing, on navigation away from Design view, on session end, before publish (captures pre-publish draft state).
Browsing and restoring: Accessible from the Design view via a "Draft history" action. Shows a timeline of snapshots with timestamps. Admin can:
- Restore entire draft: Replaces the full draft PQS ordering and all draft question contents with the snapshot state. The current state is snapshotted first (so the restore itself is undoable).
- Restore a single question: Replaces just one question's draft content from the snapshot, leaving everything else as-is. Safe because content and ordering are separate concerns.
Draft history UI (in the Design view, alongside version history):
DRAFT HISTORY (recent autosaves)
Today 14:32 3 questions changed [Restore all] [View details]
Today 11:15 1 question changed [Restore all] [View details]
Yesterday 16:45 5 questions changed [Restore all] [View details]
Mar 20 (weekly) 12 questions changed [Restore all] [View details]
[View details] expands to show individual questions:
- "What kind of TBI model?" (text changed) [Restore this question]
- "Drug Name" (options changed) [Restore this question]
- "Species" (unchanged)
Annotation Count¶
Questions that have been answered by annotators show an annotation count indicator on the node. The properties panel shows: "12 studies, 47 annotations. Content changes will create a new version on publish."
Stage Question Set States¶
| State | Description |
|---|---|
| No SQS | Stage has never had questions published. Only draft assignments exist. |
| Published (clean) | Published SQS, no pending assignment changes, all AQ versions current. |
| Published (draft assignment changes) | Published SQS exists, but admin has added/removed questions in draft. |
| Published (outdated AQ versions) | Published SQS exists, but some AQ versions are older than the latest PQS. |
| Published (draft changes + outdated) | Both pending assignment changes and outdated versions. |
3. Publishing Workflow¶
Trigger: Per-Stage Publishing with Cascading Validation¶
The admin publishes a stage, not a PQS directly. The PQS is updated implicitly as a side effect.
Admin clicks "Publish [Stage Name]"
|
+--> 1. VALIDATE: Check all questions in the draft SQS
| - Cross-question coherence (conditional references, parent integrity)
| - All required fields filled
| - No broken option references
|
+--> 2. If INVALID: Show errors, block publish
| - List which questions have problems
| - Link to each problem in Design view
| - Publish button remains disabled
|
+--> 3. If VALID: Create immutable versions
|
+--> 3a. For each AQ in this SQS that has draft changes:
| - Create new AQ version (v2, v3, etc.)
| - Clear draft state on that AQ
|
+--> 3b. Create new PQS version
| - References latest AQ versions for questions in this SQS
| - References existing AQ versions for all other questions
|
+--> 3c. Create new SQS version
| - References the new PQS version
| - Contains the ordered subset of question IDs
|
+--> 3d. Stage goes live / updates live form
- Annotators see the new SQS version
What Happens to Questions NOT in This Stage's SQS¶
- Questions assigned to other stages but not this one: Unchanged. Their draft changes (if any) remain as drafts.
- Questions not assigned to any stage: Unchanged. They stay as drafts.
- The new PQS version includes the previous versions of these unchanged questions (carried forward from the last PQS version).
What Happens to Other Stages After Publishing¶
- Other stages' SQS versions still reference their existing PQS version. They are not auto-updated.
- If a shared question got a new AQ version (because it was published with this stage), other stages still see the old version.
- The Assign view shows a notification on those stages: "N questions have newer versions available."
Publish All Stages¶
An optional "Publish all stages" action that:
- Validates ALL draft SQSes across all stages simultaneously
- Creates new AQ versions for all questions with draft changes
- Creates a single PQS version
- Creates new SQS versions for all stages
- Useful when a question change needs to be coordinated across stages
4. Design View -- Draft & Published States¶
Node Visual Indicators¶
Each question node in the tree shows its current state:
| State | Visual Treatment |
|---|---|
| Draft (new, never published) | "Draft" label in muted style. |
| Published, clean | No indicator (clean is the default, uncluttered state). |
| Published, with unpublished changes | "Published with changes" indicator. Subtle left border or background tint. |
| Validation warning | Amber warning icon on the node. Visible regardless of selection. Tooltip shows the problem. |
| Has annotations | Small annotation count indicator (e.g., "12 studies"). Visible in properties panel with full detail. |
No version numbers on nodes. Version history is accessed via the properties panel "Version history" action.
Cross-Question Validation Warnings¶
When editing a question creates a problem for another question:
During editing (live, in the properties panel):
- Impact preview section appears when changes would affect other questions
- Shows which other questions are affected and why
- Example: "Removing option 'Weight drop' will break the display condition on 3 child questions"
- This is informational -- the change is autosaved as a draft regardless
After saving (persistent, in the tree):
- Affected nodes show a warning icon
- Warning persists until the admin fixes the child question's condition
- Clicking the warning shows: "This question's display condition references option 'Weight drop' which no longer exists on parent question 'TBI model type'"
- The publish button remains disabled while warnings exist (for the stage containing these questions)
Properties Panel -- Status & Draft Changes¶
When a published question with draft changes is selected, the properties panel shows:
STATUS
------
Published (last: Mar 15, 2026) [Version history]
DRAFT CHANGES (unpublished)
----------------------------
Text: "What species?" -> "What animal species is used?"
Options: [Mouse, Rat, Dog] -> [Mouse, Rat, Dog, Cat]
These changes will be published when a stage
containing this question is next published.
[Discard draft changes]
The "Discard draft changes" action reverts to the last published content.
Version History Dialog¶
Accessible via "Version history" in the properties panel. Shows all published versions:
VERSION HISTORY: "What animal species is used?"
-----------------------------------------------
Mar 15, 2026 Chris "Added Cat to species list"
Options: [Mouse, Rat, Dog] -> [Mouse, Rat, Dog, Cat]
[Apply as draft]
Feb 28, 2026 Chris (initial publication)
Text: "What animal species is used?"
Options: [Mouse, Rat, Dog]
[Apply as draft]
- Each entry shows: date, author, change reason (if provided), and a summary of what changed
- "Apply as draft" restores that version's content as the current draft. It does NOT revert -- it creates a new draft state that must be published to take effect.
- The draft changes section in the properties panel then shows the diff between the latest published version and the restored content.
- The version history notes that the draft was derived from a previous version.
5. Assign View -- Two Visual Dimensions¶
The Assign view communicates pending changes through two orthogonal visual dimensions, replacing the previous three-indicator design. All assignment changes are autosaved -- indicators show the diff between the published state and the current autosaved draft state.
Dimension 1: Change Status (icon, per-question)¶
| Indicator | Meaning |
|---|---|
| No indicator | Identical to what is currently published for this stage |
Small amber dot / edit_note |
Has changes that will be included in next publish -- whether from admin's own edits OR from another stage's publish (merged concept, no distinction in tree) |
Warning icon (amber warning) |
Validation error that blocks publishing |
The previous "Updated version available" indicator (blue sync icon for cross-stage publishes) is merged into the generic amber "changed" indicator. The source of changes (own edits vs other-stage publishes) is NOT distinguished in the tree -- that breakdown appears in the publish wizard Step 1 and as a quiet inline notice when entering the Assign view.
Dimension 2: Assignment Changes (row treatment)¶
| Treatment | Meaning |
|---|---|
| Normal row | Currently published on this stage, will remain |
| Left green border + "+" prefix | NOT currently published, will be added |
| Left red border + "-" prefix | Currently published, will be removed |
| Unchecked, no border, muted | Not assigned, never has been |
Cross-Stage Publish Communication¶
The distinction between "your edits" and "updates from other stages" is shown:
- Assign view entry: Quiet inline notice (not a banner): "3 questions were updated when Screening was published on Mar 15."
- Publish wizard (Step 1): Changes grouped by source -- "Content changes (your edits)" vs "Updated from other stages"
- Properties panel: On-demand detail when selecting a changed question
Filter Toggle Interaction¶
The filter toggle should use All / On stage / Not on stage and should be based on the published stage assignment state (not the draft). Unpublished-change overlays (green/red indicators) remain visible on top of that baseline so admins can compare what annotators see now against what will change on next publish.
Annotations Warning on Removal¶
When a question with annotations is being removed from a stage (red indicator), an additional warning is shown: "This question has N annotations across M studies. Removing it will not delete annotations, but annotators will no longer see it for new sessions."
6. Preview View -- Draft vs Published¶
Preview Scope Options¶
The Preview view should offer two preview modes:
Option A: Stage Preview (primary)
- Select a stage from the dropdown
- Preview shows the annotation form as it would appear with the current configuration:
- If the stage has a published SQS: shows the published version by default
- Toggle to "Preview with pending changes" to see what the form will look like after publishing
- Pending changes are highlighted (new questions, changed text, removed questions shown as struck-through or absent)
Option B: Full Project Preview (secondary)
- Shows all questions across all categories without stage filtering
- Useful for reviewing the complete question set before publishing
- Available as a separate option: "Preview: [Stage Dropdown] | [Full Project]"
Draft vs Published Toggle¶
+------------------------------------------------------------------+
| Preview |
| Stage: [Extraction v] |
| |
| Showing: ( ) Published version (*) With pending changes |
| |
| [Banner: "You are previewing pending changes. 3 questions have |
| been modified, 1 added, 0 removed since the last publish."] |
+------------------------------------------------------------------+
When "With pending changes" is selected:
- Questions with draft content changes show their DRAFT text/options (not the published version)
- Questions pending addition to the stage are shown with a subtle "new" indicator
- The annotation form is fully interactive for testing conditional logic
When "Published version" is selected:
- Shows exactly what annotators currently see
- No draft content, no pending assignments
7. Publish Button Placement¶
Research Summary¶
CMS best practices (Craft CMS, Payload CMS, Webflow, Drupal) consistently show:
- The publish action should be contextual -- visible where the user is working
- Status information (draft/published) should be glanceable at all times
- The button state should change based on context (new vs published vs has changes)
Decision: Review & Publish on Assign, Publish Stage on Preview¶
Assign page (primary entrypoint):
One primary action in the action bar:
- "Review & Publish N changes": Opens the publish wizard from the Assign page. This keeps the main configuration workflow focused and avoids forcing the admin to choose between two publish entry points before they have reviewed the changes.
+------------------------------------------------------------------+
| [Action Bar] |
| 5 live on stage | 4 pending changes |
| [Discard] [Review & Publish 4 changes] |
+------------------------------------------------------------------+
Preview page (secondary entrypoint):
"Publish Stage" button in the action bar. Available when the admin is previewing pending changes. It opens the same publish wizard as the Assign page, but from the preview context.
+------------------------------------------------------------------+
| [Preview Action Bar] |
| Showing pending changes for Extraction |
| 2 edited, 1 added, 1 removed [Publish Stage] |
+------------------------------------------------------------------+
Design page: No publish button.
Shows a banner when draft changes exist: "You have draft changes. Go to Assign to configure and publish a stage."
"Publish All Stages" Placement¶
Less common action. Available in:
- The Assign view toolbar (overflow menu)
- The project-level Stage Settings page
8. Pre-Publish Validation¶
The Publish Button Should Be Disabled When Invalid¶
The publish button is always visible but disabled with a clear explanation when the configuration is invalid.
+------------------------------------------------------------------+
| [Action Bar -- INVALID STATE] |
| |
| [!] Cannot publish: 2 validation errors |
| |
| 1. "If other, specify" references option "Weight drop" on |
| parent "TBI model type" which no longer exists. |
| [Go to question in Design ->] |
| |
| 2. "Dose" is a required child of "Drug Name" but "Drug Name" |
| is not assigned to this stage. |
| [Assign "Drug Name" ->] |
| |
| [Publish Stage] (disabled) |
+------------------------------------------------------------------+
Validation Runs Continuously¶
- Validation is not just a pre-publish check -- it runs continuously as the admin makes changes
- The action bar updates in real time as questions are assigned/unassigned
- Errors link directly to the problematic question in the Design view or offer one-click fixes (like assigning a missing parent)
Validation Categories¶
| Category | Severity | Blocks Publish? |
|---|---|---|
| Broken conditional reference (option removed) | Error | Yes |
| Missing parent in stage (child assigned without parent) | Error | Yes |
| Question with empty text | Error | Yes |
| Duplicate option values | Error | Yes |
| Question has no options (dropdown/checklist with 0 options) | Error | Yes |
| Question has draft changes but no stage assignment | Warning | No |
| Outdated AQ version in stage (newer version exists) | Info | No |
9. Cross-Question Validation in Design View¶
Live Impact Preview¶
When editing a question in the Design view properties panel, if the change would affect other questions, an impact section appears:
+------------------------------------------------------------------+
| PROPERTIES: "What kind of TBI model is used?" |
| |
| Options: |
| [x] Weight drop |
| [x] Controlled cortical impact [Remove selected] |
| [x] Fluid percussion injury |
| [x] Blast injury |
| |
| [!] IMPACT: Removing "Weight drop" will affect 3 questions: |
| - "From which height (cm)?" -- condition: TBI model = Weight drop |
| - "Amount of weight used?" -- condition: TBI model = Weight drop |
| - "Did they attach to skull?" -- condition: TBI model = Weight drop|
| |
| These questions will show a validation warning until their |
| conditions are updated. |
| |
| [Discard draft changes] |
+------------------------------------------------------------------+
Draft State Allows Invalid Configurations¶
- Draft changes that create broken references are autosaved without blocking
- Warnings appear on affected nodes in the tree and persist until fixed
- The publish button is disabled for any stage containing broken questions
- This allows incremental changes without forcing the admin to fix everything in one session
10. PQS as Implicit vs Explicit Concept¶
Decision: Keep PQS/SQS Fully Hidden from Users¶
The ProjectQuestionSet and StageQuestionSet are important data model entities, but they are never surfaced in the UI. The admin thinks in terms of:
- "I'm designing questions" (Design view)
- "I'm configuring what a stage shows" (Assign view)
- "I'm publishing a stage" (Publish action)
They do not need to think about PQS or SQS as separate concepts. Publishing creates new AQ versions, not "PQS versions." The admin publishes "to a stage," not "an SQS."
If identification of a particular publish snapshot is needed, the UI uses sequential labels with timestamps: "Update 3 -- Mar 28, 14:32". Dates alone are ambiguous (multiple publishes per day).
When to Introduce the Concept Explicitly¶
The project-level question set concept becomes relevant to the user when:
- Reconciliation: The reconciled gold-standard annotation set maps to the full project question set.
- Cross-project sharing: If questions are shared between projects.
- Data export: Exporting the complete question set for a project.
Decision: Introduce the concept in the UI when reconciliation is implemented. For now, it exists in the data model but is not surfaced. The admin's mental model is "stages have questions" -- the project-level aggregation is implicit.
11. Resolved Decisions¶
Formerly open questions, now resolved through discussion.
RD-1: Publish Button Placement¶
Decision: Publish on both Assign and Preview pages, but with one primary path. Assign uses a single primary action, "Review & Publish N changes", and Preview exposes a secondary "Publish Stage" action when the admin is reviewing unpublished changes.
RD-2: Preview Scope¶
Decision: Option B -- Preview shows individual stages (via dropdown) AND a "Full Project (all questions)" option. The full project option includes an info banner: "This shows all questions across all stages. This is not what any individual stage's annotation form looks like."
RD-3: Outdated Versions Require Intentional Update (Corrected)¶
Decision: SQS updates are never automatic. When a shared question gets a newer AQ version (via another stage's publish), other stages show an "updated version available" indicator on the Assign page — but the admin must intentionally publish that stage to adopt the newer versions.
Rationale: Updating an SQS has downstream consequences for active annotation sessions. The Admin Decision Framework (see PR2398 planning docs) evaluates the impact per-session: carrying forward answers, blanking invalid answers, marking questions for re-answering, handling PAC (Parent Annotation Condition) cascades. This is not something that should happen as a side effect — it requires review and confirmation.
UI implications: The "updated version available" indicator on the Assign page is not just informational — it's a prompt for the admin to review and decide when to publish. The publish ceremony for this stage will include the Admin Decision Framework if there are active sessions.
RD-4: Draft Changes Indicator Granularity¶
Decision: Option C -- Expandable detail with progressive disclosure. Minimal indicator by default (small dot or icon). Expandable on hover/click to show a summary of what changed.
RD-5: PQS Introduction Timing¶
Decision: Option A (implicit now), with consideration for Option B (light introduction) later. The PQS is not surfaced to the admin in the current QM UI. It exists in the data model and is created implicitly when a stage publishes. The PQS concept will be introduced explicitly when reconciliation is implemented, as the reconciled gold-standard annotation set maps to the full PQS.
RD-6: Autosave vs Manual Save¶
Decision: Autosave for all draft changes (Figma/Google Docs model). No manual "Save" action. The only deliberate action is "Publish." Draft safety provided by undo/redo stack (session-level) and periodic automatic snapshots.
RD-7: Version Numbers¶
Decision: Do not show version numbers (v1, v2, etc.) on question nodes. Show only "Draft" (never published) or "Published with changes" (published, with unpublished changes). Version history with dates/timestamps accessible via properties panel "Version history" dialog. "Apply as draft" action available on previous versions.
RD-8: Review & Publish Confirmation¶
Decision: "Review & Publish" on the Assign page opens a step-by-step publish wizard. "Publish Stage" on the Preview page also triggers this wizard.
12. Remaining Open Questions¶
OQ-A: Stage Question Set Version History¶
Should there be a mechanism in the UI to view previous SQS versions and their differences? If so, where should this live -- in the Assign view, in Stage Settings, or both?
OQ-B: Draft Snapshots Browsing — RESOLVED¶
Decision: Draft history is accessible from the Design view via a "Draft history" action (separate from published version history). Shows a timeline of GFS-retained snapshots. Admin can restore the entire draft state or a single question. See State Lifecycle > Layer 2: Draft snapshots for full details.
OQ-C: Responsive Design¶
Minimum node width requirements and the trigger point for focus mode on narrow screens need prototyping. Bottom sheet behaviour for the properties panel on mobile needs design.
13. Prototype Design Decisions Addendum¶
Additional decisions established during interactive prototyping that augment this specification.
PVD-01: Assign View Filter Semantics¶
The filter toggle uses three states: All / On stage / Not on stage. These refer to the last published SQS for the selected stage, not the current draft checkbox state. Unpublished-change overlays (green/red indicators) remain visible on the filtered rows so admins can review pending additions and removals against the published baseline. A "Last published: [date]" timestamp is shown next to the filter.
PVD-02: Checkbox Propagation Rules¶
When a user checks or unchecks a question in the Assign view:
- Checking a child automatically checks all ancestors up to the root (parent integrity constraint)
- Unchecking a parent automatically unchecks all descendants
- Indeterminate state shown when a parent is checked but not ALL of its descendants are checked
- System questions are always checked and disabled
PVD-03: Change Detection Baseline¶
Changes in the Assign view are detected by comparing q.checked !== q.published — the draft state vs. the published state. The Discard button is disabled when no changes exist from the published baseline.
PVD-04: Status Icon System for Change Indicators¶
The Assign view uses icon-only indicators with tooltips (no worded badges) to reduce visual noise:
add_circle(green): "Will be added on publish"remove_circle(red): "Will be removed on publish"edit_note(orange): "Has unpublished changes"draft(purple): "Draft — never published"lock(grey): "System question"warning(amber): Validation issue
PVD-05: Preview Mode — Published vs With Changes¶
The Preview view includes a toggle between "Published" and "With Changes" modes:
- Published: Shows exactly what annotators currently see. Simulation mode banner.
- With Changes: Shows how the form will look after publishing. Orange changes banner with counts. Removed questions shown struck-through with danger-coloured outline.
PVD-06: Review Page Progress Indicators¶
Two segmented progress bars above the study card:
- Screening bar (primary blue): You screened / Remaining for you / Unavailable (sufficiently screened by others)
- Annotation bar (success green): You annotated / Remaining for you / Unavailable (sufficiently annotated by others)
Each segment has a distinct visual: solid for "You", light tint for "Remaining", hatched pattern for "Unavailable". Legends with colour keys below each bar.
PVD-07: Version History Property Previews¶
When browsing version history (both individual question and PQS level), each version entry can be expanded to show the full properties snapshot that would be applied if restored:
- All fields shown: text, help, type, control (with icon), required, multiple, options list, default checkbox status, parent question, show-when condition
- Changed fields highlighted in orange for quick identification
- "Apply as draft" action per version (individual) or "Restore all" / "Restore selected" (PQS level)
PVD-08: Project Question History — Tree with Selective Restore¶
The PQS version browser (accessible from Design toolbar) shows:
- Expandable version cards with metadata (date, stage, question count, change summary)
- Full question tree per version with hierarchy and control type icons
- Each question row is clickable to expand and preview its properties at that version
- Checkboxes on non-system questions for selective restore
- "Restore all as draft" and "Restore selected" buttons with live selection counter
PVD-09: Publish Dialog Structure¶
The Review & Publish confirmation dialog shows:
- Content changes section (questions getting new AQ versions with diffs)
- Questions being added to the stage
- Questions being removed from the stage
- Result card with new PQS/SQS version numbers
- Optional change reason text input
- "Breaking changes" checkbox for flagging potentially invalidating changes
PDD-10: Change Reason Capture -- Three-Layer Approach¶
The challenge: by publish time, the admin may have forgotten why they edited a question days or weeks ago. Research across REDCap, Figma, Git, Confluence, and Qualtrics shows that optional single-field reasons at publish time have low adoption (~10-20%), and required fields get low-quality text unless the user has immediate context.
Layer 1: Lightweight inline change notes during editing (Design view)
A small, collapsible 'note' affordance in the properties panel -- a sticky note icon that expands to a one-line text input. When the admin edits a published question (creating draft changes), they can optionally jot a note like 'Added SC route per reviewer feedback.' This note:
- Is attached to the question's
DraftContenton the AnnotationQuestion - Auto-timestamped
- Persists server-side (autosaved like everything else)
- Is carried forward to the publish dialog as a pre-populated change reason
This captures context at the moment of change without adding friction (optional, inline, no interruption).
Layer 2: Diff-assisted publish dialog (Assign view publish ceremony)
When the admin clicks 'Review and Publish,' the dialog shows:
- Auto-generated field-level diffs per question ('Options: Oral, IV, IP -> Oral, IV, IP, SC')
- Any inline notes from Layer 1 pre-populated in per-question change reason fields
- A batch-level summary field at the top for the overall SQS version reason
- The admin can edit, add to, or leave these as-is
This is the Git model: you see the diff while writing the commit message. The inline notes are like git add -p annotations, and the publish summary is the commit message.
Layer 3 (future): AI-assisted summaries
Auto-generate a draft change summary from the diffs: 'This publish updates 3 questions: simplified wording on Q1 and Q3, added new route option to Drug Route.' The admin edits/accepts. Only worth implementing after observing Layer 1+2 adoption.
Stage Publish Reason
The stage-level publish reason is the batch-level summary from the publish wizard — it describes the overall intent of the stage update (e.g., 'Updated treatment questions based on protocol amendment v3'). Per-question reasons provide granularity; the stage reason provides narrative. (Internally stored on the SQS version, but this term is not shown to users.)
Implementation Priority
- Start with Layer 2 (diff-assisted publish dialog) -- highest-value intervention
- Add Layer 1 (inline notes) in the next iteration
- Consider Layer 3 (AI summaries) only after observing real usage patterns
PDD-11: Replace with New Version (Replaces Unpublish)¶
Mental model review decision: The "unpublish" concept is replaced by a forward-only "Replace with new version" action. See mental-model-review-decisions.md, Section 6 for the full specification.
Summary: A unified "Replace with new version" action is available for ALL published questions (with or without annotations). It creates new DraftQuestions linked to the originals via replacesAnnotationQuestionId, removes originals from the draft PQS, and allows full structural editing on the replacements. At publish time, if the originals had annotations, the publish wizard handles annotation migration (Phase 8).
The previous "unpublish" design below is superseded but preserved for reference.
SUPERSEDED — Previous unpublish design (kept for historical reference):
Unpublishing demotes AnnotationQuestions back to DraftQuestion status on the project. It is a project-level design operation with cross-stage consequences.
Precondition: No annotations exist for ANY AQVersion of the affected questions on ANY stage. If annotations exist, unpublishing is blocked.
State Diagram:
PUBLISH
+-----------+ (creates AQ + +------------------------+
| DRAFT | AQVersion + PQS | PUBLISHED |
| | ------------------> | (no annotations) |
| DraftQ | | AQ + AQVersion |
| on Proj | <------------------ | on pmAQ collection |
+-----------+ UNPUBLISH +------------------------+
(see below) |
| Annotator submits
| first annotation
v
+------------------------+
| PUBLISHED |
| (with annotations) |
| CANNOT unpublish |
| (version only) |
+------------------------+
Unpublish steps:
-
Validate: Confirm no annotations exist for any AQVersion of these questions on any stage. If annotations exist, block with error message listing the stages and annotation counts.
-
For each affected AnnotationQuestion: a. Create a new DraftQuestion on the Project from the latest AQVersion content b. Mark the AnnotationQuestion as
status: 'reverted'(NOT deleted -- immutable history preserved) c. The AQVersions remain in the collection as historical records -
Update the draft PQS: a. Replace QuestionReferences from
type: 'published'totype: 'draft'pointing at the new DraftQuestion IDs -
For each Stage that references these questions: a. If the SQS draft contains references to the reverted AQs, remove them from the draft SQS b. Published SQSVersions remain immutable (historical record) c. Show a notification to the admin: 'N questions were unpublished. They have been removed from [Stage X, Stage Y] assignments.'
-
PQSVersions that referenced the now-reverted AQVersions: a. Remain in history as immutable records b. Marked with
status: 'superseded_by_unpublish'for display purposes c. Cannot be restored (the AQVersions they reference are reverted)
Key design decisions:
- AQVersions are never deleted -- they are marked as reverted
- PQSVersions are never deleted -- they are marked as superseded
- The unpublish operation is all-or-nothing per question -- you cannot unpublish a single version, only the entire question
- Cross-stage impact is clearly communicated before confirmation
- System questions are excluded from unpublish operations
Unpublish confirmation dialog:
- List of questions being unpublished
- List of stages that will be affected
- Warning: 'These questions will be removed from all stage assignments'
- If any question has annotations on any stage, the button is disabled with an explanation of which stages have annotations
PDD-12: Cross-Stage Change Detection¶
Mental model review decision: "Update available" is no longer a separate indicator in the Assign tree. Cross-stage changes are merged into the generic "changed" indicator (amber dot). The breakdown by source (own edits vs other-stage publishes) is shown in the publish wizard Step 1, not in the tree.
Detection algorithm (internal — not surfaced as separate UI state):
For each question Q assigned to this stage:
1. Get Q's AQVersion from the last published version for this stage
2. Get Q's latest published AQVersion (from any stage's publish)
3. If versions differ OR Q has draft changes:
-> Show "changed" indicator (amber dot) on the question row
4. To provide context on the source of changes (for publish wizard):
For each other Stage S in the project:
If S published a newer version of Q:
-> Record S.name and publish date for the wizard Step 1 grouping
UI representation:
- In the Assign tree: A single amber dot (merged "changed" indicator) covers both own edits and other-stage publishes. No separate "update available" icon.
- Quiet inline notice when entering the Assign view: "N questions were updated when [Stage] was published on [date]."
- In the publish wizard (Step 1: Review Changes): Changes grouped by source — "Content changes (your edits)" vs "Updated from other stages" — showing stage names, dates, and change reasons.
- In the properties panel: On-demand detail when a changed question is selected, showing whether the change is from own edits or another stage's publish.
14. Autosave Failure and Offline Resilience¶
Multi-Layer Connection Detection¶
SyRF uses SignalR with .withAutomaticReconnect() as the primary connection indicator. This is supplemented by two additional layers:
| Layer | Detection Method | Speed |
|---|---|---|
| SignalR | Heartbeat-based disconnect/reconnect events | Seconds |
| API failures | HTTP interceptor tracks consecutive save failures (>3 = failing) | Per-request |
| Network events | navigator.onLine + online/offline events, periodic health endpoint fetch |
Immediate / 30s |
These feed into a centralised ConnectionState:
interface ConnectionState {
signalR: 'connected' | 'reconnecting' | 'disconnected';
api: 'healthy' | 'failing';
network: 'online' | 'offline';
effective: 'connected' | 'degraded' | 'offline'; // computed
}
Visual Indicators¶
Save status (properties panel header, always visible during editing):
| State | Indicator |
|---|---|
| No pending changes | Nothing shown |
| Saving | sync spinner + 'Saving...' |
| Saved | check + 'Saved' (fades after 3s) |
| Save failed, retrying | warning amber + 'Retrying...' |
| Offline, queued locally | cloud_off amber + 'Saved locally' |
| Failed, manual retry | error red + 'Save failed' + [Retry] button |
Connection banner (top of content area, only when degraded/offline):
| State | Banner |
|---|---|
| Degraded | Amber: 'Connection unstable. Changes are being saved locally.' |
| Offline | Amber: 'You are offline. Changes are saved locally and will sync when reconnected.' |
| Reconnected | Green: 'Reconnected -- syncing changes...' (auto-dismisses after 5s) |
Offline Editing Policy¶
Editing is NOT blocked when offline. All draft changes are written to IndexedDB immediately (before attempting the API call). On reconnect, the queue is flushed in order.
Rationale: Every best-in-class tool (Google Docs, Figma, Notion) and every research domain tool (REDCap Mobile) allows continued editing offline. Blocking edits because of a transient network interruption while a researcher is mid-annotation is unacceptable.
Local Persistence¶
IndexedDB (not localStorage) is used for the offline queue:
User edits form
-> Debounced change detection (200ms)
-> Write to IndexedDB queue (ALWAYS, regardless of connection)
-> Attempt API save
Success -> mark queue entry as synced
Failure -> entry stays in queue, exponential backoff retry
On reconnect -> flush queue oldest-first
A beforeunload handler warns the user if they attempt to close the browser with unsynced changes.
Concurrent Edit Detection¶
Annotations are per-user-per-study, so multi-user conflicts are rare. Same-user-two-tabs is detected via the BroadcastChannel API:
const channel = new BroadcastChannel('syrf-question-edits');
// Posts questionId + tabId on edit
// Warns if another tab is editing the same question
Implementation Priority¶
- Save status indicator (low complexity, high value)
beforeunloadwarning (trivial, high value)- Connection banner with centralised ConnectionStateService (low complexity)
- IndexedDB offline queue (medium complexity, high value)
- Tab awareness via BroadcastChannel (low complexity)