Skip to content

Powered by Grav CMS + Helios Theme

Admin sections

A provider plugin can add its own screen to the KahunaCart admin. This page is for PHP and JavaScript developers building one.

What a section is

A section is a web component. The plugin registers metadata and a path to a JS file; the admin fetches the file and mounts the custom element it defines. A PayPal plugin might add a "PayPal" tab with credentials and webhook status; a shipping integration might add a rate-card editor.

The base plugin collects sections, sorts them, and serves their code. It does not know what they contain.

Tip

A section is a whole screen with its own sidebar entry. For a panel that belongs beside an existing record, such as a warranty card on an order or a licence list on a customer, mount a widget into a zone instead. Widgets use the same web-component contract, and the merchant can reorder them. See Extension zones.

Register a section

Subscribe to onKahunaCartAdminSections and append your section to sections.

PHP
public static function getSubscribedEvents(): array
{
    return [
        'onKahunaCartAdminSections' => ['onKahunaCartAdminSections', 0],
    ];
}

public function onKahunaCartAdminSections(Event $event): void
{
    $sections = $event['sections'];
    $sections[] = [
        'id' => 'paypal',
        'label' => 'PayPal',
        'icon' => 'CreditCard',
        'position' => 200,
        'script_path' => 'plugin://kahunacart-paypal/admin-next/sections/paypal.js',
    ];
    $event['sections'] = $sections;
}

The section appears in the admin nav on the next request.

Key Required Notes
id yes Lowercase, [a-z0-9][a-z0-9-]*, no underscores; used in the URL and the element name, anything else is dropped. First registration of an id wins.
label no Falls back to the id.
icon no A Lucide icon name, matching the rest of the admin.
position no Sort order, default 100. Ties break on label.
script_path yes Absolute path or Grav stream URI inside the plugins directory; anything else is refused. A section with no script is dropped.

The event fires per request, when the admin asks for the list. There is nothing to cache or invalidate.

Section endpoints

TXT
GET /kahunacart/admin/sections          → {"sections": [{id, label, icon, position}, …]}
GET /kahunacart/admin/sections/{id}.js  → the component source, application/javascript

Both require the kahunacart.orders.view permission. script_path never appears in a response; the script is fetched by id. The script route answers a conditional GET with a 304, using mtime and size as the validator.

The component contract

The script defines a custom element named kahunacart-section- plus the id.

JS
// admin-next/sections/paypal.js
class KahunaCartPayPalSection extends HTMLElement {
    connectedCallback() {
        const { apiFetch, currency, exponent, route } = this.ctx;
        // …render
    }
}

customElements.define('kahunacart-section-paypal', KahunaCartPayPalSection);

The host instantiates the element and sets el.ctx before attaching it, so connectedCallback can rely on it.

ctx key What it is
apiFetch (method, path, body) => Promise — an authenticated JSON fetch against the API. Pass paths like /kahunacart/coupons. It unwraps json.data and throws an Error carrying .status.
apiBlob (path) => Promise<Blob> — the same authenticated fetch, returning a blob.
currency The store's base currency as an ISO 4217 code, e.g. USD. A store that sells in more than one currency still has one base, and this is it.
exponent How many decimal places the base currency has: 2 for USD, 0 for JPY, 3 for KWD. Money crosses the API as minor units or as decimal strings; this is what converts between them. Never hardcode 2.
currencies Only on a store that sells in more than one currency, and missing (not empty) on any other, so test for it before you read it. The same answer as currencies in GET /kahunacart/config: { base: { code, exponent }, enabled: [{ code, exponent }] }.
base The API base URL the admin talks to, for example /api/v1.
route The storefront's base route, for example /shop.
providers slug => {label, capabilities} for every payment provider registered right now.
languages The site's supported languages, in configured order. Empty on a single-language store.
defaultLanguage The site's default language code, or null.
version The KahunaCart version string.

Important

Money crosses the API as minor units or as decimal strings. Use exponent for every conversion between the two. Hardcoding 2 breaks JPY and KWD.

Register the element name once. customElements.define throws on a duplicate, and the host may load a section more than once across navigations.

JS
if (!customElements.get('kahunacart-section-paypal')) {
    customElements.define('kahunacart-section-paypal', KahunaCartPayPalSection);
}

Conventions to follow

  • Put the file at admin-next/sections/{id}.js in your plugin. Nothing enforces it, but it is where the next person will look.
  • Ship one file with no imports. There is no bundler between your plugin and the browser; the route serves the bytes as they are on disk.
  • Check permissions on your own writes. The section renders for users with kahunacart.orders.view. If your screen changes settings, check kahunacart.settings on your own endpoints.
  • Let your data travel. If your section edits rows in tables of your own, register those tables on onKahunaCartRegisterExports, the way you declare them for reset. Then Import & Export → Export carries them, under your add-on's own name, and a restore brings them back. See Store export and restore.