From 98fa1fec7baa40da63beca34e10df216a4673c86 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 14 Mar 2025 13:49:58 +0100 Subject: [PATCH 1/5] Fix docs --- docs/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6a80343..cd35864 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,10 +10,10 @@ STAC-Manager is a react app designed for managing the values of a STAC (SpatioTe The ecosystem is composed of a web app (the client) and a plugin system that powers it. The different parts of the project are located in the `packages` directory structured as follows: -- [`@stac-manager/client`](./packages/client) - STAC-Manager web app. -- [`@stac-manager/data-core`](./packages/data-core) - Core functionality of the form builder plugin system. -- [`@stac-manager/data-widgets`](./packages/data-widgets) - Form components to be used by the form builder plugin system, when custom ones are not provided. -- [`@stac-manager/data-plugins`](./packages/data-plugins) - Data plugins for the forms. Each plugin defines how a section of the data structure is displayed and edited. +- [`@stac-manager/client`](../packages/client) - STAC-Manager web app. +- [`@stac-manager/data-core`](../packages/data-core) - Core functionality of the form builder plugin system. +- [`@stac-manager/data-widgets`](../packages/data-widgets) - Form components to be used by the form builder plugin system, when custom ones are not provided. +- [`@stac-manager/data-plugins`](../packages/data-plugins) - Data plugins for the forms. Each plugin defines how a section of the data structure is displayed and edited. The `@stac-manager/data-*` packages contain the default implementation of the plugin system, the widgets used to render the forms and some core functions to allow the system to be extended. From 9f210d12eeeefce548aa917bf6d52b3d91c3dd65 Mon Sep 17 00:00:00 2001 From: Jonas Date: Fri, 14 Mar 2025 13:52:21 +0100 Subject: [PATCH 2/5] Add note on client config --- docs/README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/README.md b/docs/README.md index cd35864..fa5015f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -22,12 +22,18 @@ Each plugin handles a specific part of the data and is responsible for defining ## Configuration +### Client + +See the client-specific instructions, such as configuring the server's STAC API, in the [README of the client package](../packages/client#client-specific-instructions). + +### Plugins + STAC-Manager's [config file](/packages/client/src/plugin-system/config.ts) specifies the plugins that the app uses for Collections and Items while extending the `Default Plugin Widget Configuration` which defines the widgets for the basic field types. _See the [data-widgets/config](/packages/data-widgets/lib/config/index.ts) for a list of existent widgets._ When creating a new plugin or a new widget, the configuration should be updated with the new plugin/widget. -### Example config +#### Example config The config object should contain a list of plugins to use for the collections and items, as well as the widget configuration (which widgets to use for which field types). From 8d25ff4643acc606f7375b0b5b7014a3d3e7d87c Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Mon, 17 Mar 2025 17:25:49 +0000 Subject: [PATCH 3/5] Add hook system for plugins to interact --- .../data-core/lib/context/plugin-config.tsx | 2 +- packages/data-core/lib/plugin-utils/plugin.ts | 36 +++++++- .../data-core/lib/plugin-utils/resolve.ts | 92 ++++++++++++++++++- packages/data-plugins/lib/collections/core.ts | 11 +-- .../lib/collections/ext-item-assets.ts | 19 +++- .../lib/collections/ext-render.ts | 15 ++- packages/data-plugins/lib/utils.ts | 44 ++++++++- 7 files changed, 200 insertions(+), 19 deletions(-) diff --git a/packages/data-core/lib/context/plugin-config.tsx b/packages/data-core/lib/context/plugin-config.tsx index 5bf273f..7ac8958 100644 --- a/packages/data-core/lib/context/plugin-config.tsx +++ b/packages/data-core/lib/context/plugin-config.tsx @@ -13,7 +13,7 @@ const PluginConfigContext = createContext( /** * Global context provider for the configuration - * + * * @param props.config Configuration object * @param props.children Child components */ diff --git a/packages/data-core/lib/plugin-utils/plugin.ts b/packages/data-core/lib/plugin-utils/plugin.ts index fcff2d0..b5debc7 100644 --- a/packages/data-core/lib/plugin-utils/plugin.ts +++ b/packages/data-core/lib/plugin-utils/plugin.ts @@ -7,8 +7,31 @@ export type PluginEditSchema = | undefined | symbol; +export interface PluginHook { + // Name of the target plugin. + name: string; + + // The `onAfterInit` hook is executed after the target plugin's `init` + // function. + onAfterInit?: (pluginInstance: Plugin, data: any) => void; + + // The `onAfterEditSchema` hook is composed with the target plugin's + // `editSchema` function. + onAfterEditSchema?: ( + pluginInstance: Plugin, + formData: any, + schema: PluginEditSchema + ) => PluginEditSchema; +} + +const HIDDEN: unique symbol = Symbol('hidden'); +const HOOKS: unique symbol = Symbol('hooks'); + export abstract class Plugin { - static HIDDEN = Symbol('hidden'); + static readonly HIDDEN: typeof HIDDEN = HIDDEN; + static readonly HOOKS: typeof HOOKS = HOOKS; + + [HOOKS]: PluginHook[] = []; name: string = 'Plugin'; @@ -25,6 +48,17 @@ export abstract class Plugin { exitData(data: any): Record { throw new Error(`Plugin [${this.name}] must implement exitData`); } + + /** + * Registers a hook to be applied on a given plugin. + * + * @param targetName - The name of the target plugin to which the hook will be + * applied. + * @param hooks - The hook details. + */ + registerHook(targetName: string, hooks: Omit) { + this[Plugin.HOOKS].push({ name: targetName, ...hooks }); + } } export type PluginConfigResolved = Plugin | Plugin[] | undefined | null; diff --git a/packages/data-core/lib/plugin-utils/resolve.ts b/packages/data-core/lib/plugin-utils/resolve.ts index b89ce74..f30ca6c 100644 --- a/packages/data-core/lib/plugin-utils/resolve.ts +++ b/packages/data-core/lib/plugin-utils/resolve.ts @@ -1,7 +1,17 @@ +import { cloneDeep } from 'lodash-es'; import { Plugin, PluginConfigItem } from './plugin'; +/** + * Resolves the plugin config. + * + * @param plugins - An array of plugin configuration items which can be + * instances of `Plugin` or functions that return a `Plugin`. + * @param data - The data to be passed to plugin functions if they are not + * instances of `Plugin`. + * @returns An array of processed `Plugin` instances after applying hooks. + */ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => { - return plugins + const p = plugins .flatMap((pl) => { if (pl instanceof Plugin) { return pl; @@ -11,4 +21,84 @@ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => { return; }) .filter((p) => p instanceof Plugin); + + return applyHooks(p); +}; + +/** + * Applies hooks from the provided plugins to their respective targets. + * + * This function iterates over each plugin and applies hooks such as + * `onAfterInit` and `onAfterEditSchema` to the corresponding target plugins. + * The hooks are executed in the context of the source plugin. + * + * @param plugins - List of plugins. All the source and target plugins must be + * on the list. + * + * + * @remarks + * - The `onAfterInit` hook is executed after the target plugin's `init` + * function. + * - The `onAfterEditSchema` hook is composed with the target plugin's + * `editSchema` function. + * + * @example + * ```typescript + * class MyPlugin extends Plugin { + * name = 'MyPlugin'; + * + * [Plugin.HOOKS]: [ + * { + * name: 'pluginA', // Target plugin + * onAfterInit: async (target, data) => { }, // Executes after pluginA's init function. + * onAfterEditSchema: (target, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one. + * }, + * { + * name: 'pluginB', // Target plugin + * onAfterInit: async (target, data) => { }, // Executes after pluginB's init function. + * } + * ]; + * } + * + * applyHooks(plugins); + * ``` + */ +export const applyHooks = (plugins: Plugin[]) => { + const pluginsCopy = cloneDeep(plugins); + + for (const plSource of pluginsCopy) { + for (const hook of plSource[Plugin.HOOKS]) { + // Target where to apply the hook + const plTarget = plugins.find((p) => p.name === hook.name); + if (!plTarget) { + continue; + } + + // The onAfterInit hook is made by executing one function after another. + if (hook.onAfterInit) { + const fn = hook.onAfterInit; + const origInit = plTarget.init; + plTarget.init = async (data: any) => { + await origInit.call(plTarget, data); + await fn.call(plSource, plTarget, data); + }; + } + + // The onAfterEditSchema hook is made by composing functions. + if (hook.onAfterEditSchema) { + const fn = hook.onAfterEditSchema; + const origEditSchema = plTarget.editSchema; + plTarget.editSchema = (formData?: any) => { + return fn.call( + plSource, + plTarget, + formData, + origEditSchema.call(plTarget, formData) + ); + }; + } + } + } + + return pluginsCopy; }; diff --git a/packages/data-plugins/lib/collections/core.ts b/packages/data-plugins/lib/collections/core.ts index a5e2526..060a141 100644 --- a/packages/data-plugins/lib/collections/core.ts +++ b/packages/data-plugins/lib/collections/core.ts @@ -99,16 +99,7 @@ export class PluginCore extends Plugin { allowOther: { type: 'string' }, - enum: [ - [ - 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json', - 'Item Assets Defenition' - ], - [ - 'https://stac-extensions.github.io/render/v2.0.0/schema.json', - 'Render' - ] - ] + enum: [] } }, spatial: { diff --git a/packages/data-plugins/lib/collections/ext-item-assets.ts b/packages/data-plugins/lib/collections/ext-item-assets.ts index 653a3f8..e209002 100644 --- a/packages/data-plugins/lib/collections/ext-item-assets.ts +++ b/packages/data-plugins/lib/collections/ext-item-assets.ts @@ -1,11 +1,26 @@ import { Plugin, PluginEditSchema } from '@stac-manager/data-core'; -import { array2Object, hasExtension, object2Array } from '../utils'; +import { + addStacExtensionOption, + array2Object, + hasStacExtension, + object2Array +} from '../utils'; export class PluginItemAssets extends Plugin { name = 'Item Assets Extension'; + constructor() { + super(); + + addStacExtensionOption( + this, + 'Item Assets Definition', + 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json' + ); + } + editSchema(data: any): PluginEditSchema { - if (!hasExtension(data, 'item-assets')) { + if (!hasStacExtension(data, 'item-assets')) { return Plugin.HIDDEN; } diff --git a/packages/data-plugins/lib/collections/ext-render.ts b/packages/data-plugins/lib/collections/ext-render.ts index 4eb4819..7b16bc9 100644 --- a/packages/data-plugins/lib/collections/ext-render.ts +++ b/packages/data-plugins/lib/collections/ext-render.ts @@ -1,7 +1,8 @@ import { Plugin, PluginEditSchema } from '@stac-manager/data-core'; import { + addStacExtensionOption, array2Object, - hasExtension, + hasStacExtension, object2Array, object2Tuple, tuple2Object @@ -10,8 +11,18 @@ import { export class PluginRender extends Plugin { name = 'Render Extension'; + constructor() { + super(); + + addStacExtensionOption( + this, + 'Render', + 'https://stac-extensions.github.io/render/v2.0.0/schema.json' + ); + } + editSchema(data: any): PluginEditSchema { - if (!hasExtension(data, 'render')) { + if (!hasStacExtension(data, 'render')) { return Plugin.HIDDEN; } diff --git a/packages/data-plugins/lib/utils.ts b/packages/data-plugins/lib/utils.ts index 468deca..92541cf 100644 --- a/packages/data-plugins/lib/utils.ts +++ b/packages/data-plugins/lib/utils.ts @@ -1,4 +1,11 @@ -import { SchemaField } from '@stac-manager/data-core'; +import { + Plugin, + PluginEditSchema, + SchemaField, + SchemaFieldArray, + SchemaFieldString +} from '@stac-manager/data-core'; +import { PluginCore } from './collections/core'; /** * @@ -182,7 +189,7 @@ export function tuple2Object(stack: string[][]) { * @returns A boolean indicating whether the specified extension (and version, * if provided) is present in the data. */ -export function hasExtension( +export function hasStacExtension( data: any, extension: string, version?: (v: string) => boolean @@ -193,3 +200,36 @@ export function hasExtension( return match && (!version || version(match[1])); }); } + +/** + * Adds a STAC extension option to the stac_extensions field of the + * CollectionsCore plugin. + * + * @param $this - The plugin instance on which the hook will be registered. Must + * be `this`. + * @param label - The label for the new STAC extension option. + * @param value - The value for the new STAC extension option. + */ +export function addStacExtensionOption( + $this: Plugin, + label: string, + value: string +) { + // Quick way to get the name. + const name = new PluginCore().name; + $this.registerHook(name, { + onAfterEditSchema: (pl, formData: any, schema: PluginEditSchema) => { + if (!schema || typeof schema === 'symbol') { + return schema; + } + + const stac_extensions = schema.properties + .stac_extensions as SchemaFieldArray; + + // Set the new extension value in the schema. + stac_extensions.items.enum!.push([value, label]); + + return schema; + } + }); +} From 8efc247e53e72d40434f242d06e77a0445c65e51 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 18 Mar 2025 10:59:12 +0000 Subject: [PATCH 4/5] Add docs on env variables --- packages/client/README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/client/README.md b/packages/client/README.md index 8f75f1f..d920b12 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -10,10 +10,20 @@ See root README.md for instructions on how to install and run the project. Some client options are controlled by environment variables. These are: ``` +# App config +## Title and description of the app for metadata APP_TITLE APP_DESCRIPTION + +# API +## If the app is being served in from a subfolder, the domain url must be set. +PUBLIC_URL REACT_APP_STAC_BROWSER REACT_APP_STAC_API + +# Theming +REACT_APP_THEME_PRIMARY_COLOR +REACT_APP_THEME_SECONDARY_COLOR ``` You must provide a value for the `REACT_APP_STAC_API` environment variable. This should be the URL of the STAC API you wish to interact with. From 6e8a34b78a9c41b78d6016bd32472c596b63f1a8 Mon Sep 17 00:00:00 2001 From: Daniel da Silva Date: Tue, 18 Mar 2025 12:16:27 +0000 Subject: [PATCH 5/5] Improve hook system and documentation --- docs/PLUGINS.md | 90 ++++++++++++++++++- packages/data-core/lib/plugin-utils/plugin.ts | 17 +++- .../data-core/lib/plugin-utils/resolve.ts | 6 +- packages/data-plugins/lib/utils.ts | 21 ++--- 4 files changed, 115 insertions(+), 19 deletions(-) diff --git a/docs/PLUGINS.md b/docs/PLUGINS.md index 463f75b..938955a 100644 --- a/docs/PLUGINS.md +++ b/docs/PLUGINS.md @@ -12,7 +12,7 @@ - [Object field](#object-field) - [Restrictions](#restrictions) - [Json field](#json-field) - + - [Hooks](#hooks) Each plugin should be created handle a specific part of the data. Avoid creating a single plugin to handle all the data, as this will make the code harder to maintain and understand. @@ -389,3 +389,91 @@ The `json` field is an escape-hatch that allows the user to input any JSON data. ``` ![JSON](images/field-json.png) + +## Hooks + +> [!WARNING] +> The hooks are an advanced feature and should be used with caution. + +Hooks are functions that can be added to the plugins to perform actions at specific points in their lifecycle. + +Available hooks: +- `onAfterInit(targetInstance: Plugin, data: any) => void` - Executed after the target plugin's `init` function. Can be a promise. +- `onAfterEditSchema(targetInstance: Plugin, formData: any, schema: PluginEditSchema) => PluginEditSchema` - Allows changing a plugin's schema. Receives the schema returned by the target plugin's `editSchema` function and the data entered in the form. Should return the modified schema. + +Hooks should be registered in the plugin's constructor: + +```ts +import { Plugin } from '@stac-manager/data-core'; + +export class PluginName extends Plugin { + name = 'Plugin Name'; + + constructor() { + super(); + this.registerHook( + 'Target plugin name', + 'onAfterInit', + (targetInstance: Plugin, data: any) => { + // Do something. + } + ); + + this.registerHook( + 'Target plugin name', + 'onAfterEditSchema', + (targetInstance: Plugin, formData: any, schema: PluginEditSchema) => { + // Do something. + return schema; + } + ); + } +} +``` + +**Real world example** +Whenever an STAC extension plugin is added, it should add the extension to the `stac_extensions` field of the CollectionsCore plugin. This will allow the interface to show the extension as an option in the dropdown. + +This can be achieved with th `onAfterEditSchema` hook: +```ts +import { Plugin } from '@stac-manager/data-core'; + +export class PluginName extends Plugin { + name = 'Plugin Name'; + + constructor() { + super(); + this.registerHook( + 'CollectionsCore', + 'onAfterEditSchema', + (targetInstance: Plugin, formData: any, schema: PluginEditSchema) => { + const stac_extensions = schema.properties + .stac_extensions as SchemaFieldArray; + + // Set the new extension value in the schema. + stac_extensions.items.enum!.push([value, label]); + + return schema; + } + ); + } +} +``` + +> [!TIP] +> Given that this is a common enough use-case there is a helper function (`addStacExtensionOption`) that can be used. +> ```ts +>import { Plugin } from '@stac-manager/data-core'; +>import { addStacExtensionOption } from '@stac-manager/data-plugins'; +> +>class PluginName extends Plugin { +> constructor() { +> super(); +> +> addStacExtensionOption( +> this, +> 'Item Assets Definition', +> 'https://stac-extensions.github.io/item-assets/v1.0.0/schema.json' +> ); +> } +>} \ No newline at end of file diff --git a/packages/data-core/lib/plugin-utils/plugin.ts b/packages/data-core/lib/plugin-utils/plugin.ts index b5debc7..d16c4dd 100644 --- a/packages/data-core/lib/plugin-utils/plugin.ts +++ b/packages/data-core/lib/plugin-utils/plugin.ts @@ -54,10 +54,21 @@ export abstract class Plugin { * * @param targetName - The name of the target plugin to which the hook will be * applied. - * @param hooks - The hook details. + * @param hookName - The name of the hook. + * @param hook - The hook function. */ - registerHook(targetName: string, hooks: Omit) { - this[Plugin.HOOKS].push({ name: targetName, ...hooks }); + registerHook>( + targetName: string, + hookName: K, + hook: PluginHook[K] + ) { + const hookEntry = this[Plugin.HOOKS].find((h) => h.name === targetName); + + if (hookEntry) { + hookEntry[hookName] = hook; + } else { + this[Plugin.HOOKS].push({ name: targetName, [hookName]: hook }); + } } } diff --git a/packages/data-core/lib/plugin-utils/resolve.ts b/packages/data-core/lib/plugin-utils/resolve.ts index f30ca6c..075b9ca 100644 --- a/packages/data-core/lib/plugin-utils/resolve.ts +++ b/packages/data-core/lib/plugin-utils/resolve.ts @@ -50,12 +50,12 @@ export const resolvePlugins = (plugins: PluginConfigItem[], data: any) => { * [Plugin.HOOKS]: [ * { * name: 'pluginA', // Target plugin - * onAfterInit: async (target, data) => { }, // Executes after pluginA's init function. - * onAfterEditSchema: (target, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one. + * onAfterInit: async (targetInstance, data) => { }, // Executes after pluginA's init function. + * onAfterEditSchema: (targetInstance, formData, origEditSchema) => { } // Composes with pluginA's editSchema function and returns a new one. * }, * { * name: 'pluginB', // Target plugin - * onAfterInit: async (target, data) => { }, // Executes after pluginB's init function. + * onAfterInit: async (targetInstance, data) => { }, // Executes after pluginB's init function. * } * ]; * } diff --git a/packages/data-plugins/lib/utils.ts b/packages/data-plugins/lib/utils.ts index 92541cf..8a59eca 100644 --- a/packages/data-plugins/lib/utils.ts +++ b/packages/data-plugins/lib/utils.ts @@ -1,6 +1,5 @@ import { Plugin, - PluginEditSchema, SchemaField, SchemaFieldArray, SchemaFieldString @@ -217,19 +216,17 @@ export function addStacExtensionOption( ) { // Quick way to get the name. const name = new PluginCore().name; - $this.registerHook(name, { - onAfterEditSchema: (pl, formData: any, schema: PluginEditSchema) => { - if (!schema || typeof schema === 'symbol') { - return schema; - } + $this.registerHook(name, 'onAfterEditSchema', (pl, formData, schema) => { + if (!schema || typeof schema === 'symbol') { + return schema; + } - const stac_extensions = schema.properties - .stac_extensions as SchemaFieldArray; + const stac_extensions = schema.properties + .stac_extensions as SchemaFieldArray; - // Set the new extension value in the schema. - stac_extensions.items.enum!.push([value, label]); + // Set the new extension value in the schema. + stac_extensions.items.enum!.push([value, label]); - return schema; - } + return schema; }); }