Skip to content

feat(menu-plugin): data-driven admin sidebar navigation#940

Open
lane711 wants to merge 155 commits into
v3from
lane711/menu-plugin-plan
Open

feat(menu-plugin): data-driven admin sidebar navigation#940
lane711 wants to merge 155 commits into
v3from
lane711/menu-plugin-plan

Conversation

@lane711

@lane711 lane711 commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Adds core-menu plugin — registers menu_item document type (no new tables), seeds 4 system nav items (Content, Collections, Users, Settings) idempotently on boot, and reconciles plugin-declared menu entries from both definePlugin({ menu: [...] }) and manifest.json adminMenu fields
  • Replaces hardcoded sidebar with data-driven items via post-response HTML block replacement (<!-- ADMIN_SIDEBAR_NAV_ITEMS --> marker) — hardcoded items remain as fallback when DB unavailable
  • Adds /admin/menu admin UI for CRUD, visibility toggle, and drag-free move-up/down reordering; locked fields prevent admins from breaking system/plugin items

Architecture

  • All writes use env.DB.prepare().bind() / db.batch() — no Drizzle (R1)
  • Tenant-scoped with tenant_id = 'default' throughout (R3)
  • 4 q_* generated columns: q_menu_parent, q_menu_visible, q_menu_source, q_menu_plugin_id
  • menu-icons.ts consolidates 30 heroicons used across admin sidebar
  • Plugin items deactivated (not deleted) when plugin no longer registered

Test plan

  • npx vitest run src/__tests__/services/menu-repository.sqlite.test.ts → 7/7 pass (real-SQLite)
  • npx tsc --noEmit → 0 errors
  • Lint passes (no new errors, pre-existing better-auth module warnings only)
  • E2E spec 82-menu-management.spec.ts — run with npm run setup:db && npm run dev then npx playwright test tests/e2e/82-menu-management.spec.ts

🤖 Generated with Claude Code

lane711 and others added 30 commits June 17, 2026 21:42
feat: merge v3 document model architecture into main
- hono: ^4.12.18 → ^4.12.26 (security fixes in 4.12.25: CORS, body-limit,
  serve-static path traversal, AWS Lambda Set-Cookie, Lambda@Edge header)
- vitest: ^4.0.5 → ^4.1.9 (root, packages/core)
- @vitest/coverage-v8: ^4.0.5 → ^4.1.9 (root, packages/core)
- my-sonicjs-app vitest: ^2.1.8 → ^4.1.9 (align with root)
- postcss: 8.5.6 → 8.5.15 (transitive, lockfile)
- qs: 6.15.0 → 6.15.2 (transitive, lockfile)
- shell-quote: 1.8.3 → 1.8.4 (transitive, lockfile)

Closes PRs: #918, #862, #851, #849, #843, #840, #839, #837

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
page_blocks was registered in the sample app but has no place in a base
install. Replaced with e2e_test collection that covers all field types
(string, slug, textarea, number, boolean, date, datetime, user, media,
select, radio, lexical, object/flat, array/blocks) so E2E specs have a
stable test fixture without polluting the starter experience.

Updated 10 E2E specs to reference e2e_test / E2E Test instead of
page_blocks / Page Blocks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…with-e2e-test-collection

refactor: replace page_blocks with e2e_test collection
…onsolidate

chore(deps): consolidate all dependabot dependency updates
…o-resolve-error

fix(create-app): bump compatibility_date to fix crypto import error on npm run dev
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…uncement

