From 7335aceaf2ce55de22eef39380667f7c5b00f44a Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Wed, 16 Jul 2025 11:35:22 +0530 Subject: [PATCH 1/7] fix:updated region --- src/RTE/types.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx index d119c099..346b4a9c 100644 --- a/src/RTE/types.tsx +++ b/src/RTE/types.tsx @@ -199,7 +199,7 @@ export declare interface IRteElementType { children: Array; } -type IDynamicFunction = ( +export type IDynamicFunction = ( element: IRteElementType ) => | Exclude From fcfa59ad4227f23b9346303365bda828ea3fb3fb Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Thu, 17 Jul 2025 16:01:40 +0530 Subject: [PATCH 2/7] feat:rte-plugin setup --- docs/api-reference.md | 6 -- src/RTE/types.tsx | 5 +- src/index.ts | 60 +++++++++++++- src/rtePlugin.ts | 186 ++++++++++++++++++++++++++++++++++++++++++ src/uiLocation.ts | 4 +- src/utils/adapter.ts | 14 ++-- 6 files changed, 258 insertions(+), 17 deletions(-) create mode 100644 src/rtePlugin.ts diff --git a/docs/api-reference.md b/docs/api-reference.md index 058809dd..019d9f88 100644 --- a/docs/api-reference.md +++ b/docs/api-reference.md @@ -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(). diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx index 346b4a9c..03c6eed1 100644 --- a/src/RTE/types.tsx +++ b/src/RTE/types.tsx @@ -13,6 +13,7 @@ import { } from "slate"; import { RTEPlugin } from "./index"; +import UiLocation from "../uiLocation"; declare interface TransformOptions { at?: Location; @@ -48,7 +49,7 @@ export declare interface IRteParam { voids?: boolean; } ) => Point | undefined; - + sdk: UiLocation; isPointEqual: (point: Point, another: Point) => boolean; }; @@ -139,7 +140,7 @@ export declare interface IRteParam { getEmbeddedItems: () => { [key: string]: any }; getVariable: (name: string, defaultValue: any) => T; setVariable: (name: string, value: T) => void; - getConfig: () => { [key: string]: T }; + sdk: UiLocation; } export declare type IRteParamWithPreventDefault = { diff --git a/src/index.ts b/src/index.ts index b36ddecc..593c3698 100755 --- a/src/index.ts +++ b/src/index.ts @@ -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"; @@ -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() + * .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. */ @@ -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; +} \ No newline at end of file diff --git a/src/rtePlugin.ts b/src/rtePlugin.ts new file mode 100644 index 00000000..8157d4f7 --- /dev/null +++ b/src/rtePlugin.ts @@ -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; + +interface PluginDefinition { + id: string; + config: Partial; + callbacks: Partial; + asyncConfigCallback?: PluginConfigCallback; + childBuilders: PluginBuilder[]; +} + +class PluginBuilder { + private id: string; + private _config: Partial = {}; + private _callbacks: Partial = {}; + 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( + 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 { + let finalConfig: Partial = { ...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 +}; diff --git a/src/uiLocation.ts b/src/uiLocation.ts index 5507b495..a198809b 100755 --- a/src/uiLocation.ts +++ b/src/uiLocation.ts @@ -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); }; @@ -484,7 +484,7 @@ class UiLocation { */ api = (url: string, option?: RequestInit): Promise => - dispatchApiRequest(url, option) as Promise; + dispatchApiRequest(url, option, {installationUID:this.installationUID, extensionUID:this.locationUID}) as Promise; /** * Method used to create an adapter for management sdk. diff --git a/src/utils/adapter.ts b/src/utils/adapter.ts index b96fb4c3..c554605b 100644 --- a/src/utils/adapter.ts +++ b/src/utils/adapter.ts @@ -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 }; @@ -56,12 +58,14 @@ export const dispatchAdapter = */ export const dispatchApiRequest = async ( url: string, - options?: RequestInit + options?: RequestInit, + context?: {installationUID: string, extensionUID: string} ): Promise => { try { const config = fetchToAxiosConfig(url, options); const axiosResponse = (await dispatchAdapter(PostRobot)( - config + config, + context )) as AxiosResponse; return axiosToFetchResponse(axiosResponse); From ed9bc4c15043cc300edc6736859c9eaee690fe91 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Thu, 17 Jul 2025 16:04:05 +0530 Subject: [PATCH 3/7] fix:updated test case --- __test__/uiLocation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/__test__/uiLocation.test.ts b/__test__/uiLocation.test.ts index 20177485..1c743a50 100644 --- a/__test__/uiLocation.test.ts +++ b/__test__/uiLocation.test.ts @@ -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"}} ); }); }); From 302607984e362a173a75a8d1a7097de3631dc441 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Thu, 17 Jul 2025 17:32:25 +0530 Subject: [PATCH 4/7] updated readme and review changes --- README.md | 2 +- docs/rte-plugin.md | 296 +++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 6 +- src/rtePlugin.ts | 20 +-- 4 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 docs/rte-plugin.md diff --git a/README.md b/README.md index 8812daf7..9e049961 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ App Config UI Location allows you to manage all the app settings centrally. Once ### RTE Location -The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. +The RTE Location allows you to create custom plugins to expand the functionality of your JSON Rich Text Editor. Using the Audience and Variables plugin, you can tailor your content as per your requirements. [RTE PLUGIN](docs/rte-plugin.md) ### Sidebar Location diff --git a/docs/rte-plugin.md b/docs/rte-plugin.md new file mode 100644 index 00000000..4ac6b6eb --- /dev/null +++ b/docs/rte-plugin.md @@ -0,0 +1,296 @@ +# JSON RTE Plugin Development Guide + +Quick reference for creating JSON Rich Text Editor plugins using the new simplified approach. + +## 🚀 Quick Start + +```typescript +import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk'; + +// Create a simple plugin +const boldPlugin = new PluginBuilder('bold-plugin') + .title('Bold') + .elementType('inline') + .on('exec', (rte) => { + rte.addMark('bold', true); + }) + .build(); + +// Register the plugin +ContentstackAppSDK.registerRTEPlugins(boldPlugin); +``` + +## 📋 Plugin Types + +### Inline Plugin +For text formatting (bold, italic, etc.) + +```typescript +const italicPlugin = new PluginBuilder('italic') + .title('Italic') + .elementType('inline') + .display(['toolbar', 'hoveringToolbar']) + .on('exec', (rte) => { + rte.addMark('italic', true); + }) + .build(); +``` + +### Block Plugin +For block-level elements (headings, paragraphs, etc.) + +```typescript +const headingPlugin = new PluginBuilder('heading') + .title('Heading') + .elementType('block') + .render(({ children, attrs }) => ( +

