Skip to content

Develop Plugins

Learn how to create plugins for Motanamy platforms (EA, SA, OS).

Quick Start

1. Install CLI

bash
npm install -g motanamy-cli

2. Create Plugin

bash
mot create my-plugin --type EA  # EA, SA, or OS
cd my-plugin

3. Develop

Edit 1.0.0/main.js for backend logic and files in 1.0.0/src/ for frontend components.

4. Test

bash
mot run

5. Build & Upload

bash
mot build
mot upload

Plugin Types

  • EA Plugins: Web extensions for Enterprise Applications
  • SA Plugins: UI extensions for Standalone Applications
  • OS Plugins: System-level extensions

Basic Structure

my-plugin/
├── 1.0.0/
│   ├── settings.json        # Version-specific settings (manifest)
│   ├── script.js           # Main plugin script for frontend loading
│   ├── README-AR.md        # Arabic documentation
│   ├── README-EN.md        # English documentation
│   ├── src/                # Frontend source code
│   ├── main.js             # Backend logic (connect to nestjs/express)
│   ├── backend/            # Express routes if needed
│   └── styles/             # Version-specific styles
├── 1.1.0/
│   ├── settings.json        # Updated settings for v1.1.0
│   ├── script.js           # Updated script for v1.1.0
│   ├── README-AR.md        # Updated Arabic docs
│   ├── README-EN.md        # Updated English docs
│   ├── src/                # Updated frontend source code
│   ├── main.js             # Updated backend logic
│   ├── backend/            # Updated express routes
│   └── styles/             # Updated styles
├── database/
│   ├── dist/               # Custom landing page or plugin
│   ├── assets/             # Database assets
│   ├── images/             # Database images
│   ├── filesystem.json     # Database filesystem
│   └── backend/            # NestJS output

Version Folder Structure

Each version folder contains version-specific files:

1.0.0/
├── settings.json           # App configuration (manifest) for this version
├── script.js              # Main plugin script for frontend loading
├── README-AR.md           # Arabic documentation
├── README-EN.md           # English documentation
├── src/                   # Frontend source code
│   ├── components/       # Vue.js components with PrimeVue
│   ├── views/            # Page/view components
│   ├── services/         # API services and business logic
│   ├── utils/            # Utility functions
│   └── config/           # Configuration files
├── main.js               # Backend logic (connect to nestjs/express/microservices)
├── backend/              # Express routes if needed
└── styles/               # Version-specific styles

Database Folder Structure

database/
├── dist/                  # Custom landing page or plugin
├── assets/                # Database assets
├── images/                # Database images
├── filesystem.json        # Database filesystem configuration
└── backend/               # NestJS output and backend files

Real-World Implementation Examples

File System Database Operations

Here's a practical example of how to work with the filesystem database in Motanamy plugins, based on real implementation patterns:

html
<!-- src/components/BannerManager.vue -->
<template>
  <div class="banner-manager">
    <div class="card flex align-items-center justify-content-between">
      <h5 class="m-0">Banner Management</h5>
      <Button
        label="Save"
        icon="pi pi-save"
        class="p-button-text"
        @click="save()"
      />
    </div>

    <div class="card">
      <Button
        icon="pi pi-plus"
        type="button"
        class="p-button-text"
        @click="addBanner"
      />

      <div
        v-for="(item, index) in banner"
        :key="index"
        class="banner-item mb-4 p-3 border-1 border-round"
      >
        <div class="flex align-items-center justify-content-between mb-3">
          <h6 class="m-0">Banner {{ index + 1 }}</h6>
          <div v-if="item.link" class="flex align-items-center gap-1">
            <i class="pi pi-link text-primary"></i>
            <small class="text-primary">Linked to Event</small>
          </div>
        </div>

        <div class="grid">
          <div class="col-12 md:col-6">
            <label class="block mb-2 font-semibold">Banner Image</label>
            <div class="p-inputgroup">
              <InputText
                v-model="banner[index].image"
                readonly
                placeholder="Select banner image"
                class="w-full"
              />
              <Button
                icon="pi pi-check-circle"
                class="p-button-primary"
                @click="openFileExplorerToGetLogo(index)"
              />
            </div>
          </div>
          <div class="col-12 md:col-6">
            <label class="block mb-2 font-semibold">Event Link (Optional)</label>
            <InputText
              v-model="banner[index].link"
              type="url"
              placeholder="https://example.com/event"
              class="w-full"
            />
          </div>
        </div>

        <div class="flex justify-content-end mt-3">
          <Button
            icon="pi pi-trash"
            v-tooltip.top="'Delete Banner'"
            type="button"
            class="p-button-text p-button-danger"
            @click="removeBanner(index)"
          />
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";
import { EventBus } from "global";
import app from "app";

