diff --git a/.yarn/cache/@serlo-editor-npm-0.20.1-01ab8fde1b-c797d8cf0c.zip b/.yarn/cache/@serlo-editor-npm-0.20.1-01ab8fde1b-c797d8cf0c.zip deleted file mode 100644 index 1ab1d25a97..0000000000 Binary files a/.yarn/cache/@serlo-editor-npm-0.20.1-01ab8fde1b-c797d8cf0c.zip and /dev/null differ diff --git a/.yarn/cache/@serlo-editor-npm-0.20.2-5be448a861-ee16329563.zip b/.yarn/cache/@serlo-editor-npm-0.20.2-5be448a861-ee16329563.zip new file mode 100644 index 0000000000..1698001694 Binary files /dev/null and b/.yarn/cache/@serlo-editor-npm-0.20.2-5be448a861-ee16329563.zip differ diff --git a/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-87bb7ee54f.zip b/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-87bb7ee54f.zip new file mode 100644 index 0000000000..9a52919119 Binary files /dev/null and b/.yarn/cache/agent-base-npm-7.1.3-b2c16e72fb-87bb7ee54f.zip differ diff --git a/.yarn/cache/cssstyle-npm-4.1.0-2bda2835e6-a8f5746430.zip b/.yarn/cache/cssstyle-npm-4.1.0-2bda2835e6-a8f5746430.zip new file mode 100644 index 0000000000..3b1d1b0d86 Binary files /dev/null and b/.yarn/cache/cssstyle-npm-4.1.0-2bda2835e6-a8f5746430.zip differ diff --git a/.yarn/cache/data-urls-npm-5.0.0-4b58b89bfe-5c40568c31.zip b/.yarn/cache/data-urls-npm-5.0.0-4b58b89bfe-5c40568c31.zip new file mode 100644 index 0000000000..2f5f097509 Binary files /dev/null and b/.yarn/cache/data-urls-npm-5.0.0-4b58b89bfe-5c40568c31.zip differ diff --git a/.yarn/cache/dompurify-npm-3.2.3-4fa119eef7-cd66ae8b22.zip b/.yarn/cache/dompurify-npm-3.2.3-4fa119eef7-cd66ae8b22.zip new file mode 100644 index 0000000000..8da1a120e0 Binary files /dev/null and b/.yarn/cache/dompurify-npm-3.2.3-4fa119eef7-cd66ae8b22.zip differ diff --git a/.yarn/cache/html-encoding-sniffer-npm-4.0.0-5f6627070d-3339b71dab.zip b/.yarn/cache/html-encoding-sniffer-npm-4.0.0-5f6627070d-3339b71dab.zip new file mode 100644 index 0000000000..374d809113 Binary files /dev/null and b/.yarn/cache/html-encoding-sniffer-npm-4.0.0-5f6627070d-3339b71dab.zip differ diff --git a/.yarn/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-670858c8f8.zip b/.yarn/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-670858c8f8.zip new file mode 100644 index 0000000000..39696ec7e8 Binary files /dev/null and b/.yarn/cache/http-proxy-agent-npm-7.0.2-643ed7cc33-670858c8f8.zip differ diff --git a/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-b882377a12.zip b/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-b882377a12.zip new file mode 100644 index 0000000000..6fd64d49a7 Binary files /dev/null and b/.yarn/cache/https-proxy-agent-npm-7.0.6-27a95c2690-b882377a12.zip differ diff --git a/.yarn/cache/isomorphic-dompurify-npm-2.19.0-4f6eb49402-42fb8a4d5e.zip b/.yarn/cache/isomorphic-dompurify-npm-2.19.0-4f6eb49402-42fb8a4d5e.zip new file mode 100644 index 0000000000..63c08f3b31 Binary files /dev/null and b/.yarn/cache/isomorphic-dompurify-npm-2.19.0-4f6eb49402-42fb8a4d5e.zip differ diff --git a/.yarn/cache/jsdom-npm-25.0.1-ccbb1f9cda-b637d28445.zip b/.yarn/cache/jsdom-npm-25.0.1-ccbb1f9cda-b637d28445.zip new file mode 100644 index 0000000000..22ef8809aa Binary files /dev/null and b/.yarn/cache/jsdom-npm-25.0.1-ccbb1f9cda-b637d28445.zip differ diff --git a/.yarn/cache/nanoid-npm-3.3.7-98824ba130-d36c427e53.zip b/.yarn/cache/nanoid-npm-3.3.7-98824ba130-d36c427e53.zip deleted file mode 100644 index 7b2fd6e1b5..0000000000 Binary files a/.yarn/cache/nanoid-npm-3.3.7-98824ba130-d36c427e53.zip and /dev/null differ diff --git a/.yarn/cache/nanoid-npm-3.3.8-d22226208b-dfe0adbc0c.zip b/.yarn/cache/nanoid-npm-3.3.8-d22226208b-dfe0adbc0c.zip new file mode 100644 index 0000000000..7b4819b1f1 Binary files /dev/null and b/.yarn/cache/nanoid-npm-3.3.8-d22226208b-dfe0adbc0c.zip differ diff --git a/.yarn/cache/nwsapi-npm-2.2.16-9ae9928240-467b36a74b.zip b/.yarn/cache/nwsapi-npm-2.2.16-9ae9928240-467b36a74b.zip new file mode 100644 index 0000000000..fe0b423e74 Binary files /dev/null and b/.yarn/cache/nwsapi-npm-2.2.16-9ae9928240-467b36a74b.zip differ diff --git a/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip b/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip new file mode 100644 index 0000000000..d751f80f9d Binary files /dev/null and b/.yarn/cache/parse5-npm-7.2.1-c48f333f28-11253cf8aa.zip differ diff --git a/.yarn/cache/rrweb-cssom-npm-0.7.1-fbf1786bb7-62e410ddba.zip b/.yarn/cache/rrweb-cssom-npm-0.7.1-fbf1786bb7-62e410ddba.zip new file mode 100644 index 0000000000..538d340a19 Binary files /dev/null and b/.yarn/cache/rrweb-cssom-npm-0.7.1-fbf1786bb7-62e410ddba.zip differ diff --git a/.yarn/cache/tldts-core-npm-6.1.69-dd413a50bb-b11f5a2461.zip b/.yarn/cache/tldts-core-npm-6.1.69-dd413a50bb-b11f5a2461.zip new file mode 100644 index 0000000000..e03554e5c0 Binary files /dev/null and b/.yarn/cache/tldts-core-npm-6.1.69-dd413a50bb-b11f5a2461.zip differ diff --git a/.yarn/cache/tldts-npm-6.1.69-2c00cd92b2-ea09623cc2.zip b/.yarn/cache/tldts-npm-6.1.69-2c00cd92b2-ea09623cc2.zip new file mode 100644 index 0000000000..1a80f13a24 Binary files /dev/null and b/.yarn/cache/tldts-npm-6.1.69-2c00cd92b2-ea09623cc2.zip differ diff --git a/.yarn/cache/tough-cookie-npm-5.0.0-93c44236b9-774f6c939c.zip b/.yarn/cache/tough-cookie-npm-5.0.0-93c44236b9-774f6c939c.zip new file mode 100644 index 0000000000..5576f8cc78 Binary files /dev/null and b/.yarn/cache/tough-cookie-npm-5.0.0-93c44236b9-774f6c939c.zip differ diff --git a/.yarn/cache/tr46-npm-5.0.0-d15754040d-8d8b021f8e.zip b/.yarn/cache/tr46-npm-5.0.0-d15754040d-8d8b021f8e.zip new file mode 100644 index 0000000000..b573c95035 Binary files /dev/null and b/.yarn/cache/tr46-npm-5.0.0-d15754040d-8d8b021f8e.zip differ diff --git a/.yarn/cache/w3c-xmlserializer-npm-5.0.0-589edd7bff-593acc1fda.zip b/.yarn/cache/w3c-xmlserializer-npm-5.0.0-589edd7bff-593acc1fda.zip new file mode 100644 index 0000000000..cb29d3d35e Binary files /dev/null and b/.yarn/cache/w3c-xmlserializer-npm-5.0.0-589edd7bff-593acc1fda.zip differ diff --git a/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip b/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip new file mode 100644 index 0000000000..13fdfcd71a Binary files /dev/null and b/.yarn/cache/whatwg-encoding-npm-3.1.1-7dfe21cf7d-f75a614224.zip differ diff --git a/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip b/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip new file mode 100644 index 0000000000..8ef2ab2ee4 Binary files /dev/null and b/.yarn/cache/whatwg-mimetype-npm-4.0.0-ebb293a688-f97edd4b4e.zip differ diff --git a/.yarn/cache/whatwg-url-npm-14.1.0-ed62f15e7a-e429d1d2a5.zip b/.yarn/cache/whatwg-url-npm-14.1.0-ed62f15e7a-e429d1d2a5.zip new file mode 100644 index 0000000000..051308caa7 Binary files /dev/null and b/.yarn/cache/whatwg-url-npm-14.1.0-ed62f15e7a-e429d1d2a5.zip differ diff --git a/.yarn/cache/xml-name-validator-npm-5.0.0-0e0ec66944-86effcc702.zip b/.yarn/cache/xml-name-validator-npm-5.0.0-0e0ec66944-86effcc702.zip new file mode 100644 index 0000000000..ee05df212d Binary files /dev/null and b/.yarn/cache/xml-name-validator-npm-5.0.0-0e0ec66944-86effcc702.zip differ diff --git a/apps/web/package.json b/apps/web/package.json index 661bf14335..778d993c4e 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -59,6 +59,7 @@ "graphql-request": "^7.1.2", "iframe-resizer": "^4.4.5", "io-ts": "^2.2.21", + "isomorphic-dompurify": "^2.19.0", "js-cookie": "^3.0.5", "json-diff": "^1.0.6", "katex": "^0.16.11", diff --git a/apps/web/src/components/pages/user/profile.tsx b/apps/web/src/components/pages/user/profile.tsx index 981b579b85..eab6a35202 100644 --- a/apps/web/src/components/pages/user/profile.tsx +++ b/apps/web/src/components/pages/user/profile.tsx @@ -86,7 +86,7 @@ export const Profile: NextPage = ({ userData }) => {
- {motivation && <>"{motivation}"} + {motivation && <>„{motivation}“} {isOwnProfile && !isNewlyRegisteredUser && renderEditMotivationLink()} diff --git a/apps/web/src/components/user-tools/share/share-modal.tsx b/apps/web/src/components/user-tools/share/share-modal.tsx index ef1d2295a3..e7f1747a99 100644 --- a/apps/web/src/components/user-tools/share/share-modal.tsx +++ b/apps/web/src/components/user-tools/share/share-modal.tsx @@ -8,6 +8,7 @@ import { faCopy, faDownload, faEnvelope, + faFileText, } from '@fortawesome/free-solid-svg-icons' import { QRCodeSVG } from 'qrcode.react' import { MouseEvent, useState, useEffect } from 'react' @@ -24,6 +25,7 @@ import { showToastNotice } from '@/helper/show-toast-notice' export interface ShareModalProps { isOpen: boolean setIsOpen: (open: boolean) => void + showCopyContent?: boolean showPdf?: boolean path?: string } @@ -39,6 +41,7 @@ interface EntryData { export function ShareModal({ isOpen, setIsOpen, + showCopyContent, showPdf, path, }: ShareModalProps) { @@ -70,10 +73,41 @@ export function ShareModal({ } } + async function copyContentToClipboard() { + if (!pathOrId) return + try { + const url = `/api/frontend/bildungsraum-share?href=${encodeURIComponent(pathOrId)}` + const res = await fetch(url) + const data = (await res.json()) as string + if (!res.ok) { + throw new Error( + 'injection-content API call failed with error: ' + data.toString() + ) + } + await navigator.clipboard.writeText(JSON.stringify(data)) + showToastNotice('👌 Erfolgreich kopiert', 'success') + } catch (e) { + // eslint-disable-next-line no-console + console.error(e) + showToastNotice( + '❌ Leider gab es ein Problem beim kopieren. Tut uns leid.', + 'warning' + ) + } + } + const shareUrl = `${window.location.protocol}//${window.location.host}/${pathOrId}` const urlEncoded = encodeURIComponent(shareUrl) const titleEncoded = encodeURIComponent(document.title) + const contentCopy = [ + { + title: 'Inhalt kopieren', + icon: faFileText, + onClick: () => copyContentToClipboard(), + }, + ] + const socialShare = [ { title: 'E-Mail', @@ -156,6 +190,24 @@ export function ShareModal({ {renderButtons(pdfData)} )} + + {showCopyContent ? ( // "de" only + <> +
+

Inhalt zum Bearbeiten kopieren

+

+ Du kannst diesen Inhalt in jedem Serlo Editor weiterbearbeiten: Hier + auf serlo.org und in LMS wie Moodle, Edu-sharing oder + itslearning, die den Serlo Editor eingebaut haben. +
+
+ Dazu einfach auf unten auf „Inhalt kopieren“ klicken, + einen Moment warten und dann Inhalt im Editor Textfeld Deines LMS + einfügen. +

+ {renderButtons(contentCopy)} + + ) : null} ) diff --git a/apps/web/src/components/user-tools/share/share.tsx b/apps/web/src/components/user-tools/share/share.tsx index dc873e2294..3a98873580 100644 --- a/apps/web/src/components/user-tools/share/share.tsx +++ b/apps/web/src/components/user-tools/share/share.tsx @@ -15,7 +15,7 @@ const ShareModal = dynamic(() => ) export function Share({ data, aboveContent }: MoreAuthorToolsProps) { - const { strings } = useInstanceData() + const { lang, strings } = useInstanceData() const [shareOpen, setShareOpen] = useState(false) const showPdf = @@ -28,6 +28,13 @@ export function Share({ data, aboveContent }: MoreAuthorToolsProps) { UuidType.Exercise, ].includes(data.typename as UuidType) + const showCopyContent = + lang === 'de' && + data && + [UuidType.Article, UuidType.ExerciseGroup, UuidType.Exercise].includes( + data.typename as UuidType + ) + return ( <> ) : null} diff --git a/apps/web/src/components/user/event.tsx b/apps/web/src/components/user/event.tsx index b313ad8d1a..e0cf688072 100644 --- a/apps/web/src/components/user/event.tsx +++ b/apps/web/src/components/user/event.tsx @@ -141,7 +141,7 @@ export function Event({ ), comment: (

- "{event.thread.thread.nodes[0].content}" + „{event.thread.thread.nodes[0].content}“

), }) diff --git a/apps/web/src/data/en/index.ts b/apps/web/src/data/en/index.ts index 69195e52f1..e3a22e868f 100644 --- a/apps/web/src/data/en/index.ts +++ b/apps/web/src/data/en/index.ts @@ -401,7 +401,7 @@ export const instanceData = { 'The provided authentication code is invalid, please try again.', code4000010: 'Have you already verified your email address?.%break% %verificationLinkText%', - code4000032: "You inserted less than 8 characters.", + code4000032: 'You inserted less than 8 characters.', code4060004: 'The recovery link is not valid or has already been used. Please try requesting an email again', code4070001: diff --git a/apps/web/src/data/es/index.ts b/apps/web/src/data/es/index.ts index 2136fd27fc..2c36cb3651 100644 --- a/apps/web/src/data/es/index.ts +++ b/apps/web/src/data/es/index.ts @@ -88,8 +88,8 @@ export const instanceData = { button: "Compartir", title: "¡Comparte!", copyLink: "Copiar enlace", - copySuccess: 'Link copied!', - copyFailed: 'Error copying link!', + copySuccess: "¡Enlace copiado!", + copyFailed: "¡Error al copiar enlace!", close: "Cerrar", pdf: "Descargar PDF", pdfNoSolutions: "PDF sin soluciones" @@ -366,7 +366,7 @@ export const instanceData = { code4000007: "Ya existe una cuenta con el mismo correo electrónico o nombre de usuario.", code4000008: "El código de autentificación proporcionado no es válido, por favor, inténtalo de nuevo.", code4000010: "¿Has verificado ya tu dirección de correo electrónico?%break%%verificationLinkText%", - code4000032: "You inserted less than 8 characters.", + code4000032: "Has introducido menos de 8 caracteres.", code4060004: "El enlace de recuperación no es válido o ya ha sido utilizado. Por favor, intenta solicitar un correo electrónico de nuevo", code4070001: "El enlace de verificación no es válido o ya ha sido utilizado. Por favor, intenta solicitar un correo electrónico de nuevo.", code4070005: "Lo sentimos, este enlace de verificación ya no es válido. Por favor, intenta solicitar un correo electrónico de nuevo." diff --git a/apps/web/src/fetcher/graphql-types/operations.ts b/apps/web/src/fetcher/graphql-types/operations.ts index a9ddc88b67..bc1a3b0011 100644 --- a/apps/web/src/fetcher/graphql-types/operations.ts +++ b/apps/web/src/fetcher/graphql-types/operations.ts @@ -2410,6 +2410,13 @@ type GetCommentsThreadsOldComments_VideoRevision_Fragment = { __typename?: 'Vide export type GetCommentsThreadsOldCommentsFragment = GetCommentsThreadsOldComments_Applet_Fragment | GetCommentsThreadsOldComments_AppletRevision_Fragment | GetCommentsThreadsOldComments_Article_Fragment | GetCommentsThreadsOldComments_ArticleRevision_Fragment | GetCommentsThreadsOldComments_Course_Fragment | GetCommentsThreadsOldComments_CoursePage_Fragment | GetCommentsThreadsOldComments_CoursePageRevision_Fragment | GetCommentsThreadsOldComments_CourseRevision_Fragment | GetCommentsThreadsOldComments_Event_Fragment | GetCommentsThreadsOldComments_EventRevision_Fragment | GetCommentsThreadsOldComments_Exercise_Fragment | GetCommentsThreadsOldComments_ExerciseGroup_Fragment | GetCommentsThreadsOldComments_ExerciseGroupRevision_Fragment | GetCommentsThreadsOldComments_ExerciseRevision_Fragment | GetCommentsThreadsOldComments_Page_Fragment | GetCommentsThreadsOldComments_PageRevision_Fragment | GetCommentsThreadsOldComments_TaxonomyTerm_Fragment | GetCommentsThreadsOldComments_User_Fragment | GetCommentsThreadsOldComments_Video_Fragment | GetCommentsThreadsOldComments_VideoRevision_Fragment; +export type ShareEditorContentQueryVariables = Exact<{ + path: Scalars['String']['input']; +}>; + + +export type ShareEditorContentQuery = { __typename?: 'Query', uuid?: { __typename: 'Applet', currentRevision?: { __typename?: 'AppletRevision', content: string } | null } | { __typename: 'AppletRevision' } | { __typename: 'Article', currentRevision?: { __typename?: 'ArticleRevision', content: string } | null } | { __typename: 'ArticleRevision' } | { __typename: 'Comment' } | { __typename: 'Course', currentRevision?: { __typename?: 'CourseRevision', content: string } | null } | { __typename: 'CoursePage', currentRevision?: { __typename?: 'CoursePageRevision', content: string } | null } | { __typename: 'CoursePageRevision' } | { __typename: 'CourseRevision' } | { __typename: 'Event', currentRevision?: { __typename?: 'EventRevision', content: string } | null } | { __typename: 'EventRevision' } | { __typename: 'Exercise', currentRevision?: { __typename?: 'ExerciseRevision', content: string } | null } | { __typename: 'ExerciseGroup', currentRevision?: { __typename?: 'ExerciseGroupRevision', content: string } | null } | { __typename: 'ExerciseGroupRevision' } | { __typename: 'ExerciseRevision' } | { __typename: 'Page', currentRevision?: { __typename?: 'PageRevision', content: string } | null } | { __typename: 'PageRevision' } | { __typename: 'TaxonomyTerm' } | { __typename: 'User' } | { __typename: 'Video', currentRevision?: { __typename?: 'VideoRevision', content: string } | null } | { __typename: 'VideoRevision' } | null }; + export type InjectionOnlyContentQueryVariables = Exact<{ path: Scalars['String']['input']; }>; diff --git a/apps/web/src/pages/api/frontend/bildungsraum-share.ts b/apps/web/src/pages/api/frontend/bildungsraum-share.ts new file mode 100644 index 0000000000..f39e3c7b2b --- /dev/null +++ b/apps/web/src/pages/api/frontend/bildungsraum-share.ts @@ -0,0 +1,101 @@ +import { parseDocumentString } from '@editor/static-renderer/helper/parse-document-string' +import type { + EditorArticleDocument, + EditorExerciseDocument, + EditorExerciseGroupDocument, +} from '@editor/types/editor-plugins' +import { gql } from 'graphql-request' +import type { NextApiRequest, NextApiResponse } from 'next' + +import { endpoint } from '@/api/endpoint' +import { ShareEditorContentQuery } from '@/fetcher/graphql-types/operations' +import { isProduction } from '@/helper/is-production' + +/** + * Allows frontend to copy Serlo content to the clipboard. + * The content is unpacked for consistent pasting in the Editor. + */ +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + const href = decodeURIComponent(String(req.query.href)) + + const [base] = href.split('#') + const path = base.startsWith('/') ? base : `/${base}` + + if (!path) { + return res.status(401).json('no path provided') + } + + try { + void fetch(endpoint, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + method: 'POST', + body: JSON.stringify({ query, variables: { path } }), + }) + .then((res) => res.json()) + .then((data: { data: ShareEditorContentQuery }) => { + if (!data.data?.uuid) { + return res.status(404).json('not found') + } + + const uuid = data.data.uuid + + if (!Object.hasOwn(uuid, 'currentRevision') || !uuid.currentRevision) { + return res.status(404).json('no current revision') + } + + if (uuid.__typename === 'Article') { + const articleDocument = parseDocumentString( + uuid.currentRevision.content + ) as EditorArticleDocument + const articleContent = articleDocument.state.content + respondWithContent(articleContent) + return + } + + if ( + uuid.__typename === 'Exercise' || + uuid.__typename === 'ExerciseGroup' + ) { + const exercise = parseDocumentString(uuid.currentRevision.content) as + | EditorExerciseDocument + | EditorExerciseGroupDocument + respondWithContent({ plugin: 'rows', state: [exercise] }) + return + } + + return res.status(422).json('unsupported entity type') + }) + .catch((e) => { + return res.status(500).json(`${String(e)} at ${path}`) + }) + } catch (e) { + return res.status(500).json(`${String(e)} at ${path}`) + } + + function respondWithContent(content: any) { + const twoDaysInSeconds = 172800 + res.setHeader('Cache-Control', `maxage=${twoDaysInSeconds}`) + if (!isProduction) res.setHeader('Access-Control-Allow-Origin', '*') + res.status(200).json(content) + } +} + +const query = gql` + query shareEditorContent($path: String!) { + uuid(alias: { path: $path, instance: de }) { + __typename + + ... on AbstractEntity { + currentRevision { + content + } + } + } + } +` diff --git a/apps/web/src/serlo-editor-integration/create-renderers.tsx b/apps/web/src/serlo-editor-integration/create-renderers.tsx index 9d730c46c7..effca42818 100644 --- a/apps/web/src/serlo-editor-integration/create-renderers.tsx +++ b/apps/web/src/serlo-editor-integration/create-renderers.tsx @@ -29,6 +29,7 @@ import type { EditorDropzoneImageDocument, EditorInteractiveVideoDocument, } from '@editor/types/editor-plugins' +import { sanitizeHref } from '@editor/utils/sanitize-href' import dynamic from 'next/dynamic' import { ComponentProps } from 'react' @@ -262,7 +263,7 @@ export function createRenderers(): InitRenderersArgs { linkRenderer: ({ href, children }: ComponentProps) => { return ( <> - {children} + {children} {href} ) diff --git a/apps/web/src/serlo-editor-integration/h5p/index.tsx b/apps/web/src/serlo-editor-integration/h5p/index.tsx index 06019f3eaa..6796605ae6 100644 --- a/apps/web/src/serlo-editor-integration/h5p/index.tsx +++ b/apps/web/src/serlo-editor-integration/h5p/index.tsx @@ -130,7 +130,7 @@ function H5pEditor({ state }: H5pProps) { Registriere dich mit deiner E-Mail-Adresse und melde dich an.
  • - Klicke auf "Neuen Inhalt erstellen" und wähle eines + Klicke auf „Neuen Inhalt erstellen“ und wähle eines der folgenden Inhaltstypen:
      {Object.values(availableH5pExercises).map((exercise) => ( @@ -140,7 +140,7 @@ function H5pEditor({ state }: H5pProps) {
    • Erstelle deinen Inhalt, speichere ihn und klicke dann auf - "Inhalt bereitstellen". + „Inhalt bereitstellen“.
    • Füge die Verknüpfung zur Bereitstellung hier ein:
    diff --git a/e2e-tests/tests/000-general.mobile.ts b/e2e-tests/tests/000-general.mobile.ts index b566715e23..92c8c1969d 100644 --- a/e2e-tests/tests/000-general.mobile.ts +++ b/e2e-tests/tests/000-general.mobile.ts @@ -33,7 +33,7 @@ Scenario('About Serlo @mobile', ({ I }) => { // Navigating around I.click('Pädagogisches Konzept') - I.waitForText('Anleitung für die Lernplattform serlo.org', 5) + I.waitForText('Anleitung für die Lernplattform serlo.org', 15) I.click('Anleitung für die Lernplattform serlo.org') I.scrollPageToBottom() I.click('Community') diff --git a/e2e-tests/tests/000-general.ts b/e2e-tests/tests/000-general.ts index 6ebf1a47d1..ac93164b46 100644 --- a/e2e-tests/tests/000-general.ts +++ b/e2e-tests/tests/000-general.ts @@ -22,7 +22,7 @@ Scenario('About Serlo', ({ I }) => { // Navigating around I.click('Pädagogisches Konzept') - I.waitForText('Anleitung für die Lernplattform serlo.org', 5) + I.waitForText('Anleitung für die Lernplattform serlo.org', 15) I.click('Anleitung für die Lernplattform serlo.org') I.scrollPageToBottom() // close newsletter modal in case it popped up diff --git a/packages/editor-web-component/package.json b/packages/editor-web-component/package.json index 5a951d1143..a066c80d8d 100644 --- a/packages/editor-web-component/package.json +++ b/packages/editor-web-component/package.json @@ -1,6 +1,6 @@ { "name": "@serlo/editor-web-component", - "version": "0.12.0", + "version": "0.12.1", "homepage": "https://de.serlo.org/editor", "bugs": { "url": "https://github.com/serlo/frontend/issues" @@ -40,13 +40,13 @@ "yalc:publish": "yarn build && yalc publish --push --sig" }, "resolutions": { - "@serlo/editor": "0.20.1" + "@serlo/editor": "0.20.2" }, "devDependencies": { "@eslint/eslintrc": "^3.1.0", "@eslint/js": "^9.14.0", "@rollup/plugin-replace": "^6.0.1", - "@serlo/editor": "0.20.1", + "@serlo/editor": "0.20.2", "@serlo/typescript-config": "workspace:*", "@types/react": "^18.0.25", "@types/react-dom": "^18.3.1", diff --git a/packages/editor/CHANGELOG.md b/packages/editor/CHANGELOG.md index cc4a62da6a..b55cdb6145 100644 --- a/packages/editor/CHANGELOG.md +++ b/packages/editor/CHANGELOG.md @@ -1,3 +1,14 @@ +## Changelog for version 0.20.2 + +- feat(editor): add learner event handler to package. Thank you [@elbotho](https://github.com/elbotho) in https://github.com/serlo/frontend/pull/4350 +- quickfix: show exercise task again. Thank you [@elbotho](https://github.com/elbotho) in https://github.com/serlo/frontend/pull/4347 +- Deployment. Thank you [@elbotho](https://github.com/elbotho) in https://github.com/serlo/frontend/pull/4346 +- refactor(image-plugin): Show better error messages when the image upload fails. Thank you [@CodingDive](https://github.com/CodingDive) in https://github.com/serlo/frontend/pull/4344 +- fix(plugin-rows): whole plugin drag handle bug. Thank you [@hejtful](https://github.com/hejtful) in https://github.com/serlo/frontend/pull/4343 +- feat(microadaptivity): integrate AI in final feedback. Thank you [@hugotiburtino](https://github.com/hugotiburtino) in https://github.com/serlo/frontend/pull/4338 + +**Full Changelog**: https://github.com/serlo/frontend/compare/v0.20.1-editor...v0.20.2-editor + ## Changelog for version 0.20.1 - feat(editor-web-component): Expose language prop. Thank you [@CodingDive](https://github.com/CodingDive) in https://github.com/serlo/frontend/pull/4340 diff --git a/packages/editor/demo/react-iframe/index.html b/packages/editor/demo/react-iframe/index.html new file mode 100644 index 0000000000..fcb64cb852 --- /dev/null +++ b/packages/editor/demo/react-iframe/index.html @@ -0,0 +1,31 @@ + + + + + + editor react | in iframe + + + + + + diff --git a/packages/editor/index.html b/packages/editor/index.html index 93f1d7b4b7..59a761936f 100644 --- a/packages/editor/index.html +++ b/packages/editor/index.html @@ -47,7 +47,8 @@

    🐦 ✏️ 🚀

    hot-reloads the editor:

    react (edit only)
    - react (with preview) + react (with preview)
    + iframed react (edit only)

    hot-reloads the component (but not the editor):

    web component (edit only)
    diff --git a/packages/editor/package.json b/packages/editor/package.json index a592b5d132..8b054c4d38 100644 --- a/packages/editor/package.json +++ b/packages/editor/package.json @@ -1,6 +1,6 @@ { "name": "@serlo/editor", - "version": "0.20.1", + "version": "0.20.2", "homepage": "https://de.serlo.org/editor", "bugs": { "url": "https://github.com/serlo/frontend/issues" @@ -50,7 +50,8 @@ "@open-iframe-resizer/react": "1.2.1", "@serlo/katex-styles": "1.0.1", "@vidstack/react": "next", - "dompurify": "^3.2.0", + "dompurify": "^3.2.3", + "isomorphic-dompurify": "^2.19.0", "lit": "^3.2.1", "motion": "^11.11.17", "react": "^18.2.0", diff --git a/packages/editor/src/editor-integration/create-renderers.tsx b/packages/editor/src/editor-integration/create-renderers.tsx index d187329f0e..1ccbc8865c 100644 --- a/packages/editor/src/editor-integration/create-renderers.tsx +++ b/packages/editor/src/editor-integration/create-renderers.tsx @@ -30,6 +30,7 @@ import { TextAreaExerciseStaticRenderer } from '@editor/plugins/text-area-exerci import { VideoStaticRenderer } from '@editor/plugins/video/static' import { EditorPluginType } from '@editor/types/editor-plugin-type' import { TemplatePluginType } from '@editor/types/template-plugin-type' +import { sanitizeHref } from '@editor/utils/sanitize-href' import { ComponentProps } from 'react' export function createRenderers(): InitRenderersArgs { @@ -127,7 +128,7 @@ export function createRenderers(): InitRenderersArgs { return ( diff --git a/packages/editor/src/editor-integration/image-with-testing-config.ts b/packages/editor/src/editor-integration/image-with-testing-config.ts index da9f1a7cb2..bc14f327f7 100644 --- a/packages/editor/src/editor-integration/image-with-testing-config.ts +++ b/packages/editor/src/editor-integration/image-with-testing-config.ts @@ -45,6 +45,10 @@ enum FileErrorCode { BAD_EXTENSION, FILE_TOO_BIG, UPLOAD_FAILED, + UNAUTHORIZED, + SECRET_MISSING, + INVALID_RESPONSE, + NETWORK_ERROR, } export interface FileError { @@ -76,66 +80,105 @@ export const createTestingImagePlugin = (secret: string) => { } function createUploadImageHandler(secret: string) { - const readFile = createReadFile(secret) + const readAndUploadFile = createReadAndUploadFile(secret) return async function uploadImageHandler(file: File): Promise { const validation = validateFile(file) if (!validation.valid) { - onError(validation.errors) + showErrorToast(validation.errors) // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors return Promise.reject(validation.errors) } - return (await readFile(file)).dataUrl + try { + const result = await readAndUploadFile(file) + return result.dataUrl + } catch (error) { + // eslint-disable-next-line no-console + console.error('Upload failed:', error) + const errorCode = + error instanceof Error + ? Number(error.message) || FileErrorCode.UPLOAD_FAILED + : FileErrorCode.UPLOAD_FAILED + + const errors = handleErrors([errorCode]) + showErrorToast(errors) + // eslint-disable-next-line @typescript-eslint/prefer-promise-reject-errors + return Promise.reject(errors) + } } } -export function createReadFile(secret: string) { - return async function readFile(file: File): Promise { - return new Promise((resolve, reject) => { - async function runFetch() { - const endpoint = 'https://api.serlo-staging.dev/graphql' - const response = await fetch(endpoint, { - headers: { - Accept: 'application/json', - 'Content-Type': 'application/json', - 'X-SERLO-EDITOR-TESTING': secret, - }, - method: 'POST', - body: JSON.stringify({ - query: uploadUrlQuery, - variables: { - mediaType: mimeTypesToMediaType[file.type as SupportedMimeType], - }, - }), - }) - const { data } = (await response.json()) as { data: MediaUploadQuery } - const reader = new FileReader() - - reader.onload = async function (e: ProgressEvent) { - if (!e.target) return - - try { - const response = await fetch(data.media.newUpload.uploadUrl, { - method: 'PUT', - headers: { 'Content-Type': file.type }, - body: file, - }) - - if (response.status !== 200) reject() - resolve({ - file, - dataUrl: data.media.newUpload.urlAfterUpload, - }) - } catch { - reject() - } - } - - reader.readAsDataURL(file) +interface GraphQlResponse { + data: MediaUploadQuery | null + errors?: Array<{ + message: string + extensions?: { + code?: string + } + }> +} + +export function createReadAndUploadFile(secret: string) { + return async function readAndUploadFile(file: File): Promise { + if (!secret) { + throw new Error(FileErrorCode.SECRET_MISSING.toString()) + } + + const endpoint = 'https://api.serlo-staging.dev/graphql' + const response = await fetch(endpoint, { + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + 'X-SERLO-EDITOR-TESTING': secret, + }, + method: 'POST', + body: JSON.stringify({ + query: uploadUrlQuery, + variables: { + mediaType: mimeTypesToMediaType[file.type as SupportedMimeType], + }, + }), + }) + + if (!response.ok) { + throw new Error(FileErrorCode.NETWORK_ERROR.toString()) + } + + const { data, errors } = (await response.json()) as GraphQlResponse + + if (errors?.length) { + // eslint-disable-next-line no-console + console.error('GraphQL errors:', errors) + if (errors[0]?.extensions?.code === 'UNAUTHENTICATED') { + throw new Error(FileErrorCode.UNAUTHORIZED.toString()) } + throw new Error(FileErrorCode.UPLOAD_FAILED.toString()) + } + + if ( + !data || + !data?.media?.newUpload?.uploadUrl || + !data.media.newUpload.urlAfterUpload + ) { + // eslint-disable-next-line no-console + console.error('Server responded with following invalid data: ', data) + throw new Error(FileErrorCode.INVALID_RESPONSE.toString()) + } - void runFetch() + const uploadResponse = await fetch(data.media.newUpload.uploadUrl, { + method: 'PUT', + headers: { 'Content-Type': file.type }, + body: file, }) + + if (!uploadResponse.ok) { + throw new Error(FileErrorCode.UPLOAD_FAILED.toString()) + } + + return { + file, + dataUrl: data.media.newUpload.urlAfterUpload, + } } } @@ -151,7 +194,7 @@ function handleErrors(errors: FileErrorCode[]): FileError[] { })) } -function onError(errors: FileError[]): void { +function showErrorToast(errors: FileError[]): void { showToastNotice(errors.map((error) => error.message).join('\n'), 'warning') } @@ -167,6 +210,14 @@ function errorCodeToMessage(error: FileErrorCode) { return 'Filesize is too big' case FileErrorCode.UPLOAD_FAILED: return 'Error while uploading' + case FileErrorCode.UNAUTHORIZED: + return 'You are not authorized to upload images. Ensure the testingSecret is correct!' + case FileErrorCode.SECRET_MISSING: + return 'Missing authentication credentials (testingSecret)!' + case FileErrorCode.INVALID_RESPONSE: + return 'Server returned invalid data' + case FileErrorCode.NETWORK_ERROR: + return 'Network error while uploading' } } diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-audio.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-audio.tsx new file mode 100644 index 0000000000..493709f67f --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-audio.tsx @@ -0,0 +1,52 @@ +export function AudioIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-dnd.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-dnd.tsx new file mode 100644 index 0000000000..f5aa6e804a --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-dnd.tsx @@ -0,0 +1,58 @@ +export function BlanksDndIcon() { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-typing.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-typing.tsx new file mode 100644 index 0000000000..4f42cf75fb --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-blanks-typing.tsx @@ -0,0 +1,27 @@ +export function BlanksTypingIcon() { + return ( + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-box.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-box.tsx new file mode 100644 index 0000000000..e35afca64b --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-box.tsx @@ -0,0 +1,34 @@ +export function BoxIcon() { + return ( + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-dropzones.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-dropzones.tsx new file mode 100644 index 0000000000..bf28da85fe --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-dropzones.tsx @@ -0,0 +1,60 @@ +export function DropzonesIcon() { + return ( + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-equations.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-equations.tsx new file mode 100644 index 0000000000..b8160f5576 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-equations.tsx @@ -0,0 +1,26 @@ +export function EquationsIcon() { + return ( + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-fallback.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-fallback.tsx new file mode 100644 index 0000000000..58d84a7bef --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-fallback.tsx @@ -0,0 +1,20 @@ +export function FallbackIcon() { + return ( + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-geogebra.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-geogebra.tsx new file mode 100644 index 0000000000..c87db8875b --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-geogebra.tsx @@ -0,0 +1,58 @@ +export function GeogebraIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-h5p.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-h5p.tsx new file mode 100644 index 0000000000..3b7b6010d7 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-h5p.tsx @@ -0,0 +1,31 @@ +export function H5PIcon() { + return ( + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-highlight.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-highlight.tsx new file mode 100644 index 0000000000..80be533006 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-highlight.tsx @@ -0,0 +1,18 @@ +export function HighlightIcon() { + return ( + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-image.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-image.tsx new file mode 100644 index 0000000000..0d3d186ce6 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-image.tsx @@ -0,0 +1,22 @@ +export function ImageIcon() { + return ( + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-injection.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-injection.tsx new file mode 100644 index 0000000000..d1fe2d323f --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-injection.tsx @@ -0,0 +1,22 @@ +export function InjectionIcon() { + return ( + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-input-exercise.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-input-exercise.tsx new file mode 100644 index 0000000000..76ab60e0a0 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-input-exercise.tsx @@ -0,0 +1,24 @@ +export function InputExerciseIcon() { + return ( + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-interactive-video.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-interactive-video.tsx new file mode 100644 index 0000000000..205119cf7f --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-interactive-video.tsx @@ -0,0 +1,69 @@ +export function InteractiveVideoIcon() { + return ( + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-mc-exercise.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-mc-exercise.tsx new file mode 100644 index 0000000000..633b3b0a92 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-mc-exercise.tsx @@ -0,0 +1,37 @@ +export function MCExerciseIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-multimedia.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-multimedia.tsx new file mode 100644 index 0000000000..d1dc8a2247 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-multimedia.tsx @@ -0,0 +1,26 @@ +export function MultimediaIcon() { + return ( + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-sc-exercise.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-sc-exercise.tsx new file mode 100644 index 0000000000..7c351ce412 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-sc-exercise.tsx @@ -0,0 +1,32 @@ +export function SCExerciseIcon() { + return ( + + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-spoiler.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-spoiler.tsx new file mode 100644 index 0000000000..2522d5c3b3 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-spoiler.tsx @@ -0,0 +1,37 @@ +export function SpoilerIcon() { + return ( + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-table.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-table.tsx new file mode 100644 index 0000000000..26a6c2ccd3 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-table.tsx @@ -0,0 +1,28 @@ +export function TableIcon() { + return ( + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-text-area.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-text-area.tsx new file mode 100644 index 0000000000..0b3b1c93e3 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-text-area.tsx @@ -0,0 +1,11 @@ +export function TextAreaIcon() { + return ( + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-text.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-text.tsx new file mode 100644 index 0000000000..1f19dab63c --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-text.tsx @@ -0,0 +1,12 @@ +export function TextIcon() { + return ( + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/icon-video.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/icon-video.tsx new file mode 100644 index 0000000000..4d9cbd6a0f --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/icon-video.tsx @@ -0,0 +1,13 @@ +export function VideoIcon() { + return ( + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/assets/plugin-icons/image-gallery/icon-image-gallery.tsx b/packages/editor/src/editor-ui/assets/plugin-icons/image-gallery/icon-image-gallery.tsx new file mode 100644 index 0000000000..9817c40c61 --- /dev/null +++ b/packages/editor/src/editor-ui/assets/plugin-icons/image-gallery/icon-image-gallery.tsx @@ -0,0 +1,124 @@ +export function ImageGalleryIcon() { + return ( + + + + + + + + + + + + + + + + + + + + ) +} diff --git a/packages/editor/src/editor-ui/editor-modal.tsx b/packages/editor/src/editor-ui/editor-modal.tsx index 5cf8747c96..359f330c85 100644 --- a/packages/editor/src/editor-ui/editor-modal.tsx +++ b/packages/editor/src/editor-ui/editor-modal.tsx @@ -1,3 +1,4 @@ +import { useIsSerlo } from '@editor/core/hooks/use-is-serlo' import { cn } from '@editor/utils/cn' import { faXmark } from '@fortawesome/free-solid-svg-icons' import * as Dialog from '@radix-ui/react-dialog' @@ -32,6 +33,7 @@ export function EditorModal({ }: EditorModalProps) { const previouslyFocusedElementRef = useRef(null) + const isSerlo = useIsSerlo() const onOpenChange = useCallback( (open: boolean) => { if (open !== false) { @@ -70,7 +72,12 @@ export function EditorModal({ className={cn(defaultModalOverlayStyles, extraOverlayClassName)} /> plugin.type === pluginType) ?? - plugins.find((plugin) => plugin.type === 'unsupported') + let contextPlugin = plugins.find((plugin) => plugin.type === pluginType) + + if (contextPlugin === undefined) { + emitUnsupportedPluginsEvent() + contextPlugin = plugins.find((plugin) => plugin.type === 'unsupported') + } return (contextPlugin?.plugin as EditorPlugin) ?? null } diff --git a/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts b/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts new file mode 100644 index 0000000000..9c2b4ce192 --- /dev/null +++ b/packages/editor/src/plugin/helpers/unsupported-plugin-event.ts @@ -0,0 +1,15 @@ +const eventName = 'unsupportedPluginType' + +const event = new Event(eventName) + +export function emitUnsupportedPluginsEvent() { + document.dispatchEvent(event) +} + +export function listenForUnsupportedPlugins(callback: () => void) { + document.addEventListener(eventName, callback, { capture: true, once: true }) +} + +export function removeUnsupportedPluginsListener(callback: () => void) { + document.removeEventListener(eventName, callback, true) +} diff --git a/packages/editor/src/plugins/edusharing-asset/renderer.tsx b/packages/editor/src/plugins/edusharing-asset/renderer.tsx index a4c4557493..2d9a819cd2 100644 --- a/packages/editor/src/plugins/edusharing-asset/renderer.tsx +++ b/packages/editor/src/plugins/edusharing-asset/renderer.tsx @@ -1,5 +1,6 @@ import EdusharingIcon from '@editor/editor-ui/assets/edusharing.svg' import { IframeResizer } from '@open-iframe-resizer/react' +import DOMPurify from 'dompurify' import * as t from 'io-ts' import { memo, useEffect, useState } from 'react' @@ -86,8 +87,15 @@ export function EdusharingAssetRenderer(props: { const html = buildHtml(htmlSnippet, defineContainerHeight) + const sanitizedHtml = DOMPurify.sanitize(html, { + // We allow