diff --git a/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue b/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue index 1e1ab9756..8bd3e9457 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/FullEditor.vue @@ -13,6 +13,9 @@ import { triggerCharacters, } from '@chatwoot/prosemirror-schema/src/mentions/plugin'; import imagePastePlugin from '@chatwoot/prosemirror-schema/src/plugins/image'; +import embedPreviewPlugin from '@chatwoot/prosemirror-schema/src/plugins/embedPreview'; +import trailingParagraphPlugin from '@chatwoot/prosemirror-schema/src/plugins/trailingParagraph'; +import { embeds as markdownEmbeds } from 'dashboard/helper/markdownEmbeds'; import { toggleMark } from 'prosemirror-commands'; import { wrapInList } from 'prosemirror-schema-list'; import { toggleBlockType } from '@chatwoot/prosemirror-schema/src/menu/common'; @@ -77,6 +80,8 @@ export default { plugins: [ imagePastePlugin(this.handleImageUpload), this.createSlashPlugin(), + embedPreviewPlugin(markdownEmbeds), + trailingParagraphPlugin(), ], isTextSelected: false, // Tracks text selection and prevents unnecessary re-renders on mouse selection showSlashMenu: false, @@ -113,6 +118,12 @@ export default { this.focusEditorInputField(); } }, + beforeUnmount() { + if (editorView) { + editorView.destroy(); + editorView = null; + } + }, methods: { createSlashPlugin() { return suggestionsPlugin({ @@ -488,4 +499,9 @@ export default { max-height: 7.5rem; overflow: auto; } + +.ProseMirror .cw-embed-preview { + max-width: 36rem; + margin: 0.5rem 0 1rem; +} diff --git a/app/javascript/dashboard/helper/markdownEmbeds.js b/app/javascript/dashboard/helper/markdownEmbeds.js new file mode 100644 index 000000000..116290220 --- /dev/null +++ b/app/javascript/dashboard/helper/markdownEmbeds.js @@ -0,0 +1,11 @@ +import config from '../../../../config/markdown_embeds.yml'; + +// Gists rely on document.write() and can't render inline in the editor. +const NON_PREVIEWABLE_EMBEDS = new Set(['github_gist']); + +export const embeds = Object.entries(config) + .filter(([key]) => !NON_PREVIEWABLE_EMBEDS.has(key)) + .map(([, { regex, template }]) => ({ + regex: new RegExp(regex), + template, + })); diff --git a/package.json b/package.json index 8d45183e0..bb5b59ba3 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@amplitude/analytics-browser": "^2.11.10", "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", - "@chatwoot/prosemirror-schema": "1.3.10", + "@chatwoot/prosemirror-schema": "1.3.11", "@chatwoot/utils": "^0.0.52", "@formkit/core": "^1.7.2", "@formkit/vue": "^1.7.2", @@ -121,6 +121,7 @@ "@iconify-json/ri": "^1.2.6", "@iconify-json/teenyicons": "^1.2.2", "@intlify/eslint-plugin-vue-i18n": "^3.2.0", + "@rollup/plugin-yaml": "^4.1.2", "@size-limit/file": "^8.2.4", "@vitest/coverage-v8": "3.0.5", "@vue/test-utils": "^2.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab9e7b241..2a3aee897 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,8 +26,8 @@ importers: specifier: 1.2.3 version: 1.2.3 '@chatwoot/prosemirror-schema': - specifier: 1.3.10 - version: 1.3.10 + specifier: 1.3.11 + version: 1.3.11 '@chatwoot/utils': specifier: ^0.0.52 version: 0.0.52 @@ -281,6 +281,9 @@ importers: '@intlify/eslint-plugin-vue-i18n': specifier: ^3.2.0 version: 3.2.0(eslint@8.57.0) + '@rollup/plugin-yaml': + specifier: ^4.1.2 + version: 4.1.2(rollup@4.59.0) '@size-limit/file': specifier: ^8.2.4 version: 8.2.6(size-limit@8.2.6) @@ -460,8 +463,8 @@ packages: '@chatwoot/ninja-keys@1.2.3': resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==} - '@chatwoot/prosemirror-schema@1.3.10': - resolution: {integrity: sha512-MtOXqFPHptFHu/AoIPhQ9TskVXOxOXCgBY/tgAtCAQRut978F7I3QxozQBECBz83ubsoXnBecpNjGNq0OPgONw==} + '@chatwoot/prosemirror-schema@1.3.11': + resolution: {integrity: sha512-+GptIqY73/EtojrhAKX4UKwZF7NUB47DMzgYxmx8Vu07DLKCGe+8JnbHJnR9oBy8p5sn5VOO1ihNf/4Pg2RzIQ==} '@chatwoot/utils@0.0.52': resolution: {integrity: sha512-e57uVqyVW4tj1gql4YJPNMykqMJPkETn5Y9AmHdhc6Y7oxDXfRXBq27fZrrDadLkZdn5RYVCZjfIhXOumyYv2Q==} @@ -1122,6 +1125,24 @@ packages: '@rails/ujs@7.1.400': resolution: {integrity: sha512-YwvXm3BR5tn+VCAKYGycLejMRVZE3Ionj5gFjEeGXCZnI0Rpi+7dKpmyu90kdUY7dRUFpHTdu9zZceEzFLl38w==} + '@rollup/plugin-yaml@4.1.2': + resolution: {integrity: sha512-RpupciIeZMUqhgFE97ba0s98mOFS7CWzN3EJNhJkqSv9XLlWYtwVdtE6cDw6ASOF/sZVFS7kRJXftaqM2Vakdw==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: '>=4.59.0' + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: '>=4.59.0' + peerDependenciesMeta: + rollup: + optional: true + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -3016,8 +3037,8 @@ packages: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true jsdom@20.0.3: @@ -3551,6 +3572,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pidtree@0.6.0: resolution: {integrity: sha512-eG2dWTVw5bzqGRztnHExczNxt5VGsE6OwTeCG3fdUf9KBsZzO3R5OIIIzWR+iZA0NtZ+RDVdaoE2dK1cn6jH4g==} engines: {node: '>=0.10'} @@ -4315,6 +4340,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + tosource@2.0.0-alpha.3: + resolution: {integrity: sha512-KAB2lrSS48y91MzFPFuDg4hLbvDiyTjOVgaK7Erw+5AmZXNq4sFRVn8r6yxSLuNs15PaokrDRpS61ERY9uZOug==} + engines: {node: '>=10'} + totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -4976,7 +5005,7 @@ snapshots: hotkeys-js: 3.8.7 lit: 2.2.6 - '@chatwoot/prosemirror-schema@1.3.10': + '@chatwoot/prosemirror-schema@1.3.11': dependencies: markdown-it-sup: 2.0.0 prosemirror-commands: 1.7.1 @@ -5328,7 +5357,7 @@ snapshots: globals: 13.24.0 ignore: 5.2.4 import-fresh: 3.3.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5342,7 +5371,7 @@ snapshots: globals: 14.0.0 ignore: 5.2.4 import-fresh: 3.3.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 minimatch: 3.1.5 strip-json-comments: 3.1.1 transitivePeerDependencies: @@ -5547,7 +5576,7 @@ snapshots: ignore: 6.0.2 import-fresh: 3.3.0 is-language-code: 3.1.0 - js-yaml: 4.1.0 + js-yaml: 4.1.1 json5: 2.2.3 jsonc-eslint-parser: 2.4.0 lodash: 4.17.21 @@ -5689,6 +5718,22 @@ snapshots: '@rails/ujs@7.1.400': {} + '@rollup/plugin-yaml@4.1.2(rollup@4.59.0)': + dependencies: + '@rollup/pluginutils': 5.3.0(rollup@4.59.0) + js-yaml: 4.1.1 + tosource: 2.0.0-alpha.3 + optionalDependencies: + rollup: 4.59.0 + + '@rollup/pluginutils@5.3.0(rollup@4.59.0)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.59.0 + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -7184,7 +7229,7 @@ snapshots: imurmurhash: 0.1.4 is-glob: 4.0.3 is-path-inside: 3.0.3 - js-yaml: 4.1.0 + js-yaml: 4.1.1 json-stable-stringify-without-jsonify: 1.0.1 levn: 0.4.1 lodash.merge: 4.6.2 @@ -7851,7 +7896,7 @@ snapshots: argparse: 1.0.10 esprima: 4.0.1 - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 @@ -8421,6 +8466,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.4: {} + pidtree@0.6.0: {} pify@2.3.0: {} @@ -8723,7 +8770,7 @@ snapshots: prosemirror-dropcursor@1.8.1: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.12.0 prosemirror-view: 1.34.1 prosemirror-gapcursor@1.3.2: @@ -8736,14 +8783,14 @@ snapshots: prosemirror-history@1.4.1: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.12.0 prosemirror-view: 1.34.1 rope-sequence: 1.3.2 prosemirror-inputrules@1.4.0: dependencies: prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.12.0 prosemirror-keymap@1.2.2: dependencies: @@ -8783,7 +8830,7 @@ snapshots: prosemirror-keymap: 1.2.2 prosemirror-model: 1.22.3 prosemirror-state: 1.4.3 - prosemirror-transform: 1.10.0 + prosemirror-transform: 1.12.0 prosemirror-view: 1.34.1 prosemirror-transform@1.10.0: @@ -9308,6 +9355,8 @@ snapshots: dependencies: is-number: 7.0.0 + tosource@2.0.0-alpha.3: {} + totalist@3.0.1: {} tough-cookie@4.1.4: diff --git a/vite.config.ts b/vite.config.ts index e06b0f1fb..17d47fc76 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -22,6 +22,7 @@ import { defineConfig } from 'vite'; import ruby from 'vite-plugin-ruby'; import path from 'path'; import vue from '@vitejs/plugin-vue'; +import yaml from '@rollup/plugin-yaml'; const isLibraryMode = process.env.BUILD_MODE === 'library'; const isTestMode = process.env.TEST === 'true'; @@ -34,12 +35,12 @@ const vueOptions = { }, }; -let plugins = [ruby(), vue(vueOptions)]; +let plugins = [ruby(), vue(vueOptions), yaml()]; if (isLibraryMode) { plugins = []; } else if (isTestMode) { - plugins = [vue(vueOptions)]; + plugins = [vue(vueOptions), yaml()]; } export default defineConfig({