Skip to content

Commit f20779a

Browse files
authored
Merge pull request #162 from contentstack/staging
Staging
2 parents dfadda7 + 911c231 commit f20779a

File tree

9 files changed

+372
-87
lines changed

9 files changed

+372
-87
lines changed

.talismanrc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
scopeconfig:
2+
- scope: node
13
fileignoreconfig:
2-
- filename: .github/workflows/secrets-scan.yml
3-
ignore_detectors:
4-
- filecontent
4+
- filename: package-lock.json
5+
ignore_detectors:
6+
- filecontent
7+
- filename: .github/workflows/secrets-scan.yml
8+
ignore_detectors:
9+
- filecontent
510
version: "1.0"

__test__/uiLocation.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,7 @@ describe("UI Location", () => {
310310
const config = await uiLocation.getConfig();
311311
expect(config).toEqual({});
312312
expect(postRobotSendToParentMock).toHaveBeenLastCalledWith(
313-
"getConfig"
313+
"getConfig", {"context": {"extensionUID": "extension_uid", "installationUID": "installation_uid"}}
314314
);
315315
});
316316
});

package-lock.json

Lines changed: 108 additions & 69 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@contentstack/app-sdk",
3-
"version": "2.3.2",
3+
"version": "2.3.3",
44
"types": "dist/src/index.d.ts",
55
"description": "The Contentstack App SDK allows you to customize your Contentstack applications.",
66
"main": "dist/index.js",
@@ -62,6 +62,7 @@
6262
"jsonfile": "^6.1.0",
6363
"loader-utils": "^3.2.1",
6464
"post-robot": "^8.0.31",
65+
"rxjs": "^7.8.1",
6566
"ssri": "^12.0.0",
6667
"wolfy87-eventemitter": "^5.2.9"
6768
},

src/RTE/types.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
} from "slate";
1414

1515
import { RTEPlugin } from "./index";
16+
import UiLocation from "../uiLocation";
1617

1718
declare interface TransformOptions {
1819
at?: Location;
@@ -48,7 +49,7 @@ export declare interface IRteParam {
4849
voids?: boolean;
4950
}
5051
) => Point | undefined;
51-
52+
sdk: UiLocation;
5253
isPointEqual: (point: Point, another: Point) => boolean;
5354
};
5455

@@ -140,6 +141,7 @@ export declare interface IRteParam {
140141
getVariable: <T = unknown>(name: string, defaultValue: any) => T;
141142
setVariable: <T = unknown>(name: string, value: T) => void;
142143
getConfig: <T>() => { [key: string]: T };
144+
sdk: UiLocation;
143145
}
144146

