From 26841bcdfa65fadbfaa3b1f83b7e9eeb525f78f9 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:15 +0530 Subject: [PATCH 01/24] sca-scan.yml From 5c723cda577d0308d9af0093abb8c853cc17e18c Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:19 +0530 Subject: [PATCH 02/24] policy-scan.yml --- .github/workflows/policy-scan.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .github/workflows/policy-scan.yml diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml new file mode 100644 index 00000000..13bd3623 --- /dev/null +++ b/.github/workflows/policy-scan.yml @@ -0,0 +1,27 @@ +name: Checks the security policy and configurations +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + security-policy: + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@master + - name: Checks for SECURITY.md policy file + run: | + if ! [[ -f "SECURITY.md" || -f ".github/SECURITY.md" ]]; then exit 1; fi + security-license: + if: github.event.repository.visibility == 'public' + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - uses: actions/checkout@master + - name: Checks for License file + run: | + if ! [[ -f "LICENSE" || -f "License.txt" || -f "LICENSE.md" ]]; then exit 1; fi \ No newline at end of file From 54b583f3e96d8e7143ce91e1e6e9a5cefd4235e1 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:30 +0530 Subject: [PATCH 03/24] issues-jira.yml --- .github/workflows/issues-jira.yml | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/issues-jira.yml diff --git a/.github/workflows/issues-jira.yml b/.github/workflows/issues-jira.yml new file mode 100644 index 00000000..7bf04694 --- /dev/null +++ b/.github/workflows/issues-jira.yml @@ -0,0 +1,31 @@ +name: Create Jira Ticket for Github Issue + +on: + issues: + types: [opened] + +jobs: + issue-jira: + runs-on: ubuntu-latest + steps: + + - name: Login to Jira + uses: atlassian/gajira-login@master + env: + JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} + JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} + JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + + - name: Create Jira Issue + id: create_jira + uses: atlassian/gajira-create@master + with: + project: ${{ secrets.JIRA_PROJECT }} + issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} + summary: Github | Issue | ${{ github.event.repository.name }} | ${{ github.event.issue.title }} + description: | + *GitHub Issue:* ${{ github.event.issue.html_url }} + + *Description:* + ${{ github.event.issue.body }} + fields: "${{ secrets.ISSUES_JIRA_FIELDS }}" \ No newline at end of file From f5c66108a312af0d690b2f8e1fbfea1b0e24572c Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:31 +0530 Subject: [PATCH 04/24] Delete jira.yml --- .github/workflows/jira.yml | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 .github/workflows/jira.yml diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml deleted file mode 100644 index 250abc76..00000000 --- a/.github/workflows/jira.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Create JIRA ISSUE -on: - pull_request: - types: [opened] -jobs: - security-jira: - if: ${{ github.actor == 'dependabot[bot]' || github.actor == 'snyk-bot' || contains(github.event.pull_request.head.ref, 'snyk-fix-') || contains(github.event.pull_request.head.ref, 'snyk-upgrade-')}} - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Login into JIRA - uses: atlassian/gajira-login@master - env: - JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }} - JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} - JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} - - name: Create a JIRA Issue - id: create - uses: atlassian/gajira-create@master - with: - project: ${{ secrets.JIRA_PROJECT }} - issuetype: ${{ secrets.JIRA_ISSUE_TYPE }} - summary: | - Snyk | Vulnerability | ${{ github.event.repository.name }} | ${{ github.event.pull_request.title }} - description: | - PR: ${{ github.event.pull_request.html_url }} - - fields: "${{ secrets.JIRA_FIELDS }}" - - name: Transition issue - uses: atlassian/gajira-transition@v3 - with: - issue: ${{ steps.create.outputs.issue }} - transition: ${{ secrets.JIRA_TRANSITION }} From 618c3cc44bad62ffecf106168555120c6926ce53 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:32 +0530 Subject: [PATCH 05/24] Delete sast-scan.yml --- .github/workflows/sast-scan.yml | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 .github/workflows/sast-scan.yml diff --git a/.github/workflows/sast-scan.yml b/.github/workflows/sast-scan.yml deleted file mode 100644 index 3b9521a5..00000000 --- a/.github/workflows/sast-scan.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: SAST Scan -on: - pull_request: - types: [opened, synchronize, reopened] -jobs: - security-sast: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Semgrep Scan - run: docker run -v /var/run/docker.sock:/var/run/docker.sock -v "${PWD}:/src" returntocorp/semgrep semgrep scan --config auto \ No newline at end of file From 9f9ce970d5501b7ed434607de3ef34ae01e191a0 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Tue, 15 Apr 2025 18:12:36 +0530 Subject: [PATCH 06/24] Updated codeowners From d0c50468aea1702cc59d9eba4b6b5d210cea4489 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Wed, 16 Apr 2025 13:00:30 +0530 Subject: [PATCH 07/24] policy-scan.yml From 012a3c7f0d35c6a292891c5aa00542ff01c83808 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Wed, 16 Apr 2025 13:00:41 +0530 Subject: [PATCH 08/24] issues-jira.yml From 8fa8f8f94c41bcfbda1992593bc66d62c3889ab4 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Wed, 16 Apr 2025 13:00:46 +0530 Subject: [PATCH 09/24] Updated codeowners From 56f7058ec0c8ced90246bedc978ca2bb05780fcc Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Wed, 23 Apr 2025 21:31:36 +0530 Subject: [PATCH 10/24] policy-scan.yml --- .github/workflows/policy-scan.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml index 13bd3623..ff259231 100644 --- a/.github/workflows/policy-scan.yml +++ b/.github/workflows/policy-scan.yml @@ -24,4 +24,23 @@ jobs: - uses: actions/checkout@master - name: Checks for License file run: | - if ! [[ -f "LICENSE" || -f "License.txt" || -f "LICENSE.md" ]]; then exit 1; fi \ No newline at end of file + expected_license_files=("LICENSE" "LICENSE.txt" "LICENSE.md" "License.txt") + license_file_found=false + current_year=$(date +"%Y") + + for license_file in "${expected_license_files[@]}"; do + if [ -f "$license_file" ]; then + license_file_found=true + # check the license file for the current year, if not exists, exit with error + if ! grep -q "$current_year" "$license_file"; then + echo "License file $license_file does not contain the current year." + exit 2 + fi + break + fi + done + + if [ "$license_file_found" = false ]; then + echo "No license file found. Please add a license file to the repository." + exit 1 + fi \ No newline at end of file From d3ac70603a14d61e53411261e46fcd171bb7afbf Mon Sep 17 00:00:00 2001 From: Naveen S Date: Thu, 10 Apr 2025 13:13:30 +0530 Subject: [PATCH 11/24] fix: renamed org full page to global full page --- __test__/organizationFullPage.test.ts | 16 +-- __test__/uiLocation.test.ts | 154 +++++++++++++++----------- package-lock.json | 2 +- src/types.ts | 22 ++-- src/uiLocation.ts | 10 +- 5 files changed, 121 insertions(+), 83 deletions(-) diff --git a/__test__/organizationFullPage.test.ts b/__test__/organizationFullPage.test.ts index 75b63509..34ead959 100644 --- a/__test__/organizationFullPage.test.ts +++ b/__test__/organizationFullPage.test.ts @@ -1,13 +1,13 @@ -import { IOrgFullPageLocationInitData, LocationType } from "../src/types"; +import { IGlobalFullPageLocationInitData, LocationType } from "../src/types"; import { OrganizationDetails } from "../src/types/organization.types"; -const mockData: IOrgFullPageLocationInitData = { - type: LocationType.ORGANIZATION_FULL_PAGE, +const mockData: IGlobalFullPageLocationInitData = { + type: LocationType.GLOBAL_FULL_PAGE_LOCATION, app_id: "app_id", installation_uid: "installation_uid", extension_uid: "extension_uid", region: "NA", - endpoints:{CMA:"",APP:"",DEVELOPER_HUB:""}, + endpoints: { CMA: "", APP: "", DEVELOPER_HUB: "" }, stack: {} as any, user: {} as any, currentBranch: "currentBranch", @@ -22,11 +22,13 @@ afterEach(() => { }); test("should return organization details", () => { - expect(organizationFullPage.currentOrganization).toBe(mockData.organization); + expect(organizationFullPage.currentOrganization).toBe( + mockData.organization + ); }); test("should handle missing organization details", () => { - const invalidData: IOrgFullPageLocationInitData = { + const invalidData: IGlobalFullPageLocationInitData = { ...mockData, organization: null as any, // check missing organization details }; @@ -34,4 +36,4 @@ test("should handle missing organization details", () => { currentOrganization: invalidData.organization, }; expect(invalidOrganizationFullPage.currentOrganization).toBeNull(); -}); \ No newline at end of file +}); diff --git a/__test__/uiLocation.test.ts b/__test__/uiLocation.test.ts index ebdacb11..20177485 100644 --- a/__test__/uiLocation.test.ts +++ b/__test__/uiLocation.test.ts @@ -1,3 +1,5 @@ +import { AxiosRequestConfig, AxiosResponse } from "axios"; + import postRobot from "post-robot"; import UiLocation from "../src/uiLocation"; @@ -12,9 +14,8 @@ import { LocationType, Region, } from "../src/types"; -import { RequestOption } from '../src/types/common.types'; -import { RequestConfig } from '../src/types/api.type'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { RequestOption } from "../src/types/common.types"; +import { RequestConfig } from "../src/types/api.type"; jest.mock("post-robot"); jest.mock("wolfy87-eventemitter"); @@ -60,7 +61,11 @@ const initData: IAppConfigInitData = { installation_uid: "installation_uid", extension_uid: "extension_uid", region: "NA", - endpoints: { CMA: "https://api.contentstack.io", APP: "https://app.contentstack.app",DEVELOPER_HUB:"" }, + endpoints: { + CMA: "https://api.contentstack.io", + APP: "https://app.contentstack.app", + DEVELOPER_HUB: "", + }, stack: mockStackData, user: {} as any, currentBranch: "currentBranch", @@ -135,7 +140,7 @@ describe("UI Location", () => { }); }); - describe('dispatchPostRobotRequest', () => { + describe("dispatchPostRobotRequest", () => { let mockPostRobot: typeof postRobot; let opts: RequestOption; let uiLocationInstance: UiLocation; @@ -143,35 +148,40 @@ describe("UI Location", () => { beforeEach(() => { mockPostRobot = postRobot; - opts = { method: 'GET' }; + opts = { method: "GET" }; uiLocationInstance = new UiLocation(initData); onError = jest.fn(); uiLocationInstance.api = jest.fn().mockResolvedValue({ - method: 'GET', - url: "https://test.com/test?limit=10&skip=0" + method: "GET", + url: "https://test.com/test?limit=10&skip=0", }); }); - it('should call sendToParent with the correct arguments and resolve with data', async () => { + it("should call sendToParent with the correct arguments and resolve with data", async () => { const mockData = { success: true }; // Call the method that uses uiLocationInstance.api - const result = await uiLocationInstance.api("https://test.com/test?limit=10&skip=0",{ - method: 'GET' - }); + const result = await uiLocationInstance.api( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ); // Assertions - expect(uiLocationInstance.api).toHaveBeenCalledWith('https://test.com/test?limit=10&skip=0',{ - method: 'GET' - }); + expect(uiLocationInstance.api).toHaveBeenCalledWith( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ); expect(result).toEqual({ - method: 'GET', - url: 'https://test.com/test?limit=10&skip=0', + method: "GET", + url: "https://test.com/test?limit=10&skip=0", }); - }); - it('should call onError if sendToParent rejects', async () => { - const mockError = new Error('Test error'); + it("should call onError if sendToParent rejects", async () => { + const mockError = new Error("Test error"); // Mock the api method to reject with an error uiLocationInstance.api = jest.fn().mockRejectedValue(mockError); @@ -182,84 +192,102 @@ describe("UI Location", () => { }); // Call the method that uses uiLocationInstance.api and expect it to throw an error - await expect(uiLocationInstance.api("https://test.com/test?limit=10&skip=0",{ - method: 'GET' - })).rejects.toThrow('Test error'); + await expect( + uiLocationInstance.api( + "https://test.com/test?limit=10&skip=0", + { + method: "GET", + } + ) + ).rejects.toThrow("Test error"); }); }); - describe("createSDKAdapter", () => { let mockPostRobot: typeof postRobot; let opts: RequestConfig; let uiLocationInstance: UiLocation; let onError: jest.Mock; - + beforeEach(() => { mockPostRobot = postRobot; - opts = { method: 'GET', baseURL: "https://test.com", url: "/test?limit10&skip=0" }; + opts = { + method: "GET", + baseURL: "https://test.com", + url: "/test?limit10&skip=0", + }; uiLocationInstance = new UiLocation(initData); onError = jest.fn(); - uiLocationInstance.createAdapter = jest.fn().mockImplementation(() => async (config: AxiosRequestConfig) => { - return { - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} - } as unknown as AxiosResponse; - }); + uiLocationInstance.createAdapter = jest + .fn() + .mockImplementation( + () => async (config: AxiosRequestConfig) => { + return { + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, + } as unknown as AxiosResponse; + } + ); }); - + afterEach(() => { postRobotOnMock.mockClear(); postRobotSendToParentMock.mockClear(); - + jest.clearAllMocks(); window["postRobot"] = undefined; window["iframeRef"] = undefined; }); - - it('should call createAdapter with the correct arguments and resolve with data', async () => { + + it("should call createAdapter with the correct arguments and resolve with data", async () => { const mockData = { success: true }; // Call the method that uses uiLocationInstance.createAdapter const result = await uiLocationInstance.createAdapter()({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, }); - + expect(result).toEqual({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, }); }); - - it('should call onError if createAdapter rejects', async () => { - const mockError = new Error('Test error'); - + + it("should call onError if createAdapter rejects", async () => { + const mockError = new Error("Test error"); + // Mock the createAdapter method to reject with an error - uiLocationInstance.createAdapter = jest.fn().mockImplementation(() => async (config: AxiosRequestConfig) => { - throw mockError; - }); - + uiLocationInstance.createAdapter = jest + .fn() + .mockImplementation( + () => async (config: AxiosRequestConfig) => { + throw mockError; + } + ); + // Mock the onError implementation onError.mockImplementation((error) => { throw error; }); - + // Call the method that uses uiLocationInstance.createAdapter and expect it to throw an error - await expect(uiLocationInstance.createAdapter()({ - method: 'GET', - url: '/test?limit=10&skip=0', - baseURL: 'https://test.com', - data: {} - })).rejects.toThrow('Test error'); + await expect( + uiLocationInstance.createAdapter()({ + method: "GET", + url: "/test?limit=10&skip=0", + baseURL: "https://test.com", + data: {}, + }) + ).rejects.toThrow("Test error"); }); }); - + describe("getConfig", () => { it("should return config if no installation uid present", async () => { const uiLocation = new UiLocation(initDataJsonRte as any); diff --git a/package-lock.json b/package-lock.json index 9be09aac..03a3bf97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,7 +46,7 @@ "webpack-dev-server": "^4.7.3" }, "engines": { - "node": "18.x" + "node": ">=18.x" } }, "node_modules/@adobe/css-tools": { diff --git a/src/types.ts b/src/types.ts index c71b5136..c351baa3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -7,7 +7,7 @@ import { GenericObjectType } from "./types/common.types"; import { Entry } from "./types/entry.types"; import { Asset, ContentType, Schema, StackDetail } from "./types/stack.types"; import { OrganizationDetails } from "./types/organization.types"; -import { ContentstackEndpoints } from './types/api.type'; +import { ContentstackEndpoints } from "./types/api.type"; import { User } from "./types/user.types"; import Window from "./window"; @@ -69,7 +69,7 @@ export declare interface IAppConfigWidget { stack: Stack; } -export declare interface IOrgFullPageLocation { +export declare interface IGlobalFullPageLocation { currentOrganization: OrganizationDetails; } @@ -88,7 +88,7 @@ export enum LocationType { RTE = "RTE", WIDGET = "WIDGET", CONTENT_TYPE_SIDEBAR_WIDGET = "CONTENT_TYPE_SIDEBAR_WIDGET", - ORGANIZATION_FULL_PAGE = "ORGANIZATION_FULL_PAGE", + GLOBAL_FULL_PAGE_LOCATION = "GLOBAL_FULL_PAGE_LOCATION", } // Init data @@ -106,10 +106,11 @@ declare interface ICommonInitData { endpoints: ContentstackEndpoints; } -export declare interface IOrgFullPageLocationInitData extends ICommonInitData { +export declare interface IGlobalFullPageLocationInitData + extends ICommonInitData { organization: OrganizationDetails; config?: GenericObjectType; - type: LocationType.ORGANIZATION_FULL_PAGE; + type: LocationType.GLOBAL_FULL_PAGE_LOCATION; } export declare interface IDashboardInitData extends ICommonInitData { @@ -217,7 +218,7 @@ export type InitializationData = | IRTEInitData | ISidebarInitData | IContentTypeSidebarInitData - | IOrgFullPageLocationInitData; + | IGlobalFullPageLocationInitData; /** * installation details API response @@ -259,4 +260,11 @@ export enum Region { GCP_EU = "GCP_EU", } -export type RegionType = "UNKNOWN" | "NA" | "EU" | "AZURE_NA" | "AZURE_EU" | "GCP_NA" | string; \ No newline at end of file +export type RegionType = + | "UNKNOWN" + | "NA" + | "EU" + | "AZURE_NA" + | "AZURE_EU" + | "GCP_NA" + | string; diff --git a/src/uiLocation.ts b/src/uiLocation.ts index 443aca9b..5507b495 100755 --- a/src/uiLocation.ts +++ b/src/uiLocation.ts @@ -27,7 +27,7 @@ import { InitializationData, LocationType, Manifest, - IOrgFullPageLocation, + IGlobalFullPageLocation, RegionType, } from "./types"; import { GenericObjectType } from "./types/common.types"; @@ -132,7 +132,7 @@ class UiLocation { FullPage: IFullPageLocation | null; FieldModifierLocation: IFieldModifierLocation | null; ContentTypeSidebarWidget: ContentTypeSidebarWidget | null; - OrganizationFullPage: IOrgFullPageLocation | null; + GlobalFullPageLocation: IGlobalFullPageLocation | null; }; constructor(initData: InitializationData) { @@ -180,7 +180,7 @@ class UiLocation { FullPage: null, FieldModifierLocation: null, ContentTypeSidebarWidget: null, - OrganizationFullPage: null, + GlobalFullPageLocation: null, }; window["postRobot"] = postRobot; @@ -289,8 +289,8 @@ class UiLocation { break; } - case LocationType.ORGANIZATION_FULL_PAGE: { - this.location.OrganizationFullPage = { + case LocationType.GLOBAL_FULL_PAGE_LOCATION: { + this.location.GlobalFullPageLocation = { currentOrganization: initializationData.organization, }; break; From c69c8744a1d2ab8072cb123fc5b8dde2cc7bb9ab Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Mon, 5 May 2025 21:34:03 +0530 Subject: [PATCH 12/24] policy-scan.yml From 3b571ffb48625e5ea05e4c31acc8a9ca7e977cf4 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Mon, 5 May 2025 21:34:10 +0530 Subject: [PATCH 13/24] issues-jira.yml From e471fec216d78b8da4c2d2573fb19507142f9140 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Mon, 5 May 2025 21:34:11 +0530 Subject: [PATCH 14/24] secrets-scan.yml --- .github/workflows/secrets-scan.yml | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 .github/workflows/secrets-scan.yml diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml new file mode 100644 index 00000000..049c02f4 --- /dev/null +++ b/.github/workflows/secrets-scan.yml @@ -0,0 +1,29 @@ +name: Secrets Scan +on: + pull_request: + types: [opened, synchronize, reopened] +jobs: + security-secrets: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: '2' + ref: '${{ github.event.pull_request.head.ref }}' + - run: | + git reset --soft HEAD~1 + - name: Install Talisman + run: | + # Download Talisman + wget https://github.com/thoughtworks/talisman/releases/download/v1.37.0/talisman_linux_amd64 -O talisman + + # Checksum verification + checksum=$(sha256sum ./talisman | awk '{print $1}') + if [ "$checksum" != "8e0ae8bb7b160bf10c4fa1448beb04a32a35e63505b3dddff74a092bccaaa7e4" ]; then exit 1; fi + + # Make it executable + chmod +x talisman + - name: Run talisman + run: | + # Run Talisman with the pre-commit hook + ./talisman --githook pre-commit \ No newline at end of file From 8a28676626070c8dcd74bdabcc6cbdb0dde24269 Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Mon, 5 May 2025 21:34:16 +0530 Subject: [PATCH 15/24] Updated codeowners From a2807cf9512d4bd3ba2016adb8188157e1f2125e Mon Sep 17 00:00:00 2001 From: Aravind Kumar Date: Mon, 5 May 2025 23:23:53 +0530 Subject: [PATCH 16/24] talismanrc file updated --- .talismanrc | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .talismanrc diff --git a/.talismanrc b/.talismanrc new file mode 100644 index 00000000..345ca2f2 --- /dev/null +++ b/.talismanrc @@ -0,0 +1,5 @@ +fileignoreconfig: +- filename: .github/workflows/secrets-scan.yml + ignore_detectors: + - filecontent +version: "1.0" \ No newline at end of file From f6f6c126aee7edfd0815ffdcef4ebd68b63705f6 Mon Sep 17 00:00:00 2001 From: "Rijil T. R" Date: Thu, 5 Jun 2025 18:54:36 +0530 Subject: [PATCH 17/24] feat(rte): Introduce PluginBuilder for declarative RTE plugin definition --- src/RTE/index.tsx | 2 +- src/RTE/types.tsx | 12 ++-- src/index.ts | 59 +++++++++++++++- src/rtePlugin.ts | 167 +++++++++++++++++++++++++++++++++++++++++++++ src/types/rte.d.ts | 7 ++ 5 files changed, 238 insertions(+), 9 deletions(-) create mode 100644 src/rtePlugin.ts create mode 100644 src/types/rte.d.ts diff --git a/src/RTE/index.tsx b/src/RTE/index.tsx index b4cdd78b..deeffe76 100644 --- a/src/RTE/index.tsx +++ b/src/RTE/index.tsx @@ -1,10 +1,10 @@ import { + IConfig, IConfigCallback, IContainerMetaData, IOnFunction, IPluginMetaData, IRteParam, - IConfig, } from "./types"; export class RTEPlugin { diff --git a/src/RTE/types.tsx b/src/RTE/types.tsx index d119c099..e94b2633 100644 --- a/src/RTE/types.tsx +++ b/src/RTE/types.tsx @@ -1,15 +1,15 @@ import React, { ReactElement } from "react"; import { + Editor, + ElementEntry, Location, + Node, NodeEntry, + NodeMatch, Path, Point, - Node, - ElementEntry, - Transforms, - Editor, Span, - NodeMatch, + Transforms, } from "slate"; import { RTEPlugin } from "./index"; @@ -199,7 +199,7 @@ export declare interface IRteElementType { children: Array; } -type IDynamicFunction = ( +export type IDynamicFunction = ( element: IRteElementType ) => | Exclude diff --git a/src/index.ts b/src/index.ts index b36ddecc..bf73559c 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,8 +1,11 @@ import postRobot from "post-robot"; -import UiLocation from "./uiLocation"; import { version } from "../package.json"; -import { InitializationData } from "./types"; +import { RTEPlugin } from "./RTE"; +import { IRteParam } from "./RTE/types"; +import { PluginDefinition, registerPlugins } from "./rtePlugin"; +import { InitializationData, IRTEInitData } from "./types"; +import UiLocation from "./uiLocation"; postRobot.CONFIG.LOG_LEVEL = "error"; @@ -43,6 +46,58 @@ 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: IRTEInitData, 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[] + ): Promise<{ + __isPluginBuilder__: boolean; + version: string; + plugins: (context: IRTEInitData, rte: IRteParam) => Promise<{ + [key: string]: RTEPlugin; + }>; + }> { + return { + __isPluginBuilder__: true, + version, + plugins: registerPlugins(...pluginDefinitions) + }; + } + /** * Version of Contentstack App SDK. */ diff --git a/src/rtePlugin.ts b/src/rtePlugin.ts new file mode 100644 index 00000000..857a4e3d --- /dev/null +++ b/src/rtePlugin.ts @@ -0,0 +1,167 @@ +import { RTEPlugin as Plugin, rtePluginInitializer } from "./RTE"; +import { + IConfig, + IDisplayOnOptions, + IDynamicFunction, + IElementTypeOptions, + IOnFunction, + IRteElementType, + IRteParam, +} from "./RTE/types"; +import { 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) => finalConfig + ); + Object.entries(pluginDef.callbacks).forEach(([type, callback]) => { + plugin.on(type as keyof IOnFunction, callback); + }); + 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: IRTEInitData, + rte: IRteParam +) => Promise<{ [key: string]: Plugin }> { + const definitionsToProcess = [...pluginDefinitions]; + const plugins = async (context: IRTEInitData, rte: IRteParam) => { + try { + const sdk = new UiLocation(context); + const materializedPlugins: { [key: string]: Plugin } = {}; + for (const def of definitionsToProcess) { + const pluginInstance = await materializePlugin(def, sdk); + materializedPlugins[def.id] = pluginInstance; + } + rte.sdk = sdk; + 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/types/rte.d.ts b/src/types/rte.d.ts new file mode 100644 index 00000000..01ce0c9d --- /dev/null +++ b/src/types/rte.d.ts @@ -0,0 +1,7 @@ +import type { UiLocation } from '../uiLocation'; + +declare module "../RTE/types" { + export interface IRteParam { + sdk: UiLocation; + } +} \ No newline at end of file From d405deed03c3fe2def711004cd79027588f1ec8a Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Wed, 25 Jun 2025 12:25:37 +0530 Subject: [PATCH 18/24] fix:updated exported format Signed-off-by: Amitkanswal --- src/index.ts | 34 ++-------------------------------- 1 file changed, 2 insertions(+), 32 deletions(-) diff --git a/src/index.ts b/src/index.ts index bf73559c..4d1b89b2 100755 --- a/src/index.ts +++ b/src/index.ts @@ -3,7 +3,7 @@ import postRobot from "post-robot"; import { version } from "../package.json"; import { RTEPlugin } from "./RTE"; import { IRteParam } from "./RTE/types"; -import { PluginDefinition, registerPlugins } from "./rtePlugin"; +import { PluginDefinition, registerPlugins,PluginBuilder } from "./rtePlugin"; import { InitializationData, IRTEInitData } from "./types"; import UiLocation from "./uiLocation"; @@ -30,7 +30,6 @@ class ContentstackAppSDK { * A static variable that stores the instance of {@link UiLocation} class after initialization */ static _uiLocation: UiLocation; - /** * Initializes the App SDK and returns an instance of {@link UiLocation} class */ @@ -48,36 +47,6 @@ class ContentstackAppSDK { /** * 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: IRTEInitData, 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. @@ -107,4 +76,5 @@ class ContentstackAppSDK { } export default ContentstackAppSDK; +export { PluginBuilder }; module.exports = ContentstackAppSDK; From 85309861c92c2a6147bd682124db2bbb6f0d8f6c Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Fri, 27 Jun 2025 00:32:24 +0530 Subject: [PATCH 19/24] fix:added id's support in init --- src/index.ts | 64 ++++++++++++++++++++++++++++++++-------------------- src/types.ts | 21 +++++++++++++++++ 2 files changed, 60 insertions(+), 25 deletions(-) diff --git a/src/index.ts b/src/index.ts index 4d1b89b2..d4a4ca5f 100755 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,12 @@ import postRobot from "post-robot"; import { version } from "../package.json"; -import { RTEPlugin } from "./RTE"; import { IRteParam } from "./RTE/types"; -import { PluginDefinition, registerPlugins,PluginBuilder } from "./rtePlugin"; -import { InitializationData, IRTEInitData } from "./types"; +import { PluginDefinition, PluginBuilder, registerPlugins } from "./rtePlugin"; +import { Extension, InitializationData, RTEContext } from "./types"; import UiLocation from "./uiLocation"; +// Configure post-robot logging postRobot.CONFIG.LOG_LEVEL = "error"; /** @@ -23,58 +23,72 @@ postRobot.CONFIG.LOG_LEVEL = "error"; * }) * @return {Promise} A promise object which will be resolved with an instance of the {@link UiLocation} class. * @hideconstructor - */ - +*/ class ContentstackAppSDK { /** * A static variable that stores the instance of {@link UiLocation} class after initialization */ static _uiLocation: UiLocation; - /** + private static _rteInitData: Extension | null = null; + + /** * Initializes the App SDK and returns an instance of {@link UiLocation} class */ static init(): Promise { if (this._uiLocation) { - return Promise.resolve(this._uiLocation); + return Promise.resolve(this._uiLocation); } + return UiLocation.initialize(version) .then((initializationData: InitializationData) => { - this._uiLocation = new UiLocation(initializationData); - return Promise.resolve(this._uiLocation); + // Merge with RTE context if available + const mergedInitData = this._rteInitData + ? { + ...initializationData, + app_id: this._rteInitData.app_uid, + installation_uid: this._rteInitData.app_installation_uid, + extension_uid: this._rteInitData.uid, + } + : initializationData; + + this._uiLocation = new UiLocation(mergedInitData); + return this._uiLocation; }) .catch((e: Error) => Promise.reject(e)); } /** - * Registers RTE plugins with the Contentstack platform. - * 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. + * Register RTE plugins with enhanced context capture + * @param pluginDefinitions Plugin definitions to register + * @returns Plugin registration object */ - static async registerRTEPlugins( - ...pluginDefinitions: PluginDefinition[] - ): Promise<{ - __isPluginBuilder__: boolean; - version: string; - plugins: (context: IRTEInitData, rte: IRteParam) => Promise<{ - [key: string]: RTEPlugin; - }>; - }> { + static async registerRTEPlugins(...pluginDefinitions: PluginDefinition[]) { return { __isPluginBuilder__: true, version, - plugins: registerPlugins(...pluginDefinitions) + plugins: (context: RTEContext, rte: IRteParam) => { + // Capture RTE context for SDK enhancement + this._rteInitData = context.extension; + return registerPlugins(...pluginDefinitions)(context, rte); + } }; } /** - * Version of Contentstack App SDK. + * Get SDK version */ static get SDK_VERSION() { return version; } } +// ES6 exports export default ContentstackAppSDK; export { PluginBuilder }; -module.exports = ContentstackAppSDK; + +// CommonJS compatibility +if (typeof module !== 'undefined' && module.exports) { + module.exports = ContentstackAppSDK; + module.exports.default = ContentstackAppSDK; + module.exports.PluginBuilder = PluginBuilder; +} diff --git a/src/types.ts b/src/types.ts index c351baa3..b97881e7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -268,3 +268,24 @@ export type RegionType = | "AZURE_EU" | "GCP_NA" | string; + +export type Extension = { + app_installation_uid: string + app_uid: string + config: GenericObjectType + created_at: string + created_by: string + signed: boolean + src: string + tags: string[] + title: string + type: string + uid: string + updated_at: string + updated_by: string + _version: number +} + +export type RTEContext = IRTEInitData & { + extension: Extension +} \ No newline at end of file From 3c1ed2fc098d4c624eca0c422064ba94a8b5bd58 Mon Sep 17 00:00:00 2001 From: Amitkanswal Date: Fri, 27 Jun 2025 00:45:28 +0530 Subject: [PATCH 20/24] doc:updated readme file with rte example --- README.md | 2 + docs/rte-plugin.md | 296 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 298 insertions(+) create mode 100644 docs/rte-plugin.md diff --git a/README.md b/README.md index 8812daf7..34800435 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ App Config UI Location allows you to manage all the app settings centrally. Once 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. +New RTE plugin examples [RTE PLUGIN](/docs/rte-plugin.md) + ### Sidebar Location The Sidebar Location provides powerful tools for analyzing and recommending ideas for your entry. Use the [Smartling](https://help.smartling.com/hc/en-us/articles/4865477629083) sidebar location to help translate your content. 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 }) => ( +