Add collapsible sections, shared includes, copy/paste, perf cache, and recently-used blocks#68
Add collapsible sections, shared includes, copy/paste, perf cache, and recently-used blocks#68helmutkaufmann wants to merge 53 commits into
Conversation
- normalizeBlockFields() translates collapsible/collapsed YAML shorthands
on section fields into containerAttributes so the WinterCMS form widget's
bindCollapsibleSections JS picks them up:
collapsible: true → data-field-collapsible attribute
collapsed: true (default) → starts collapsed (core JS behaviour)
collapsed: false → starts open (handled by collapsible.js)
- collapsible.js re-opens sections marked data-field-collapsible-open after
the core form widget collapses them all on init; also fires on ajaxSuccess
for dynamically added repeater items
- getGroupFormFieldConfig() override passes tabs and secondaryTabs from the
block definition through to Backend\Widgets\Form, enabling native WinterCMS
tab syntax in .block files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Documents the collapsible/collapsed YAML shorthand and tabs/secondaryTabs block-level support added in this fork. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
WinterCMS calls bindCollapsibleSections() after every ajaxSuccess, which re-collapses all collapsible sections including ones the user manually opened. Track user-toggled open state via data-user-opened on the element and restore it after each ajaxSuccess alongside the configured-open sections. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
setTimeout was unreliable — the repeater's post-AJAX init could trigger another bindCollapsibleSections() call after our timeout, causing a visible "briefly shows then collapses" flicker. Switch to a MutationObserver that immediately undoes programmatic re-collapses of user-opened (data-user-opened) sections. A userTogglingSection flag suppresses the observer during genuine user click-to-collapse so the user can still close sections normally. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Reactive approaches (MutationObserver, setTimeout) lose the race because the repeater's post-AJAX widget init can trigger another bindCollapsibleSections pass after our correction fires. Instead, prevent the problem: on ajaxBeforeSend, strip data-field-collapsible from user-opened sections so bindCollapsibleSections() cannot select them. Restore the attribute on ajaxComplete. Keep MutationObserver as backup. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: core FormWidget.bindCollapsibleSections() runs on every widget init scoped to the closest form. Adding a nested repeater item inits a new FormWidget, which re-collapses all data-field-collapsible sections in the shared form and binds a SECOND click handler to each — the doubled handler made "Add item" appear to stall and snapped open sections shut. Fix: emit data-block-collapsible (core's JS ignores it) and handle collapse entirely in collapsible.js with a one-time per-section init guard and a single delegated click handler that cannot double-bind. Reuses core's is-collapsible / collapsed CSS classes so styling is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DOMContentLoaded / ajaxSuccess / render via native addEventListener never fired: block section fields are injected by AJAX, and the backend dispatches lifecycle events through jQuery, which native listeners don't receive. Result was that nothing collapsed at all. Detect sections with a MutationObserver instead — fires on any DOM insertion regardless of framework. Per-section guard class keeps init idempotent; childList-only observation avoids a feedback loop from our class/style writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Blocks widget resolves addJs('js/collapsible.js') relative to
formwidgets/blocks/assets/ (via guessViewPath), not assets/dist/. The file
only existed under assets/dist/js/, so it 404'd and never loaded — which is
why collapsible sections stopped working after switching to the
data-block-collapsible attribute that core's JS ignores.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
addJs path/combiner resolution proved unreliable (the file resolved to a location where it 404'd). Embed the collapse logic inline in _block.php with a global one-time guard so it is guaranteed to load. Uses data-block-collapsible (ignored by core) plus a MutationObserver, so sections collapse correctly and nested repeaters no longer stall. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The standalone collapsible.js never reliably loaded via addJs. The collapse logic now lives inline in the block widget partial, so both copies of collapsible.js and the addJs registration are dead and removed. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A block may declare `include: path` (or a list of paths) to merge fields, tabs, secondaryTabs, and config from external plain-YAML files. Included definitions form the base; the block's own definitions override on collision. Paths resolve via File::symbolizePath() ($/, ~/, #/). Lets shared sections, tabs, and field groups be reused across blocks without copy-paste. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- BlockManager: resolve nested includes recursively with a circular-reference guard; warn when an include redefines a field with a different type; log missing include files instead of skipping silently. - Block widget partial: persist each collapsible section's open/closed state in localStorage; pin recently used blocks to the top of the add-block palette. - winter.mix.js: document the two distinct blocks.js files (frontend Snowboard build vs backend FormWidget script) to prevent accidental merging. - Add CHANGELOG.md summarising fork additions; update README. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BlockManagerTest covers resolveIncludes: field/tab/config merging, block-overrides-include precedence, nested includes, circular-include guard, missing-file skip, multiple includes, and no-include no-op. BlocksTest covers normalizeBlockFields: collapsible/collapsed shorthand translation to data-block-collapsible[-open] and that non-collapsible fields are untouched. Adds YAML fixtures under tests/fixtures/blocks/includes/. Merge behaviour verified end-to-end against the real resolveIncludes() code path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
These features are intended to be proposed upstream, so the docs describe them as plugin features rather than fork-specific additions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy and Cut buttons on each block item serialize field values to sessionStorage. A Paste entry appears at the top of the add-block palette when clipboard data is present and the block type is allowed in the target widget. Paste works across pages within the same browser session. The MutationObserver that already watches for new block items now fills clipboard values into a newly added item immediately after onAddItem's AJAX response is injected, before any debounced re-init runs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Align copy/cut/paste/remove buttons horizontally with flexbox - Move paste out of the add-block palette into each block's own toolbar: a paste icon button (hidden until clipboard has data) appears next to copy/cut/remove on every block item - Clicking paste inserts the copied block immediately after the clicked block by firing onAddItem via the palette template's handler name, then the MutationObserver moves and fills the new <li> before any re-init runs - updatePasteButtons() shows/hides paste buttons based on clipboard state and whether the copied block type is available in the same widget Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
A 'Paste block' link appears in the add-item row of every blocks widget when the clipboard holds a compatible block type. This allows pasting into a widget that has no existing items (e.g. an empty right column), where the per-item paste button would not be present. The pasted block is appended at the end of the list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs in the copy/cut/paste feature: - Paste buttons never appeared: the availability check and add-handler lookup compared against `data-block-code=` using curly quotes (U+201D) instead of straight ASCII quotes, so indexOf/querySelector never matched the real palette markup. Replaced with straight quotes. - Toolbar buttons overlapped: copy/cut/paste/remove reused Bootstrap's `.close` class, inheriting float and a large font-size/line-height that crammed the glyphs together. Replaced with dedicated `.block-item-action` buttons in a flex `.block-item-toolbar`, sized and spaced consistently. Styles live in blocks.less and are also injected inline (once, guarded) so the toolbar renders correctly even if the compiled CSS is stale. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paste availability check hid the paste options whenever it could not positively match the block code in the palette template (fail closed), which could hide the "Paste block" link even for a valid block. It now fails open: a paste option is hidden only when the template positively proves the block type is not offered by that widget. The paste action already no-ops safely on an unknown type. Also dropped the wn-icon-paste class on the add-item link (which could render with no visible label) in favour of an explicit icon + text. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The append link was a second inline anchor inside the centered add-item <li>, so it wrapped under the "+" button and could be clipped by the item height. It now renders as its own full-width list row beneath the Add button, toggled via a row container. Added matching styles in blocks.less and the inline-injected CSS. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paste availability check and add-handler lookup parsed the popover <script> template at click time. That was fragile: when parsing failed, availability failed open (paste link always shown) and the handler lookup returned null (paste did nothing) — exactly the "always shown and does not work" symptom. Now the widget renders two explicit attributes on .field-blocks: - data-add-handler: the onAddItem AJAX handler name - data-block-codes: comma list of block codes offered by this widget blockTypeAvailable() and findAddHandler() read these directly, so the paste link shows only for compatible blocks and the paste action always has a valid handler to fire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces the separate absolutely-positioned collapse chevron, copy/cut/ paste buttons and remove button (which were constrained to a fixed 20px box and overflowed) with a single horizontal flex toolbar in this order: collapse, cut, paste, duplicate, [config], delete. - Overrides the core fixed width on .repeater-item-remove so icons never overflow; icons use the neutral backend text colour, not blue. - Copy is replaced by Duplicate: clones the block in place and also puts it on the clipboard (so cross-widget paste still works). - The collapse chevron now lives in the toolbar, outside the core .repeater-item-collapse wrapper its delegated handler targets, so we bind our own document-level collapse toggle (also more robust for dynamically added blocks). Added a collapsed-state rotate rule for the new location. - "Paste block" append affordance restyled from a blue text link to a neutral primary-style icon+label. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- The toolbar width override lost to core's more specific selector (.field-blocks ul.field-block-items > li > .repeater-item-remove), so the icons stayed in a 20px box and overflowed. Force width/height/display with !important (in both blocks.less and the inline-injected CSS) and keep the row on one line (white-space:nowrap, flex:0 0 auto on each action). - Replaced the separate "Paste block" row (which looked out of place) with a "Paste block" entry injected at the top of the + Add New Item palette popover. The popover renders at body level, so the active widget is tracked on add-group click and used to check compatibility and target the insert. The popover closes itself via its own delegated handler. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Re-added a Copy button (non-destructive: clipboard only) alongside Duplicate (clone in place). Toolbar order is now collapse, copy, cut, paste, duplicate, [config], delete, using distinct icons (copy vs clone). - Fixed the accumulating "+ Add New Item" rows. onAddItem returns an empty add-item plus a fresh one; the core popover flow removes the empty leftovers on ajaxUpdateComplete, but our direct paste/duplicate requests bypassed that handler so the empties piled up. Added a requestAdd() helper that runs the same cleanup after each direct request, and runAll() now also sweeps stray empty add-items so any existing accumulation self-heals. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The palette paste entry called a hand-rolled add request that didn't take effect. Replace it with clicking the actual block-add link inside the same popover grid, after setting the pending-paste fields. This reuses core's proven add flow (form context, load indicator, empty-add-item cleanup); the MutationObserver then fills the new block from the clipboard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
addLink.click() did not reliably trigger Winter's AJAX, so palette "Paste
block" did nothing. Call $(addLink).request(handler, {data}) directly,
reusing the link's own onAddItem handler and its data('request-form')
context (the link lives in the body-level popover, outside the form, so
that data binding is what makes the add work). Also runs the empty-add-item
cleanup on completion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… affordances - Palette paste: replace cloneNode+replaceChild (which triggered MutationObserver → runAll → injectPalettePaste → another clone → infinite loop) with removeEventListener/addEventListener on the existing button element - Palette paste button moved to search bar (outside scrollpad) as a plain <button> so click events are not intercepted by the scrollpad/filelist controls - updatePasteButtons: add has-paste class to field-block-item so the toolbar is dimly visible (opacity 0.45) when clipboard holds a compatible block, making the paste icon discoverable without hovering - blocks.less: add flex layout for search container, remove unused blocks-paste-item rule Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inject "Paste block" as a first-class grid item that looks identical to real block entries. The infinite-loop fix: check data-paste-group on the existing item before touching the DOM — if the group matches, return immediately, so the MutationObserver loop terminates after a single injection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove trailing dead string concatenation (+ '') in injectToolbarCss - Remove duplicate pendingPaste/ajaxUpdateComplete setup in palette paste handler — requestAdd already handles both - Fix toolbar order comments to include 'copy' (was missing from two places) - Correct applyPalettePaste comment to describe the actual idempotency mechanism Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Froala and CodeMirror maintain internal state and ignore native change events on the underlying textarea. Call their own APIs directly using the data keys Winter's widget plugins set on the element. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
updatePasteButtons and cleanupAddItems were running on every DOM mutation (text changes, class toggles, widget updates) via runAll(). Now they only run when the observer detects that new element nodes were actually added to the DOM. Copy/cut/duplicate handlers already call updatePasteButtons directly, so observer-triggered scans are only needed when blocks are added. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The block item partial renders fields via individual renderField() calls in a loop, which never emits a tab strip. The tabs/secondaryTabs config was wired up on the PHP side but silently ignored at render time, making any tab: declarations in .block files a no-op. - Drop tabs/secondaryTabs from BlockManager::$mergeKeys (includes no longer merge those keys) and simplify warnOnTypeCollisions accordingly - Remove processGroupMode() entries for tabs/secondaryTabs - Remove getGroupFormFieldConfig() override (existed solely to pass tabs through to the form widget) - Update test, fixture, README, and CHANGELOG to match Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Block::listInTheme() rescans every block file in the active theme on each call (~17ms warm, ~90ms cold for ~100 blocks), and getConfigs() rebuilds the full config set (re-reading attributes and any included YAML) without caching. Both are hit several times per backend request — twice during plugin boot, once per Blocks widget, and once per block via getConfig()/getDefaultConfig() during rendering — so the cost compounds to hundreds of ms on pages using the blocks editor. Memoize both per request. Block definitions cannot change within a request, callers only read the results, and per-widget allow/ignore filtering happens after getConfigs(), so the shared caches are safe. A simulated request workload drops from ~540ms to a single ~22ms theme scan. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
BlocksDatasource calls getBlocks() in its constructor, before it has been added to the AutoDatasource chain. Caching that early, incomplete result caused all subsequent calls to return a stale collection, making blocks render as empty rows in the backend. getConfigs() memoization is unaffected and still in place. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
getConfigs() scans and parses every block file (~17ms warm, ~97ms cold for ~100 blocks) on the first call of each request. The in-request memo avoids repeats within a request but every request still paid the scan. Cache the built configs in the application cache store, keyed by a content signature derived from block-file mtimes. The signature is computed in ~0.1ms from the in-memory registered-paths list without triggering a Halcyon scan, so requests after the first skip the build entirely (~5ms cache hit vs ~17ms). The cache self-invalidates: changing/adding/removing a .block file changes the signature, and the mtimes of any included YAML files are stored with the entry and re-checked on read, so editing a shared include invalidates it too. A TTL is kept only as a safety net. This caches getConfigs() (never called during BlocksDatasource construction), not getBlocks(), so it avoids the constructor-ordering issue that the earlier getBlocks() memoization hit. Verified the cached output is byte-identical to a fresh build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous copy/paste scraped field values from the DOM by their trailing
[key] segment and re-applied them client-side. That silently lost any field it
couldn't represent flatly: switches (it read the static value, not the checked
state), mediafinders (preview never refreshed), and nested repeaters (rows
don't exist in a freshly added block, and leaf keys collide). Galleries, which
use a media repeater plus switches and mediafinders, came across as structure
only.
Move copy and paste to the server:
- onCopyItem builds the item's Form widget and returns getSaveData(), which
reads the posted values and delegates to each field widget (including nested
repeaters and mediafinders). This captures every field type correctly. The
clipboard now holds { group, config, data }.
- onAddItem accepts optional _paste_data / _paste_config and seeds it via a
getValueFromIndex() override, so the new item's form renders fully populated
server-side — the same path Winter uses when loading a saved page.
- The client just stores the copied payload and, on paste, repositions the new
<li>; the lossy serialize/fill/refresh code is removed.
Verified end to end: galleries paste back with their media directories,
switches, dropdowns and mediafinder values intact.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Repeater::$onAddItemCalled is a static flag set during init() of the repeater handling an onAddItem request. While set, every repeater — including any nested repeater inside the new item — skips processItems() (Repeater.php:158,179). That is intended to stop a freshly added empty item pulling a sibling's data, but it also prevented a pasted block's nested repeaters (e.g. a gallery's media directories) from building their rows from the seeded paste data, so they rendered empty. When pasting, clear the static flag for the duration of building and rendering the new item (restored in a finally), so nested repeaters populate from the seeded data. Non-paste palette adds are unaffected — the flag is only cleared when _paste_data is present. Verified: pasting/duplicating a gallery now brings across its media directory rows with folder values and switch states intact. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nested-repeater paste fix cleared the static $onAddItemCalled flag before prepareVars(), which made the outer repeater re-process every existing item's form widget on each paste — wasted work on pages with many blocks, since the handler only renders the new item. Run prepareVars() with the flag still set (outer skips re-processing), then clear it only around building and rendering the new item, so nested repeaters still populate from the seeded data without touching the rest of the list. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (5)
💤 Files with no reviewable changes (1)
🚧 Files skipped from review as they are similar to previous changes (4)
WalkthroughThis PR introduces several interrelated features to the WinterCMS blocks plugin. Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
⚔️ Resolve merge conflicts
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@helmutkaufmann can you add screenshots for the changes you've made? Claude is really good at using playwright to generate those for you FYI. Ideal if able to provide before / afters to make it easier to review such a large PR. |
|
Yes, I can |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
CHANGELOG.md (1)
1-70:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDocument the "Tabs and secondaryTabs support" feature or remove it from version.yaml.
The version.yaml file (line 5) lists "Tabs and secondaryTabs support in block definitions" as part of the 1.1.0 release, but this feature is not documented anywhere in the CHANGELOG.md Unreleased section. The PR objectives also do not mention this feature—only five features are listed (collapsible sections, shared field includes, recently used blocks, copy/paste/duplicate blocks, and performance cache). Ensure consistency between version.yaml and CHANGELOG.md: either add documentation for tabs/secondaryTabs to the CHANGELOG, or remove this item from version.yaml if it is not part of this release.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@CHANGELOG.md` around lines 1 - 70, The version.yaml file lists "Tabs and secondaryTabs support in block definitions" as part of the 1.1.0 release, but this feature is not documented in the CHANGELOG.md Unreleased section. To fix this inconsistency, either add documentation for the tabs/secondaryTabs feature to the CHANGELOG.md Unreleased section (following the same format and structure as the other features like collapsible sections, shared field includes, etc.), or remove the tabs/secondaryTabs line from version.yaml if this feature is not actually part of this release. Ensure both files are consistent about what features are included in this version.
🧹 Nitpick comments (1)
tests/classes/BlockManagerTest.php (1)
149-167: ⚡ Quick winAdd a collision-precedence assertion for multi-include order.
testMultipleIncludes()currently checks only presence. It should also assert which include wins on field-key collisions, so ordering regressions are caught.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/classes/BlockManagerTest.php` around lines 149 - 167, The testMultipleIncludes method in BlockManagerTest.php currently only verifies that fields from multiple includes are present, but does not test collision-precedence (which include wins when field keys overlap). Add an assertion to check the actual content or properties of a field that exists in both _base.yaml and _shared.yaml to verify that the correct include takes precedence based on the order specified in the include array. This ensures that changes to the include-merging order are caught as regressions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@classes/BlockManager.php`:
- Around line 292-296: The issue is that when an included file cannot be found
or symbolized in the BlockManager class, it is skipped without being recorded in
the $touchedIncludes tracking mechanism. This causes includesUnchanged() to
incorrectly report unchanged includes when a previously missing file is later
created. To fix this, ensure that the $path (or $realPath if available) is added
to $touchedIncludes even when File::exists($realPath) returns false or when
symbolizePath fails. This way, the cache metadata will properly track missing
files and invalidate when they are subsequently created. Apply the same fix to
both the first occurrence at lines 292-296 and the second occurrence at lines
304-307 where similar file existence checks occur.
- Around line 321-327: The issue is that `$own` is being extracted from the
already-merged `$config` array, causing earlier includes to take precedence over
later ones instead of following list-order precedence. To fix this in the
BlockManager.php code block around the warnOnTypeCollisions method call, obtain
`$own` from the raw, original block configuration before any merging occurs,
rather than from the current state of `$config`. This ensures that later
includes in the list can properly override earlier includes when collisions
happen, maintaining correct precedence order.
In `@formwidgets/Blocks.php`:
- Around line 201-206: In the onCopyItem() method in formwidgets/Blocks.php, add
a call to $this->prepareVars() before the existing call to makeItemFormWidget()
to ensure that $groupDefinitions is properly populated. This is necessary
because makeItemFormWidget() requires the group definitions to correctly build
the form widget with the proper schema for the specified group code, and without
calling prepareVars() first, the copy operation may fail or use incorrect field
schemas. Follow the same pattern used in onAddItem() and render() methods which
both call prepareVars() before working with form widgets.
In `@formwidgets/blocks/assets/less/blocks.less`:
- Around line 64-68: The `.transform(rotate(180deg));` LESS mixin call in the
collapsed repeater item rule is not recognized by Stylelint and causes linting
errors. Replace the mixin call with the standard CSS transform property syntax:
change `.transform(rotate(180deg));` to `transform: rotate(180deg);` within the
`.field-block-item.collapsed > .repeater-item-remove
.repeater-item-collapse-one` selector block.
In `@formwidgets/blocks/partials/_block.php`:
- Around line 386-389: The storageKey function is generating persistence keys
using only the data-field-name attribute, which causes sections with identical
field names in different block groups or widgets to share the same storage key
and overwrite each other's collapsed state. Modify the storageKey function to
include additional context that uniquely identifies the block or widget
containing the section (such as a parent block identifier, widget reference, or
other distinguishing attribute) alongside the field name when constructing the
COLLAPSE_PREFIX key. This will ensure each section's collapsed state is stored
independently based on both its field name and its containing context.
---
Outside diff comments:
In `@CHANGELOG.md`:
- Around line 1-70: The version.yaml file lists "Tabs and secondaryTabs support
in block definitions" as part of the 1.1.0 release, but this feature is not
documented in the CHANGELOG.md Unreleased section. To fix this inconsistency,
either add documentation for the tabs/secondaryTabs feature to the CHANGELOG.md
Unreleased section (following the same format and structure as the other
features like collapsible sections, shared field includes, etc.), or remove the
tabs/secondaryTabs line from version.yaml if this feature is not actually part
of this release. Ensure both files are consistent about what features are
included in this version.
---
Nitpick comments:
In `@tests/classes/BlockManagerTest.php`:
- Around line 149-167: The testMultipleIncludes method in BlockManagerTest.php
currently only verifies that fields from multiple includes are present, but does
not test collision-precedence (which include wins when field keys overlap). Add
an assertion to check the actual content or properties of a field that exists in
both _base.yaml and _shared.yaml to verify that the correct include takes
precedence based on the order specified in the include array. This ensures that
changes to the include-merging order are caught as regressions.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: c5771813-68f9-43c2-94e1-c236e72dbb86
📒 Files selected for processing (18)
.gitignoreCHANGELOG.mdREADME.mdclasses/BlockManager.phpcomposer.jsonformwidgets/Blocks.phpformwidgets/blocks/assets/less/blocks.lessformwidgets/blocks/partials/_block.phpformwidgets/blocks/partials/_block_item.phptests/classes/BlockManagerTest.phptests/fixtures/blocks/includes/_base.yamltests/fixtures/blocks/includes/_cycle_a.yamltests/fixtures/blocks/includes/_cycle_b.yamltests/fixtures/blocks/includes/_shared.yamltests/fixtures/blocks/includes/_with_nested.yamltests/formwidgets/BlocksTest.phpupdates/version.yamlwinter.mix.js
|
@LukeTowers: These are the only visible changes. Note that the Gallery block, when adding a new block to the repeater, has moved to the front as it has been recently used. And apologies for the big PR. I will do better next time. Think though that these are some good extensions to the plugin. |
…torageKey scoping, and dead code - resolveIncludes: capture original block fields before the include loop so the block always wins on collision and later includes correctly override earlier ones (not the already-merged result) - resolveIncludes: record missing include paths in touchedIncludes with mtime 0 so the cache invalidates if the file is later created - onCopyItem: call prepareVars() before makeItemFormWidget() so groupDefinitions is populated (matches onAddItem/render pattern) - storageKey: scope localStorage key to data-widget-id so sections with the same field name in different block widgets don't share state - warnOnTypeCollisions: remove unreachable is_array guard (params are typed array) - blocks.less: replace .transform() mixin with plain CSS transform property - version.yaml: remove stale "Tabs and secondaryTabs" entry not in this PR Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@helmutkaufmann can you fix the merge conflict? |


Summary
This PR adds several independent editor and developer-experience improvements to the Blocks plugin.
Collapsible sections
type: sectionfields supportcollapsible: trueshorthand, withcollapsed: true|falsecontrolling the initial state.localStorage(keyed by field name).data-block-collapsibleattribute bootstrapped inline in the block widget partial, independent of core's collapsible-section JS. This is intentional: core re-collapses and re-binds every section on each form-widget init (including when a nested repeater adds an item), which broke manually-opened sections and stalled repeater "Add item" clicks.Shared field includes
include:(string or list) to mergefieldsandconfigfrom external plain-YAML files.type.Recently used blocks
localStorage.Copy / Cut / Paste / Duplicate blocks
onCopyItem, which builds the block's form widget server-side and callsgetSaveData(). This correctly captures every field type — switches, mediafinders, nested repeaters with their own rows — which a client-side DOM scrape cannot.allow/ignore/tagsconstraints.sessionStorage).Performance: cross-request config cache
BlockManager::getConfigs()now stores built block configs in the Laravel cache, keyed by an md5 signature of every block file's mtime..blockfile orincluded YAML file changes — no manualcache:clearneeded.Tests
BlockManagerTest: include merging, block-overrides-include precedence, nested includes, circular-include guard, missing-file skip, multiple includes, and the no-include no-op.BlocksTest:collapsible/collapsedshorthand translation, and that non-section / plain-section fields are left untouched.tests/fixtures/blocks/includes/.Test plan
collapsible: truesection to a block, confirm it collapses/expands and state survives a page reload_shared.yamlinclude and confirm fields merge correctly (block fields override)allow/ignore/tags.blockfile and confirm the config cache invalidates (no stale data)php artisan test --filter=BlockManagerTestandphp artisan test --filter=BlocksTest🤖 Generated with Claude Code
Summary by CodeRabbit
.DS_Storeto ignore rules and clarified build runtime separation forblocks.jsfiles