Skip to content

[6.x] Restore the New entry button in entry selection modals - #20652

Open
brandonkelly wants to merge 8 commits into
6.xfrom
bugfix/20598-new-entry-in-entry-selector
Open

brandonkelly wants to merge 8 commits into
6.xfrom
bugfix/20598-new-entry-in-entry-selector

Conversation

@brandonkelly

@brandonkelly brandonkelly commented Nov 13, 2026 •

Copy link
Copy Markdown
Member

Description

Craft 6's Vue element selector modal never got Craft 5's “New {section} entry” button, so entries couldn't be created from a relation field's modal.

 <craft-element-selector-modal>
   <ElementPicker>            # now also publishes its sources and site
   <div slot="secondary-actions">
     <AssetUploadButton>      # assets
+    <NewEntryButton>         # entries: one button, or a menu of entry types

It offers the sections the modal's index shows that the user can publish in on the index's site. These are the same rules as the entry index's New entry button, now shared through useCreatableEntryTypes().

The selector is a top-layer <dialog>, and slideouts open beneath it, so the flow differs from Craft 5's:

click New entry
  POST entries/create            # modal stays open, busy
  controller.close()
  openSlideout(new draft)        # focus returns to the field's Add button
    onSaved (not an autosave)
      options.onSelect([entry])  # the relation field relates it

This is how Craft 5's search-input “Create entry” option behaved. In a section whose entries need approval, the slideout's primary action saves the draft for review, so the field relates the entry while it's still an unpublished draft. The button stays away when the field has a selection condition: the modal is closed while the entry is edited, so nothing could check the saved entry against the condition before relating it. The rest of #20598, the relation field's missing search input, isn't covered here.

Evidence

  • Before: in the Field Showcase workbench entry with the Entries field limited to one section, the modal footer had only Cancel and Add an entry. The new test fails: expected undefined to be 'New Authors entry'.
    After: the footer shows “+ New Pages entry” (or a “New entry” menu grouped by section with sources: '*'). Creating an entry from it opens a slideout, and saving it relates the entry to the field, with focus back on Add an entry. Cancelling the slideout relates nothing. Keyboard: Tab reaches the button, Enter opens the slideout.
ElementSelectorModal › the new entry button
  emit sources/source/site → button reads “New Authors entry”
  click → posts {siteId: 2, section: authors, type: author}, modal closes, slideout opens for draftId 5
  onSaved({draft: true}) → onSelect not called
  onSaved({element: {id: 34, title: 'Jane Austen'}}) → onSelect([{id: 34, siteId: 2, label: 'Jane Austen'}])

vp test run (element-selector-modal, NewEntryButton, content index): 157 passed. vue-tsc --noEmit passes.

Screenshots aren't attached. I couldn't upload them to GitHub from here.

Related issues

brandonkelly and others added 2 commits November 13, 2026 01:29
The Vue element selector modal never got Craft 5's "New {section} entry"
button. It now offers one for the sections its index shows that the user
can publish in, sharing NewEntryButton's rules via useCreatableEntryTypes.

The modal is a top-layer dialog that a slideout can't open over, so the
button closes it, opens the new entry in a slideout, and hands the entry
to the opener's onSelect once it's saved.

Fixes #20598

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

📚 Storybook previews

@craftcms/ui — open Storybook

No changed components detected in this Storybook.

resources/js — open Storybook

Changed components:

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved new-entry flow bugs break relation replacement, leave legacy pages without an editor, and duplicate relation chips on repeated saves.

5 open issues
What changed in this PR

Restores entry creation from relation-field selection modals, addressing the missing button reported in #20598. The separate search-input feature remains out of scope.

Changes:

  • Adds a section-aware “New entry” button or entry-type menu that opens an editor slideout and selects the saved entry.
  • Shares creation eligibility rules with the content index and passes selector sources, site, and focus target through the modal.
  • Adds regression coverage and documents the restored workflow.
