From 7b7937f8649e5d229242accbce1e2c8b0586726f Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Mon, 30 Dec 2024 14:42:42 +0100 Subject: [PATCH 01/78] refactor: use editor package in frontend (WIP) --- .../src/pages/___editor_package_preview.tsx | 208 ++++++++++++++++++ .../editor-renderer.tsx | 13 +- .../serlo-editor-integration/serlo-editor.tsx | 104 +++++---- .../use-handle-learner-event.tsx | 2 +- .../create-basic-plugins.tsx | 37 +++- packages/editor/src/package/editor.tsx | 2 +- packages/editor/src/package/index.ts | 24 +- 7 files changed, 319 insertions(+), 71 deletions(-) create mode 100644 apps/web/src/pages/___editor_package_preview.tsx diff --git a/apps/web/src/pages/___editor_package_preview.tsx b/apps/web/src/pages/___editor_package_preview.tsx new file mode 100644 index 0000000000..b0d31830ea --- /dev/null +++ b/apps/web/src/pages/___editor_package_preview.tsx @@ -0,0 +1,208 @@ +import { + defaultPlugins, + type SerloEditorProps, + SerloRenderer, +} from '@editor/package' +import dynamic from 'next/dynamic' +import NextAdapterPages from 'next-query-params/pages' +import { useMemo, useState } from 'react' +import { QueryParamProvider } from 'use-query-params' + +import { FrontendClientBase } from '@/components/frontend-client-base/frontend-client-base' +import { EditorPageData } from '@/fetcher/fetch-editor-data' +import { renderedPageNoHooks } from '@/helper/rendered-page' + +const SerloEditor = dynamic( + () => import('@editor/package').then((mod) => mod.SerloEditor), + { + ssr: false, + } +) + +export const exampleInitialState: SerloEditorProps['initialState'] = { + plugin: 'rows', + state: [ + { + plugin: 'text', + state: [ + { + type: 'h', + level: 1, + children: [ + { + text: 'Beispiel überschrift', + }, + ], + }, + { + type: 'p', + children: [ + { + text: 'Bestimme den Differenzenquotient der Funktion ', + }, + { + type: 'math', + src: 'f(x)=x^2', + inline: true, + children: [ + { + text: 'f(x)=x^2', + }, + ], + }, + { + text: ' im Intervall  ', + }, + { + type: 'math', + src: '\\left[1;3\\right]', + inline: true, + children: [ + { + text: '\\left[1;3\\right]', + }, + ], + }, + { + text: ' ', + }, + { + type: 'math', + src: '\\Rightarrow x_1=1', + inline: true, + children: [ + { + text: '\\Rightarrow x_1=1\\;x_2=3', + }, + ], + }, + { + text: ' und ', + }, + { + type: 'math', + src: 'x_2=3', + inline: true, + children: [ + { + text: '', + }, + ], + }, + { + text: '.', + }, + ], + }, + ], + }, + { + plugin: 'equations', + state: { + transformationTarget: 'equation', + firstExplanation: { + plugin: 'text', + state: [ + { + type: 'p', + children: [{}], + }, + ], + }, + steps: [ + { + left: 'm', + sign: 'equals', + right: '\\frac{f(3)-f(1)}{3-1}', + transform: '', + explanation: { + plugin: 'text', + state: [ + { + type: 'p', + children: [ + { + text: 'Ausrechnen', + }, + ], + }, + ], + }, + }, + { + left: '', + sign: 'equals', + right: '4', + transform: '', + explanation: { + plugin: 'text', + state: [ + { + type: 'p', + children: [{}], + }, + ], + }, + }, + ], + }, + }, + ], +} + +export default renderedPageNoHooks((props) => { + return ( + +
+ + + +
+
+ ) +}) + +function Content() { + const [editorState, setEditorState] = useState(exampleInitialState) + + const editor = useMemo( + () => ( + { + console.log('newState: ', newState) + setEditorState(newState.document) + }} + > + {(editor) => { + console.log('editor: ', editor) + return
{editor.element}
+ }} +
+ ), + [] + ) + + return ( +
+
+
{editor}
+
+
+

Preview

+
+ +
+
+
+ ) +} diff --git a/apps/web/src/serlo-editor-integration/editor-renderer.tsx b/apps/web/src/serlo-editor-integration/editor-renderer.tsx index d33e8afa51..b49bd8c7c6 100644 --- a/apps/web/src/serlo-editor-integration/editor-renderer.tsx +++ b/apps/web/src/serlo-editor-integration/editor-renderer.tsx @@ -1,14 +1,21 @@ -import { StaticRenderer } from '@editor/static-renderer/static-renderer' -import { AnyEditorDocument } from '@editor/types/editor-plugins' +import { SerloRenderer } from '@editor/package' + +import { useSerloHandleLearnerEvent } from './use-handle-learner-event' export function EditorRenderer({ document, }: { document: unknown }): JSX.Element { + const handleLearnerEvent = useSerloHandleLearnerEvent() + return (
- +
) } diff --git a/apps/web/src/serlo-editor-integration/serlo-editor.tsx b/apps/web/src/serlo-editor-integration/serlo-editor.tsx index 555e035c43..efd6dabdb6 100644 --- a/apps/web/src/serlo-editor-integration/serlo-editor.tsx +++ b/apps/web/src/serlo-editor-integration/serlo-editor.tsx @@ -1,83 +1,77 @@ -import { type EditorProps } from '@editor/core' -import { EditorMetaContext } from '@editor/core/contexts/editor-meta-context' -import { EditStringsProvider } from '@editor/i18n/edit-strings-provider' -import { editStrings as editStringsDe } from '@editor/i18n/strings/de/edit' -import { editStrings as editStringsEn } from '@editor/i18n/strings/en/edit' -import { editorLearnerEvent } from '@editor/plugin/helpers/editor-learner-event' -import { editorPlugins } from '@editor/plugin/helpers/editor-plugins' -import { editorRenderers } from '@editor/plugin/helpers/editor-renderer' -import { AnyEditorDocument } from '@editor/types/editor-plugins' +import { + defaultPlugins, + EditorPluginType, + type SerloEditorProps as EditorProps, +} from '@editor/package' import { TemplatePluginType } from '@editor/types/template-plugin-type' import { SerloOnlyFeaturesContext } from '@editor/utils/serlo-extra-context' import dynamic from 'next/dynamic' -import { mergeDeepRight } from 'ramda' -import { type ReactNode } from 'react' import { ArticleAddModal } from './components/article-add-modal/article-add-modal' import { ExternalRevisionLoader } from './components/external-revision-loader' -import { SaveButton } from './components/save-button' -import { createPlugins } from './create-plugins' -import { createRenderers } from './create-renderers' -import { useSerloHandleLearnerEvent } from './use-handle-learner-event' +// import { SaveButton } from './components/save-button' import { useAuthentication } from '@/auth/use-authentication' import { useInstanceData } from '@/contexts/instance-context' import type { SetEntityMutationData } from '@/mutations/use-set-entity-mutation/types' -const Editor = dynamic(() => import('@editor/core').then((mod) => mod.Editor), { - ssr: false, -}) +const Editor = dynamic( + () => import('@editor/package').then((mod) => mod.SerloEditor), + { + ssr: false, + } +) export interface SerloEditorProps { - children?: ReactNode isInTestArea?: boolean onSave: (data: SetEntityMutationData) => Promise initialState: EditorProps['initialState'] } export function SerloEditor({ - onSave, - isInTestArea, + // onSave, + // isInTestArea, initialState, - children, }: SerloEditorProps) { const { lang, licenses } = useInstanceData() const auth = useAuthentication() - const handleLearnerEvent = useSerloHandleLearnerEvent() - - // simplest way to provide plugins to editor that can also easily be adapted by edusharing - editorPlugins.init(createPlugins({ lang })) - - // some plugins rely on static renderes - editorRenderers.init(createRenderers()) - - editorLearnerEvent.init(handleLearnerEvent) - - const editString = - lang === 'de' ? mergeDeepRight(editStringsEn, editStringsDe) : editStringsEn - - const isNewEntity = !(initialState.state as AnyEditorDocument)?.id + const isNewEntity = !(initialState as { state?: { id?: string } }).state?.id return ( - - + - - - - {isNewEntity ? ( - - ) : null} - - {children} - - - - + {(editor) => { + return ( + <> + {/* */} + {isNewEntity ? ( + + ) : null} + {editor.element} + + ) + }} + + ) } diff --git a/apps/web/src/serlo-editor-integration/use-handle-learner-event.tsx b/apps/web/src/serlo-editor-integration/use-handle-learner-event.tsx index 59727b3902..438d7ce3f9 100644 --- a/apps/web/src/serlo-editor-integration/use-handle-learner-event.tsx +++ b/apps/web/src/serlo-editor-integration/use-handle-learner-event.tsx @@ -1,4 +1,4 @@ -import { LearnerEventData } from '@editor/plugin/helpers/editor-learner-event' +import type { LearnerEventData } from '@editor/package' export function useSerloHandleLearnerEvent() { function handleLearnerEvent(data: LearnerEventData) { diff --git a/packages/editor/src/editor-integration/create-basic-plugins.tsx b/packages/editor/src/editor-integration/create-basic-plugins.tsx index 371e7b5ba4..985aa58db2 100644 --- a/packages/editor/src/editor-integration/create-basic-plugins.tsx +++ b/packages/editor/src/editor-integration/create-basic-plugins.tsx @@ -1,3 +1,6 @@ +import type { SupportedLanguage } from '@editor/package' +import { anchorPlugin } from '@editor/plugins/anchor' +import { articlePlugin } from '@editor/plugins/article' import { createBlanksExercisePlugin } from '@editor/plugins/blanks-exercise' import { createBoxPlugin } from '@editor/plugins/box' import { createDropzoneImagePlugin } from '@editor/plugins/dropzone-image' @@ -7,13 +10,18 @@ import { exercisePlugin } from '@editor/plugins/exercise' import { geoGebraPlugin } from '@editor/plugins/geogebra' import { createHighlightPlugin } from '@editor/plugins/highlight' import { createImageGalleryPlugin } from '@editor/plugins/image-gallery' +import { injectionPlugin } from '@editor/plugins/injection' import { createInputExercisePlugin } from '@editor/plugins/input-exercise' -import { createMultimediaPlugin } from '@editor/plugins/multimedia' +import { + createArticleIntroduction, + createMultimediaPlugin, +} from '@editor/plugins/multimedia' import { createRowsPlugin } from '@editor/plugins/rows' import { createScMcExercisePlugin } from '@editor/plugins/sc-mc-exercise' import { createSerloInjectionPlugin } from '@editor/plugins/serlo-injection' import { SerloInjectionStaticRenderer } from '@editor/plugins/serlo-injection/static' import { createSerloTablePlugin } from '@editor/plugins/serlo-table' +import { articleTypePlugin } from '@editor/plugins/serlo-template-plugins/article' import { genericContentTypePlugin } from '@editor/plugins/serlo-template-plugins/generic-content' import { solutionPlugin } from '@editor/plugins/solution' import { createSpoilerPlugin } from '@editor/plugins/spoiler' @@ -28,7 +36,8 @@ import { createTestingImagePlugin } from './image-with-testing-config' export function createBasicPlugins( plugins: (EditorPluginType | TemplatePluginType)[], - testingSecret?: string | null + testingSecret?: string | null, + language: SupportedLanguage = 'de' ) { if (plugins.includes(EditorPluginType.Image) && !testingSecret) { /* eslint-disable no-console */ @@ -135,6 +144,30 @@ export function createBasicPlugins( type: TemplatePluginType.GenericContent, plugin: genericContentTypePlugin, }, + { + type: TemplatePluginType.Article, + plugin: articleTypePlugin, + }, + { + type: EditorPluginType.Article, + plugin: articlePlugin, + }, + { + type: EditorPluginType.ArticleIntroduction, + plugin: createArticleIntroduction( + language === 'de' + ? 'Fasse das Thema des Artikels kurz zusammen' + : 'Write a short introduction' + ), + }, + { + type: EditorPluginType.Injection, + plugin: injectionPlugin, + }, + { + type: EditorPluginType.Anchor, + plugin: anchorPlugin, + }, ] return allPlugins.filter(({ type }) => plugins.includes(type)) diff --git a/packages/editor/src/package/editor.tsx b/packages/editor/src/package/editor.tsx index 397eaba57b..76319620d2 100644 --- a/packages/editor/src/package/editor.tsx +++ b/packages/editor/src/package/editor.tsx @@ -67,7 +67,7 @@ export function SerloEditor(props: SerloEditorProps) { const { staticStrings, editStrings } = editorData[language] - const allPlugins = createBasicPlugins(plugins, _testingSecret) + const allPlugins = createBasicPlugins(plugins, _testingSecret, language) editorPlugins.init(allPlugins) const basicRenderers = createRenderers() diff --git a/packages/editor/src/package/index.ts b/packages/editor/src/package/index.ts index e07daf3c00..625e6438df 100644 --- a/packages/editor/src/package/index.ts +++ b/packages/editor/src/package/index.ts @@ -5,18 +5,24 @@ export type { SupportedLanguage } from '@editor/types/language-data' export type { BaseEditor } from '@editor/core' export type { LearnerEventData } from '@editor/plugin/helpers/editor-learner-event' -// We need to make a distinction between entires on our menu and technical -// plugin types Internally we have for example a `scMcExercise` which has a -// configuration whether it is a single choice or multiple choice exercise. -// However on the menu there are the two entries `singleChoiceExercise` and -// `multipleChoiceExercise`. When somebody else uses our editor as a block -// inside their own editor they would like to have the two entries -// `singleChoiceExercise` and `multipleChoiceExercise`. -// Thus we export our menu entries here as plugin. +/** + * We need to make a distinction between entries in our menu and + * technical plugin types. For example, internally we have a + * `scMcExercise` which has a configuration that defines whether + * it is a single choice or a multiple choice exercise. However, + * in the menu there are the two entries: `singleChoiceExercise` + * and `multipleChoiceExercise`. When somebody else uses our editor + * as a block inside their own editor they would like to have two + * entries: `singleChoiceExercise` and `multipleChoiceExercise`. + * Thus we export our menu entries here. + */ export { pluginMenuDe, pluginMenuEn } from './plugin-menu-export' export { type PluginMenuType as Plugin } from '@editor/plugins/rows/utils/plugin-menu' export { EditorPluginType } from '@editor/types/editor-plugin-type' -// Exported only so that integrations like serlo-editor-for-edusharing can customize available plugins based on the default plugins +/** + * Exported so that integrations can customize available plugins + * based on the default plugins. + */ export { defaultPlugins } from './config' From 9a22f05dd742e16a46b8eae31424d6ea5bf0f9a4 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Fri, 3 Jan 2025 11:24:43 +0100 Subject: [PATCH 02/78] refactor(editor): export TemplatePluginType in package --- apps/web/src/serlo-editor-integration/serlo-editor.tsx | 2 +- packages/editor/src/package/index.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/web/src/serlo-editor-integration/serlo-editor.tsx b/apps/web/src/serlo-editor-integration/serlo-editor.tsx index efd6dabdb6..f34e7dbd84 100644 --- a/apps/web/src/serlo-editor-integration/serlo-editor.tsx +++ b/apps/web/src/serlo-editor-integration/serlo-editor.tsx @@ -1,9 +1,9 @@ import { defaultPlugins, EditorPluginType, + TemplatePluginType, type SerloEditorProps as EditorProps, } from '@editor/package' -import { TemplatePluginType } from '@editor/types/template-plugin-type' import { SerloOnlyFeaturesContext } from '@editor/utils/serlo-extra-context' import dynamic from 'next/dynamic' diff --git a/packages/editor/src/package/index.ts b/packages/editor/src/package/index.ts index 625e6438df..6c12af903d 100644 --- a/packages/editor/src/package/index.ts +++ b/packages/editor/src/package/index.ts @@ -21,6 +21,11 @@ export { type PluginMenuType as Plugin } from '@editor/plugins/rows/utils/plugin export { EditorPluginType } from '@editor/types/editor-plugin-type' +/** + * Exported for serlo.org + */ +export { TemplatePluginType } from '@editor/types/template-plugin-type' + /** * Exported so that integrations can customize available plugins * based on the default plugins. From 8f453d0a9d15352095a47c7f9b7ef043e69258c9 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Fri, 3 Jan 2025 12:27:45 +0100 Subject: [PATCH 03/78] fix: enable SaveButton --- apps/web/src/data/en/index.ts | 4 ++++ .../components/save-button.tsx | 16 ++++++++-------- .../serlo-editor-integration/serlo-editor.tsx | 13 +++++++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/apps/web/src/data/en/index.ts b/apps/web/src/data/en/index.ts index e3a22e868f..24549fa507 100644 --- a/apps/web/src/data/en/index.ts +++ b/apps/web/src/data/en/index.ts @@ -478,6 +478,10 @@ export const instanceData = { title: 'Title', trashed: 'Trashed…', }, + saveButton: { + noChangesWarning: 'Nothing changed so there is no need to save yet', + save: 'Save', + }, }, } export const instanceLandingData = { diff --git a/apps/web/src/serlo-editor-integration/components/save-button.tsx b/apps/web/src/serlo-editor-integration/components/save-button.tsx index ac9d7a8f61..2238f815c6 100644 --- a/apps/web/src/serlo-editor-integration/components/save-button.tsx +++ b/apps/web/src/serlo-editor-integration/components/save-button.tsx @@ -1,31 +1,31 @@ -import { FaIcon } from '@editor/editor-ui/fa-icon' -import { showToastNotice } from '@editor/editor-ui/show-toast-notice' -import { useEditStrings } from '@editor/i18n/edit-strings-provider' -import { selectHasPendingChanges, useAppSelector } from '@editor/store' import { faSave } from '@fortawesome/free-solid-svg-icons' import { useState } from 'react' import { createPortal } from 'react-dom' import { SaveModal } from './save-modal' import type { SerloEditorProps } from '../serlo-editor' +import { FaIcon } from '@/components/fa-icon' +import { useInstanceData } from '@/contexts/instance-context' +import { showToastNotice } from '@/helper/show-toast-notice' import { useLeaveConfirm } from '@/helper/use-leave-confirm' export function SaveButton({ onSave, + isChanged, isInTestArea, }: { onSave: SerloEditorProps['onSave'] + isChanged: boolean isInTestArea?: boolean }) { - const isChanged = useAppSelector(selectHasPendingChanges) const [saveModalOpen, setSaveModalOpen] = useState(false) - const editStrings = useEditStrings() + const saveButtonStrings = useInstanceData().strings.saveButton const handleClick = () => isChanged ? setSaveModalOpen(true) - : showToastNotice('👀 ' + editStrings.noChangesWarning) + : showToastNotice('👀 ' + saveButtonStrings.noChangesWarning) useLeaveConfirm(isChanged) @@ -36,7 +36,7 @@ export function SaveButton({ return createPortal(
{(editor) => { + const hasPendingChanges = editor.history.pendingChanges !== 0 return ( <> - {/* */} + {isNewEntity ? ( Date: Wed, 8 Jan 2025 11:00:54 +0100 Subject: [PATCH 04/78] refactor: remove BoxRenderer import from ExamsInfoBox --- apps/web/src/components/exams-info-box.tsx | 116 ++++++++++----------- 1 file changed, 58 insertions(+), 58 deletions(-) diff --git a/apps/web/src/components/exams-info-box.tsx b/apps/web/src/components/exams-info-box.tsx index c0084a42fc..f44c4b1c07 100644 --- a/apps/web/src/components/exams-info-box.tsx +++ b/apps/web/src/components/exams-info-box.tsx @@ -1,4 +1,3 @@ -import { BoxRenderer } from '@editor/plugins/box/renderer' import { faDiscord } from '@fortawesome/free-brands-svg-icons' import { faGraduationCap } from '@fortawesome/free-solid-svg-icons' @@ -31,69 +30,70 @@ export function ExamsInfoBox({ examsFolderId }: { examsFolderId: number }) { '2xl:w-[270px]' )} > - div.my-block]:first:mt-3.5 [&>div.my-block]:last:mb-3.5' + )} > <> -
-

- 🎓 Prüfungsbereich für{' '} - {deRegions[region].title}
+

+ 🎓 Prüfungsbereich für{' '} + {deRegions[region].title}
+
+

+
+

+ + Weitere Bundesländer{' '} + & Aufgaben: +
-

-
-

- - Weitere Bundesländer{' '} - & Aufgaben: - -
- + - - - Mathe- Prüfungen{' '} - Startseite - - -

-

- Austausch & Hilfe: -
- + + Mathe- Prüfungen{' '} + Startseite + + +

+

+ Austausch & Hilfe: +
+ + - - Prüfungen-Discord - -

-
+ /> + Prüfungen-Discord + +

{region === 'niedersachsen' ? ( // only for niedersachsen
@@ -103,7 +103,7 @@ export function ExamsInfoBox({ examsFolderId }: { examsFolderId: number }) {
) : null} - + {extraMeta ? : null}