export default {
  data() {
    return {
      banner: []
    };
  },
  created() {
    this.init();
  },
  methods: {
    async init() {
      const data = (
        await axios.get(
          `plugins/database?path=${["quran-suhba", "banner.json"].join(",")}`
        )
      ).data;

      // Handle backward compatibility - convert old string format to new object format
      this.banner = data.map(item => {
        if (typeof item === 'string') {
          return { image: item, link: '' };
        }
        return item;
      });
    },
    addBanner() {
      this.banner.push({ image: '', link: '' });
    },
    removeBanner(index) {
      this.banner.splice(index, 1);
    },
    openFileExplorerToGetLogo(index) {
      EventBus.emit("openFileExplorer", {
        type: "file",
        exe: [],
        callback: ele => {
          this.banner[index].image = [...app.role.fileExplorer, ...ele.path].join(
            ","
          );
        }
      });
    },
    async save() {
      // Validate URLs before saving
      const invalidUrls = this.banner.filter(item =>
        item.link && !this.isValidUrl(item.link)
      );

      if (invalidUrls.length > 0) {
        this.$toast.add({
          severity: "error",
          summary: "Invalid URLs",
          detail: "Please enter valid URLs for event links"
        });
        return;
      }

      await axios.post(
        `plugins/database?path=${["quran-suhba", "banner.json"].join(",")}`,
        { data: this.banner }
      );
      this.$toast.add({
        severity: "success",
        summary: "Banners",
        detail: "Saved successfully"
      });
    },
    isValidUrl(url) {
      try {
        new URL(url);
        return true;
      } catch {
        return false;
      }
    }
  }
};
</script>

<style scoped>
.banner-item {
  background-color: #f8f9fa;
  border-color: #e9ecef;
}

.banner-item:hover {
  background-color: #f1f3f4;
  border-color: #dadce0;
}

.font-semibold {
  font-weight: 600;
}

.mb-4 {
  margin-bottom: 1rem;
}

.mb-2 {
  margin-bottom: 0.5rem;
}

.mt-3 {
  margin-top: 0.75rem;
}

.p-3 {
  padding: 0.75rem;
}

.border-1 {
  border-width: 1px;
}

.border-round {
  border-radius: 6px;
}

.text-primary {
  color: #007bff;
}

.gap-1 {
  gap: 0.25rem;
}

.m-0 {
  margin: 0;
}

.w-full {
  width: 100% !important;
}
</style>

Key Points for Filesystem Database:

  • Files are stored in the database/ folder of your plugin
  • Use plugins/database endpoint with path parameter
  • Path format: ['plugin-name', 'filename.json'].join(',')
  • Handle backward compatibility for data format changes
  • Use EventBus for file explorer integration
  • Always validate data before saving

API Integration with Axios

Here's a complete example of API integration using axios for plugin data management:

