Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion __test__/uiLocation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ describe("UI Location", () => {
const config = await uiLocation.getConfig();
expect(config).toEqual({});
expect(postRobotSendToParentMock).toHaveBeenLastCalledWith(
"getConfig"
"getConfig", {"context": {"extensionUID": "extension_uid", "installationUID": "installation_uid"}}
);
});
});
Expand Down
6 changes: 0 additions & 6 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2299,12 +2299,6 @@ Following are a list of helpful functions and properties for a JSON RTE instance
| `title` | Title of the field | string |
| `uid` | Unique ID for the field | string |

### `rte.getConfig: () => Object`

Provides configuration which are defined while creating the plugin or while selecting a plugin in the content type builder page.

For example, if your plugin requires API Key or any other config parameters then, you can specify these configurations while creating a new plugin or you can specify field specific configurations from the content type builder page while selecting the plugin. These configurations can be accessed through the `getConfig() `method.

### Methods:

These methods are part of the RTE instance and can be accessed as rte.methodName().
Expand Down
7 changes: 4 additions & 3 deletions src/RTE/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from "slate";

import { RTEPlugin } from "./index";
import UiLocation from "../uiLocation";

declare interface TransformOptions {
at?: Location;
Expand Down Expand Up @@ -48,7 +49,7 @@ export declare interface IRteParam {
voids?: boolean;
}
) => Point | undefined;

sdk: UiLocation;
isPointEqual: (point: Point, another: Point) => boolean;
};

Expand Down Expand Up @@ -139,7 +140,7 @@ export declare interface IRteParam {
getEmbeddedItems: () => { [key: string]: any };
getVariable: <T = unknown>(name: string, defaultValue: any) => T;
setVariable: <T = unknown>(name: string, value: T) => void;
getConfig: <T>() => { [key: string]: T };
sdk: UiLocation;
}

