Source

client/core/modules/plugins.js

/**
 * 
 * ## Plugin manager
 * 
 * Plugins are used to add features to the application.
 * 
 * Each feature can be added to a specific place in the application.
 * For example, a plugin can add a feature to the form section, or the form menu.
 * A single plugin can add multiple features at the same time.
 * 
 * Feature types are:
 * - form-section: adds a section to the form
 * - form-header: adds a section to the form header
 * - form-footer: adds a section to the form footer
 * - form-menu: adds an item to the form menu
 * - model-menu: adds an item to the model menu
 * - attachment-menu: adds an item to the attachment field menu (e.g. to process attachments)
 * 
 * A plugin can also have:
 * - an init() method, which will be automatically called when the plugin is loaded
 * - custom methods, which will be available in the plugin object
 * - fields, which will be added to the target model
 * 
 * Plugin manage localization, so that the texts can be translated into multiple languages.
 * 
 * Check the example for further details.
 * 
 * @namespace
 */
kiss.plugins = {

	/**
	 * Array of loaded plugins
	 */
	plugins: [],

	/**
	 * Adds a plugin to the list
	 * 
	 * @param {object} plugin
	 * 
	 * @example
	 * 
	 * // Defines the plugin which adds the form feature: "See JSON data"
	 * kiss.plugins.add({
	 *     // Plugin id
	 *     id: "form-feature-show-json",
	 * 
	 *     // The plugin has an icon to recognize it
	 *     icon: "fab fa-node-js",
	 * 
	 *     // The plugin has an order, which is used to display the list of plugins to the user.
	 *     order: "0003",
	 * 
	 *     // A plugin can be disabled, so that it is not loaded by default.
	 *     disabled: false,
	 * 
	 *     // A plugin can be admin-only, so that it is only available to administrators to configure a feature.
	 *     admin: false,
	 * 
	 *     // A plugin must support multi-language texts
	 *     texts: {
	 *         // A plugin must at least have name and a description
	 *         "name": {
	 *             en: "see data as JSON",
	 *             fr: "Voir les données JSON"
	 *         },
	 *         "description": {
	 *             en: "this plugin displays the record data as JSON",
	 *             fr: "ce plugin affiche les données JSON du document"
	 *         }
	 *     },
	 * 
	 *     // A plugin is an array of features because a single plugin can add multiple features at the same time.
	 *     features: [{
	 *         // The plugin "type" tells the application WHERE to integrate the plugin.
	 *         // There are many places where a feature can be plugged into airprocess.
	 *         // The most common one is "form-section", which adds a... form section, of course.
	 *         type: "form-section",
	 * 
	 *         // The "renderer" is the function that will render the plugin into the page.
	 *         // In the case of a form section, the function will receive the form itself as input parameter.
	 *         // There are only 2 rules to follow to build the renderer:
	 *         // - the function **must** return an HTMLElement.
	 *         // - the HTMLElement **must** have a class with the plugin id. The class will be used as a selector when showing/hiding the plugin.
	 *         renderer: function (form) {
	 
	 *             // The "record" property of the form allows to access the current record being edited.
	 *             const record = form.record
	 * 
	 *             // Return your UI: can be any HTMLElement or KissJS component. The example below is a KissJS component:
	 *             return createHtml({
	 *                 class: "form-feature-show-json",
	 * 
	 *                 collections: [
	 *                     kiss.app.collections[record.model.id]
	 *                 ],
	 * 
	 *                 methods: {
	 *                     async load() {
	 *                         let recordDataToHtml = JSON.stringify(record.getRawData(), null, 4)
	 *                         recordDataToHtml = recordDataToHtml.replaceAll("\n", "<br>")
	 *                         recordDataToHtml = recordDataToHtml.replaceAll(" ", "&nbsp;")
	 *                         this.setInnerHtml(recordDataToHtml)
	 *                     }
	 *                 }
	 *             })
	 *         }
	 *     }],
	 * 
	 *     // A plugin can add fields to the target model.
	 *     fields: [
	 *        {
	 *           id: "workflow-initiator",
	 *           type: "directory",
	 *           label: "workflow initiator"
	 *        },
	 *        // ...
	 *     ],
	 * 
	 *     // The plugin can have an optional "init" method, which will be called when the plugin is loaded
	 *     async init() {
	 *        log("Plugin " + this.id + " initialized")
	 *     },
	 * 
	 *     // The plugin can have methods, which will be available in the plugin object
	 *     methods: {
	 *        doThis() {
	 *           log("Doing this in the plugin " + this.id)
	 *        }
	 *     }
	 * });
	 * 
	 * // The method can be used like this:
	 * kiss.plugins.get("form-feature-show-json").doThis()
	 */
	add(plugin) {
		try {
			log("kiss.plugins - Adding plugin <" + plugin.name + ">", 1, plugin)
			kiss.plugins.plugins.push(plugin)

		} catch (err) {
			log("kiss.plugins - The plugin " + plugin.id + " is not well formatted", 4, plugin)
		}
	},

	/**
	 * Get one or all the plugin definitions
	 * 
	 * @param {string} [pluginId] - If provided, returns only the specified plugin. Otherwise, return all plugins.
	 * @returns {*} The array of all plugins, or only the specified plugin
	 */
	get(pluginId) {
		if (!pluginId) return kiss.plugins.plugins.sortBy("order")
		return kiss.plugins.plugins.find(plugin => plugin.id == pluginId)
	},

	/**
	 * Get the texts of the plugin
	 * 
	 * @param {string} pluginId
	 * @returns {object} Plugin texts
	 */
	getTexts(pluginId) {
		const plugin = kiss.plugins.get(pluginId)
		return plugin.texts
	},

	/**
	 * Translate main properties into the right language, at runtime
	 */
	initTexts() {
		this.plugins.forEach(plugin => {
			plugin.name = txtTitleCase("name", plugin.texts)
			plugin.description = txtTitleCase("description", plugin.texts)
			plugin.instructions = txtTitleCase("instructions", plugin.texts)
		})
	},

	/**
	 * Init all the plugins at once
	 */
	async init() {
		for (let plugin of kiss.plugins.plugins) {

			// Check if the plugin has a method "init" and call it if it exists
			if (plugin.init) await plugin.init()

			// Assign plugin methods, if any
			if (plugin.methods) Object.keys(plugin.methods).forEach(method => plugin[method] = plugin.methods[method])
		}
	}
}