+ {children} +

+ )) + .on('exec', (rte) => { + rte.insertNode({ + type: 'heading', + attrs: { level: 2 }, + children: [{ text: 'New Heading' }] + }); + }) + .build(); +``` + +### Void Plugin +For self-closing elements (images, embeds, etc.) + +```typescript +const imagePlugin = new PluginBuilder('image') + .title('Image') + .elementType('void') + .render(({ attrs }) => ( + {attrs.alt + )) + .on('exec', (rte) => { + const src = prompt('Enter image URL:'); + if (src) { + rte.insertNode({ + type: 'image', + attrs: { src }, + children: [{ text: '' }] + }); + } + }) + .build(); +``` + +## 🎛️ Builder Methods + +### Basic Configuration +```typescript +new PluginBuilder('plugin-id') + .title('Plugin Name') // Toolbar button text + .icon() // Button icon (React element) + .elementType('block') // 'inline' | 'block' | 'void' +``` + +### Display Options +```typescript + .display(['toolbar']) // Show in main toolbar only + .display(['hoveringToolbar']) // Show in hover toolbar only + .display(['toolbar', 'hoveringToolbar']) // Show in both +``` + +### Event Handlers +```typescript + .on('exec', (rte) => {}) // Button click + .on('keydown', ({ event, rte }) => {}) // Key press + .on('paste', ({ rte, preventDefault }) => {}) // Paste event +``` + +### Advanced Options +```typescript + .render(ComponentFunction) // Custom render component + .shouldOverride((element) => boolean) // Override existing elements + .configure(async (sdk) => {}) // Dynamic configuration +``` + +## 🔧 Event Handling + +### Click Handler +```typescript +.on('exec', (rte) => { + // Insert text + rte.insertText('Hello World'); + + // Add formatting + rte.addMark('bold', true); + + // Insert node + rte.insertNode({ + type: 'custom-element', + attrs: { id: 'unique-id' }, + children: [{ text: 'Content' }] + }); +}) +``` + +### Keyboard Handler +```typescript +.on('keydown', ({ event, rte }) => { + if (event.key === 'Enter' && event.ctrlKey) { + event.preventDefault(); + // Custom enter behavior + rte.insertBreak(); + } +}) +``` + +## 📦 Container Plugins (Dropdowns) + +Create grouped plugins in a dropdown menu: + +```typescript +const mediaContainer = new PluginBuilder('media-dropdown') + .title('Media') + .icon() + .addPlugins( + imagePlugin, + videoPlugin, + audioPlugin + ) + .build(); +``` + +## 🔄 Plugin Registration + +### Single Plugin +```typescript +ContentstackAppSDK.registerRTEPlugins(myPlugin); +``` + +### Multiple Plugins +```typescript +ContentstackAppSDK.registerRTEPlugins( + boldPlugin, + italicPlugin, + headingPlugin, + imagePlugin +); +``` + +### With Enhanced SDK Context +```typescript +// Register plugins first (captures RTE context) +await ContentstackAppSDK.registerRTEPlugins(myPlugin); + +// Then initialize SDK (gets enhanced context) +const sdk = await ContentstackAppSDK.init(); +``` + +## 💡 Real-World Examples + +### YouTube Embed Plugin +```typescript +const youtubePlugin = new PluginBuilder('youtube') + .title('YouTube') + .elementType('void') + .render(({ attrs }) => ( +