diff --git a/.github/workflows/issues-jira.yml b/.github/workflows/issues-jira.yml new file mode 100644 index 0000000..7bf0469 --- /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 diff --git a/.github/workflows/jira.yml b/.github/workflows/jira.yml deleted file mode 100644 index 250abc7..0000000 --- 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 }} diff --git a/.github/workflows/policy-scan.yml b/.github/workflows/policy-scan.yml new file mode 100644 index 0000000..ff25923 --- /dev/null +++ b/.github/workflows/policy-scan.yml @@ -0,0 +1,46 @@ +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: | + 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 diff --git a/.github/workflows/sast-scan.yml b/.github/workflows/sast-scan.yml deleted file mode 100644 index 3b9521a..0000000 --- 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 diff --git a/.github/workflows/secrets-scan.yml b/.github/workflows/secrets-scan.yml new file mode 100644 index 0000000..049c02f --- /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 diff --git a/.talismanrc b/.talismanrc new file mode 100644 index 0000000..345ca2f --- /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 diff --git a/README.md b/README.md index 8812daf..3480043 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 0000000..4ac6b6e --- /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 }) => ( +