Story: Consolidate history dialogs onto HistoryDialogBase

Table of Contents

This page documents a story in Sprint 19. It captures the goal, current status, acceptance criteria, and the tasks that compose it.

Goal

Every entity history dialog derives from HistoryDialogBase and shares one implementation of the common machinery, so that fixes and UX changes land once instead of 67 times, and the per-dialog code shrinks to what is genuinely entity-specific (field comparisons and labels).

Survey (2026-06-05, the day the CI broke on exactly this duplication):

  • 67 *HistoryDialog classes across ores.qt sub-libraries.
  • HistoryDialogBase already exists in ores.qt.api (signals, markAsStale(), code()) but has exactly one adopter (WorkspaceHistoryDialog).
  • 64 dialogs duplicate the version-list/load pipeline (onVersionSelected, loadHistory, QFutureWatcher plumbing).
  • 6 dialogs (Account, SystemSetting, ChangeReason, ChangeReasonCategory, Country, Currency) hand-roll a changes tab with field-level diffs using four different idioms: a free function, per-call lambdas, a CHECK_DIFF_STRING macro, and inline if-blocks. The Account variant named a class-private alias from an anonymous-namespace function, breaking GCC/AppleClang builds (fixed tactically; this story is the proper fix).

Design direction — two layers:

  1. Server-side diffs (the deeper fix). Computing field-level diffs in the UI is the wrong place: when history views are added to the shell, Wt and HTTP frontends the same comparison code would be repeated in each. Instead:
    • A per-entity mapper converts a domain type to an ordered list of (field name, value) strings — one definition per entity, server side, reusable by every frontend and candidate for codegen from the entity models.
    • A generic NATS history message returns the computed diffs between consecutive versions (field, old value, new value), rather than shipping two full versions for the client to compare. Frontends only render.
  2. Qt consolidation onto the base.
    • DiffResult (list of (field, (old, new)) rows) is defined once in HistoryDialogBase.
    • The base owns the changes-tab rendering flow and calls an abstract calculateDiff(current, previous) template method; derived dialogs implement only the per-entity field comparisons. Once the server-side diff message lands, calculateDiff implementations collapse into rendering the server-provided rows, and the template method may disappear entirely.
    • Shared checkString / checkInt / checkBool helpers live with the base so all dialogs format values identically in the interim.
    • Version-list population, async load, stale handling and toolbar wiring migrate into the base (or an intermediate templated CRTP helper if the version types resist a common interface).

Architecture

Three layers, from the wire up; full implementation detail lives in each task's Plan.

  1. ores.diff — a dependency-light leaf component owning the vocabulary of "what changed": field_value, diff_entry, diff_result, and a compute engine. Every domain service computes with it; every frontend renders from it; std + rfl only.
  2. Per-entity field mappers, server side — domain type → ordered, human-labelled, rendered field list, living beside the entity's other mappers. Hand-written for the pilot; a codegen candidate later.
  3. Extended history responses — the existing typed per-entity subjects remain; each version gains fields (full render — the detail panel needs complete values) and changes (diff vs the previous version). Frontends — Qt today; shell, Wt, HTTP tomorrow — are dumb renderers. Full domain payloads stay during migration.

Migration runs A→D: grow the base and migrate the six hand-rolled dialogs (codegen-free); pilot currency end to end (codegen-free); template rollout (gated on the codegen org-model migration — a template-only edit, but the regeneration sweep must not race the in-flight model migration); codegen the mappers (deferred).

Status

Field Value
State STARTED
Parent sprint Sprint 19
Now Shell unified diff; phase C gated on codegen migration.
Waiting on Nothing.
Next Phase C: template rollout (gated on codegen migration).
Last touched 2026-06-05

Acceptance

  • Every entity history dialog derives from HistoryDialogBase; the per-dialog code is limited to what is genuinely entity-specific.
  • Field-level diffs are computed server-side; no frontend holds comparison logic.
  • ores.diff exists with exhaustive engine tests; mappers and handlers are tested at their own layers.
  • The shell can render history as a unified diff.
  • No codegen change is forced while the org-model migration is in flight; the template rollout is explicitly gated.

Tasks

Task State Start End Description
Design the history diff architecture and testing strategy DONE 2026-06-05 2026-06-05 Design distributed into this story's Architecture and the tasks' Plans.
Grow HistoryDialogBase and migrate the six hand-rolled dialogs DONE 2026-06-05 2026-06-05 Scope grew to all 64 version-history dialogs; event-log dialogs renamed to Audit. PR 1080.
Create the ores.diff component DONE 2026-06-05 2026-06-05 Flat diff model + engine, std+rfl only; 14 tests. PR 1091.
Pilot server-side history diffs end to end on currency DONE 2026-06-05 2026-06-05 Currency mapper + extended response + handler compose; dialog renders server rows; dogfooded. PR 1097.
Adopt the base in the history-dialog template and roll out BACKLOG     Phase C: template target form + per-domain rollout. Gated on codegen org-model migration.
Codegen field mappers from entity models BACKLOG     Phase D, deferred: generate mappers once the hand-written shape is proven.
Show history as a unified diff in ores.shell DONE 2026-06-05 2026-06-06 Unified-diff rendering of the changes rows in the shell; no comparison logic client-side.

Decisions

  • A dedicated ores.diff component (not a corner of ores.utility): the types cross every domain service and every frontend; a leaf component keeps the dependency arrows clean and matches the many-small-components style.
  • Keep the typed per-entity history subjects; extend responses rather than introducing a generic untyped subject.
  • Responses carry both fields and changes — mildly redundant, but it makes every frontend a dumb renderer, which is the point.
  • Phases A and B are codegen-free by construction; the template rollout is gated on the codegen org-model migration to avoid a circular dependency; codegen'd mappers are deferred outright.

Out of scope

Emacs 29.1 (Org mode 9.6.6)