Commera

Commera

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Dashboard entries

Your plugin adds pages, cards, actions, palette commands and a Settings tab to the Commera dashboard at /commera. You write each one as a Vue file. The build kit, @commera/plugin-kit, checks it and builds it. Commera draws the frame: the page header, the card title, the dialog and its buttons.

Where your plugin shows up

Each folder under commera/ fills one place in the dashboard. The images show the Qikink plugin. An orange box marks what the plugin adds when the image shows more than that.

The Plugins group in the sidebar, with the Qikink orders row

Each page adds a row under Plugins, with the logo from your plugin. Folders: commera/pages/<name>/index.vue and commera/plugin-icon.svg.

Plugin page

The Qikink orders page, with its title and Sync now button in the header

Commera draws the header. Your page sets the title, breadcrumbs and buttons with usePage(), and fills the rest. Folder: commera/pages/<name>/index.vue.

Order cards

The Qikink card in the right column of an order page

Your card goes in the right column of the order page, below Commera's cards. Folder: commera/order/cards/<name>/index.vue.

Order actions

The More actions menu on an order, with Send to Qikink in the Plugins group

Your action is a row in the Plugins group of the More actions menu. Folder: commera/order/actions/<name>/index.vue.

Action dialog

The Set Qikink SKUs dialog, with one SKU field per size and Save SKUs

An action with a template opens in a dialog. Commera draws the title, Cancel and the primary button, and your index.vue is the body. The Qikink order actions all use method, so this image shows its product action. Folder: commera/product/actions/<name>/index.vue.

Failed delivery notice

An order page with the notice "Qikink couldn't process this order" and a Retry button

You do not write this one. When an event handler from hooks.py fails on every attempt, Commera shows this notice on the order with Retry.

Product cards

The Qikink card on a product page, below Last 30 days and Needs attention

Your card goes in the right column of the product page. Folder: commera/product/cards/<name>/index.vue.

Product actions

The More actions menu on a product, with Set Qikink SKUs in the Plugins group at the bottom

Your action is a row in the Plugins group at the bottom of the More actions menu. Folder: commera/product/actions/<name>/index.vue.

Customer cards

A Gift wrap card in the right column of a customer page

Your card goes in the right column of the customer page, below the address and note. Qikink has no customer card, so this image shows the Gift Wrap example plugin. Folder: commera/customer/cards/<name>/index.vue.

Customer actions

The More actions menu on a customer, with Set default gift message in the Plugins group

Your action is a row in the Plugins group of the More actions menu. This image also shows the Gift Wrap example plugin. Folder: commera/customer/actions/<name>/index.vue.

Settings tab

Commera settings with the Qikink tab open under Installed plugins

Your tab is listed under Installed plugins in Settings. Folder: commera/settings/index.vue.

Settings > Plugins

The Plugins list in Settings, with the Qikink row and its 10 additions

Settings > Plugins lists each installed plugin, the number of things it adds and the number of failed deliveries. You do not write this page.

The Qikink detail in Settings > Plugins, with what it adds and its recent deliveries

Click Open to see each thing the plugin adds and its recent deliveries. A failed delivery has Retry. Commera builds this from your folders.

Search palette

The search palette with qikink typed, showing Qikink orders under Go to and Sync Qikink orders under Plugins

Your pages show under Go to, your settings tab under Settings, and your commands under Plugins. Folder for a command: commera/commands/<name>/index.vue.

Set up the plugin

Create a Frappe app, go into its folder and run bench commera init. It makes the app whose folder you are in a Commera plugin.

bench new-app my_app
cd apps/my_app
bench commera init
bench --site <site> install-app my_app

The command adds required_apps = ["commera"], commera_api_version = [1] and commera_plugin_title to hooks.py. commera_plugin_title is the name Commera shows for your plugin: the sidebar row of a plugin with several pages, Settings → Plugins and the search palette. A plugin with one page shows that page's label in the sidebar instead. It starts as your app_title. Without it, Commera uses app_title. It writes package.json, commera/vite.config.js, a starter page, my_app/api.py and commera/README.md, and then runs yarn install.

my_app/
├── package.json
├── yarn.lock                        # commit it
├── commera/
│   ├── vite.config.js
│   ├── pages/jobs/index.vue
│   ├── order/cards/status/index.vue
│   ├── order/actions/resend/index.vue
│   ├── settings/index.vue
│   ├── plugin-icon.svg
│   └── shared/                      # your own code
└── my_app/public/commera/           # build output, gitignored

The starter page calls my_app.api.get_summary. Delete both when you write your own page.

Add a placement

Run bench commera add <kind> <name> inside your plugin's folder to add one placement with a starter that builds. A running dev server builds it and reloads the page.

