[6.x] Element select field - #19476
Closed
brianjhanson wants to merge 27 commits into
Closed
Conversation
The modal classes were ported to modern TypeScript, but the registry that resolves one for an element type stayed in the legacy bundle -- so the modern module had to reach back into `Craft._elementSelectorModalClasses` to register its own asset modal. The registry and factory move here. In-repo callers import `createElementSelectorModal` directly instead of going through `Craft`, and the legacy trio is deleted from `Craft.js`. `Craft.createElementSelectorModal` and `registerElementSelectorModalClass` are still assigned, so plugins are unaffected, as are the PHP-emitted `new Craft.VolumeFolderSelectorModal(...)` boots. The legacy bundle runs before this module, so anything already sitting on the old registry object is adopted on load, and the built-in asset registration yields to it.
ContentIndexViewModel (the Inertia payload) and ElementIndexHtml (the server-rendered shell behind the element-selector modal and the remaining legacy screens) had grown independent implementations of the same pre-query decisions: which sources an index lists, which table columns and sort options are selectable, and whether the site/status menus apply. ElementIndexState is now the one implementation, and both call it. Source restriction — previously only in ElementIndexHtml, inline — is exposed as an explicit `restrictTo` parameter, along with the heading pruning, nested key recovery and sidebar computation that go with it. Context stays a first-class parameter so each caller keeps resolving under the context it always did. No intended behavior change, with two exceptions that fall out of the shared implementation always returning a Collection of sources: - `sources: false` (the deletion-blocker detail panes) fatally errored with "Call to a member function every() on array" whenever the site menu resolved to true, which it does for any localized element type. - Restricting to a nested source key that had to be slotted in behind another key hit array_splice() with a Collection, a TypeError. ElementIndexHtml keeps passing `showStatusMenu: 'auto'` straight through rather than resolving it: the toolbar template treats any truthy value as "render the menu" and lets the JS index fill in the statuses, so resolving it server-side would change what renders.
… feature/element-select-field
The modal assembled its own DOM with jQuery — `$('<div/>', {…}).appendTo()`
chains plus `Craft.ui.createButton`/`createSubmitButton` — which meant its
markup was only readable by executing it.
The chrome is now two Lit templates: `chromeTemplate()` for the shell and the
footer buttons, and the sidebar toggle view's header pair. The `$`-prefixed
fields keep handing back jQuery objects, captured with Lit's `ref` directive,
because subclasses and the element select input manipulate them.
The button markup is reproduced rather than delegated, so the `btn submit`
classes, the `.label` inside its `.inline-flex` wrapper and the absolutely
positioned spinner all survive — the CP stylesheet selects on each. Tests now
pin that markup, and no longer stub `Craft.ui` at all.
The sidebar headers render detached and are moved into place: the element
index owns those containers, and `render()` claims whatever it renders into.
One behavior difference: the main heading interpolated the active source name
into an HTML string, so a name containing markup was parsed as HTML. As a Lit
text binding it is escaped.
Step toward the modal rendering the same index the CP screens do. `element-selector-modals/body` now returns the index payload alongside the HTML it already returned. Nothing consumes the props yet — the modal still boots the legacy index off `html` — so this is additive and changes no behavior. The payload comes from a new `ModalIndexViewModel`, which is `ContentIndexViewModel` with two differences the modal needs: sources resolve in the `modal` context rather than `index`, and they are narrowed to the source keys the opener allows, since a relation field may offer only some of an element type's sources. Both ride on the resolution `ElementIndexState` already shares with the server-rendered shell. `indexState()` becomes protected so the subclass can reach it.
`useContentIndexData()` read the index payload straight off `usePage()`, so every composable built on it — view state, filters, columns, sort, pagination — could only run inside an Inertia page. It now takes an optional source to read from instead. `usePage()` is only called when none is given, so an index outside an Inertia app doesn't reach for one. The keys and their reactivity are identical either way, which is what lets the rest of the `useElementIndex*` stack stay unaware of where its payload came from. Groundwork for the element selector modal, which will drive the same payload over XHR from `element-selector-modals/body` rather than a page visit. No behavior change for the index screens.
The view-mode, sort and pagination composables each built their own `createIndexVisitor(route)`, which keeps index state in the page URL and re-requests through an Inertia visit. Neither works for an index inside a modal: it has no URL of its own, and the visit would replace the page behind it. Each now accepts a visitor instead of always making one. Pages pass nothing and are unchanged. Adds the modal's implementation of that contract: the query lives in a ref and `visit()` asks a loader for a fresh payload. Same shape, so the `useElementIndex*` composables drive it without knowing the difference.
Composes the same stack the index screens use — view state, columns, sort, pagination, view mode — against a payload refreshed over XHR from `element-selector-modals/body` and the modal's visitor. Not `useElementIndexPage`: that registers itself as the screen's active index, which would hijack the page behind the modal, and its filter composable submits through an Inertia form. Search and status go through the visitor here instead. Exposes selection in the shape the relation field consumes — the same keys `Craft.getElementInfo()` read off chip data attributes, since `onModalSelect` and `app/render-elements` both depend on them.
The modal rendered a server-built HTML shell and booted the legacy jQuery index into it. It now mounts the same Vue index the CP screens use, driven by the props the body action already returns. Selection moves with it. The modal no longer asks an index for jQuery sets: the index pushes plain objects up, `hasSelection()` reads their count, and `getElementInfo()` passes them through. The relation field's `updateDisabledElementsInModal()` assigns the whole disabled set rather than enabling and disabling ids one at a time, which also lets `removeElements()` stop reaching into the modal's internals. `VolumeFolderSelectorModal` deliberately keeps the legacy index: folder picking reads `sourcePath` — the breadcrumb of the folder you navigated into, which is what selecting the current folder means — and the Vue index has no equivalent yet. That's now the only thing keeping `ElementIndexHtml` and the HTML `element-indexes/*` endpoints alive.
Selecting an element returned a 422 from `app/render-elements`, which requires a siteId. Index rows are `id` plus column HTML — enough to render a table, nothing that describes the element. The legacy modal didn't have this problem because it read each row's chip data attributes via `Craft.getElementInfo()`; with rows as plain data there is nowhere for that to come from. The modal's view model now adds the keys that reader produced — siteId, label, status, url, hasThumb — through the existing `extraRowData()` hook, so they ride along in every view mode. `hasThumb` asks the element for its thumbnail rather than the type for `hasThumbs()`: an asset with no preview has none even where its type does. Verified end to end in the CP: the modal lists assets, selecting one enables Select, choosing adds the chip, and saving persists the relation.
Importing the registry from the relation field pulled the modal, Lit, Vue and the whole element index component tree into every page carrying a relation field, whether or not a modal was ever opened. `createModal()` imports the registry on demand and `showModal()` awaits it, guarded so a double click can't open two modals. The index component, Vue and the component registry are likewise imported when the body action resolves rather than at module scope. Verified in the CP after the change: the modal still opens on the first click, selection still enables Select, and choosing still adds the chip.
…-field # Conflicts: # resources/js/common/types/globals.d.ts # resources/js/modules/element-selector-modal/asset-selector-modal.ts # resources/js/modules/element-selector-modal/base-element-selector-modal.test.ts # resources/js/modules/element-selector-modal/base-element-selector-modal.ts # resources/js/modules/element-selector-modal/index.ts # resources/js/modules/element-selector-modal/volume-folder-selector-modal.ts # resources/js/modules/elements/composables/useContentIndexData.ts # resources/js/modules/elements/composables/useElementEditor.ts # resources/js/modules/link-field/craft-link-field.ts # resources/js/modules/markdown-field/behaviors/assets.ts
📚 Storybook previews@craftcms/ui — open Storybook Changed components: resources/js — open Storybook No changed components detected in this Storybook. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Related issues