Version 2.4.0

November 3, 2026

Added

  • Default reviewer. A new defaultReviewerId setting preselects one reviewer in the Submit for review dialog.
  • Skip reviewer selection. With skipReviewerSelection on, the Submit for review button sends the review to the default reviewer directly, without the dialog. If the default reviewer is the author or cannot view drafts in the section, the dialog opens as usual.
  • Notification settings. A new notifications setting selects which emails Delta sends: submitted, approved, declined, comment, published. All are on by default.
  • Edit draft from the review page. An open review shows an "Edit draft" button when the user can save the draft. A reviewer can now correct the content directly before they approve or publish.

Version 2.3.0

October 28, 2026

Added

  • Reviewer groups setting (#16). Limit the reviewer picker, and the reviewer check on submit, to members of selected user groups. Admins hold every permission, so before this every admin was listed, including developer and agency accounts. Empty (the default) keeps the old behaviour; if none of the selected groups exist any more, all reviewers are listed again.

Version 2.2.5

July 29, 2026

Fixed

  • Titles of nested entries are diffed again (#15). A Matrix block whose entry type has “Show the Title field” enabled reported no changes when only its title was edited: a block's Title is a native entry attribute, not a custom field, so MatrixDiffer's walk over the block's field layout (getCustomFields()) never saw it and the block was classified as unchanged context. The title is now diffed explicitly — for block types that show a Title field only, since a title generated from a title format would just echo the field changes listed below it — and it is accept/reject-able like any other sub-field via a matrix-field:<field>:<blockUid>:title atom, which MergeService applies to the block's native title rather than looking for a custom field of that handle.

Version 2.2.4

July 15, 2026

Added

  • Multi-site review (#7). Reviewing an entry that propagates to several sites no longer silently drops the other sites' changes: the review header warns when a draft carries changes beyond the site being viewed, a site switcher moves between them, and granular apply merges every site's accepted atoms into a single publish — gated on all sites having been decided.
  • Per-field accept/reject inside modified Matrix blocks (#10). A modified block is decided field by field rather than all-or-nothing, via a new matrix-field: atom and a field-level merge.
  • Unchanged Matrix blocks are shown as context (#12), hidden by default, so a changed block can be read in the surrounding order rather than in isolation.
  • Rejecting an atom opens its comment thread inline (#11), so the reason for a rejection is captured where the rejection happens.
  • Unchanged native attributes (Title, Slug) now appear in the all-fields view (#8), instead of being omitted only because they hadn't changed.

Fixed

  • Link changes inside rich text now surface even when the visible anchor text is unchanged (#4) — HtmlDiffer projects each <a href> to text [href] before stripping tags, so an add, removal or retarget is a real diff instead of an invisible one.
  • Whole added/removed lines keep their green/red highlight (#5): the line-level change class on <tbody> is allowed through the diff purifier again.
  • An absent-vs-off lightswitch no longer reads as a change (#9) — null (never set) and false (explicitly off) are the same state.
  • The apply button gives immediate feedback instead of reading as a no-op (#6).
  • Content Block fields (craft\fields\ContentBlock, Craft 5.8+) showed no content changes. The field type was unmapped, so it fell back to a scalar value diff — and because a content block has no title, Element::__toString() renders it as Content Block <id>. A draft owns its own copy of the block, so the diff read Content Block 2149 → Content Block 2153: two element ids, and none of the actual sub-field changes. A new ContentBlockDiffer now diffs the block's sub-fields, each recursing through FieldDiffService so rich text, Matrix and the rest still get their proper differ.
  • Addresses fields (craft\fields\Addresses) showed no changes at all — ever. Unmapped, they too fell back to a scalar value diff, whose value is an AddressQuery; ElementQuery::__toString() returns the same constant string regardless of content, so both sides always compared equal. A new AddressesDiffer diffs each address by canonical id (added / removed / modified / reordered), covering both an address's native attributes — street, city, postal code, country, organization, names, coordinates — and any custom fields on its layout. Attribute labels come from Craft, so they stay country-aware (“ZIP code” vs “Postal code”).

Neither of those two field types could reuse an existing differ: a content block is a craft\base\Element but not an Entry, and addresses are owned nested elements rather than relations, so an id-based relation diff would report every address as removed-and-re-added on each draft. Both render through the existing block-diff UI, and both are read-only for now, as Neo is — their changes apply via publish, not delta's granular accept/reject, which would need MergeService to re-own the nested elements the way it serializes Matrix blocks.

Version 2.2.3

July 6, 2026

Fixed

  • Install's migration (plugin install) unconditionally ran CREATE TABLE for all three review tables, so re-running it against a database where they already existed — plugin reinstall, or a migration-history mismatch after a DB restore — failed with Base table or view already exists. safeUp() now checks each table before creating it, matching the guard already used by m260608_000000_review_tables and m260608_000002_review_comments.

Version 2.2.2

July 2, 2026

Added

  • A declined review can now be resubmitted, not just a withdrawn one — submit() reopens it in place (round + 1, decision fields reset) instead of rejecting it as "already exists". The entry sidebar shows a link to the reviewer's note directly above the "Submit for review" button, so the author can read why before revising.
  • craft-delta/smoke/prepare-review's --reviewer=<email|username> override now actually works — it was documented but silently ignored.

Fixed

  • The review page's Publish/Schedule buttons now also require craftdelta-applyReview, not just reviewer/author standing. A submit-only author previously saw these buttons on their own approved review and got a 403 on click.
  • The author's "ready to publish" nav badge no longer counts already-scheduled approved reviews — a scheduled review publishes itself, so it's not waiting on anyone.
  • The review page's Approve gate ("decide every change before approving") now matches by live atom id instead of comparing counts, closing an edge case where stale localStorage decisions from before a draft revision could satisfy the gate without covering the current fields.

Version 2.2.1

July 2, 2026

Changed

  • Simplified the action row on an approved review: it now shows a single back-out button, the author's Withdraw. The reviewer's second back-out button was removed — it read as a near-duplicate of Withdraw and let a reviewer unilaterally kill an already-approved review. A reviewer who wants changes now leaves a comment instead.

Version 2.2.0

June 30, 2026

Added

  • Diff support for popular third-party field types, resolved through FieldDiffService's differ map (unmapped types still fall back to a scalar value diff):
    • TinyMCE (spicyweb/craft-tinymce) → word-level HTML diff (reuses HtmlDiffer).
    • Code (nystudio107/craft-code-field) → word-level text diff (reuses TextDiffer).
    • Neo (spicyweb/craft-neo) → block-level diff (added / removed / modified / reordered, including nesting-level changes) with recursive sub-field diffs, via a new NeoDiffer.
    • Composite SEO/meta fields — Beacon's BeaconSeoField and SEOmatic's SeoSettings → per-attribute diff (title, description, canonical, OG/Twitter tags, …) via a new generic StructDiffer that flattens any model/struct value to sorted key: value text and reuses TextDiffer.
    • Link fields — Hyper's HyperField → per-link diff with friendly labels (type, value, text, new window, aria) via a new LinkDiffer.
  • Third-party field classes are referenced only as ::class map keys, so the integrations stay optional (no new Composer dependencies).

Changed

  • Matrix block accept/decline actions and comment triggers now sit in a top header bar beside the block toggle, matching field-level review UI.

Notes

  • Neo blocks render read-only in Review Mode (no per-block accept/reject) and apply via publish; per-block granular apply remains Matrix-only for now.

Version 2.1.1

June 19, 2026

Maintenance release — no functional changes from 2.1.0.

Version 2.1.0

June 19, 2026

Reworks the Reviews dashboard into a native Craft index and tightens the review queue.

Added

  • The Reviews dashboard (/admin/delta-reviews) is now a native Craft element-style index: a left source sidebar (Awaiting my verdict / My submissions / All reviews, each with a count badge) drives a single sortable, searchable, paginated table, replacing the three stacked tables.

Changed

  • Awaiting my verdict now lists only reviews still awaiting your verdict (pending in the current round), so it matches the control-panel nav badge.
  • The actionable queues (Awaiting my verdict / My submissions) hide concluded reviews (published / declined / withdrawn); the admin All reviews source keeps the full audit history.
  • The Status column sorts by workflow state rather than by the localized label text.

Fixed

  • Reviewer pills stay on a single line and truncate long names with an ellipsis (full name on hover) instead of wrapping into a multi-line blob.

Performance

  • Dashboard rows batch-load their entry titles in a single query instead of one lookup per row, and the table fetches only the requested source bucket instead of computing all three.

Version 2.0.0

June 18, 2026

Major release: adds an inline Review Mode and a multi-reviewer submit-for-review workflow on top of the v1.x diff viewer.

Added

Review Mode

  • Accept or reject individual changes on the review page, then publish only the accepted changes to the canonical entry as a new revision. Per-field and per-Matrix-block granularity, with block reordering as its own decision.
  • Keyboard-driven stepper (J/K to navigate, A/R to decide). Decisions persist in browser localStorage and resume across restarts; stale-state detection against the canonical entry's dateUpdated guards against mid-review edits. The cursor highlight only appears once you start navigating, so the diff isn't pre-highlighted on load.
  • An Also delete source draft option (off by default) when the source is a draft — publishes, then deletes the draft. Left unchecked, the draft persists as a queue of the changes that weren't accepted.
  • MergeService owns all write logic — atom validation against a fresh diff, field/Matrix apply, a single saveElement followed by applyDraft for atomic publication.
  • DiffController::actionApply endpoint with structured error codes (no-changes, source-not-found, stale-atoms, validation-failed).
  • Enable Review Mode setting (default on) as a kill switch.

Submit-for-review workflow

  • Authors submit a published draft to one or more reviewers. Reviewers Approve or Decline (with an optional note emailed to the author). To request changes, a reviewer leaves a comment — there is no separate "request changes" verdict.
  • A configurable approval policy decides when a review passes: any one reviewer (default), all assigned reviewers, or at least N (clamped to the number assigned).
  • Reviews happen on a dedicated full-page workspace at /admin/delta-review?reviewId=N: reviewer roster + round, verdict actions, the diff with inline (GitHub-style) anchored comments, a general discussion, and — for reviewers who can apply — always-on per-change accept/reject with a live "decided" counter and Apply N accepted. The diff slideout and the delta-compare page are diff-only and link to the review page ("Open review").
  • A live draft preview renders beside the diff on the review page for sections with front-end URLs (a tokenized render of the draft). Open it full-width in a new window, hide it per reviewer (remembered across reviews), or turn it off globally with the Enable Preview setting.
  • A submitted draft isn't locked: the author can keep revising it (reviewers see the updated diff), withdraw an active request (re-submitting re-opens it in a new round), or the review is finalized by a verdict. Decline is terminal.
  • Publishing an approved review — immediately, or scheduled for later via a queued job and Craft's native date + time picker — requires the craftdelta-applyReview permission and Craft's native save permission on the entry, so a review-only role can't push content live. A schedule is rescinded automatically when the review leaves the approved state (declined/withdrawn) or its draft is deleted.
  • A Granular review path: Review Mode on a submitted draft; applying accepted atoms publishes them and closes the review (a partial apply finalizes the review; see the README).
  • A Reviews dashboard in the CP nav (awaiting your verdict / your submissions / all for admins), and a Workflow status column on entry index pages (In review / Approved / Approved — scheduled / Declined / Withdrawn / Published).
  • Review comments: general or diff-atom-anchored, with one level of replies, resolve/unresolve, and automatic outdated detection against the live diff. Posting a comment notifies the other side (author ↔ reviewers), debounced so a burst sends at most one email per recipient per review (workflow/comment, workflow/resolve-comment, workflow/thread).
  • Three general (section-agnostic) permissions under Craft Delta: Submit drafts for review (craftdelta-submitDraft), Review submitted drafts (craftdelta-reviewDraft), and Apply review-mode changes (craftdelta-applyReview). Which sections a user can act on is governed by Craft's native section permissions; the reviewer picker lists only users who can view peer drafts in the draft's section.
  • Email notifications on every transition (submitted, approved, declined, published, scheduled-publish, and new comment), each sent in the recipient's preferred language. The five templates are overridable from config/craft-delta.php (emailTemplates) — map any of submitted / approved / declined / published / comment to your own site template; an unmapped or missing template falls back to the bundled default.
  • WorkflowService events for third-party integration: EVENT_AFTER_SUBMIT, EVENT_AFTER_APPROVE, EVENT_AFTER_DECLINE, EVENT_AFTER_WITHDRAW, EVENT_AFTER_PUBLISH — each carrying the Review model as $event->review.
  • Enable Workflow setting (default on) as a kill switch covering both the UI and the endpoints.

Other

  • MatrixDiffer emits each change's canonical blockUid so atom keys round-trip across canonical / draft / revision.
  • Translations across all 8 supported locales (English, German, Spanish, French, Italian, Dutch, Polish, Portuguese).
  • A bundled config.php documenting every setting (all overridable per-environment via config/craft-delta.php) and the variables each email template receives.
  • PHPUnit coverage for atom parsing, validation, the Matrix merge/order algorithm, the workflow state machine, and the approval policy.

Changed

  • Schema version is 2.1.3. The upgrade migrations create three tables: craftdelta_reviews, craftdelta_review_reviewers, and craftdelta_review_comments. Reviews survive publication as audit records (reviews.draftId is nulled when the draft is applied and deleted).
  • Applying changes (Review Mode or the workflow's Granular review) requires the dedicated Apply review-mode changes (craftdelta-applyReview) permission; edit/save permissions alone are not sufficient. Admins have it implicitly.
  • The full-page compare view moved to the delta-compare CP URL so it no longer requires the Access Craft Delta plugin permission (the old craft-delta/compare URL still resolves).

Security

  • Publishing an approved review requires the craftdelta-applyReview permission, matching the granular Review-Mode apply path — native entry save rights alone are no longer sufficient. Verified: a user with section save rights but without applyReview is rejected (403) at workflow/publish; applyReview holders and admins are unaffected.

Fixed

  • A reply no longer shows a "Reply" affordance (the data model supports only one level of replies), which previously dead-ended in a generic "could not be saved" error.
  • Closed/terminal reviews (published, declined, cancelled) render comments read-only — the composer, reply, and add-comment affordances are hidden, since the backend rejects posts on an inactive review.
  • Review UI polish. Non-primary buttons no longer use the CP accent color — "Approve" reads as green and "Decline" as red, rather than the install's accent. "Submit comment" is a quiet right-aligned button rather than a full-width CTA. Zero-change reviews no longer render an "Apply 0 accepted" toolbar. The reviewer picker is a styled checkbox list instead of a raw native multi-select. Scheduling uses a native date + time picker instead of a free-text prompt.

Compatibility

  • No breaking changes for users without workflow permissions — the v1.x read-only diff slideout and Compare Revisions button behave identically.

Upgrading from a 2.0.0 pre-release

  • The pre-release "Request changes" verdict and the re-request / review-rounds loop are gone. Reviewers now Approve or Decline only; requests for changes are expressed as comments. An in-flight changes_requested review is reopened by the schema 2.1.3 migration (→ open, reviewer verdicts reset to pending) so existing rows aren't stranded.
  • Workflow permissions are no longer scoped per section. If a pre-release granted craftdelta-*:<sectionUid> keys, those are obsolete — re-grant the three general permissions and manage section access through Craft's native section permissions.
  • The pre-release single-reviewer craftdelta_draft_workflows table is dropped and replaced by the reviews tables; pre-release workflow rows are not migrated.

Version 1.1.0

April 15, 2026

Added

  • Asset relation diffs now render thumbnails, filenames and metadata (dimensions, file size) instead of bare titles.
  • Sticky tab headers and a jump-to-section navigation bar at the top of the diff, with a live highlight of the section currently in view while scrolling.
  • "Tab:" prefix on tab headers and jump-nav items so it's always clear what kind of section a heading refers to.

Changed

  • Diff colors are now stable across selection changes. Comparisons are always sorted chronologically server-side, so additions stay green and removals stay red regardless of which version sits in which dropdown. The redundant swap button has been replaced with a static arrow indicator.

Fixed

  • Relation diffs inside unsaved Matrix sub-fields no longer leak raw element IDs ("+ 900") into the rendered output. RelationDiffer now hydrates raw integer IDs and handles ElementCollection values from Craft 5's eager-load path.
  • RelationDiffer::indexById no longer crashes on transient elements with a null id.

Tests

  • Added RelationDifferTest with 20 unit tests covering null/empty inputs, set-difference logic, HTML escaping, ElementCollection, raw-ID hydration, and the null-id guard. Five Asset rendering tests are explicitly skipped pending a Craft kernel test bootstrap.

Version 1.0.0

March 2, 2026
  • Initial release