html
<!-- src/components/UserList.vue -->
<template>
  <div>
    <DataTable :value="users" @row-click="edited($event.data.id)" :rows="20" :globalFilterFields="['type']"
      v-model:filters="filters" data-key="id" v-model:selection="selectedUsers" filterDisplay="row"
      :paginator="users.length > 20" responsiveLayout="scroll" class="card mb-2">
      <template #header>
        <div class="flex align-items-center justify-content-between">
          Users
          <div>
            <Button icon="pi pi-plus" class="p-button-text" @click="edited()" />
            <Button icon="pi pi-refresh" class="p-button-text" @click="init()" />
            <Button v-tooltip.top="'Notifications'" icon="mdi mdi-bell-outline" :disabled="selectedUsers.length == 0"
              class="p-button-text" @click="openNotification(selectedUsers)"></Button>
          </div>
        </div>
      </template>
      <Column selectionMode="multiple" headerStyle="width: 3rem"></Column>
      <Column field="id" header="ID" :sortable="true"></Column>
      <Column field="email" header="Email" :sortable="true">
        <template #filter="{ filterModel, filterCallback }">
          <InputText v-model="filterModel.value" type="text" @input="filterCallback()" class="p-column-filter"
            placeholder="Search by email" />
        </template>
      </Column>
      <Column field="name" header="Name" :sortable="true">
        <template #filter="{ filterModel, filterCallback }">
          <InputText v-model="filterModel.value" type="text" @input="filterCallback()" class="p-column-filter"
            placeholder="Search by name" />
        </template>
      </Column>
      <Column field="phone" header="Phone" :sortable="true"></Column>
      <Column field="type" header="Type" :sortable="true" :showFilterMenu="false">
        <template #body="{ data }">
          <Chip :label="data.type" />
        </template>
        <template #filter="{ filterModel, filterCallback }">
          <Dropdown v-model="filterModel.value" class="p-column-filter" style="min-width: 12rem"
            @change="filterCallback()" :showClear="true" :options="['user', 'supervisor', 'admin', 'examer']">
            <template #option="slotProps">
              <Tag :value="slotProps.option" :severity="slotProps.option" />
            </template>
          </Dropdown>
        </template>
      </Column>
      <Column field="organization.name" header="Organization" :sortable="true">
        <template #filter="{ filterModel, filterCallback }">
          <InputText v-model="filterModel.value" type="text" @input="filterCallback()" class="p-column-filter"
            placeholder="Search by organization" />
        </template>
      </Column>
      <Column field="verified" header="Verified" :sortable="true">
        <template #body="{ data }">
          <i :class="data.verified ? 'pi pi-check text-green-500' : 'pi pi-times text-red-500'"></i>
        </template>
      </Column>
      <Column field="blocked" header="Blocked" :sortable="true">
        <template #body="{ data }">
          <i :class="data.blocked ? 'pi pi-ban text-red-500' : 'pi pi-check text-green-500'"></i>
        </template>
      </Column>
      <Column field="wallet" header="Wallet" :sortable="true">
        <template #body="{ data }">
          {{ data.wallet || 0 }}
        </template>
      </Column>
      <Column header="Actions">
        <template #body="{ data }">
          <div class="flex flex-row gap-2">
            <Button v-tooltip.top="'Notifications'" @click="openNotification([data])" icon="mdi mdi-bell-outline"
              type="button" class="p-button-text"></Button>
            <Button icon="pi pi-search" v-tooltip.top="'Edited'" type="button" class="p-button-text"
              @click="edited(data.id)"></Button>
            <Button icon="pi pi-trash" v-tooltip.top="'Delete'" type="button" class="p-button-text p-button-danger"
              @click="deleteElemet(data)"></Button>
          </div>
        </template>
      </Column>
    </DataTable>
    <notifications ref="EditedDialog" />
  </div>
</template>

<script>
import axios from "axios";
import notifications from "vue:quran-suhba/users/notification.vue";

export default {
  components: {
    notifications
  },
  data() {
    return {
      users: [],
      selectedUsers: [],
      filters: {
        email: { value: null },
        name: { value: null },
        "organization.name": { value: null },
        type: { value: null, matchMode: "equals" }
      },
      filter: {}
    };
  },
  created() {
    this.init();
  },
  methods: {
    openNotification(data) {
      this.$refs.EditedDialog.open(data);
    },
    edited(id = "add") {
      this.$router.push(`/app/suhba/users/${id}`);
    },
    init() {
      axios.get(`suhba/users`).then(res => {
        this.users = res.data;
      });
    },
    deleteElemet(data) {
      this.$confirm.require({
        header: "Confirmation",
        message: "Are you sure you want to delete this user?",
        acceptLabel: "Yes",
        rejectLabel: "No",
        acceptClass: "p-button-danger",
        accept: () => {
          axios
            .delete(`suhba/users/${data.id}`)
            .then(res => {
              this.init();
              this.$toast.add({
                severity: "success",
                summary: "Users",
                detail: "User deleted successfully"
              });
            })
            .catch(err => {
              this.$toast.add({
                severity: "error",
                summary: "Users",
                detail: "Failed to delete user"
              });
            });
        }
      });
    }
  }
};
</script>

Key Points for API Integration:

  • Use axios for HTTP requests to plugin endpoints
  • Plugin endpoints follow the pattern: plugin-name/endpoint
  • Handle loading states and error responses
  • Use PrimeVue components for rich UI (DataTable, Button, etc.)
  • Implement proper error handling with toast notifications
  • Use confirmation dialogs for destructive actions
  • Implement filtering and pagination for large datasets

Settings File (Manifest)

json
{
  "versionNumber": "0",           // Version counter (increments with updates)
  "version": "1.0.0",             // Semantic version string
  "name": "my-plugin",            // Plugin/app name
  "init": true,                   // Whether plugin is initialized
  "user": 1,                      // User ID who owns/created the plugin
  "type": "EA",                   // Platform type: EA (Enterprise), SA (Standalone), OS (Operating System)
  "main": "main.js",              // Main entry point file
  "assets": "assets",             // Assets directory path
  "script": "script.js",          // Frontend script file
  "id": "unique-plugin-id"        // Unique plugin identifier
}

Best Practices

  • Keep plugins focused on single functionality
  • Handle errors gracefully
  • Test on all target platforms
  • Follow security guidelines
  • Document your plugin clearly

Resources