145147
export declare type IRteParamWithPreventDefault = {
@@ -199,7 +201,7 @@ export declare interface IRteElementType {
199201
children: Array<IRteElementType | IRteTextType>;
200202
}
201203

202-
type IDynamicFunction = (
204+
export type IDynamicFunction = (
203205
element: IRteElementType
204206
) =>
205207
| Exclude<IElementTypeOptions, "text">

src/index.ts

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import postRobot from "post-robot";
22

3+
import { InitializationData } from "./types";
4+
import { IRteParam } from "./RTE/types";
5+
import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin";
36
import UiLocation from "./uiLocation";
47
import { version } from "../package.json";
5-
import { InitializationData } from "./types";
68

79
postRobot.CONFIG.LOG_LEVEL = "error";
810

@@ -43,6 +45,55 @@ class ContentstackAppSDK {
4345
.catch((e: Error) => Promise.reject(e));
4446
}
4547

48+
/**
49+
* Registers RTE plugins with the Contentstack platform.
50+
* This method is the primary entry point for defining and registering custom RTE plugins
51+
* built using the PluginBuilder pattern. It returns a function that the Contentstack
52+
* platform will invoke at runtime, providing the necessary context.
53+
*
54+
* @example
55+
* // In your plugin's entry file (e.g., src/index.ts):
56+
* import ContentstackAppSDK, { PluginBuilder } from '@contentstack/app-sdk';
57+
*
58+
* const MyCustomPlugin = new PluginBuilder("my-plugin-id")
59+
* .title("My Plugin")
60+
* .icon(<MyIconComponent />)
61+
* .elementType("block")
62+
* .display("toolbar")
63+
* .render(()=>{return <Comment />})
64+
* .on("exec", (rte: IRteParam) => {
65+
* // Access SDK via rte.sdk if needed:
66+
* const sdk = rte.sdk;
67+
* // ... plugin execution logic ...
68+
* })
69+
* .build();
70+
*
71+
* export default ContentstackAppSDK.registerRTEPlugins(
72+
* MyCustomPlugin
73+
* );
74+
*
75+
* @param {...PluginDefinition} pluginDefinitions - One or more plugin definitions created using the `PluginBuilder`.
76+
* Each `PluginDefinition` describes the plugin's configuration, callbacks, and any child plugins.
77+
* @returns {Promise<{ __isPluginBuilder__: boolean; version: string; plugins: (context: RTEContext, rte: IRteParam) => Promise<{ [key: string]: RTEPlugin; }>; }>}
78+
* A Promise that resolves to an object containing:
79+
* - `__isPluginBuilder__`: A boolean flag indicating this is a builder-based plugin export.
80+
* - `version`: The version of the SDK that registered the plugins.
81+
* - `plugins`: An asynchronous function. This function is designed to be invoked by the
82+
* Contentstack platform loader, providing the `context` (initialization data) and
83+
* the `rte` instance. When called, it materializes and returns a map of the
84+
* registered `RTEPlugin` instances, keyed by their IDs.
85+
*/
86+
87+
static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) {
88+
return {
89+
__isPluginBuilder__: true,
90+
version,
91+
plugins: (context: InitializationData, rte: IRteParam) => {
92+
return registerPlugins(...pluginDefinitions)(context, rte);
93+
}
94+
};
95+
}
96+
4697
/**
4798
* Version of Contentstack App SDK.
4899
*/
@@ -52,4 +103,11 @@ class ContentstackAppSDK {
52103
}
53104

54105
export default ContentstackAppSDK;
55-
module.exports = ContentstackAppSDK;
106+
export { PluginBuilder };
107+
108+
// CommonJS compatibility
109+
if (typeof module !== 'undefined' && module.exports) {
110+
module.exports = ContentstackAppSDK;
111+
module.exports.default = ContentstackAppSDK;
112+
module.exports.PluginBuilder = PluginBuilder;
113+
}

src/rtePlugin.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE";
2+
import {
3+
IConfig,
4+
IDisplayOnOptions,
5+
IDynamicFunction,
6+
IElementTypeOptions,
7+
IOnFunction,
8+
IRteElementType,
9+
IRteParam,
10+
} from "./RTE/types";
11+
import { InitializationData } from "./types";
12+
import UiLocation from "./uiLocation";
13+
14+
type PluginConfigCallback = (sdk: UiLocation) => Promise<IConfig> | IConfig;
15+
16+
interface PluginDefinition {
17+
id: string;
18+
config: Partial<IConfig>;
19+
callbacks: Partial<IOnFunction>;
20+
asyncConfigCallback?: PluginConfigCallback;
21+
childBuilders: PluginBuilder[];
22+
}
23+
24+
class PluginBuilder {
25+
private id: string;
26+
private _config: Partial<IConfig> = {};
27+
private _callbacks: Partial<IOnFunction> = {};
28+
private _asyncConfigCallback?: PluginConfigCallback;
29+
private _childBuilders: PluginBuilder[] = [];
30+
31+
constructor(id: string) {
32+
this.id = id;
33+
this._config.title = id;
34+
}
35+
36+
title(title: string): PluginBuilder {
37+
this._config.title = title;
38+
return this;
39+
}
40+
icon(icon: React.ReactElement | null): PluginBuilder {
41+
this._config.icon = icon;
42+
return this;
43+
}
44+
display(display: IDisplayOnOptions | IDisplayOnOptions[]): PluginBuilder {
45+
this._config.display = display;
46+
return this;
47+
}
48+
elementType(
49+
elementType:
50+
| IElementTypeOptions
51+
| IElementTypeOptions[]
52+
| IDynamicFunction
53+
): PluginBuilder {
54+
this._config.elementType = elementType;
55+
return this;
56+
}
57+
render(renderFn: (element: React.ReactElement, attrs: { [key: string]: any }, path: number[], rte: IRteParam) => React.ReactElement): PluginBuilder {
58+
this._config.render = renderFn;
59+
return this;
60+
}
61+
shouldOverride(
62+
shouldOverrideFn: (element: IRteElementType) => boolean
63+
): PluginBuilder {
64+
this._config.shouldOverride = shouldOverrideFn;
65+
return this;
66+
}
67+
on<T extends keyof IOnFunction>(
68+
type: T,
69+
callback: IOnFunction[T]
70+
): PluginBuilder {
71+
this._callbacks[type] = callback;
72+
return this;
73+
}
74+
configure(callback: PluginConfigCallback): PluginBuilder {
75+
this._asyncConfigCallback = callback;
76+
return this;
77+
}
78+
addPlugins(...builders: PluginBuilder[]): PluginBuilder {
79+
this._childBuilders.push(...builders);
80+
return this;
81+
}
82+
83+
/**
84+
* Builds and returns a definition of the RTE Plugin, ready to be materialized
85+
* into a concrete RTEPlugin instance later when the SDK and Plugin Factory are available.
86+
* This method no longer performs the actual creation of RTEPlugin instances.
87+
*/
88+
build(): PluginDefinition {
89+
return {
90+
id: this.id,
91+
config: this._config,
92+
callbacks: this._callbacks,
93+
asyncConfigCallback: this._asyncConfigCallback,
94+
childBuilders: this._childBuilders,
95+
};
96+
}
97+
}
98+
99+
async function materializePlugin(
100+
pluginDef: PluginDefinition,
101+
sdk: UiLocation
102+
): Promise<Plugin> {
103+
let finalConfig: Partial<IConfig> = { ...pluginDef.config };
104+
if (pluginDef.asyncConfigCallback) {
105+
const dynamicConfig = await Promise.resolve(
106+
pluginDef.asyncConfigCallback(sdk)
107+
);
108+
finalConfig = { ...finalConfig, ...dynamicConfig };
109+
}
110+
111+
const plugin = rtePluginInitializer(
112+
pluginDef.id,
113+
(rte: IRteParam | void) => {
114+
return finalConfig;
115+
}
116+
);
117+
118+
Object.entries(pluginDef.callbacks).forEach(([type, callback]) => {
119+
plugin.on(type as keyof IOnFunction, callback);
120+
});
121+
122+
if (pluginDef.childBuilders.length > 0) {
123+
const childPlugins = await Promise.all(
124+
pluginDef.childBuilders.map((childBuilder) =>
125+
materializePlugin(childBuilder.build(), sdk)
126+
)
127+
);
128+
plugin.addPlugins(...childPlugins);
129+
}
130+
131+
return plugin;
132+
}
133+
134+
function registerPlugins(
135+
...pluginDefinitions: PluginDefinition[]
136+
): (
137+
context: InitializationData,
138+
rte: IRteParam
139+
) => Promise<{ [key: string]: Plugin }> {
140+
const definitionsToProcess = [...pluginDefinitions];
141+
const plugins = async (context: InitializationData, rte: IRteParam) => {
142+
try {
143+
const sdk = new UiLocation(context);
144+
const materializedPlugins: { [key: string]: Plugin } = {};
145+
for (const def of definitionsToProcess) {
146+
const pluginInstance = await materializePlugin(def, sdk);
147+
materializedPlugins[def.id] = pluginInstance;
148+
}
149+
rte.sdk = sdk;
150+
return materializedPlugins;
151+
} catch (err) {
152+
console.error("Error during plugin registration:", err);
153+
throw err;
154+
}
155+
};
156+
return plugins;
157+
}
158+
159+
export {
160+
IConfig,
161+
IDisplayOnOptions,
162+
IDynamicFunction,
163+
IElementTypeOptions,
164+
IOnFunction,
165+
IRteElementType,
166+
IRteParam,
167+
Plugin,
168+
PluginBuilder,
169+
PluginDefinition,
170+
registerPlugins
171+
};

src/uiLocation.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -431,7 +431,7 @@ class UiLocation {
431431
return Promise.resolve(this.config);
432432
}
433433
return this.postRobot
434-
.sendToParent("getConfig")
434+
.sendToParent("getConfig", {context:{installationUID:this.installationUID, extensionUID:this.locationUID}})
435435
.then(onData)
436436
.catch(onError);
437437
};
@@ -485,7 +485,7 @@ class UiLocation {
485485

486486
api = (url: string, option?: RequestInit): Promise<Response> =>
487487
dispatchApiRequest(url, option) as Promise<Response>;
488-
488+
489489
/**
490490
* Method used to create an adapter for management sdk.
491491
*/
@@ -519,4 +519,4 @@ class UiLocation {
519519
}
520520
}
521521

522-
export default UiLocation;
522+
export default UiLocation;

0 commit comments

Comments
 (0)