Kind Writes
page commera/pages/<name>/index.vue; with --detail, also [...id].vue, one record's view, which the list opens with navigate('<name>/<id>')
order-card, product-card, customer-card commera/<record>/cards/<name>/index.vue
order-action, product-action, customer-action commera/<record>/actions/<name>/index.vue and a whitelisted <name>_<record>(name) in your api.py
settings commera/settings/index.vue (no name: one per plugin)
command commera/commands/<name>/index.vue and a whitelisted <name>() in your api.py

The command never overwrites: it stops when the placement or the method already exists.

Placements

<name> is 1 to 40 lowercase letters, digits and hyphens, and starts with a letter or a digit. It is also the URL slug.

Folder under commera/ Where it shows Required fields Optional fields
pages/<name>/index.vue /commera/plugins/<app>/<name>, plus a sidebar row label icon, requires, condition, sidebar, order
{order,product,customer}/cards/<name>/index.vue A card on that record's page label requires, condition, order
{order,product,customer}/actions/<name>/index.vue A row in that page's More actions menu label icon, requires, condition, method, confirm, order
commands/<name>/index.vue A row in the search palette's Plugins group label, method icon, keywords, requires, condition, confirm, order
settings/index.vue Your plugin's tab in Settings label icon, requires, condition, doctype

Order cards and actions get a Sales Order, product ones an Item, and customer ones a Customer.

The plugin block

Each placement's index.vue starts with a plain <script> that holds one literal object:

<script>
export const plugin = {
	label: 'Print status',
	condition: 'my_app.conditions.order_has_jobs',
};
</script>

<script setup>
// the card's own code
</script>
Field Value
label The text on the sidebar row, card title, menu row or tab
icon Optional; without it the entry uses the plugin's icon (commera/plugin-icon.svg). A name from Commera's icon list, commera/sdk/plugin_icons.json in the Commera repo, for example gift or printer. Any other name fails the build
requires A DocType name. Only users who can read it see the entry
condition A dotted path to a function in your plugin that returns a bool. A page, command or settings condition takes no arguments. A card or action condition gets (doctype, name)
sidebar Pages only. false hides the sidebar row
order A number that sorts entries of the same kind
method Actions and commands. A dotted path to a whitelisted method that Commera runs
confirm Actions and commands. A question that Commera asks before it runs method
keywords Commands only. Extra words that match the command in the palette
doctype Settings only. A Single DocType that Commera draws as a form

A condition that raises counts as false, and Commera writes an Error Log.

Actions

An action has a template, which Commera opens in a dialog, or a method. Never both.

<script>
export const plugin = {
	label: 'Send to printer',
	icon: 'printer',
	method: 'my_app.api.send_order',
	confirm: 'Send this order to the printer?',
};
</script>
import frappe
from frappe import _


@frappe.whitelist()
def send_order(name):
	frappe.has_permission("Sales Order", "write", doc=name, throw=True)
	...
	return _("Order {0} sent").format(name)

Commands

A command is a row in the search palette that runs one method. Staff open the palette with Cmd+K (Ctrl+K on Windows and Linux). The plain <script> is the whole file. The build refuses a command with a <template> or a <script setup>.

<script>
export const plugin = {
	label: "Sync Qikink orders",
	icon: "refresh-cw",
	keywords: ["qikink", "print"],
	method: "commera_qikink.api.sync_open_orders",
};
</script>
  • Commera calls method() with no arguments, after the confirm dialog if you set confirm.
  • A returned string shows in the success toast. Any other return value shows "
  • condition takes no arguments, like a page condition. When it returns False, the command does not show.
  • The palette lists your plugin's pages under Go to without a command. Add a command only for work that a page link cannot do.

condition only hides the row. Any user can still call the whitelisted method, so check permissions in the method. The tutorial step Add a palette command builds this command.

Settings tab

A settings tab has a template, or a doctype. Never both.

<script>
export const plugin = { label: 'Printer', icon: 'settings', doctype: 'My App Settings' };
</script>

With doctype, Commera draws each field of the Single as a row that saves itself. A mandatory field shows a *, and a row cannot clear it. Each row saves alone, so other mandatory rows can still be blank. Your validate must allow blanks, and your code must check the values before it uses them. A blank Password row keeps the stored secret.

Composables

Import these from @commera/admin.

In Use To
a page usePage() setTitle(text), setBreadcrumbs([{ label, to }]), setActions([{ label, icon, variant, onClick, loading, disabled }]). Each takes a value, a ref or a getter. Past two actions, the rest fold into a menu
a card useCard() hide(), show(), setHidden(bool). Hiding during setup never shows an empty card
an action useAction() setPrimary({ label, disabled, loading }), onSubmit(async () => …), close(result). Return to close. Return { reload: true } to reload the record. Throw to keep the dialog open with the error under the form
all usePlugin() plugin, path, query, record ({ doctype, name } on cards and actions), reload(), navigate(to), toast, __

