Version 2.4.0
Added
- Default reviewer. A new
defaultReviewerIdsetting preselects one reviewer in the Submit for review dialog. - Skip reviewer selection. With
skipReviewerSelectionon, 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
notificationssetting 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
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
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 amatrix-field:<field>:<blockUid>:titleatom, whichMergeServiceapplies to the block's nativetitlerather than looking for a custom field of that handle.
Version 2.2.4
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) —
HtmlDifferprojects each<a href>totext [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) andfalse(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 asContent Block <id>. A draft owns its own copy of the block, so the diff readContent Block 2149 → Content Block 2153: two element ids, and none of the actual sub-field changes. A newContentBlockDiffernow diffs the block's sub-fields, each recursing throughFieldDiffServiceso 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 anAddressQuery;ElementQuery::__toString()returns the same constant string regardless of content, so both sides always compared equal. A newAddressesDifferdiffs 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
Fixed
Install's migration (plugin install) unconditionally ranCREATE TABLEfor 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 withBase table or view already exists.safeUp()now checks each table before creating it, matching the guard already used bym260608_000000_review_tablesandm260608_000002_review_comments.
Version 2.2.2
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
localStoragedecisions from before a draft revision could satisfy the gate without covering the current fields.
Version 2.2.1
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
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 (reusesHtmlDiffer). - Code (
nystudio107/craft-code-field) → word-level text diff (reusesTextDiffer). - Neo (
spicyweb/craft-neo) → block-level diff (added / removed / modified / reordered, including nesting-level changes) with recursive sub-field diffs, via a newNeoDiffer. - Composite SEO/meta fields — Beacon's
BeaconSeoFieldand SEOmatic'sSeoSettings→ per-attribute diff (title,description,canonical, OG/Twitter tags, …) via a new genericStructDifferthat flattens any model/struct value to sortedkey: valuetext and reusesTextDiffer. - Link fields — Hyper's
HyperField→ per-link diff with friendly labels (type,value,text,new window,aria) via a newLinkDiffer.
- TinyMCE (
- Third-party field classes are referenced only as
::classmap 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
Maintenance release — no functional changes from 2.1.0.
Version 2.1.0
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
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/Kto navigate,A/Rto decide). Decisions persist in browserlocalStorageand resume across restarts; stale-state detection against the canonical entry'sdateUpdatedguards 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.
MergeServiceowns all write logic — atom validation against a fresh diff, field/Matrix apply, a singlesaveElementfollowed byapplyDraftfor atomic publication.DiffController::actionApplyendpoint with structured error codes (no-changes,source-not-found,stale-atoms,validation-failed).Enable Review Modesetting (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 thedelta-comparepage 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-applyReviewpermission 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 ofsubmitted/approved/declined/published/commentto your own site template; an unmapped or missing template falls back to the bundled default. WorkflowServiceevents for third-party integration:EVENT_AFTER_SUBMIT,EVENT_AFTER_APPROVE,EVENT_AFTER_DECLINE,EVENT_AFTER_WITHDRAW,EVENT_AFTER_PUBLISH— each carrying theReviewmodel as$event->review.Enable Workflowsetting (default on) as a kill switch covering both the UI and the endpoints.
Other
MatrixDifferemits each change's canonicalblockUidso 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.phpdocumenting every setting (all overridable per-environment viaconfig/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, andcraftdelta_review_comments. Reviews survive publication as audit records (reviews.draftIdis 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-compareCP URL so it no longer requires the Access Craft Delta plugin permission (the oldcraft-delta/compareURL still resolves).
Security
- Publishing an approved review requires the
craftdelta-applyReviewpermission, matching the granular Review-Mode apply path — native entry save rights alone are no longer sufficient. Verified: a user with section save rights but withoutapplyReviewis rejected (403) atworkflow/publish;applyReviewholders 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_requestedreview is reopened by the schema2.1.3migration (→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_workflowstable is dropped and replaced by the reviews tables; pre-release workflow rows are not migrated.
Version 1.1.0
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.
RelationDiffernow hydrates raw integer IDs and handlesElementCollectionvalues from Craft 5's eager-load path. RelationDiffer::indexByIdno longer crashes on transient elements with a nullid.
Tests
- Added
RelationDifferTestwith 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
- Initial release