Repository navigation
feat(plugins): Add sidebar actions in plugin.config.ts and openSettings - #253
Conversation
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>
| 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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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' | |||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Review verdict: REQUEST CHANGESI read the diff and the surrounding code. I did not run the tests. Must fix
Should fix
This adds a public plugin API surface, so label it as a feature. 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.
|
Addressed in 53c3d1a:
Still open, can follow: a dev-server test for the config branch, and tests for the |
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()gainsopenSettings(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.tsholds contributions that draw nothing. Commands can move to this file later.What changed
commera/plugin.config.ts(or.js), with adefinePlugin()export from@commera/plugin-kit. Itssidebaritems havenameandlabel(required),icon,order,requires,conditionandrun(context). The rows sit in the plugin's sidebar group after its pages.rungetsopenSettings(tab),navigate(to),openUrl(url)andtoast. The file can import onlydefinePlugin.plugin.config.js. Asidebar/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.Example:
Screenshots
The Buy Links plugin with a Settings sidebar action after its two pages:
🤖 Generated with Claude Code