navigate(to) and a breadcrumb's to resolve against /commera/plugins/<app>/. 'jobs/JOB-1', '../' and '/orders/SO-1' all work.

@commera/admin also exports EmptyState, ListPagination, ListSkeleton, ResponsiveButton, StatusBadge, money, shortDate and usePolling.

Call your methods

Use useMethodRead for reads and useMethodAction for writes. Point them at a whitelisted method by its full dotted path.

import { usePlugin, useMethodAction, useMethodRead } from '@commera/admin';

const { record, toast } = usePlugin();
const jobs = useMethodRead('my_app.api.get_jobs', {
	params: () => ({ sales_order: record.value.name }),
});
const send = useMethodAction('my_app.api.send_order');

async function sendOrder() {
	const message = await send.submit({ name: record.value.name });
	if (send.error) return;
	toast.success(message);
	jobs.reload();
}
  • useMethodRead sends a GET request and loads on mount. Frappe does not commit a GET request, so never write data in a read method.
  • useMethodAction sends a POST request when you call submit(params). submit resolves to the method's return value, or to null on an error. Check request.error after it.
  • Both show a toast for an error. Inside an action dialog, useMethodAction does not. Throw request.error from onSubmit and the dialog shows it once.

Put an optional commera/plugin-icon.svg next to pages/. bench commera init writes a placeholder there: the Commera logo in violet, orange, blue or green. The colour is picked at random when the file is written, and running bench commera init again never changes it. Replace it with your own logo. It is the icon of everything your plugin adds: the sidebar, Settings, the search palette and More actions rows. An entry with its own icon shows that one instead, and a plugin with only one page shows that page's icon in the sidebar when it has one. Without plugin-icon.svg, the sidebar row uses the first page's icon.

Commera shows the logo as it is, in its own colours, in light and dark mode. Pick colours that read on both. A currentColor in the file draws black.

Styling

Use any frappe-ui or Tailwind class, including arbitrary values such as min-w-[60rem]. Your plugin ships no CSS of its own: Commera compiles your classes into the dashboard's one stylesheet, so they always sort the way Tailwind expects next to the dashboard's own classes.

  • While you work, the dashboard's yarn dev adds a new class as soon as you save.
  • In production, a new class is styled after bench build --app commera or bench build. Your plugin's own bench build --app my_app still builds, and warns which classes are waiting for the Commera build.

Build checks

The build reports all folder and plugin problems together.

Check Example that fails
An index.vue with plugin outside a placement ordr/cards/status/index.vue
A placement index.vue without a plugin block pages/jobs/index.vue with only a template
A <name> that is not a slug pages/Print_Jobs/
A value that is not a literal label: t('Jobs'), ...base
Another statement in the plain <script> import x from './x'
An unknown, missing or wrong field sidebr: false, no label, sidebar on a card
An action or settings tab with both or neither of a template and method/doctype
An icon that is not in the list icon: 'printr'
A dotted path outside your plugin condition: 'frappe.client.get_list'
A frame that the dashboard draws importing AppPageHeader, PageBody, PluginCard or frappe-ui's Dialog
A frappe-ui resource that calls Frappe's v1 API createResource, createListResource, createDocumentResource, useCall, useList, useDoc, useDoctype, useNewDoc, frappeRequest, call
An plugin-icon.svg that is not a plain SVG over 20 kB, a <script>, an on* attribute
A <style> block or a stylesheet import <style>.x { color: red }</style>
A name that the shared modules do not export import { Foo } from 'frappe-ui'
A subpath that is not shared import { TextEditor } from 'frappe-ui/editor'

The class, export and icon checks read the files that the Commera dashboard build writes. Build Commera before you build your plugin.

Develop

  1. Build Commera one time.
  2. Turn on developer mode on your site.
  3. Run yarn dev in apps/commera/dashboard.
  4. Open /commera on port 8080 with your site's host name, for example http://my.site:8080/commera, and log in there.

The dev server loads your plugin's .vue files from source. When you save a file, the page changes in place, without a reload. Your plugin has no yarn dev of its own.

Each save also runs yarn build in your plugin's folder, so the checks above run while you work. A failed check shows in the browser's error overlay. A new placement folder, or a changed plugin field, reloads the page. The build reads the folders on disk: a placement folder that you rename or move stays a live entry until you delete it.

For production, run bench build --app my_app. bench build --app commera also builds every installed plugin, after the dashboard. If one plugin fails, the others still build, and the command ends with an error that names it.

Next steps

Last updated 3 days ago
Was this helpful?
Thanks!