As a user I want to see the full edit history of a document so I can track what changed and who changed it #38

Closed
opened 2026-03-20 19:25:35 +01:00 by marcel · 0 comments
Owner

Background

Documents are edited collaboratively over time. There is currently no way to see what changed, when, or who made the change. A version history with a diff view makes edits transparent and allows mistakes to be spotted and corrected.

Depends on #35 (profile page) so that the editor's display name is available and meaningful.

Desired behaviour

  • Every time a document is created or saved, a version snapshot is stored automatically — the user does nothing extra
  • A "History" section on the document detail page lists all versions: display name of editor, timestamp, and a short summary of what changed (e.g. "title, transcription")
  • Clicking a version shows a diff view against the previous version by default
  • A "Compare any two" mode lets the user select any two versions from dropdowns and compare them
  • The diff view shows:
    • Text fields (title, summary, transcription): word-level inline diff — added words in green, removed words in red with strikethrough
    • Scalar fields (date, location): old value → new value
    • Relation fields (sender, receivers, tags): persons/tags added shown in green, removed shown in red
    • Fields that did not change are collapsed / hidden by default

Data model

New table document_versions:

CREATE TABLE document_versions (
    id           UUID         PRIMARY KEY DEFAULT gen_random_uuid(),
    document_id  UUID         NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    saved_at     TIMESTAMP    NOT NULL DEFAULT now(),
    editor_id    UUID         REFERENCES app_users(id) ON DELETE SET NULL,
    editor_name  VARCHAR(200) NOT NULL,   -- denormalized at write time
    snapshot     JSONB        NOT NULL
);
CREATE INDEX ON document_versions (document_id, saved_at DESC);

