Skip to content

[Fix/208 menus parenting] fix(admin): support parent selection and parentId for menu items (fix #208)#232

Open
WebDevByCam wants to merge 3 commits intoemdash-cms:mainfrom
WebDevByCam:fix/208-menus-parenting
Open

[Fix/208 menus parenting] fix(admin): support parent selection and parentId for menu items (fix #208)#232
WebDevByCam wants to merge 3 commits intoemdash-cms:mainfrom
WebDevByCam:fix/208-menus-parenting

Conversation

@WebDevByCam
Copy link
Copy Markdown

@WebDevByCam WebDevByCam commented Apr 4, 2026

What does this PR do?

Adds parent selection UI to the Menu Editor and ensures parentId is propagated on create/update/reorder so menu nesting and order persist correctly.

Closes #208

Type of change

  • Bug fix
  • Feature (requires approved Discussion)
  • Refactor (no behavior change)
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes
  • pnpm --silent lint:json | jq '.diagnostics | length' returns 0
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: https://github.com/emdash-cms/emdash/discussions/...

Copilot AI review requested due to automatic review settings April 4, 2026 08:20
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 4, 2026

🦋 Changeset detected

Latest commit: 49d41d4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 10 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/plugin-ai-moderation Patch
@emdash-cms/plugin-atproto Patch
@emdash-cms/plugin-audit-log Patch
@emdash-cms/plugin-color Patch
@emdash-cms/plugin-embeds Patch
@emdash-cms/plugin-forms Patch
@emdash-cms/plugin-webhook-notifier Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds parent selection support to the Admin Menu Editor so menu items can be nested, and ensures parentId is sent through create/update flows to persist nesting correctly (addressing issue #208).

Changes:

  • Introduces a “Parent” selector in the Menu Editor UI for creating menu items (custom links and content items).
  • Propagates parentId in createMenuItem and updateMenuItem mutation payloads.
  • Adds a changeset for an @emdash-cms/admin patch release describing the behavior change.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 9 comments.

File Description
packages/admin/src/components/MenuEditor.tsx Adds parent selection UI and passes parentId through menu item create/update flows.
.changeset/fix-menus-parenting.md Documents the admin patch release for menu parenting support.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +234 to +238
<label className="text-sm text-kumo-subtle mr-2">Parent</label>
<select
value={newParentId}
onChange={(e) => setNewParentId(e.target.value)}
className="h-10 rounded-md border border-kumo-line bg-kumo-base px-3 py-2 text-sm"
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

The "Parent" label isn’t programmatically associated with the , so screen readers won’t announce it. Add an id to the select and htmlFor on the label (or add an aria-label) so the control is accessible.

Copilot uses AI. Check for mistakes.
Comment on lines +235 to +237
<select
value={newParentId}
onChange={(e) => setNewParentId(e.target.value)}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

newParentId can become stale if the selected parent item is deleted or the menu items refresh; the state may still hold an ID that no longer exists in localItems, and subsequent creates (e.g. Add Content) could send an invalid parentId. Consider resetting newParentId to "" when it’s not found in the latest items.

Copilot uses AI. Check for mistakes.
<option value="">Top level</option>
{localItems.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Rendering option text as it.label[0] + '\u200B' + it.label.slice(1) will produce incorrect output (or "undefined" prefix) when it.label is an empty string. Prefer rendering the full label directly, or guard against empty labels before indexing.

Suggested change
{it.label[0] + '\u200B' + it.label.slice(1)}
{it.label.length > 0
? it.label[0] + "\u200B" + it.label.slice(1)
: it.label}

Copilot uses AI. Check for mistakes.
Comment on lines +309 to +316
<select name="parentId" defaultValue={newParentId} className="h-10 rounded-md border border-kumo-line bg-kumo-base px-3 py-2 text-sm">
<option value="">Top level</option>
{localItems.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
</option>
))}
</select>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

This parent in the add dialog has no accessible name (no , no aria-label), which makes it hard to use with assistive tech and hard to target in tests. Add a visible label tied via htmlFor/id or at least an aria-label. <div className="space-y-2"> <label htmlFor="add-custom-link-parent" className="text-sm font-medium"> Parent </label> <select id="add-custom-link-parent" name="parentId" defaultValue={newParentId} className="h-10 rounded-md border border-kumo-line bg-kumo-base px-3 py-2 text-sm" > <option value="">Top level</option> {localItems.map((it) => ( <option key={it.id} value={it.id}> {it.label[0] + '\u200B' + it.label.slice(1)} </option> ))} </select> </div>

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +314
{localItems.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
</option>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Rendering option text as it.label[0] + '\u200B' + it.label.slice(1) will produce incorrect output (or "undefined" prefix) when it.label is an empty string. Prefer rendering the full label directly, or guard against empty labels before indexing.

Copilot uses AI. Check for mistakes.
Comment on lines +461 to +470
<select name="parentId" defaultValue={editingItem.parent_id ?? ""} className="h-10 rounded-md border border-kumo-line bg-kumo-base px-3 py-2 text-sm">
<option value="">Top level</option>
{localItems
.filter((it) => it.id !== editingItem.id)
.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
</option>
))}
</select>
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

This parent in the edit dialog has no accessible name (no , no aria-label), which makes it difficult for screen readers and also prevents getByLabelText-style tests. Add a visible label tied via htmlFor/id or at least an aria-label. <div className="space-y-2"> <label htmlFor="edit-menu-item-parent" className="text-sm font-medium"> Parent </label> <select id="edit-menu-item-parent" name="parentId" defaultValue={editingItem.parent_id ?? ""} className="h-10 w-full rounded-md border border-kumo-line bg-kumo-base px-3 py-2 text-sm" > <option value="">Top level</option> {localItems .filter((it) => it.id !== editingItem.id) .map((it) => ( <option key={it.id} value={it.id}> {it.label[0] + '\u200B' + it.label.slice(1)} </option> ))} </select> </div>

Copilot uses AI. Check for mistakes.
Comment on lines +463 to +469
{localItems
.filter((it) => it.id !== editingItem.id)
.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
</option>
))}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Parent selection only filters out the item itself. This still allows creating parent/child cycles (e.g. setting an item’s parent to one of its own descendants), and there’s no server-side validation in handleMenuItemUpdate to prevent that. Consider filtering out descendants here (and/or validating on the API) to avoid corrupt menu trees.

