diff --git a/CHANGELOG.md b/CHANGELOG.md index a3d27877d85..83e1bd0703f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,17 @@ - The global `axios` is now only loaded in the control panel when `craftcms/yii2-adapter` is installed. `Craft.sendActionRequest()` or `actionClient` from `@craftcms/ui` should be used instead. - Removed `Cp.$axios`. - Fixed a bug where legacy embedded element indexes were missing actions, exporters, and reorder controls. ([#19789](https://github.com/craftcms/cms/pull/19789)) +- Added `CraftCms\Cms\Form\Controls\NestedElements` and `CraftCms\Cms\Element\NestedElementManager::formControl()`, allowing plugins to manage custom nested element types as cards or embedded element indexes. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Unified entry edit pages and nested element slideouts under the generic element editor, with a default `CraftCms\Cms\Http\ViewModels\ElementEditViewModel` for custom element types. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Added `CraftCms\Cms\Element\Events\ElementEditorPayloadResolving`. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- The user Addresses screen now uses the shared nested element manager, including duplicating, deleting, and the element index view for users with many addresses. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Removed the `pasteableEntryTypeIds` nested element manager setting. `pasteableData` should be used instead. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where Addresses fields’ configured Cards and Index view modes weren’t used in element forms. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where nested element cards’ Copy, Duplicate, and Delete actions were always disabled. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where nested elements without their own edit page, such as addresses, couldn’t be opened from their cards. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where nested element slideouts for element types other than entries always used the legacy editor. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where addresses couldn’t be saved from an element editor slideout. ([#19792](https://github.com/craftcms/cms/pull/19792)) +- Fixed a bug where element actions couldn’t find a user’s addresses. ([#19792](https://github.com/craftcms/cms/pull/19792)) - Fixed a bug where users’ breadcrumb chips weren’t getting hyperlinked. - Fixed a bug where `craft:up` could fail on installs that didn’t have a migrations table yet. ([#19796](https://github.com/craftcms/cms/pull/19796)) - Fixed a bug where subsequent embedded index requests lost configuration supplied by non-Matrix nested element managers. ([#19788](https://github.com/craftcms/cms/pull/19788)) diff --git a/docs/forms.md b/docs/forms.md index 2bb31e9932c..cc77c3ea54a 100644 --- a/docs/forms.md +++ b/docs/forms.md @@ -126,6 +126,43 @@ Return zero or one root Node. It may contain children or a composite Control. Th mode. Listen for `FieldLayoutFormResolving` to add, remove, or reorder typed Nodes after compilation; do not mutate rendered HTML or persisted layout data. +### Nested elements + +Elements nested in an owner — Matrix and Addresses fields, a user's addresses, or a plugin's own nested element type — +are managed by a `NestedElementManager`, which builds the shared `NestedElements` Control (`craft:nested-elements`). It +renders the elements as cards or an embedded element index outside the owner's form, and handles creating, editing in +a slideout, reordering, pasting, duplicating, and deleting them, including preparing the owner's draft first: + +```php +use CraftCms\Cms\Form\Contracts\Control; + +public function formControl(FieldContext $context): Control +{ + return $this->manager()->formControl($context->path, $context->element, 'cards', [ + 'canCreate' => true, + 'sortable' => true, + 'maxElements' => $this->maxItems, + ]); +} +``` + +The view mode is `cards`, `cards-grid`, or `index`. The frontend retains the initial index's display settings and +sends them with later requests, including paging and inline saves. Fields and owner elements do not need a +configuration provider. The frontend uses `maxElements` to disable operations when the whole selection will not +fit. The backend resolves the owner and nested element scope on each request and checks the element type's or +field's policies. Authoritative limits belong in those policies and field validation. A posted display setting +cannot bypass them. As in 5.x, duplication can partially succeed if the selection no longer fits when it runs. + +Screens that manage an owner's nested elements outside an element editor render `NestedElements.vue` directly with the +Control's props, passing `savedNestedOwner()` as its `owner` so changes apply to the saved owner and the screen +re-renders afterwards (see `pages/users/Addresses.vue`). + +Nested element types use the shared Inertia element editor automatically. They supply their field layout and +`sidebarForm()`, and can extend `ElementEditViewModel` through `ElementInterface::editViewModelClass()` when they +need additional payload or a different save action. `ElementEditorPayloadResolving` lets listeners modify the +prepared payload for edit screens and autosave responses. With the Yii adapter installed, existing editor HTML events +and `prepareEditScreen()` customizations are rendered around the native forms. + ### FieldLayout component settings Field layout components — tabs and layout elements — describe the form shown in the designer's settings slideout by diff --git a/docs/slideouts.md b/docs/slideouts.md index 31c29bec4a0..5bc6957e253 100644 --- a/docs/slideouts.md +++ b/docs/slideouts.md @@ -240,32 +240,18 @@ open and keeps saving as the user types, so an opener that refreshes on it shoul ### The element editor -The `elements/edit` screen is the exception: it doesn't submit itself. Drafts, autosaving, -provisional drafts, delta submission and tab error indicators all live in `Craft.ElementEditor`, so -the panel hands that screen over to it rather than reimplementing any of it — the same deal -`ElementEditorSlideout` strikes for the jQuery stack. - -`useElementEditor()` builds the editor and gives it the shell's regions: the panel `
`, the -content and details columns, the header as a spinner host. Everything else is adapted through -callbacks — `updateTabs`/`getTabManager` onto a `Craft.Tabs` the bridge owns, submit results onto -the panel's own success and error handling. - -Two wrinkles worth knowing: - -- **Settings arrive as `screen.elementEditorSettings`,** not through `$(container).data()`. - `EditElementController` branches on `$request->inertia()` and calls `CpScreenResponse::screenData()` - instead of emitting the usual script. That script looks the container up by id, and it runs while - Vue still has the panel's subtree detached from the document, so the lookup finds nothing. Any - screen with the same problem can use `screenData()` the same way. -- **The editor is built on a `ready` signal, not on mount.** Fragments are appended asynchronously - (`appendHeadHtml()` resolves only once the screen's assets have loaded), and the editor snapshots - the form to detect changes — snapshotting an empty form would read every field as an edit. So - `cp/Screen` waits until *all* of its fragments have reported in, then calls the callback provided - under `ScreenContentReadyKey`. - -Once the editor is running it owns the screen: `SlideoutScreen` routes Save to it, hides the Save -button on a static screen (a revision), and lets it rename Cancel to "Close" once a provisional -draft exists. +Inertia requests to `elements/edit` render the shared `elements/Edit` page. Entry edit URLs +use the same controller. Every element type has a default `ElementEditViewModel`; types can +extend it to customize their payload and save parameters. + +`useElementEditor()` drives the native Form renderers, drafts, autosave, and saves in both pages +and slideouts. The slideout provides its own payload and owner context through the same editor. +With the Yii adapter installed, element HTML events and `prepareEditScreen()` can wrap or replace the native content and sidebar; +inputs added by those customizations are included in saves, and registered assets load after the +content is ready. + +Legacy jQuery slideouts continue to request JSON without the Inertia header. They receive the +legacy `CpScreenResponse` and use `Craft.ElementEditor`. ### Unsaved changes diff --git a/packages/craftcms-ui/src/utilities/dom.ts b/packages/craftcms-ui/src/utilities/dom.ts index 1df539c333e..3944209abd2 100644 --- a/packages/craftcms-ui/src/utilities/dom.ts +++ b/packages/craftcms-ui/src/utilities/dom.ts @@ -262,13 +262,15 @@ export function serializeFormInputs(container: HTMLElement): string { * Names are kept verbatim — PHP-style bracket names (`settings[path]`) stay * flat keys, matching what the server would see after parsing the string * form. Repeated names are grouped into arrays rather than last-one-wins. + * An excluded subtree can contain a native form whose state is tracked separately. */ export function serializeFormInputsAsObject( - container: HTMLElement + container: HTMLElement, + exclude?: HTMLElement | null ): Record { const object: Record = {}; - for (const [name, value] of collectFormInputs(container)) { + for (const [name, value] of collectFormInputs(container, exclude)) { const existing = object[name]; if (existing === undefined) { @@ -283,14 +285,17 @@ export function serializeFormInputsAsObject( return object; } -function collectFormInputs(container: HTMLElement): URLSearchParams { +function collectFormInputs( + container: HTMLElement, + exclude?: HTMLElement | null +): URLSearchParams { const params = new URLSearchParams(); const controls = container.querySelectorAll< HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement >('input[name], select[name], textarea[name]'); for (const control of controls) { - if (control.disabled) { + if (control.disabled || exclude?.contains(control)) { continue; } @@ -321,7 +326,7 @@ function collectFormInputs(container: HTMLElement): URLSearchParams { } for (const host of container.querySelectorAll('*')) { - if (!host.tagName.includes('-')) { + if (!host.tagName.includes('-') || exclude?.contains(host)) { continue; } diff --git a/resources/js/modules/elements/components/ElementEditor.vue b/resources/js/modules/elements/components/ElementEditor.vue index bcd7a92672c..98e3b60f445 100644 --- a/resources/js/modules/elements/components/ElementEditor.vue +++ b/resources/js/modules/elements/components/ElementEditor.vue @@ -6,7 +6,13 @@ * save controls and details column go. */ import {t} from '@craftcms/ui'; - import {computed, nextTick, provide, useTemplateRef} from 'vue'; + import { + computed, + nextTick, + provide, + useTemplateRef, + type Component, + } from 'vue'; import {router, usePage} from '@inertiajs/vue3'; import DynamicHtmlRenderer from '@/common/components/DynamicHtmlRenderer.vue'; import MetadataDetailsContent from '@/common/components/MetadataDetailsContent.vue'; @@ -43,8 +49,17 @@ * piece of the pipeline (e.g. an entry's `entryId`/`sectionId`). */ saveData?: () => FormValues; + transform?: (data: object) => FormValues; + formWrapper?: Component; + showDetails?: boolean; }>(); + const editor = useElementEditor({ + saveData: props.saveData, + transform: props.transform, + root: () => contentEl.value, + }); + const { activity, activityTimelineVersion, @@ -68,10 +83,7 @@ submitAction, updatePayload, workflowReviewLocked, - } = useElementEditor({ - saveData: props.saveData, - root: () => contentEl.value, - }); + } = editor; provide(NestedOwnerEditorKey, { async prepare(path) { @@ -115,6 +127,7 @@ const hasDetails = computed( () => + Boolean(props.showDetails) || Boolean(sidebarPayload.value) || Boolean(payload.metadataHtml) || Boolean(payload.activityTimelineUrl) || @@ -370,18 +383,23 @@
- + + + @@ -406,17 +424,22 @@ - diff --git a/resources/js/modules/elements/composables/useElementAutosave.ts b/resources/js/modules/elements/composables/useElementAutosave.ts index 37c14401688..04a17e6b6ff 100644 --- a/resources/js/modules/elements/composables/useElementAutosave.ts +++ b/resources/js/modules/elements/composables/useElementAutosave.ts @@ -34,6 +34,8 @@ export interface ElementAutosaveOptions { * the server needs to resolve it through the owner it's being edited in. */ params?: () => FormValues; + /** Adapts the editor's form state into the save request. */ + transform?: (data: object) => FormValues; /** How long to wait after the last edit before saving, per change kind. */ debounceMs?: Partial>; /** @@ -120,7 +122,7 @@ export function useElementAutosave( siteId: options.siteId, ...options.params?.(), }; - Object.assign(payload, form.data()); + Object.assign(payload, options.transform?.(form.data()) ?? form.data()); // No draft yet means this request creates one; an existing provisional // draft is targeted by id and stays provisional. diff --git a/resources/js/modules/elements/composables/useElementEditor.test.ts b/resources/js/modules/elements/composables/useElementEditor.test.ts index 37c816bf5a9..529971f8c1e 100644 --- a/resources/js/modules/elements/composables/useElementEditor.test.ts +++ b/resources/js/modules/elements/composables/useElementEditor.test.ts @@ -351,8 +351,8 @@ describe('useElementEditor', () => { component: 'craft:field', props: {label: 'Cards', instructions: null, required: false}, control: { - type: 'CraftCms\\Cms\\Form\\Controls\\NestedEntries', - component: 'craft:nested-entries', + type: 'CraftCms\\Cms\\Form\\Controls\\NestedElements', + component: 'craft:nested-elements', props: { viewMode: 'cards', manager: null, diff --git a/resources/js/modules/elements/composables/useElementEditor.ts b/resources/js/modules/elements/composables/useElementEditor.ts index d31d03c159d..7a8ef534904 100644 --- a/resources/js/modules/elements/composables/useElementEditor.ts +++ b/resources/js/modules/elements/composables/useElementEditor.ts @@ -85,6 +85,7 @@ export interface ElementContextMenuItem { /** The shared payload every {@link ElementEditViewModel} emits. */ export interface ElementEditPayload { + editorContainerId?: string; elementId: number | null; canonicalId: number | null; elementType: string; @@ -165,6 +166,8 @@ interface Options { * whatever the type's save action needs to resolve the element it's saving. */ saveData?: () => FormValues; + /** Adapts form state before autosave and explicit submissions. */ + transform?: (data: object) => FormValues; /** * Where the screen's content is rendered. Failed saves announce invalid * nested elements from here, so the nested element fields inside hear it. @@ -180,7 +183,7 @@ interface Options { * Element-type pages supply only what their save action needs via * {@link Options.saveData}; everything else comes from the shared payload. */ -export function useElementEditor({saveData, root}: Options = {}) { +export function useElementEditor({saveData, root, transform}: Options = {}) { // Not `usePage()`: inside a slideout that's the page *behind* the panel, so // the editor would read the index's props and find no payload at all. This // resolves to the panel's own props there, and to `usePage()` on a full page. @@ -286,6 +289,9 @@ export function useElementEditor({saveData, root}: Options = {}) { return { ...props.nestedContext, ...(props.fresh ? {fresh: 1} : {}), + ...(props.editorContainerId + ? {editorContainerId: props.editorContainerId} + : {}), }; } @@ -298,6 +304,7 @@ export function useElementEditor({saveData, root}: Options = {}) { draftId: props.draftId, isProvisional: props.isProvisionalDraft, enabled: props.canAutosave, + transform: requestValues, // Autosave moves the draft's `dateUpdated` without a visit, so the poller // has to re-baseline against what the save just wrote — otherwise it reads // our own keystrokes back as an edit from elsewhere. `activity` is @@ -462,6 +469,10 @@ export function useElementEditor({saveData, root}: Options = {}) { } } + function requestValues(data: object): FormValues { + return transform?.(data) ?? ({...data} as FormValues); + } + /** * Re-renders the field layout from the server, keeping unsaved values. * @@ -603,7 +614,7 @@ export function useElementEditor({saveData, root}: Options = {}) { const transformed = pendingAction.value?.includeFormData === false ? {} - : {...saveData?.(), ...data}; + : {...saveData?.(), ...requestValues(data)}; // Shared `elements/*` actions need generic identity params: the // type-specific ones (an entry's `entryId`) mean nothing there. if (autosave.draftId.value !== null) { diff --git a/resources/js/modules/elements/nested-owner.test.ts b/resources/js/modules/elements/nested-owner.test.ts index 027e0e5ef3f..448a4c2d73e 100644 --- a/resources/js/modules/elements/nested-owner.test.ts +++ b/resources/js/modules/elements/nested-owner.test.ts @@ -5,8 +5,8 @@ import type {FormPayload} from '@/modules/forms/types'; describe('nestedOwnerId', () => { it('resolves the requested nested block owner rather than the root element', () => { const control = (path: string[], ownerId: number) => ({ - type: 'NestedEntries', - component: 'craft:nested-entries', + type: 'NestedElements', + component: 'craft:nested-elements', mode: 'editable', path, deltaGroup: path, diff --git a/resources/js/modules/elements/nested-owner.ts b/resources/js/modules/elements/nested-owner.ts index 3069e8ea6a7..d06f2ea6fde 100644 --- a/resources/js/modules/elements/nested-owner.ts +++ b/resources/js/modules/elements/nested-owner.ts @@ -30,7 +30,7 @@ export function nestedOwnerContext( let context: NestedOwnerContext | null = null; visitControls(form.nodes, (control) => { if ( - control.component !== 'craft:nested-entries' || + control.component !== 'craft:nested-elements' || !pathsMatch(control.path, path) ) { return; @@ -54,6 +54,26 @@ export function nestedOwnerContext( return context; } +/** + * An owner that's always saved and never drafted, e.g. a user on their own + * Addresses screen: nested element changes apply to it directly, and the + * screen re-renders the manager through `refresh()`. + */ +export function savedNestedOwner( + ownerId: number, + refresh: () => Promise +): NestedOwnerEditor { + return { + prepare: async () => ({ + ownerId, + ownerIsDerivative: false, + ownerIsInDerivativeTree: false, + ownerIsUnpublishedDraft: false, + }), + refresh, + }; +} + export const NestedOwnerEditorKey: InjectionKey = Symbol( 'nested-owner-editor' ); diff --git a/resources/js/modules/forms/ElementSelectControl.test.ts b/resources/js/modules/forms/ElementSelectControl.test.ts index 353fbb1393e..160fa6a0e80 100644 --- a/resources/js/modules/forms/ElementSelectControl.test.ts +++ b/resources/js/modules/forms/ElementSelectControl.test.ts @@ -757,21 +757,33 @@ describe('ElementSelectControl', () => { ); }); - it('opens entries in a Vue slideout when the page hosts one', async () => { - vi.stubGlobal('Craft', {createElementEditor, openSlideout: vi.fn()}); - const root = await mount({ - props: {elementType: 'CraftCms\\Cms\\Entry\\Elements\\Entry'}, - value: [5], - }); - - doubleClick(chip(root)); - - expect(createElementEditor).not.toHaveBeenCalled(); - expect(stub.openSlideout).toHaveBeenCalledOnce(); - const [href] = stub.openSlideout.mock.calls[0] as unknown as [string]; - const params = new URL(href, 'https://example.test').searchParams; - expect(params.get('elementId')).toBe('5'); - }); + it.each([ + 'CraftCms\\Cms\\Entry\\Elements\\Entry', + 'CraftCms\\Cms\\Asset\\Elements\\Asset', + ])( + 'opens %s in the generic editor when it has no edit URL', + async (elementType) => { + vi.stubGlobal('Craft', {createElementEditor, openSlideout: vi.fn()}); + const root = await mount({ + props: { + elementType, + elements: [{id: 5, label: 'Some element', siteId: 2}], + }, + value: [5], + }); + + doubleClick(chip(root)); + + expect(createElementEditor).not.toHaveBeenCalled(); + expect(stub.openSlideout).toHaveBeenCalledOnce(); + const [href] = stub.openSlideout.mock.calls[0] as unknown as [string]; + const url = new URL(href, 'https://example.test'); + expect(url.pathname).toMatch(/\/elements\/edit$/); + expect(url.searchParams.get('elementType')).toBe(elementType); + expect(url.searchParams.get('elementId')).toBe('5'); + expect(url.searchParams.get('siteId')).toBe('2'); + } + ); it('opens an element’s own edit screen in a Vue slideout', async () => { vi.stubGlobal('Craft', {createElementEditor, openSlideout: vi.fn()}); @@ -794,16 +806,6 @@ describe('ElementSelectControl', () => { ); }); - it('keeps elements without an edit screen in the legacy editor', async () => { - vi.stubGlobal('Craft', {createElementEditor, openSlideout: vi.fn()}); - const root = await mount({value: [5]}); - - doubleClick(chip(root)); - - expect(stub.openSlideout).not.toHaveBeenCalled(); - expect(createElementEditor).toHaveBeenCalledOnce(); - }); - it('stays put when the double-click lands on the chip’s checkbox', async () => { const root = await mount({props: {limit: 3}, value: [5]}); // `craft-chip` renders its own checkbox in shadow DOM; the composed path diff --git a/resources/js/modules/forms/ElementSelectControl.vue b/resources/js/modules/forms/ElementSelectControl.vue index 6d1f37d0318..cf37bf27dbd 100644 --- a/resources/js/modules/forms/ElementSelectControl.vue +++ b/resources/js/modules/forms/ElementSelectControl.vue @@ -613,20 +613,17 @@ const elementType = props.control.props.elementType; if (canUseVueSlideout()) { - // The element's own edit screen, the way an element index opens it. - // Elements picked since the last render don't know it yet; entries can - // still get their editor through the generic edit action. const href = element.cpEditUrl ?? - (elementType === 'CraftCms\\Cms\\Entry\\Elements\\Entry' - ? EditElementController.url(undefined, { - query: { - elementType, - elementId: element.id, - siteId: element.siteId ?? null, - }, - }) - : null); + EditElementController[ + '/{cpTrigger?}/{actionTrigger?}/elements/edit' + ].url(undefined, { + query: { + elementType, + elementId: element.id, + siteId: element.siteId ?? null, + }, + }); if (href) { void openSlideout(href, { diff --git a/resources/js/modules/forms/FormRenderer.test.ts b/resources/js/modules/forms/FormRenderer.test.ts index 8259208e42e..e1f16554d01 100644 --- a/resources/js/modules/forms/FormRenderer.test.ts +++ b/resources/js/modules/forms/FormRenderer.test.ts @@ -2479,8 +2479,8 @@ describe('FormRenderer', () => { deltaGroup: ['settings', 'matrix'], }), node({ - type: 'CraftCms\\Cms\\Form\\Controls\\NestedEntries', - component: 'craft:nested-entries', + type: 'CraftCms\\Cms\\Form\\Controls\\NestedElements', + component: 'craft:nested-elements', props: { viewMode: 'cards', cards: [], diff --git a/resources/js/modules/forms/entry-field-layout-form-host.test.ts b/resources/js/modules/forms/entry-field-layout-form-host.test.ts index ad1065d0941..e94a62dd71c 100644 --- a/resources/js/modules/forms/entry-field-layout-form-host.test.ts +++ b/resources/js/modules/forms/entry-field-layout-form-host.test.ts @@ -125,8 +125,8 @@ it('submits Entry Form values and preserves refresh context', async () => { component: 'test:owner-prepare', props: {}, control: { - type: 'NestedEntries', - component: 'craft:nested-entries', + type: 'NestedElements', + component: 'craft:nested-elements', mode: 'editable', path: ['editor', 'cards'], deltaGroup: ['editor', 'cards'], @@ -438,8 +438,8 @@ it('submits Entry Form values and preserves refresh context', async () => { component: 'test:owner-prepare', props: {}, control: { - type: 'NestedEntries', - component: 'craft:nested-entries', + type: 'NestedElements', + component: 'craft:nested-elements', mode: 'editable', path: ['editor', 'matrix', 'entries', 'block-c', 'cards'], deltaGroup: ['editor', 'matrix', 'entries', 'block-c', 'cards'], diff --git a/resources/js/modules/forms/nested-elements/NestedElementBlocksControl.vue b/resources/js/modules/forms/nested-elements/NestedElementBlocksControl.vue index 0da95e9f6fd..43089dc9d60 100644 --- a/resources/js/modules/forms/nested-elements/NestedElementBlocksControl.vue +++ b/resources/js/modules/forms/nested-elements/NestedElementBlocksControl.vue @@ -44,8 +44,8 @@ import {useSelectable} from '@/common/composables/useSelectable'; import SelectableCardList from '@/common/components/SelectableCardList.vue'; import FormNodeList from '../FormNodeList.vue'; - import NestedEntriesCreateButton from './NestedEntriesCreateButton.vue'; - import {isPasteable} from './nested-entries'; + import NestedElementsCreateButton from './NestedElementsCreateButton.vue'; + import {isPasteable} from './nested-elements'; import type {ActionItems} from '@/common/types'; import {useMessages} from '@/modules/messages/useMessages'; import { @@ -252,9 +252,11 @@ const fits = isPasteable(elements, { elementType, - entryTypeIds: Object.values(create.entryTypeIds), + pasteableData: { + attribute: 'entryTypeId', + values: Object.values(create.entryTypeIds), + }, room: true, - requireEntryTypeId: true, }); return fits ? elements : []; @@ -1580,7 +1582,7 @@
- ; singleColumn: boolean; editable: boolean; busy: boolean; reorderable: boolean; - operations: NestedEntryOperations; - quickEdit: NestedEntryQuickEdit; + operations: NestedElementOperations; + quickEdit: NestedElementQuickEdit; editOpener?: HTMLElement | null; }>(); function actionIds(): number[] { - return props.selection.isSelected(props.entry.id) + return props.selection.isSelected(props.element.id) ? [...props.selection.selectedIds.value] - : [props.entry.id]; + : [props.element.id]; } function actions() { const ids = actionIds(); - const selectedEntries = props.entries.filter((entry) => - ids.includes(entry.id) + const selectedElements = props.elements.filter((element) => + ids.includes(element.id) ); - const index = props.entries.findIndex( - (candidate) => candidate.id === props.entry.id + const index = props.elements.findIndex( + (candidate) => candidate.id === props.element.id ); - return nestedEntryActions({ - entry: props.entry, - selectedEntries, + return nestedElementActions({ + element: props.element, + selectedElements, ids, index, - count: props.entries.length, + count: props.elements.length, editable: props.editable, busy: props.busy, canPaste: props.operations.canPaste.value, @@ -56,21 +56,23 @@ function editLabel(): string { return t('Edit {title}', { - title: props.entry.cardAttributes?.data?.label ?? `#${props.entry.id}`, + title: + props.element.cardAttributes?.data?.label ?? `#${props.element.id}`, }); } function actionLabel(): string { return t('Actions for {title}', { - title: props.entry.cardAttributes?.data?.label ?? `#${props.entry.id}`, + title: + props.element.cardAttributes?.data?.label ?? `#${props.element.id}`, }); }