File Description
resources/​js/​pages/​content/​Index.vue Imports the shared publishable-section type.
resources/​js/​modules/​elements/​composables/​useCreatableEntryTypes.ts Extracts creation eligibility and grouped menu logic.
resources/​js/​modules/​elements/​components/​NewEntryButton.vue Uses the shared composable for index creation links.
resources/​js/​modules/​element-selector-modal/​README.md Documents the selector’s creation workflow.
resources/​js/​modules/​element-selector-modal/​NewEntryButton.vue Adds entry creation, slideout editing, and selection callbacks.
resources/​js/​modules/​element-selector-modal/​ElementSelectorModal.vue Adds the footer button and forwards selector context.
resources/​js/​modules/​element-selector-modal/​ElementSelectorModal.test.ts Covers creation and selection after a non-autosave save.
resources/​js/​modules/​element-selector-modal/​ElementPicker.vue Emits available sources and the current site.
resources/​js/​modules/​element-selector-modal/​create-element-selector-modal.ts Passes the trigger element for focus restoration.
CHANGELOG.md Records the restored creation button.
CHANGELOG-WIP.md Notes direct selection after saving and entry-type choices.

Review effort: Balanced

- Report a failed entries/create request and keep the selector open.
- Relate the new entry once, not again on each later save.
- Keep a pending Replace across the modal-to-slideout handoff: the
  jQuery inputs now clear it when Add opens the modal, not when the modal
  closes.
- Remount the entry type menu when its section label changes, since
  ActionMenu renders its invoker once.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical slideout opener bugs can break the Yii2 new-entry flow or close the parent editor instead of nesting the new editor.

3 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Deleted drafts can be selected as saved entries

resources/​js/​modules/​element-selector-modal/​NewEntryButton.vue:111

Destructive actions also notify onSaved without setting draft (useElementActionMenu.ts:194,321). Deleting the newly created draft therefore reaches onSelect with its deleted ID, potentially adding an invalid relation or replacing an existing one. Distinguish deletion notifications from successful saves in the slideout result contract, and select only after a save.

5 resolved since last review

Review effort: Balanced

- Unwrap the Yii2 adapter's jQuery trigger before using it as the
  slideout opener, so the store can find its panel.
- A full relation field has no Add button, so it offers its own root as
  the trigger, and a slideout opened from its modal nests in the panel
  the field is in rather than replacing it.
- Relate the new entry only on a save that returns an undeleted element,
  not when its draft is deleted from the panel's action menu.
- Search results clear an abandoned Replace in the jQuery inputs.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

Copy link
Copy Markdown
Member Author

Re the previously missed issue (deleted drafts selected as saved entries): fixed in 519cd66. NewEntryButton.vue now relates the entry only when the save payload includes an element without dateDeleted, so neither the deletion manager's bare saved() nor a destructive action's response selects it. The modal test now asserts both are ignored.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

A missing opener can cause the new-entry flow to replace the parent editor instead of nesting, while save callbacks can select the original draft or an entry excluded by the field’s selection condition.

No open issues 🎉

3 previously missed issues

In code that hasn't changed since last review

High severity Fall back to the field container when resolving the opener

resources/​js/​modules/​element-selector-modal/​NewEntryButton.vue:64

For a full class-backed relation field, both inputs’ getNextLogicalFocusElement() still look for .action-btn, which current chip menu invokers do not have, so this opener can be null. Replace → New entry inside a slideout can then replace the parent editor instead of nesting, because the still-focused selector is mounted outside that panel. Add a connected field/container fallback to the trigger callbacks in both resources/js/modules/element-select-input/base-element-select-input.ts:1139 and yii2-adapter/resources/js/element-select-input/base-element-select-input.ts:1144, handling the adapter’s empty jQuery collection explicitly.

Medium severity Use the saved entry ID and title for slideout selection

resources/​js/​modules/​element-selector-modal/​NewEntryButton.vue:126

Choosing “Save as a new entry” in the slideout returns a different record in data.element, but this callback relates the original unfinished draft while displaying the saved entry’s title. Use the saved element’s ID for the selection and fallback label; the one-shot chosen guard otherwise prevents later saves from correcting the relation.

Medium severity Validate saved entries against the relation selection condition

resources/​js/​modules/​element-selector-modal/​NewEntryButton.vue:132

