From 2c8cfec83553a36140728162f42568ed8f1e2360 Mon Sep 17 00:00:00 2001 From: MaitriGurey1 Date: Wed, 14 May 2025 13:22:31 +0530 Subject: [PATCH 01/14] feat: added getDraftData method --- src/entry.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/entry.ts b/src/entry.ts index b8bb4d6f..279016a5 100755 --- a/src/entry.ts +++ b/src/entry.ts @@ -91,6 +91,19 @@ class Entry { return this._data; } + /** + * Gets the draft data of the current entry. + * If no changes are available, returns an empty object. + * @return {Object} Returns the draft entry data (_changedData) if available; otherwise, returns an empty object. + */ + getDraftData() { + if (this._changedData && Object.keys(this._changedData).length > 0) { + return this._changedData; + } else { + return {}; + } + } + /** * * From fb887ed49dc74e529d2bda617e24097dd78513a8 Mon Sep 17 00:00:00 2001 From: MaitriGurey1 Date: Wed, 14 May 2025 14:34:42 +0530 Subject: [PATCH 02/14] fix: added test case --- __test__/entry.test.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/__test__/entry.test.ts b/__test__/entry.test.ts index 5fdfff96..a7e506b4 100644 --- a/__test__/entry.test.ts +++ b/__test__/entry.test.ts @@ -57,6 +57,17 @@ describe("Entry", () => { expect(testData.entry).toEqual(entry.getData()); }); + it("getDraftData", () => { + entry._changedData = { title: "Draft Title", content: "Draft Content" }; + expect(entry.getDraftData()).toEqual({ + title: "Draft Title", + content: "Draft Content", + }); + + entry._changedData = {}; + expect(entry.getDraftData()).toEqual({}); + }); + describe("getField", () => { it("getField undefined", function () { const uid = "group1.group"; @@ -133,7 +144,7 @@ describe("Entry", () => { }); it("should use custom Field instance if internal flag is set", () => { const fieldInstance: any = jest.fn(); - entry = new Entry(testData as any, connection as any, emitter ,{ + entry = new Entry(testData as any, connection as any, emitter, { _internalFlags: { FieldInstance: fieldInstance, }, @@ -182,4 +193,4 @@ describe("Entry", () => { "Callback must be a function" ); }); -}); \ No newline at end of file +}); From 6f590ebc88075dbf834ca320de1c4a7f7efa3098 Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Wed, 11 Jun 2025 17:17:10 +0530 Subject: [PATCH 03/14] test: getDraftData --- src/entry.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/entry.ts b/src/entry.ts index 279016a5..bcbcb689 100755 --- a/src/entry.ts +++ b/src/entry.ts @@ -17,6 +17,7 @@ import { import { ContentType, PublishDetails, Schema } from "./types/stack.types"; import { GenericObjectType } from "./types/common.types"; import EventRegistry from "./EventRegistry"; +import { onData, onError } from "./utils/utils"; /** Class representing an entry from Contentstack UI. Not available for Dashboard UI Location. */ @@ -94,14 +95,15 @@ class Entry { /** * Gets the draft data of the current entry. * If no changes are available, returns an empty object. - * @return {Object} Returns the draft entry data (_changedData) if available; otherwise, returns an empty object. + * @return {Promise} Returns a promise that resolves to the draft entry data (_changedData) if available; otherwise, returns an empty object. */ - getDraftData() { - if (this._changedData && Object.keys(this._changedData).length > 0) { - return this._changedData; - } else { - return {}; - } + async getDraftData(): Promise { + const changedData = this._changedData || {}; + console.log("changedData", changedData); + return this._connection + .sendToParent("getDraftData", { changedData }) + .then(onData) + .catch(onError); } /** From 22dd5cab212bd09337b5616d5ebd8f3a5f77464d Mon Sep 17 00:00:00 2001 From: Abhishek Ezhava Date: Thu, 12 Jun 2025 18:11:47 +0530 Subject: [PATCH 04/14] fix: test-cases & version update --- __test__/entry.test.ts | 53 ++++++++++++++++++++++++++++++++++++------ package-lock.json | 4 ++-- package.json | 2 +- src/entry.ts | 22 +++++++++++------- 4 files changed, 62 insertions(+), 19 deletions(-) diff --git a/__test__/entry.test.ts b/__test__/entry.test.ts index a7e506b4..2548935d 100644 --- a/__test__/entry.test.ts +++ b/__test__/entry.test.ts @@ -57,15 +57,54 @@ describe("Entry", () => { expect(testData.entry).toEqual(entry.getData()); }); - it("getDraftData", () => { - entry._changedData = { title: "Draft Title", content: "Draft Content" }; - expect(entry.getDraftData()).toEqual({ - title: "Draft Title", - content: "Draft Content", + describe("getDraftData", () => { + it("should return draft data successfully", async () => { + const mockDraftData = { + title: "Draft Title", + description: "Draft Description", + }; + const sendToParentSpy = jest + .spyOn(connection, "sendToParent") + .mockResolvedValue({ data: mockDraftData }); + + const result = await entry.getDraftData(); + + expect(sendToParentSpy).toHaveBeenCalledWith("getDraftData"); + expect(result).toEqual(mockDraftData); }); - entry._changedData = {}; - expect(entry.getDraftData()).toEqual({}); + it("should return empty object when response data is null", async () => { + const sendToParentSpy = jest + .spyOn(connection, "sendToParent") + .mockResolvedValue({ data: null }); + + const result = await entry.getDraftData(); + + expect(sendToParentSpy).toHaveBeenCalledWith("getDraftData"); + expect(result).toEqual({}); + }); + + it("should return empty object when response data is undefined", async () => { + const sendToParentSpy = jest + .spyOn(connection, "sendToParent") + .mockResolvedValue({ data: undefined }); + + const result = await entry.getDraftData(); + + expect(sendToParentSpy).toHaveBeenCalledWith("getDraftData"); + expect(result).toEqual({}); + }); + + it("should throw error when sendToParent fails", async () => { + const sendToParentSpy = jest + .spyOn(connection, "sendToParent") + .mockRejectedValue(new Error("Connection failed")); + + await expect(entry.getDraftData()).rejects.toThrow( + "Failed to retrieve draft data." + ); + expect(sendToParentSpy).toHaveBeenCalledWith("getDraftData"); + }); }); describe("getField", () => { diff --git a/package-lock.json b/package-lock.json index 99a0c721..6b31b370 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.1", + "version": "2.3.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@contentstack/app-sdk", - "version": "2.3.1", + "version": "2.3.2", "license": "MIT", "dependencies": { "axios": "^1.7.9", diff --git a/package.json b/package.json index 30f8cff3..5fa7d7c6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@contentstack/app-sdk", - "version": "2.3.1", + "version": "2.3.2", "types": "dist/src/index.d.ts", "description": "The Contentstack App SDK allows you to customize your Contentstack applications.", "main": "dist/index.js", diff --git a/src/entry.ts b/src/entry.ts index bcbcb689..14ecd768 100755 --- a/src/entry.ts +++ b/src/entry.ts @@ -93,17 +93,21 @@ class Entry { } /** - * Gets the draft data of the current entry. - * If no changes are available, returns an empty object. - * @return {Promise} Returns a promise that resolves to the draft entry data (_changedData) if available; otherwise, returns an empty object. + * Retrieves the draft data of the current unsaved entry. + * Returns an empty object if there are no changes. + * + * @returns {Promise} The draft entry data or an empty object. */ async getDraftData(): Promise { - const changedData = this._changedData || {}; - console.log("changedData", changedData); - return this._connection - .sendToParent("getDraftData", { changedData }) - .then(onData) - .catch(onError); + try { + const response = + await this._connection.sendToParent( + "getDraftData" + ); + return response?.data ?? {}; + } catch (error) { + throw new Error("Failed to retrieve draft data."); + } } /** From 7e94ebaad68bd29764718c5df567a601cd37bc19 Mon Sep 17 00:00:00 2001 From: Amit Kanswal <41462986+Amitkanswal@users.noreply.github.com> Date: Wed, 9 Jul 2025 12:23:23 +0530 Subject: [PATCH 05/14] feat:updated region support (#151) * fix:region support --- src/types.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index c351baa3..bee397cb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -254,6 +254,7 @@ export enum Region { UNKNOWN = "UNKNOWN", NA = "NA", EU = "EU", + AU = "AU", AZURE_NA = "AZURE_NA", AZURE_EU = "AZURE_EU", GCP_NA = "GCP_NA", @@ -264,7 +265,9 @@ export type RegionType = | "UNKNOWN" | "NA" | "EU" + | "AU" | "AZURE_NA" | "AZURE_EU" | "GCP_NA" - | string; + | "GCP_EU" + | (string & {}); From 7335aceaf2ce55de22eef39380667f7c5b00f44a Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Wed, 16 Jul 2025 11:35:22 +0530 Subject: [PATCH 06/14] 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 aafd86eb4abcc3abb73f81f27c6fb9a862eb6eaf Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Wed, 24 Sep 2025 12:07:30 +0530 Subject: [PATCH 07/14] feat:json rte new implementaion --- README.md | 4 +- __test__/uiLocation.test.ts | 2 +- docs/api-reference.md | 8 +- docs/rte-plugin.md | 296 ++++++++++++++++++++++++++++++++++++ src/RTE/types.tsx | 7 +- src/index.ts | 62 +++++++- src/rtePlugin.ts | 181 ++++++++++++++++++++++ src/uiLocation.ts | 6 +- src/utils/adapter.ts | 16 +- 9 files changed, 558 insertions(+), 24 deletions(-) create mode 100644 docs/rte-plugin.md create mode 100644 src/rtePlugin.ts diff --git a/README.md b/README.md index 8812daf7..c9470dfb 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 @@ -118,4 +118,4 @@ This guide provides instructions for migrating your application to App SDK versi ## License -Licensed under [MIT](https://opensource.org/licenses/MIT). +Licensed under [MIT](https://opensource.org/licenses/MIT). \ No newline at end of file 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"}} ); }); }); diff --git a/docs/api-reference.md b/docs/api-reference.md index 058809dd..af202ee9 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(). @@ -2409,4 +2403,4 @@ const Asset = RTE("asset-picker", () => { Asset.addPlugins(ChooseAsset, UploadAsset); ``` - + \ No newline at end of file diff --git a/docs/rte-plugin.md b/docs/rte-plugin.md new file mode 100644 index 00000000..80a6e794 --- /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 }) => ( +