diff --git a/apps/frontend/src/app/global.scss b/apps/frontend/src/app/global.scss index 2227ab94..fd801a0e 100644 --- a/apps/frontend/src/app/global.scss +++ b/apps/frontend/src/app/global.scss @@ -652,6 +652,11 @@ html[dir='rtl'] [dir='ltr'] { } } +.tiptap { + a { + @apply underline; + } +} .tiptap { :first-child { margin-top: 0; diff --git a/apps/frontend/src/components/new-launch/a.component.tsx b/apps/frontend/src/components/new-launch/a.component.tsx new file mode 100644 index 00000000..10e6802f --- /dev/null +++ b/apps/frontend/src/components/new-launch/a.component.tsx @@ -0,0 +1,51 @@ +'use client'; + +import { FC, useCallback } from 'react'; + +export const AComponent: FC<{ + editor: any; + currentValue: string; +}> = ({ editor }) => { + const mark = () => { + const previousUrl = editor?.getAttributes('link')?.href; + const url = window.prompt('URL', previousUrl); + + // cancelled + if (url === null) { + return; + } + + // empty + if (url === '') { + editor?.chain()?.focus()?.extendMarkRange('link')?.unsetLink()?.run(); + + return; + } + + // update link + try { + editor?.chain()?.focus()?.extendMarkRange('link')?.setLink({ href: url })?.run(); + } catch (e) { + } + editor?.commands?.focus(); + }; + return ( +
+ + + +
+ ); +}; diff --git a/apps/frontend/src/components/new-launch/editor.tsx b/apps/frontend/src/components/new-launch/editor.tsx index 5105ac7d..533e1539 100644 --- a/apps/frontend/src/components/new-launch/editor.tsx +++ b/apps/frontend/src/components/new-launch/editor.tsx @@ -28,10 +28,10 @@ import { UpDownArrow } from '@gitroom/frontend/components/launches/up.down.arrow import { deleteDialog } from '@gitroom/react/helpers/delete.dialog'; import { useExistingData } from '@gitroom/frontend/components/launches/helpers/use.existing.data'; import { useCopilotAction, useCopilotReadable } from '@copilotkit/react-core'; -import { LinkedinCompanyPop } from '@gitroom/frontend/components/launches/helpers/linkedin.component'; import { useDropzone } from 'react-dropzone'; import { useUppyUploader } from '@gitroom/frontend/components/media/new.uploader'; import { Dashboard } from '@uppy/react'; +import Link from '@tiptap/extension-link'; import { useEditor, EditorContent, @@ -53,6 +53,7 @@ import { HeadingComponent } from '@gitroom/frontend/components/new-launch/headin import Mention from '@tiptap/extension-mention'; import { suggestion } from '@gitroom/frontend/components/new-launch/mention.component'; import { useFetch } from '@gitroom/helpers/utils/custom.fetch'; +import { AComponent } from '@gitroom/frontend/components/new-launch/a.component'; const InterceptBoldShortcut = Extension.create({ name: 'preventBoldWithUnderline', @@ -521,18 +522,23 @@ export const Editor: FC<{ editor={editorRef?.current?.editor} currentValue={props.value!} /> - {(editorType === 'markdown' || editorType === 'html') && identifier !== 'telegram' && ( - <> - - - - )} + {(editorType === 'markdown' || editorType === 'html') && + identifier !== 'telegram' && ( + <> + + + + + )}
setEmojiPickerOpen(!emojiPickerOpen)} @@ -703,6 +709,66 @@ export const OnlyEditor = forwardRef< InterceptUnderlineShortcut, BulletList, ListItem, + Link.configure({ + openOnClick: false, + autolink: true, + defaultProtocol: 'https', + protocols: ['http', 'https'], + isAllowedUri: (url, ctx) => { + try { + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`${ctx.defaultProtocol}://${url}`); + + // use default validation + if (!ctx.defaultValidate(parsedUrl.href)) { + return false; + } + + // disallowed protocols + const disallowedProtocols = ['ftp', 'file', 'mailto']; + const protocol = parsedUrl.protocol.replace(':', ''); + + if (disallowedProtocols.includes(protocol)) { + return false; + } + + // only allow protocols specified in ctx.protocols + const allowedProtocols = ctx.protocols.map((p) => + typeof p === 'string' ? p : p.scheme + ); + + if (!allowedProtocols.includes(protocol)) { + return false; + } + + // all checks have passed + return true; + } catch { + return false; + } + }, + shouldAutoLink: (url) => { + try { + // construct URL + const parsedUrl = url.includes(':') + ? new URL(url) + : new URL(`https://${url}`); + + // only auto-link if the domain is not in the disallowed list + const disallowedDomains = [ + 'example-no-autolink.com', + 'another-no-autolink.com', + ]; + const domain = parsedUrl.hostname; + + return !disallowedDomains.includes(domain); + } catch { + return false; + } + }, + }), ...(internal?.integration?.id ? [ Mention.configure({ diff --git a/libraries/helpers/src/utils/strip.html.validation.ts b/libraries/helpers/src/utils/strip.html.validation.ts index b21fe255..cddd7346 100644 --- a/libraries/helpers/src/utils/strip.html.validation.ts +++ b/libraries/helpers/src/utils/strip.html.validation.ts @@ -178,7 +178,13 @@ export const stripHtmlValidation = ( }) .replace(/

([.\s\S]*?)<\/p>/g, (match, p1) => { return `

${p1}

\n`; - }), + }) + .replace( + /([.\s\S]*?)<\/a>/g, + (match, p1, p2) => { + return `[${p2}](${p1})`; + } + ), convertMentionFunction ) ); @@ -203,6 +209,12 @@ export const stripHtmlValidation = ( const processedHtml = convertMention( convertToAscii( html + .replace( + /([.\s\S]*?)<\/a>/g, + (match, p1, p2) => { + return `${p1}`; + } + ) .replace(/
    /, '\n
      ') .replace(/<\/ul>\n/, '
    ') .replace(/([.\s\S]*?)<\/li.*?>/gm, (match, p1) => { diff --git a/package.json b/package.json index 13cf704d..ee298420 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@tiptap/extension-document": "^3.0.6", "@tiptap/extension-heading": "^3.0.7", "@tiptap/extension-history": "^3.0.7", + "@tiptap/extension-link": "^3.0.9", "@tiptap/extension-list": "^3.0.7", "@tiptap/extension-mention": "^3.0.7", "@tiptap/extension-paragraph": "^3.0.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 790cf36d..03a22762 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: '@tiptap/extension-history': specifier: ^3.0.7 version: 3.0.7(@tiptap/extensions@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)) + '@tiptap/extension-link': + specifier: ^3.0.9 + version: 3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/extension-list': specifier: ^3.0.7 version: 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) @@ -5914,11 +5917,11 @@ packages: peerDependencies: '@tiptap/core': ^3.0.6 - '@tiptap/extension-link@3.0.6': - resolution: {integrity: sha512-BQZNnXx52jncbWrS60PXCtL7kYewA9j4XuYj6U+V943mjmXE+pJ8KczF1YZRmbU7YRLRLrGOtMrSUC8ioJpq6Q==} + '@tiptap/extension-link@3.0.9': + resolution: {integrity: sha512-cOsG3vpct7/JuenxCePDj5dlaSUEe2eK/g/jlRixgW4Llx5DvG2yj8+gha4MHdCUp/MrUBR4M+NJk1dOOSKXGw==} peerDependencies: - '@tiptap/core': ^3.0.6 - '@tiptap/pm': ^3.0.6 + '@tiptap/core': ^3.0.9 + '@tiptap/pm': ^3.0.9 '@tiptap/extension-list-item@3.0.6': resolution: {integrity: sha512-gu3WJ+7GhIi7gPQuaD59Si1oXjBJHKt9wndLKHjYgzlQZb8pfHvix7MqkdSrF/wY+5ScYm2bZToCZku2baoAJw==} @@ -10949,8 +10952,8 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - linkifyjs@4.3.1: - resolution: {integrity: sha512-DRSlB9DKVW04c4SUdGvKK5FR6be45lTU9M76JnngqPeeGDqPwYc0zdUErtsNVMtxPXgUWV4HbXbnC4sNyBxkYg==} + linkifyjs@4.3.2: + resolution: {integrity: sha512-NT1CJtq3hHIreOianA8aSXn6Cw0JzYOuDQbOrSPe7gqFnCpKP++MQe3ODgO3oh2GJFORkAAdqredOa60z63GbA==} lit-element@4.2.0: resolution: {integrity: sha512-MGrXJVAI5x+Bfth/pU9Kst1iWID6GHDLEzFEnyULB/sFiRLgkd8NPK/PeeXxktA3T6EIIaq8U3KcbTU5XFcP2Q==} @@ -22346,11 +22349,11 @@ snapshots: dependencies: '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) - '@tiptap/extension-link@3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)': + '@tiptap/extension-link@3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)': dependencies: '@tiptap/core': 3.0.6(@tiptap/pm@3.0.6) '@tiptap/pm': 3.0.6 - linkifyjs: 4.3.1 + linkifyjs: 4.3.2 '@tiptap/extension-list-item@3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6))': dependencies: @@ -22447,7 +22450,7 @@ snapshots: '@tiptap/extension-heading': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6)) '@tiptap/extension-horizontal-rule': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/extension-italic': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6)) - '@tiptap/extension-link': 3.0.6(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) + '@tiptap/extension-link': 3.0.9(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/extension-list': 3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6) '@tiptap/extension-list-item': 3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)) '@tiptap/extension-list-keymap': 3.0.6(@tiptap/extension-list@3.0.7(@tiptap/core@3.0.6(@tiptap/pm@3.0.6))(@tiptap/pm@3.0.6)) @@ -29153,7 +29156,7 @@ snapshots: dependencies: uc.micro: 2.1.0 - linkifyjs@4.3.1: {} + linkifyjs@4.3.2: {} lit-element@4.2.0: dependencies: