Skip to content

feat(plugins): Add sidebar actions in plugin.config.ts and openSettings - #253

Merged
Rl0007 merged 5 commits into
developfrom
feat/plugin-sidebar-actions
Oct 14, 2026
Merged

Rl0007 merged 5 commits into
developfrom
feat/plugin-sidebar-actions

Conversation

@NagariaHussain

@NagariaHussain NagariaHussain commented Oct 10, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A plugin can now add sidebar rows that run a function on click, for example to open the plugin's tab in Settings. The rows are declared in one file, commera/plugin.config.ts. usePlugin() gains openSettings(tab).

This PR builds on #244, which builds on #242. GitHub moves the base as each one merges.

Why

A plugin's sidebar group could only hold pages. A plugin's settings live in the Settings dialog, so a merchant inside the plugin had no direct way to reach them. A button on one of the plugin's pages reads as a setting of that page, not of the plugin.

A sidebar action draws nothing, so it does not fit the folder model. The rule is: folders hold everything that draws UI, and plugin.config.ts holds contributions that draw nothing. Commands can move to this file later.

What changed

  • New file commera/plugin.config.ts (or .js), with a definePlugin() export from @commera/plugin-kit. Its sidebar items have name and label (required), icon, order, requires, condition and run(context). The rows sit in the plugin's sidebar group after its pages.
  • run gets openSettings(tab), navigate(to), openUrl(url) and toast. The file can import only definePlugin.
  • The build checks each item like a folder entry, with the item index in the error. It ships the file as one module, plugin.config.js. A sidebar/ folder fails the build and points to the config file.
  • usePlugin().openSettings(tab): without a tab, it opens the plugin's own Settings tab. A Commera tab name such as 'payments' opens that tab. This adds a member and changes no existing one.
  • Settings > Plugins lists the new entries as Sidebar action.
  • Docs: the plugin-kit README has a plugin.config.ts section. The docs.bwh.tech Dashboard entries page needs the same section.

Example:

import { definePlugin } from '@commera/plugin-kit'

export default definePlugin({
  sidebar: [
    { name: 'settings', label: 'Settings', icon: 'settings', order: 9, run: ({ openSettings }) => openSettings() },
  ],
})

Screenshots

The Buy Links plugin with a Settings sidebar action after its two pages:

🤖 Generated with Claude Code

NagariaHussain and others added 3 commits October 10, 2026 01:10
A plugin could only add pages to its sidebar group, so a row that opens its
Settings tab, or does any other quick job, had no home. A new placement,
sidebar/<name>/index.vue, adds a row after the plugin's pages that runs the
file's <script setup> on click (no template, with the usual composables).
usePlugin() gains openSettings(tab): no tab opens the plugin's own Settings
tab, a Commera tab name opens that one. bench commera add sidebar <name>
writes a starter that opens Settings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A sidebar action draws nothing, so a folder with a template-less .vue file was the wrong shape. Folders now hold only contributions that draw UI. Sidebar actions are items in commera/plugin.config.ts, made with definePlugin from @commera/plugin-kit. The kit reads the file for the manifest and ships it as one plugin.config.js module. A click imports it and calls run() with openSettings, navigate, openUrl and toast. A sidebar folder fails the build and points to the config file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@NagariaHussain NagariaHussain changed the title feat(plugins): add sidebar actions and openSettings to the plugin API feat(plugins): Add sidebar actions in plugin.config.ts and openSettings Oct 10, 2026
@Rl0007 Rl0007 added the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@github-actions github-actions Bot removed the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@Rl0007 Rl0007 added the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@github-actions github-actions Bot removed the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@Rl0007 Rl0007 added the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@github-actions github-actions Bot removed the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@Rl0007 Rl0007 added the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@github-actions github-actions Bot removed the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@Rl0007 Rl0007 added the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
@github-actions github-actions Bot removed the claude-review Run frappe-reviewer on this PR label Oct 12, 2026
try {
if (entry.error || !module_url) throw new Error(entry.error || 'This plugin has not been built yet.')
const url = import.meta.env.DEV ? `/@commera-plugin/${app}/${place}/${name}` : module_url
const config = (await import(/* @vite-ignore */ url)).default

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The README says plugin.config.ts may import only @commera/plugin-kit, but the guard doesn't enforce that. evaluateConfig in packages/plugin-kit/vite.js uses an onResolve filter of ^[^./], so relative and absolute imports pass. The config also runs in Node at build time with full access. This is the plugin author's own code, so it isn't an RCE hole, but the claim and the guard disagree. Either reject relative and absolute imports, or document that the config executes at build time.

Also, dev mode builds the URL from name and place. parseKey splits on /, and there's no dev-server test for the config branch of moduleCode. Please add one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Documented in 53c3d1a: the README now says the build runs plugin.config in Node, so its top-level code runs at build time and it should hold plain data. Package imports are already blocked in the files it pulls in too. Its own relative imports stay allowed, because the plugin author owns that code. parseKey is safe here: sidebar has no slash and name cannot hold one. A dev-server test for the config branch is still open and can follow.

@@ -0,0 +1,26 @@
import { toast } from 'frappe-ui'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

openSettings is wired twice (here and in PluginHost.vue), each with the same tab ?? settingsTabValue(app) fallback. The imports also differ: ../ia/settingsRoute here, ../ia/settings in PluginHost. plugins.js and sidebar.js import each other, which is why router is imported lazily. Extract one openPluginSettings(app, tab) helper next to settingsTabValue and the cycle goes away.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Fixed in 53c3d1a: both callers now use one openPluginSettings(app, tab) in ia/plugins.js. The plugins.js/sidebar.js import cycle stays, because the nav needs runSidebarAction. It is harmless: each side calls the other only on click. The lazy router import is for a different cycle, router → plugins → sidebar.


function pageNavItem(entry) {
return { label: entry.label, icon: pluginIcon(entry), to: pageRoute(entry) }
function navItem(entry) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

A single sidebar action becomes a top-level row with no to and the app icon. NavSection.vue only handles a child without to in the children loop, not the top-level branch. Please confirm a top-level item without to renders and fires onClick, and show it in a screenshot. Also check an app with one action and no pages: there's no /plugins/${app} route, but appLocation and openSettings rely on the app's settings tab.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The top-level branch of NavSection.vue already handles an item without to: :active="Boolean(item.to) && …" and @click="item.onClick?.()", the same path the Search row uses. No route is needed for an app with only actions: nothing links to /plugins/<app> there. If the plugin has no Settings tab, openSettings() falls back to General, and the README now says so. Two related bugs are fixed in 53c3d1a: a group holding an action used to open itself on any page outside the nav (such as Settings), and the phone's More sheet lost an action's logo icon.

@github-actions

Copy link
Copy Markdown

Review verdict: REQUEST CHANGES

I read the diff and the surrounding code. I did not run the tests.

Must fix

  1. add_places in the scaffold creates a commera/sidebar/ folder that the new build error forbids.
  2. The README says plugin.config.ts may import only @commera/plugin-kit, but the esbuild guard lets relative and absolute imports through. Enforce the rule or document that the config runs in Node at build time.
  3. Confirm with a screenshot that a top-level sidebar action with no to renders and fires onClick. Also check an app with one action and no pages.

Should fix

  • Dedupe the openSettings wiring and break the plugins.js / sidebar.js import cycle.
  • Add a type check for non-object items in readPluginConfig, and share validation with discoverPlugins.
  • Add tests for a dev-server config branch, a sidebar manifest entry without a condition, and the requires permission gate.

This adds a public plugin API surface, so label it as a feature. API_VERSION stays 1.

The design is sound and the build-time checks are thorough.

…ld folders

An action row has no route, so a group holding one matched any page outside the nav,
such as the Settings dialog, and opened itself. The phone's More sheet drew action
icons as a class, so a logo icon vanished. The scaffold no longer writes a
commera/sidebar/ folder that the build rejects. openSettings goes through one
openPluginSettings helper, and the README says plugin.config runs in Node at build time.
@Rl0007
Rl0007 changed the base branch from feat/plugin-storefront-switch to develop October 14, 2026 02:47
@Rl0007

Rl0007 commented Oct 14, 2026

Copy link
Copy Markdown
Collaborator

Addressed in 53c3d1a:

  • The scaffold no longer writes commera/sidebar/. The test checks that the folder is absent.
  • openSettings is wired once. The README says what the config may import and that it runs at build time.
  • Two more fixes: a nav group holding an action opened itself on pages outside the nav, and action icons vanished in the mobile More sheet.

Still open, can follow: a dev-server test for the config branch, and tests for the requires gate and an entry with no condition. The PR is retargeted to develop now that #244 has merged.

@Rl0007
Rl0007 merged commit a20aee8 into develop Oct 14, 2026
7 of 8 checks passed
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