docs(marketing): v3 beta Discord announcement and blog post
…mp, simplify post-install output (#925)

- Remove prompt for example blog collection; always include it
- Fix admin seed crash: Date.now() → new Date() for Drizzle timestamp_ms columns
- Move Cloudflare resource creation to optional "Deploy to Cloudflare" section;
  only show migration/seed steps when they actually failed
…r local dev

- Rewrite generated seed-admin.ts to use raw SQL (bypass Drizzle schema
  mismatch with auth_user columns missing from migration)
- Create auth_account record so Better Auth credential login works
- Use PBKDF2 via Web Crypto for password hashing (matches auth system)
- Auto-generate .dev.vars with BETTER_AUTH_SECRET on project creation
  (missing secret caused 500 on every auth request)
- Add --admin-email / --admin-password flags for non-interactive use
- Replace wrangler resource commands in success output with npm run deploy
Per-developer Claude Code settings should not be shared. settings.local.json
contained machine-specific rules and shared auto-approve allowlists.
Untrack it so each dev keeps their own local copy.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Move v3.0.0-beta.3 entry from under "Version 2.x" into "Version 3.x"
- Add "Version 3.x" section to changelog sidebar nav
- Fix JWT default lifetime: 24h → 30d (matches DEFAULT_JWT_EXPIRES_IN_SECONDS)
- Fix stale comment labeling v3.0.0-beta.3 entry as v2.19.0 on homepage
… by default (#927)

- Switch blog_post content field from quill to lexical
- Update blog_post schema: name, slug, author type (user), remove stale fields
- Set core-cache defaultActive to false (only lexical should be on by default)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Resource creation runs before npm install, so wrangler isn't in
node_modules/.bin/ yet. Users running via npx also won't have a
global wrangler binary. Fall back to `npx wrangler` in both cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add deploy-stats.yml CI workflow (triggers on packages/stats/** or
  packages/core/src/** changes to main + workflow_dispatch)
- Build core, type-check, then wrangler deploy --env production
- Add deploy:production script to packages/stats
- Add deploy:stats convenience script to root package.json
- Update wrangler compatibility_date to 2025-05-05

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- rehype.mjs: guard parentNode.properties before set — mdxJsxFlowElement
  nodes (inline code in JSX <li>/<span>) have no properties object,
  causing "Cannot set properties of undefined (setting 'language')"
- blog/[slug]/page.tsx: pass Callout to compileMDX components map —
  blog posts using <Callout> crashed with "Expected component Callout
  to be defined"
…934)

* fix(www): resolve build errors blocking deploy

- rehype.mjs: guard parentNode.properties before set — mdxJsxFlowElement
  nodes (inline code in JSX <li>/<span>) have no properties object,
  causing "Cannot set properties of undefined (setting 'language')"
- blog/[slug]/page.tsx: pass Callout to compileMDX components map —
  blog posts using <Callout> crashed with "Expected component Callout
  to be defined"

* chore(www): upgrade next and opennextjs-cloudflare for Next 16 compat

next: 16.2.1 → 16.2.9
@opennextjs/cloudflare: 1.18.0 → 1.19.11

1.18.0 couldn't bundle next-server.js with Next 16 (missing shims for
node-environment, node-polyfill-crypto, etc). 1.19.11 adds Next 16 support
and requires next >= 16.2.3.
lane711 and others added 28 commits June 26, 2026 15:41
- plugins/page.mdx: add Example Plugin under Core Plugins section
- plugins/development/page.mdx: callout + reference section for example plugin
- blog: building-your-first-sonicjs-plugin tutorial covering all extension points

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New /plugins/example page: full reference covering file structure,
  two-phase boot, /example/* routing rationale, factory pattern,
  configSchema settings, moods collection, hooks, data seeding,
  customization, and removal
- plugins/core/page.mdx: Example Plugin section in feature grid,
  sections array, and body with key patterns table + removal snippet

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- plugins/page.mdx: add buttons → /plugins/example + blog post
- plugins/development/page.mdx: add buttons → /plugins/example + blog post
- blog post: replace plain /plugins/development link with /plugins/example first

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…/blog/...

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root cause: plugin wirePlugins() has no bootstrapComplete guard, so a
static-asset request (favicon, etc.) can fire onBoot before
bootstrapMiddleware has run autoRegisterCollectionDocumentTypes. D1
local enforces the FK on documents.type_id — the seed INSERT fails
with SQLITE_CONSTRAINT_FOREIGNKEY, gets swallowed by the catch, and
the once-guard means it never retries.

Fix: INSERT OR IGNORE the example document_type row inside the seed
try-block before the COUNT check. Idempotent when the row already
exists; races bootstrap safely.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Settings tab now defaults as active when a plugin has user-configurable
settings. Info and Activity follow.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removing a field from configSchema had no effect on stored settings —
the field persisted in the DB blob and kept appearing in the admin UI.

Two fixes:
1. POST /admin/plugins/:id/configure: replace the naive spread-merge
   with schema-aware merge that only carries forward non-schema keys
   (internal `_`-prefixed keys like `_routes`) from existing settings.
   Schema-removed fields are pruned on next save.
2. GET /admin/plugins/:id (legacy settings page): filter `enrichedSettings`
   to schema-defined + `_`-prefixed keys before passing to the template,
   so stale stored keys never render in the UI.

Fixes: #972

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
onBoot populated pluginOptions once at isolate warm-up; admin UI saves
updated settings to DB but the in-memory cache was never refreshed, so
GET /example always returned the stale boot-time values.

Fix: read greeting/defaultName from DB per-request via PluginService.getPlugin
in a getPluginSettings() helper, parallel with getRandomMood. The stale
options param is removed from createExampleApiRoutes.

Also fixes greeting being declared in configSchema but never used in the
response (was hardcoded). Now uses ${settings.greeting}.

Applies to both my-sonicjs-app and packages/create-app starter template.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Reflects the settings-not-saved bug fix shipped in #975.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…rom GitHub

Scaffolded plugin files are ejected copies — npm install updates
@sonicjs-cms/core but leaves plugin source untouched. This script
fetches the latest files from the canonical starter template on
GitHub main and overwrites the local copies.

Usage:
  npm run update:plugin                 # update example plugin
  npm run update:plugin -- example      # explicit
  npm run update:plugin -- example --dry-run

Ships in both my-sonicjs-app and packages/create-app starter template
so new projects get the script at scaffold time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…n pages

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
)

POST /:id/settings was overwriting the full settings blob without merging,
wiping _routes and _adminPath written by onBoot on every settings save.
Now merges with existing settings before writing, matching the /configure handler.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…er (#976)

- Search bar filters all columns including JSON data fields via LIKE
- Search term persists across pagination, sort, and page-size changes
- JSON/long text cells render as clickable links opening a modal viewer
- Modal shows syntax-highlighted pretty-printed JSON with copy button
- Closes on backdrop click, close button, or Escape key
- E2E spec 91 covers search input, URL params, clear, modal open/close

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
- MediaDocumentService.list() now runs a parallel COUNT(*) with the
  same WHERE filters and returns `total` in MediaListResult
- Admin media route uses `total` for totalFiles and fixes hasNextPage
  (was capped at page size 24 instead of real library total)
- Media selector search input: add missing `name="search"` attr so the
  typed term is actually sent with the HTMX request
- Add hx-swap="outerHTML" + hx-select="#media-selector-grid" to prevent
  the full panel (search box + grid) from nesting inside the grid on
  each keystroke

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
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.

1 participant