editor_name is denormalized at write time (first name + last name from #35, or username as fallback) so history stays readable even if the user is later deleted or renames themselves.

snapshot is the full document as it looked after the save — the same shape as the existing document API response.

Implementation notes

Backend

  • DocumentService.create() writes the first snapshot immediately — so "Version 1 — created by X" is always the baseline. Without this, the first diff would show all fields as "added" with no prior state.
  • DocumentService.update() writes a snapshot row immediately after persisting the change — same transaction.
  • GET /api/documents/{id}/versions — returns the version list (id, saved_at, editor_name, changed_fields summary) without full snapshots
  • GET /api/documents/{id}/versions/{versionId} — returns the full snapshot for one version

The changed_fields summary (a short list like ["title", "transcription"]) is computed by diffing the new snapshot against the previous one at write time and stored in the row — avoids loading all snapshots just to render the version list.

Version restore is out of scope for this issue but the data model fully supports it: a future POST /api/documents/{id}/versions/{versionId}/restore would re-save the snapshot as the current document state, which itself creates a new snapshot entry — keeping the full audit trail intact.

Storage policy: Keep all versions indefinitely for now. If storage becomes a concern, a configurable retention policy (keep last N versions) can be added later without changing the schema.

Frontend

  • Diff computation happens client-side using the diff npm package (diffWords() for text fields, simple comparison for scalars and relations)
  • History panel is a collapsible section at the bottom of the document detail page (not a separate route)
  • Version list is loaded lazily when the panel is first opened
  • Snapshot JSON for the two selected versions is fetched on demand when the diff is rendered

Testing

  • DocumentServiceTest — a snapshot row is written on create; a snapshot row is written on update; snapshot contains the correct field values; changed_fields reflects the actual change
  • @WebMvcTest — versions endpoint returns the list; snapshot endpoint returns the correct JSON
  • E2E Playwright spec — edit a document twice, open history, verify both versions appear, verify the diff highlights the changed field

Dependencies

  • #35 must be merged first — editor_name is only useful once users have a display name

User Journey

User is on a document detail page. At the bottom they see a collapsible "History" section. They open it and see a list of entries — each shows who saved it and when, plus a brief note of which fields changed (e.g. "title, transcription"). They click an entry and a diff panel appears showing the title with changed words highlighted in green (added) and red with strikethrough (removed). Fields that didn't change are hidden. If they want to compare two specific versions — not necessarily adjacent — they pick both from dropdowns and the diff updates accordingly.

E2E Scenarios

Scenario: History section shows versions after edits
  Given I am logged in and viewing a document
  When I edit the document title and save
  And I open the History section on the detail page
  Then I see at least two version entries
  And each entry shows the editor's name and a timestamp

Scenario: Diff view highlights the changed field
  Given a document has been edited at least once
  When I click the most recent version entry in History
  Then I see the title field with the change highlighted
  And unchanged fields are not shown in the diff

Scenario: Compare two arbitrary versions
  Given a document has three or more versions
  When I select version 1 and version 3 from the compare dropdowns
  Then I see a diff of all changes between those two versions
## Background Documents are edited collaboratively over time. There is currently no way to see what changed, when, or who made the change. A version history with a diff view makes edits transparent and allows mistakes to be spotted and corrected. **Depends on #35** (profile page) so that the editor's display name is available and meaningful. ## Desired behaviour - Every time a document is **created or saved**, a version snapshot is stored automatically — the user does nothing extra - A "History" section on the document detail page lists all versions: display name of editor, timestamp, and a short summary of what changed (e.g. "title, transcription") - Clicking a version shows a **diff view** against the previous version by default - A "Compare any two" mode lets the user select any two versions from dropdowns and compare them - The diff view shows: - **Text fields** (title, summary, transcription): word-level inline diff — added words in green, removed words in red with strikethrough - **Scalar fields** (date, location): old value → new value - **Relation fields** (sender, receivers, tags): persons/tags added shown in green, removed shown in red - Fields that did not change are collapsed / hidden by default ## Data model New table `document_versions`: ```sql CREATE TABLE document_versions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), document_id UUID NOT NULL REFERENCES documents(id) ON DELETE CASCADE, saved_at TIMESTAMP NOT NULL DEFAULT now(), editor_id UUID REFERENCES app_users(id) ON DELETE SET NULL, editor_name VARCHAR(200) NOT NULL, -- denormalized at write time snapshot JSONB NOT NULL ); CREATE INDEX ON document_versions (document_id, saved_at DESC); ``` `editor_name` is denormalized at write time (first name + last name from #35, or username as fallback) so history stays readable even if the user is later deleted or renames themselves. `snapshot` is the full document as it looked after the save — the same shape as the existing document API response. ## Implementation notes **Backend** - `DocumentService.create()` writes the first snapshot immediately — so "Version 1 — created by X" is always the baseline. Without this, the first diff would show all fields as "added" with no prior state. - `DocumentService.update()` writes a snapshot row immediately after persisting the change — same transaction. - `GET /api/documents/{id}/versions` — returns the version list (id, saved_at, editor_name, changed_fields summary) without full snapshots - `GET /api/documents/{id}/versions/{versionId}` — returns the full snapshot for one version The `changed_fields` summary (a short list like `["title", "transcription"]`) is computed by diffing the new snapshot against the previous one at write time and stored in the row — avoids loading all snapshots just to render the version list. **Version restore** is out of scope for this issue but the data model fully supports it: a future `POST /api/documents/{id}/versions/{versionId}/restore` would re-save the snapshot as the current document state, which itself creates a new snapshot entry — keeping the full audit trail intact. **Storage policy:** Keep all versions indefinitely for now. If storage becomes a concern, a configurable retention policy (keep last N versions) can be added later without changing the schema. **Frontend** - Diff computation happens **client-side** using the `diff` npm package (`diffWords()` for text fields, simple comparison for scalars and relations) - History panel is a collapsible section at the bottom of the document detail page (not a separate route) - Version list is loaded lazily when the panel is first opened - Snapshot JSON for the two selected versions is fetched on demand when the diff is rendered ## Testing - `DocumentServiceTest` — a snapshot row is written on create; a snapshot row is written on update; snapshot contains the correct field values; `changed_fields` reflects the actual change - `@WebMvcTest` — versions endpoint returns the list; snapshot endpoint returns the correct JSON - E2E Playwright spec — edit a document twice, open history, verify both versions appear, verify the diff highlights the changed field ## Dependencies - **#35** must be merged first — `editor_name` is only useful once users have a display name --- ## User Journey User is on a document detail page. At the bottom they see a collapsible "History" section. They open it and see a list of entries — each shows who saved it and when, plus a brief note of which fields changed (e.g. "title, transcription"). They click an entry and a diff panel appears showing the title with changed words highlighted in green (added) and red with strikethrough (removed). Fields that didn't change are hidden. If they want to compare two specific versions — not necessarily adjacent — they pick both from dropdowns and the diff updates accordingly. ## E2E Scenarios ``` Scenario: History section shows versions after edits Given I am logged in and viewing a document When I edit the document title and save And I open the History section on the detail page Then I see at least two version entries And each entry shows the editor's name and a timestamp Scenario: Diff view highlights the changed field Given a document has been edited at least once When I click the most recent version entry in History Then I see the title field with the change highlighted And unchanged fields are not shown in the diff Scenario: Compare two arbitrary versions Given a document has three or more versions When I select version 1 and version 3 from the compare dropdowns Then I see a diff of all changes between those two versions ```
marcel added the featurecollaboration labels 2026-03-20 19:26:51 +01:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#38