A relation field restricted to one entry type can still offer other types from the same section, and this callback unconditionally relates them after saving. The index applies selectionCondition separately (ContentIndexViewModel.php:1341), but the creation menu only filters by sources and site. Validate the saved entry against the field’s selection condition before calling onSelect, and show a clear explanation if it cannot be selected.

3 resolved since last review

Review effort: Balanced

- Relate the entry the slideout saved, which "Save as a new entry" makes
  a different one than the draft.
- Hide the button when the field has a selection condition: the modal is
  closed while the entry is edited, so nothing could check it against the
  condition before relating it.
- Give a full field's selector its container as the opener, since its
  chips' action menus have no `.action-btn` to fall back on, so a
  slideout opened from Replace still nests in the field's panel.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

Copy link
Copy Markdown
Member Author

Addressed the three "previously missed" items from the latest review in d8671ce:

  • Opener fallback for full fields: confirmed. Current chips render a craft-action-menu invoker with no .action-btn, so getNextLogicalFocusElement() returned null (core) or an empty jQuery set (adapter) once the field was full. Both inputs' triggerElement callbacks now fall back to the field container, with tabindex="-1" as the existing fallback sets. getLastActionBtn() itself is unchanged, since other focus paths use it.
  • "Save as a new entry": the slideout callback now relates data.element.id, and takes its siteId and title from the saved element, instead of the draft's.
  • Selection condition: the New entry button now stays hidden when the field has a selection condition. The modal is closed while the entry is edited, so nothing could check the saved entry against the condition before relating it. Craft 5 kept its modal open and re-ran the index search, which quietly left out entries the condition excluded. Validating instead would need a new endpoint; that can follow if the button is wanted on conditioned fields.

Each has a test that fails without its change.

Resolves the overlap with #20653's focus return: the modal's `returnFocus` now also opens the New entry slideout, and a full field's trigger falls back from the chosen element to the field itself.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Missing opener context can close the parent editor instead of nesting the new-entry slideout, while removed or non-focusable return targets can prevent focus restoration after saving or canceling.

3 open issues

Review effort: Balanced

async function create({section, entryType}: CreatableType): Promise<void> {
const {controller, siteId} = props;
const onSelect = controller.options.onSelect;
const opener = props.opener?.() ?? null;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 0ca0dee. The Link field now passes its Choose button as triggerElement, and the slideout store's originPanel() looks past shadow roots (through closestComposed(), now exported from focus-return), so the new entry's slideout nests in the panel the Link field is in. I didn't use the craft-link-field host itself because it can't take focus, and isFocusable() would still accept it as a return target. In the compact (popover) Link control the button is hidden while the selector is open, so focus falls back to the popover's invoker the way it already does for menus.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Followed up in b6ea601 for the popover case. The Link control's own popover is teleported into the panel, so 0ca0dee already covered it. The Markdown field's link popover, though, is appended to <body> and anchored to its toolbar button, so an opener inside it still had no panel ancestry. originPanel() now follows a popover or menu to its invoker (through menuInvokerOf(), now exported from focus-return) until it finds a [data-slideout-id], and also looks past shadow roots. A test covers an opener in a body-mounted popover anchored inside a panel.

- The Link field's element selector now passes its Choose button as the
  trigger, and slideouts find the panel an opener is in across shadow
  roots, so a new entry created from a Link field inside a slideout nests
  there instead of replacing the parent editor.
- The New entry slideout asks the field where focus goes again once it
  closes: relating the entry can fill the field, taking its Add button
  away.
- A full relation field's trigger falls back to the chip being replaced,
  or its last chip, before the field itself, which can't take focus.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Human review is needed to assess selection-failure recovery and popover ownership edge cases that can leave saved entries unrelated or disrupt the parent editor.

1 open issue
2 resolved since last review

Review effort: Balanced

The Markdown field's link popover is mounted on <body> and anchored to its
toolbar button, so a slideout opened from inside it, such as a new entry
from the Link field's selector, found no panel above it and replaced the
parent editor. The store now follows a popover or menu to its invoker
when working out which panel an opener is in.

Amp-Thread-ID: https://ampcode.com/threads/T-e712b71f-c4c3-44f5-a7d6-ebcba118cb03
Co-authored-by: Amp <amp@ampcode.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants