feat(menu-plugin): DB-driven admin sidebar navigation manager#989
Open
lane711 wants to merge 31 commits into
Open
feat(menu-plugin): DB-driven admin sidebar navigation manager#989lane711 wants to merge 31 commits into
lane711 wants to merge 31 commits into
Conversation
Replaces hardcoded sidebar nav with document-model-backed menu items. Plugins and admins can contribute, reorder, and toggle nav entries without code changes; four system items (Content, Collections, Users, Settings) are seeded idempotently on first boot. Key pieces: - `core-menu` plugin: registers `menu_item` document type with q_* generated columns, seeds system items via `upsertSystemItem`, reconciles plugin-declared menu entries via `reconcileMenuFromPlugins` - `menu-repository.ts`: CRUD layer (raw D1 SQL, tenant-scoped, R1/R3 compliant) - `menu.ts` middleware: post-response HTML block replacement swaps `<!-- ADMIN_SIDEBAR_NAV_ITEMS -->` marker with data-driven items; falls back to hardcoded items if DB unavailable - Admin routes at `/admin/menu`: list, create/edit/delete user items, visibility toggle, move-up/move-down reorder, locked-field enforcement - `menu-icons.ts`: consolidated 30-icon map + `resolveIcon` helper - 7 real-SQLite unit tests (menu-repository.sqlite.test.ts) - E2E spec 82-menu-management.spec.ts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
menuPlugin is no longer auto-registered. Developers enable it by adding it to config.plugins.register in their app entry point. menuMiddleware is now mounted inside the plugin's register() so it only runs when the plugin is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Ran generate-plugin-registry.mjs to pick up the new menu-plugin manifest.json. core-menu now appears in the /admin/plugins list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…in page - Rename plugin id core-menu → menu (no 'core' prefix) - Re-add to corePluginsBeforeCatchAll so /admin/menu routes always mount - Regenerate manifest-registry.ts to pick up renamed id - /admin/plugins/menu redirects to /admin/menu (the actual editor) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
adminMenu: null in manifest stops pluginMenuMiddleware from injecting a second 'Menu' link via <!-- DYNAMIC_PLUGIN_MENU -->. Added 'Menu' as a 5th system seed item (sortOrder 50) so it still appears in the data-driven sidebar managed by menuMiddleware. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ed plugin DB records - Add 'Menu' item to baseMenuItems in admin-layout-catalyst so the link always shows even before menu_item docs are seeded (hardcoded fallback) - Filter installed plugins whose IDs are absent from PLUGIN_REGISTRY (e.g. stale 'core-menu' DB records after rename to 'menu') so they no longer cause duplicates on the plugins list page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
menuMiddleware checks the plugins table for the 'menu' plugin status before replacing the sidebar. If status is 'inactive' (disabled via admin UI), the hardcoded sidebar fallback is used instead. Treats missing DB record as active so initial seeding still works. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… plugins table PluginService stores plugins in documents (type_id='plugin', slug=id), not the legacy Drizzle-schema plugins table. The previous query always returned null → fell back to true → sidebar never reverted when disabled. Now queries json_extract(data, '$.status') from documents correctly. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…tive Menu item was in baseMenuItems unconditionally. Now only appears via the data-driven sidebar (menuMiddleware) when plugin is active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Plugins can now self-register a custom settings tab via definePlugin's settingsTabContent field: loadData(db) fetches server-side data, render() produces the HTML embedded in the Settings tab of /admin/plugins/:id. Menu plugin uses this to embed the menu editor inline, replacing the redirect to /admin/menu. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
reconcileMenuFromPlugins now queries inactive plugin IDs from the documents table and excludes them from the active set, so deactivateStalePluginRows hides their menu items. reconcile also runs immediately on activate/deactivate so the change takes effect without waiting for next boot. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- form.submit() doesn't fire submit event so CSRF auto-injection in catalyst layout never ran; switch to requestSubmit() which fires it - renderMenuSettingsContent runs in v2 layout (no CSRF script) so add inline CSRF injection script scoped to that embedded content - fix visible checkbox value from '1' to 'true' to match route check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…r list Add fetchPluginStatuses() to query plugin status from documents table. Plugin-sourced menu items now show an 'enabled' or 'disabled' badge alongside the source badge in both the standalone /admin/menu page and the embedded settings tab. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace IN-clause query (which required exact pluginId→slug match) with a query that fetches all inactive plugin slugs, then checks membership. Avoids binding issues and ID mismatch edge cases. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was filtering WHERE status='inactive' which missed plugins whose status field is NULL. Now fetches all plugin docs and reads status directly, matching PluginService.getAllPlugins() logic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…erage - fetchPluginStatuses: default missing plugins to inactive (not active) - admin-menu-list template: remove /edit suffix from edit link (404 fix) - app.ts: register menuMiddleware before admin routes (Hono ordering fix) - menu-plugin/index.ts: remove duplicate menuMiddleware registration - menu.ts: replace all marker pairs via regex (fixes mobile sidebar) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds /admin/plugins link (sortOrder 50) before Menu (now 60) so the Plugins page is always present in the DB-driven sidebar by default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTML forms can't send DELETE method; the template POSTs to /:id/delete but only DELETE /:id existed. Adds the matching POST route. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Matches the icon used in the hardcoded sidebar fallback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds onclick to <tr> that navigates to /admin/menu/:id, skipping clicks on interactive elements (buttons, links, inputs, forms). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Fresh install has no plugin document in DB. Previously treated as active (showing DB-driven sidebar on first boot). Now defaults to inactive so the hardcoded sidebar shows until user explicitly activates the plugin. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
DB-driven sidebar now filters plugin-sourced items by active status, matching the hardcoded sidebar's behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Revert middleware plugin-status filter; visible flag drives sidebar - Replace fetchInactivePluginIds with fetchActivePluginIds; only explicitly active plugins get visible=true on first insert - deactivateStalePluginRows now uses activeIds (not byPluginId keys) so inactive plugins are hidden even if still in the registry - Also sync visible column alongside data->$.visible on deactivate Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HTMX hx-put was silently failing (redirect + hx-target="body" race). Switch edit form to plain POST /admin/menu/:id/update. Extract shared handler so PUT /:id (API) and POST /:id/update (HTML form) share logic. Also fix visible checkbox: unchecked = absent from FormData = false. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
parent is an organizational choice, not content — admins should always be able to move items under a different parent. Remove parent from lockedFields in defaults, reconcile, and the updateItem guard. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Form always submits all fields; locked ones (url for system/plugin items) were hitting the updateItem guard. Now skip locked fields when building changes — parent stays always-editable as before. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Conflicts: - wrangler.toml: kept branch DB config - admin-plugin-settings.template.ts: merged hasUserSettings/defaultTab from main with settingsTabContent support from branch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.
Summary
settingsTabContentextension point to the plugin SDK so plugins can render custom settings tabs without hardcoded per-plugin renderersChanges
Core
packages/core/src/plugins/core-plugins/menu-plugin/— new plugin with full CRUD admin UI, drag-to-reorder, visibility toggles, parent/child nestingpackages/core/src/middleware/menu.ts— post-render HTML replacement of sidebar markers; regex-replaces both desktop + mobile instances; off by default until plugin activatedpackages/core/src/app.ts— registersmenuMiddlewarebefore all admin routes (Hono ordering fix)packages/core/src/plugins/sdk.ts— addssettingsTabContent: { loadData, render }extension pointpackages/core/src/templates/pages/admin-plugin-settings.template.ts— renders plugin-registered settings tab contentMenu Plugin Behavior
visible=truevisible=falsewhen plugin is inactive;visible=truewhen activevisibleflag exclusively controls sidebar visibility; toggling a plugin's active state auto-updates visibility on next bootparentfromlockedFields);urlstays locked for system/plugin itemsPOST /:id/delete(HTML form compatible)Testing
🤖 Generated with Claude Code