export declare type IRteParamWithPreventDefault = {
Expand Down Expand Up @@ -199,7 +200,7 @@ export declare interface IRteElementType {
children: Array<IRteElementType | IRteTextType>;
}

type IDynamicFunction = (
export type IDynamicFunction = (
element: IRteElementType
) =>
| Exclude<IElementTypeOptions, "text">
Expand Down
60 changes: 58 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import postRobot from "post-robot";

import { InitializationData } from "./types";
import { IRteParam } from "./RTE/types";
import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin";
import UiLocation from "./uiLocation";
import { version } from "../package.json";
import { InitializationData } from "./types";

postRobot.CONFIG.LOG_LEVEL = "error";

Expand Down Expand Up @@ -43,6 +45,53 @@ class ContentstackAppSDK {
.catch((e: Error) => Promise.reject(e));
}

/**
* Registers RTE plugins with the Contentstack platform.
* This method is the primary entry point for defining and registering custom RTE plugins
* built using the PluginBuilder pattern. It returns a function that the Contentstack
* platform will invoke at runtime, providing the necessary context.
*
* @example
* // In your plugin's entry file (e.g., src/index.ts):
* import ContentstackAppSDK from '@contentstack/app-sdk';
* import { PluginBuilder, IRteParam } from '@contentstack/app-sdk/rtePlugin';
*
* const MyCustomPlugin = new PluginBuilder("my-plugin-id")
* .title("My Plugin")
* .icon(<MyIconComponent />)
* .on("exec", (rte: IRteParam) => {
* // Access SDK via rte.sdk if needed:
* const sdk = rte.sdk;
* // ... plugin execution logic ...
* })
* .build();
*
* export default ContentstackAppSDK.registerRTEPlugins(
* MyCustomPlugin
* );
*
* @param {...PluginDefinition} pluginDefinitions - One or more plugin definitions created using the `PluginBuilder`.
* Each `PluginDefinition` describes the plugin's configuration, callbacks, and any child plugins.
* @returns {Promise<{ __isPluginBuilder__: boolean; version: string; plugins: (context: RTEContext, rte: IRteParam) => Promise<{ [key: string]: RTEPlugin; }>; }>}
* A Promise that resolves to an object containing:
* - `__isPluginBuilder__`: A boolean flag indicating this is a builder-based plugin export.
* - `version`: The version of the SDK that registered the plugins.
* - `plugins`: An asynchronous function. This function is designed to be invoked by the
* Contentstack platform loader, providing the `context` (initialization data) and
* the `rte` instance. When called, it materializes and returns a map of the
* registered `RTEPlugin` instances, keyed by their IDs.
*/

static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) {
return {
__isPluginBuilder__: true,
version,
plugins: (context: InitializationData, rte: IRteParam) => {
return registerPlugins(...pluginDefinitions)(context, rte);
}
};
}

/**
* Version of Contentstack App SDK.
*/
Expand All @@ -52,4 +101,11 @@ class ContentstackAppSDK {
}

export default ContentstackAppSDK;
module.exports = ContentstackAppSDK;
export { PluginBuilder };

// CommonJS compatibility
if (typeof module !== 'undefined' && module.exports) {
module.exports = ContentstackAppSDK;
module.exports.default = ContentstackAppSDK;
module.exports.PluginBuilder = PluginBuilder;
}
186 changes: 186 additions & 0 deletions src/rtePlugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE";
import {
IConfig,
IDisplayOnOptions,
IDynamicFunction,
IElementTypeOptions,
IOnFunction,
IRteElementType,
IRteParam,
} from "./RTE/types";
import { InitializationData, IRTEInitData } from "./types";
import UiLocation from "./uiLocation";

type PluginConfigCallback = (sdk: UiLocation) => Promise<IConfig> | IConfig;

interface PluginDefinition {
id: string;
config: Partial<IConfig>;
callbacks: Partial<IOnFunction>;
asyncConfigCallback?: PluginConfigCallback;
childBuilders: PluginBuilder[];
}

class PluginBuilder {
private id: string;
private _config: Partial<IConfig> = {};
private _callbacks: Partial<IOnFunction> = {};
private _asyncConfigCallback?: PluginConfigCallback;
private _childBuilders: PluginBuilder[] = [];

constructor(id: string) {
this.id = id;
this._config.title = id;
}

title(title: string): PluginBuilder {
this._config.title = title;
return this;
}
icon(icon: React.ReactElement | null): PluginBuilder {
this._config.icon = icon;
return this;
}
display(display: IDisplayOnOptions | IDisplayOnOptions[]): PluginBuilder {
this._config.display = display;
return this;
}
elementType(
elementType:
| IElementTypeOptions
| IElementTypeOptions[]
| IDynamicFunction
): PluginBuilder {
this._config.elementType = elementType;
return this;
}
render(renderFn: (...params: any) => React.ReactElement): PluginBuilder {
this._config.render = renderFn;
return this;
}
shouldOverride(
shouldOverrideFn: (element: IRteElementType) => boolean
): PluginBuilder {
this._config.shouldOverride = shouldOverrideFn;
return this;
}
on<T extends keyof IOnFunction>(
type: T,
callback: IOnFunction[T]
): PluginBuilder {
this._callbacks[type] = callback;
return this;
}
configure(callback: PluginConfigCallback): PluginBuilder {
this._asyncConfigCallback = callback;
return this;
}
addPlugins(...builders: PluginBuilder[]): PluginBuilder {
this._childBuilders.push(...builders);
return this;
}

/**
* Builds and returns a definition of the RTE Plugin, ready to be materialized
* into a concrete RTEPlugin instance later when the SDK and Plugin Factory are available.
* This method no longer performs the actual creation of RTEPlugin instances.
*/
build(): PluginDefinition {
return {
id: this.id,
config: this._config,
callbacks: this._callbacks,
asyncConfigCallback: this._asyncConfigCallback,
childBuilders: this._childBuilders,
};
}
}

async function materializePlugin(
pluginDef: PluginDefinition,
sdk: UiLocation
): Promise<Plugin> {
let finalConfig: Partial<IConfig> = { ...pluginDef.config };
if (pluginDef.asyncConfigCallback) {
const dynamicConfig = await Promise.resolve(
pluginDef.asyncConfigCallback(sdk)
);
finalConfig = { ...finalConfig, ...dynamicConfig };
}
const plugin = rtePluginInitializer(
pluginDef.id,
(rte: IRteParam | void) => {
// The rte parameter is passed when the plugin is actually used
// finalConfig already contains the merged configuration
return finalConfig;
}
);
Object.entries(pluginDef.callbacks).forEach(([type, callback]) => {
// Wrap callbacks with error handling
const wrappedCallback = (params: any) => {
try {
return callback(params);
} catch (error) {
console.error(`Error in plugin callback ${type}:`, error);
// Don't re-throw to prevent breaking the RTE
}
};
plugin.on(type as keyof IOnFunction, wrappedCallback);
});
if (pluginDef.childBuilders.length > 0) {
const childPlugins = await Promise.all(
pluginDef.childBuilders.map((childBuilder) =>
materializePlugin(childBuilder.build(), sdk)
)
);
plugin.addPlugins(...childPlugins);
}

return plugin;
}

function registerPlugins(
...pluginDefinitions: PluginDefinition[]
): (
context: InitializationData,
rte: IRteParam
) => Promise<{ [key: string]: Plugin }> {
const definitionsToProcess = [...pluginDefinitions];
const plugins = async (context: InitializationData, rte: IRteParam) => {
try {
const sdk = new UiLocation(context);
console.log("sdk", sdk);

const materializedPlugins: { [key: string]: Plugin } = {};
console.log("materializedPlugins", materializedPlugins);

for (const def of definitionsToProcess) {
const pluginInstance = await materializePlugin(def, sdk);
materializedPlugins[def.id] = pluginInstance;
}
rte.sdk = sdk;
console.log("rte", rte);
console.log("materializedPlugins", materializedPlugins);

return materializedPlugins;
} catch (err) {
console.error("Error during plugin registration:", err);
throw err;
}
};
return plugins;
}

export {
IConfig,
IDisplayOnOptions,
IDynamicFunction,
IElementTypeOptions,
IOnFunction,
IRteElementType,
IRteParam,
Plugin,
PluginBuilder,
PluginDefinition,
registerPlugins
};
4 changes: 2 additions & 2 deletions src/uiLocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,7 +431,7 @@ class UiLocation {
return Promise.resolve(this.config);
}
return this.postRobot
.sendToParent("getConfig")
.sendToParent("getConfig", {context:{installationUID:this.installationUID, extensionUID:this.locationUID}})
.then(onData)
.catch(onError);
};
Expand Down Expand Up @@ -484,7 +484,7 @@ class UiLocation {
*/

api = (url: string, option?: RequestInit): Promise<Response> =>
dispatchApiRequest(url, option) as Promise<Response>;
dispatchApiRequest(url, option, {installationUID:this.installationUID, extensionUID:this.locationUID}) as Promise<Response>;

/**
* Method used to create an adapter for management sdk.
Expand Down
14 changes: 9 additions & 5 deletions src/utils/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,19 @@ import {
} from "axios";

import { axiosToFetchResponse, fetchToAxiosConfig } from "./utils";

/**
* Dispatches a request using PostRobot.
* @param postRobot - The PostRobot instance.
* @returns A function that takes AxiosRequestConfig and returns a promise.
*/
export const dispatchAdapter =
(postRobot: typeof PostRobot) => (config: AxiosRequestConfig) => {
(postRobot: typeof PostRobot) => (config: AxiosRequestConfig, context?: {installationUID: string, extensionUID: string}) => {
return new Promise((resolve, reject) => {
postRobot
.sendToParent("apiAdapter", config)
.sendToParent("apiAdapter", {
config,
context
})
.then((event: unknown) => {
const { data: response } = event as { data: AxiosResponse };

Expand Down Expand Up @@ -56,12 +58,14 @@ export const dispatchAdapter =
*/
export const dispatchApiRequest = async (
url: string,
options?: RequestInit
options?: RequestInit,
context?: {installationUID: string, extensionUID: string}
): Promise<Response> => {
try {
const config = fetchToAxiosConfig(url, options);
const axiosResponse = (await dispatchAdapter(PostRobot)(
config
config,
context
)) as AxiosResponse;

return axiosToFetchResponse(axiosResponse);
Expand Down
Loading