Copilot uses AI. Check for mistakes.
.filter((it) => it.id !== editingItem.id)
.map((it) => (
<option key={it.id} value={it.id}>
{it.label[0] + '\u200B' + it.label.slice(1)}
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

Rendering option text as it.label[0] + '\u200B' + it.label.slice(1) will produce incorrect output (or "undefined" prefix) when it.label is an empty string. Prefer rendering the full label directly, or guard against empty labels before indexing.

Suggested change
{it.label[0] + '\u200B' + it.label.slice(1)}
{it.label ? it.label[0] + '\u200B' + it.label.slice(1) : ""}

Copilot uses AI. Check for mistakes.
Comment on lines +135 to 142
const parentVal = formData.get("parentId");
createMutation.mutate({
type: "custom",
label: typeof labelVal === "string" ? labelVal : "",
customUrl: typeof urlVal === "string" ? urlVal : "",
target: (typeof targetVal === "string" ? targetVal : "") || undefined,
parentId: typeof parentVal === "string" && parentVal !== "" ? parentVal : undefined,
});
Copy link

Copilot AI Apr 4, 2026

Choose a reason for hiding this comment

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

MenuEditor has existing component tests, but the new parent selection + parentId propagation path isn’t covered. Add tests that select a parent and assert createMenuItem / updateMenuItem are called with the expected parentId (including clearing it when "Top level" is selected).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unable to create child menus in menu interface

2 participants