Skip to content

SA Development Guide

Prerequisites

  • Node.js 18+
  • Motanamy CLI (npm install -g motanamy-cli)
  • A running Motanamy SA instance for testing

File Structure

Every SA plugin follows this structure:

my-plugin/
├── 1.0.0/                  ← version folder
│   ├── setting.json        ← plugin manifest
│   ├── script.js           ← registration script (runs on load)
│   ├── widget.vue          ← dashboard widget (optional)
│   ├── setting.vue         ← settings page (optional)
│   ├── my-page.vue         ← additional pages (optional)
│   ├── README-AR.md        ← Arabic documentation
│   ├── README-EN.md        ← English documentation
│   └── images/             ← plugin images/icons
└── database/
    └── my-plugin/
        └── data.json       ← initial data

When you release a new version, add a new version folder alongside 1.0.0/:

my-plugin/
├── 1.0.0/
├── 1.1.0/          ← new version
└── database/

setting.json

json
{
    "versionNumber": "0",
    "version": "1.0.0",
    "name": "my-plugin",
    "init": true,
    "type": "SA",
    "script": "script.js",
    "id": "_uniqueId"
}
FieldDescription
versionNumberInternal version index (start at "0")
versionSemver version string shown in store
namePlugin identifier — must match the folder name
inittrue runs script.js at startup
typeAlways "SA"
scriptEntry script filename
idUnique store ID (assigned on upload)

script.js

script.js runs once when SA starts. Register your plugin by assigning a function to plugins["name"]. Inside the function, this exposes all registration methods.

js
plugins["my-plugin"] = function () {
    // 1. Register translations
    this.addLanguage("ar", "my-plugin", { name: "البرنامج" });
    this.addLanguage("en", "my-plugin", { name: "My Plugin" });

    // 2. Add to sidebar nav
    this.addTo({
        label: "My Plugin",
        key: "my-plugin.name",
        icon: "mdi mdi-puzzle-outline",
        to: "/app/my-plugin"
    }, "setting");

    // 3. Register the page route
    this.addPage({
        name: "/[my-plugin]index.vue",
        path: "my-plugin",
        file: ["my-plugin", "index.vue"]
    }, "setting");

    // 4. Register a dashboard widget
    this.addWidget({
        name: "/[my-plugin]widget.vue",
        id: "init-my-plugin",
        file: ["my-plugin", "widget.vue"]
    });
}

See the script.js API Reference for all available methods.

widget.vue

Widgets are displayed on the SA dashboard. Always wrap the root element in the responsive grid column so widgets align with the SA layout:

vue
<template>
    <div class="col-12 lg:col-6 xl:col-3 p-1">
        <div class="card h-full">
            <h5>{{ $t("my-plugin.name") }}</h5>
            <p>{{ value }}</p>
        </div>
    </div>
</template>

<script>
import api from 'api';

export default {
    data() {
        return { value: null }
    },
    created() {
        this.value = api.database("my-plugin").readJson(["data.json"]);
    }
}
</script>

The col-12 lg:col-6 xl:col-3 p-1 wrapper is required — it places the widget in the dashboard grid.

Pages

Regular and settings pages are plain Vue components. Use the standard card layout:

vue
<template>
    <div>
        <div class="card flex align-items-center justify-content-between">
            <h5 class="m-0">{{ $t("my-plugin.name") }}</h5>
            <Button icon="pi pi-save" class="p-button-text" @click="save()" />
        </div>
        <div class="card">
            <!-- content -->
        </div>
    </div>
</template>

Database

SA plugins use the api module to read and write local JSON files from the database/ folder:

js
import api from 'api';

// Read
const data = api.database("my-plugin").readJson(["data.json"]);

// Write
api.database("my-plugin").writeJson(["data.json"], data);

// Nested path
const item = api.database("my-plugin").readJson(["folder", "items.json"]);

The database() argument is the plugin name — it matches the folder under database/my-plugin/.

Unlike EA, there is no axios call or HTTP server. All reads/writes are synchronous local operations.

Icon Picker

SA provides a built-in icon picker via EventBus:

js
import { EventBus } from 'global';

EventBus.emit("openIcons", (selectedIcon) => {
    this.form.icon = selectedIcon; // e.g. "mdi mdi-home"
});

Use this to let users pick an icon for any field instead of typing it manually.

i18n Usage

Translations registered via addLanguage() are available in templates with $t():

vue
<template>
    <div>
        <h5>{{ $t("my-plugin.name") }}</h5>
        <Button :label="$t('button.save')" />
    </div>
</template>

Development Workflow

  1. Create the plugin folder under the SA plugins directory
  2. Write setting.json and script.js
  3. Run mot run to link your plugin folder to the running SA instance
  4. SA picks up changes on reload — no build step needed
  5. When ready, run mot build then upload via the store

Resources