From e9a0ce859c2a57adf5a59bf51d1467575374d186 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 19 May 2024 14:01:16 +0200 Subject: [PATCH 01/10] wip: Add first prototype for datenraum integration --- .../create-plugins.tsx | 10 + .../plugins/datenraum-integration/index.tsx | 230 ++++++++++++++++++ .../editor/src/types/editor-plugin-type.ts | 2 + 3 files changed, 242 insertions(+) create mode 100644 packages/editor/src/plugins/datenraum-integration/index.tsx diff --git a/apps/web/src/serlo-editor-integration/create-plugins.tsx b/apps/web/src/serlo-editor-integration/create-plugins.tsx index fe36d6a6ec..c6d253af1f 100644 --- a/apps/web/src/serlo-editor-integration/create-plugins.tsx +++ b/apps/web/src/serlo-editor-integration/create-plugins.tsx @@ -55,6 +55,7 @@ import { shouldUseFeature } from '@/components/user/profile-experimental' import { type LoggedInData, UuidType } from '@/data-types' import { isProduction } from '@/helper/is-production' import { imagePlugin } from '@/serlo-editor-integration/image-with-serlo-config' +import {datenraumIntegrationPlugin} from '@editor/plugins/datenraum-integration' export function createPlugins({ editorStrings, @@ -142,6 +143,15 @@ export function createPlugins({ icon: , }, ]), + ...(isProduction + ? [] + : [ + { + type: EditorPluginType.DatenraumIntegration, + plugin: datenraumIntegrationPlugin, + visibleInSuggestions: true, + }, + ]), { type: EditorPluginType.Anchor, plugin: anchorPlugin, diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx new file mode 100644 index 0000000000..e11cdafeb9 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -0,0 +1,230 @@ +import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useState } from 'react' +import Modal from 'react-modal' + +import { EditorPluginProps, object, optional, string } from '../../plugin' + +const state = object({ + resource: optional( + object({ + url: string(), + title: string(), + description: string(), + }) + ), +}) + +type DatenraumIntegrationState = typeof state +type DatenraumIntegrationProps = EditorPluginProps + +export const datenraumIntegrationPlugin = { + state, + config: {}, + Component: DatenraumIntegrationEditor, +} + +function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { + const [showSearch, setShowSearch] = useState(false) + const { resource } = props.state + + return ( + <> + {renderSearchModal()} + {resource.defined + ? renderResource({ + title: resource.title.value, + url: resource.url.value, + description: resource.description.value, + }) + : renderEmptyPlugin()} + + ) + + function renderEmptyPlugin() { + return ( +
+ +
+ ) + } + + function renderResource(resource: LearningResource) { + return ( + alert(resource.title)} + /> + ) + } + + function renderSearchModal() { + return ( + setShowSearch(false)} + style={{ + content: { + width: '80%', + height: '80vh', + top: '50%', + left: '50%', + bottom: 'auto', + right: 'auto', + transform: 'translate(-50%, -50%)', + }, + overlay: { zIndex: 100 }, + }} + > + + + ) + } + + function handleSelectResource(resource: LearningResource) { + const resourceState = props.state.resource + + if (resourceState.defined) { + resourceState.description.set(resource.description) + resourceState.title.set(resource.title) + resourceState.url.set(resource.url) + } else { + resourceState.create(resource) + } + + setShowSearch(false) + } +} + +function SearchPanel({ + onSelect, +}: { + onSelect: (resource: LearningResource) => void +}) { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [loading, setLoading] = useState(false) + + const handleSearch = async () => { + if (!loading) { + setLoading(true) + await search(query) + setLoading(false) + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value) + } + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + void handleSearch() + } + } + + return ( +
+
+ + +
+ + {loading && } + + {results.map((resource, index) => ( + onSelect(resource)} + /> + ))} +
+ ) + + async function search(query: string) { + const response = await fetch( + `/api/experimental/search-datenraum?q=${query}` + ) + + setResults([ + { + url: 'https://serlo.org/1855', + title: 'Addition', + description: 'Artikel zur Addition', + }, + { + url: 'https://vhs.de/1666', + title: 'Kurs', + description: 'Kurs der VHS', + }, + ]) + } +} + +function LearningResourceComponent({ + onClick, + resource, +}: { + resource: LearningResource + onClick: (resource: LearningResource) => void +}) { + const { url, description, title } = resource + + return ( +
onClick(resource)} + > + +
+

{title}

+

{description}

+
+
+ ) +} + +function Icon({ url }: { url: string }) { + if (url.includes('serlo')) { + return ( + Serlo + ) + } else if (url.includes('vhs')) { + return ( + Serlo + ) + } +} + +interface LearningResource { + url: string + title: string + description: string +} diff --git a/packages/editor/src/types/editor-plugin-type.ts b/packages/editor/src/types/editor-plugin-type.ts index 075e17c304..c80d6db5a8 100644 --- a/packages/editor/src/types/editor-plugin-type.ts +++ b/packages/editor/src/types/editor-plugin-type.ts @@ -33,4 +33,6 @@ export enum EditorPluginType { Solution = 'solution', Unsupported = 'unsupported', + + DatenraumIntegration = 'datenraumIntegration', } From d2f7575b29ff48f38d4d51bd71d646c641ab726f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 19 May 2024 14:39:07 +0200 Subject: [PATCH 02/10] feat: Add /api/experimential/datenraum-search --- .../api/experimental/search-datenraum.ts | 106 ++++++++++++++++++ .../plugins/datenraum-integration/index.tsx | 19 ++-- 2 files changed, 113 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/pages/api/experimental/search-datenraum.ts diff --git a/apps/web/src/pages/api/experimental/search-datenraum.ts b/apps/web/src/pages/api/experimental/search-datenraum.ts new file mode 100644 index 0000000000..e6652c69f9 --- /dev/null +++ b/apps/web/src/pages/api/experimental/search-datenraum.ts @@ -0,0 +1,106 @@ +import * as t from 'io-ts' +import type { NextApiRequest, NextApiResponse } from 'next' + +import { isProduction } from '@/helper/is-production' + +const AccessTokenResponse = t.type({ + access_token: t.string, +}) + +const SearchResponse = t.type({ + _embedded: t.type({ + nodes: t.array(t.unknown), + }), +}) + +const SearchNode = t.type({ + title: t.string, + description: t.string, + metadata: t.type({ Amb: t.type({ id: t.string }) }), +}) + +export default async function handler( + req: NextApiRequest, + res: NextApiResponse +) { + if (isProduction) { + res.status(404).end() + return + } + + const cliendId = process.env.DATENRAUM_CLIENT_ID + const clientSecret = process.env.DATENRAUM_CLIENT_SECRET + + if (!cliendId || !clientSecret) { + res.status(500).json({ message: 'Datenraum credentials not set' }) + return + } + + const { q: query } = req.query + + if (!query || Array.isArray(query)) { + res.status(400).json({ + message: 'Query parameter missing or multiple parameter are passed to it', + }) + return + } + + const accessTokenResponse = await fetch( + 'https://aai.demo.meinbildungsraum.de/realms/nbp-aai/protocol/openid-connect/token', + { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Authorization: `Basic ${btoa(`${cliendId}:${clientSecret}`)}`, + }, + body: 'grant_type=client_credentials', + } + ) + + if (!accessTokenResponse.ok) { + res.status(500).json({ message: 'Failed to get access token' }) + return + } + + const accessTokenResponseJson = (await accessTokenResponse.json()) as unknown + + if (!AccessTokenResponse.is(accessTokenResponseJson)) { + res.status(500).json({ message: 'Access token missing' }) + return + } + + const { access_token: accessToken } = accessTokenResponseJson + + const searchResponse = await fetch( + `https://dam.demo.meinbildungsraum.de/datenraum/api/core/nodes?search=${encodeURIComponent(query)}&offset=0&limit=30`, + { + headers: { + Accept: 'application/json', + Authorization: `Bearer ${accessToken}`, + }, + } + ) + + const searchResults = (await searchResponse.json()) as unknown + + if (!SearchResponse.is(searchResults)) { + res.status(500).json({ message: 'Failed to get search results' }) + return + } + + const nodes = searchResults._embedded.nodes.filter(SearchNode.is) + + res.json( + nodes.map((node) => ({ + title: node.title, + description: node.description, + url: node.metadata.Amb.id, + })) + ) +} + +export const config = { + api: { + externalResolver: true, + }, +} diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx index e11cdafeb9..94b9f9c42d 100644 --- a/packages/editor/src/plugins/datenraum-integration/index.tsx +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -165,18 +165,13 @@ function SearchPanel({ `/api/experimental/search-datenraum?q=${query}` ) - setResults([ - { - url: 'https://serlo.org/1855', - title: 'Addition', - description: 'Artikel zur Addition', - }, - { - url: 'https://vhs.de/1666', - title: 'Kurs', - description: 'Kurs der VHS', - }, - ]) + if (!response.ok) { + alert('Failed to fetch search results: ' + (await response.text())) + setResults([]) + return + } + + setResults((await response.json()) as LearningResource[]) } } From 1aa68fee59e0cbfb0f5e965c16fa5b76b04538d9 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 19 May 2024 14:42:34 +0200 Subject: [PATCH 03/10] feat: No alert message in edit mode --- .../src/plugins/datenraum-integration/index.tsx | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx index 94b9f9c42d..337cf43ea6 100644 --- a/packages/editor/src/plugins/datenraum-integration/index.tsx +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -55,12 +55,7 @@ function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { } function renderResource(resource: LearningResource) { - return ( - alert(resource.title)} - /> - ) + return } function renderSearchModal() { @@ -176,11 +171,11 @@ function SearchPanel({ } function LearningResourceComponent({ - onClick, + onClick = () => {}, resource, }: { resource: LearningResource - onClick: (resource: LearningResource) => void + onClick?: (resource: LearningResource) => void }) { const { url, description, title } = resource From de69a389a8df40ce119d856080e19c26f0204e5f Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 19 May 2024 15:06:18 +0200 Subject: [PATCH 04/10] feat(datenraum-integration): Add toolbar --- .../plugins/datenraum-integration/index.tsx | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx index 337cf43ea6..c860e4f4a7 100644 --- a/packages/editor/src/plugins/datenraum-integration/index.tsx +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -1,3 +1,5 @@ +import { PluginToolbar } from '@editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useState } from 'react' @@ -30,6 +32,7 @@ function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { return ( <> + {renderPluginToolbar()} {renderSearchModal()} {resource.defined ? renderResource({ @@ -55,7 +58,11 @@ function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { } function renderResource(resource: LearningResource) { - return + return ( +
+ +
+ ) } function renderSearchModal() { @@ -94,6 +101,28 @@ function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { setShowSearch(false) } + + function renderPluginToolbar() { + if (!props.focused) return null + + return ( + } + pluginSettings={ + <> + + + } + /> + ) + } } function SearchPanel({ From dde5c6d96ba77ec440dd2701cb1a0bcd2ba5d974 Mon Sep 17 00:00:00 2001 From: Stephan Kulla Date: Sun, 19 May 2024 15:46:18 +0200 Subject: [PATCH 05/10] feat(datenraum-integration): Add static renderer --- .../create-renderers.tsx | 12 ++++ .../plugins/datenraum-integration/index.tsx | 66 ++---------------- .../plugins/datenraum-integration/state.ts | 13 ++++ .../plugins/datenraum-integration/static.tsx | 68 +++++++++++++++++++ 4 files changed, 97 insertions(+), 62 deletions(-) create mode 100644 packages/editor/src/plugins/datenraum-integration/state.ts create mode 100644 packages/editor/src/plugins/datenraum-integration/static.tsx diff --git a/apps/web/src/serlo-editor-integration/create-renderers.tsx b/apps/web/src/serlo-editor-integration/create-renderers.tsx index bac2a9d91e..2b4c55ee3b 100644 --- a/apps/web/src/serlo-editor-integration/create-renderers.tsx +++ b/apps/web/src/serlo-editor-integration/create-renderers.tsx @@ -5,6 +5,7 @@ import { import { AnchorStaticRenderer } from '@editor/plugins/anchor/static' import { ArticleStaticRenderer } from '@editor/plugins/article/static' import { BoxStaticRenderer } from '@editor/plugins/box/static' +import { DatenraumIntegrationDocument } from '@editor/plugins/datenraum-integraton/static' import { RowsStaticRenderer } from '@editor/plugins/rows/static' import type { MathElement } from '@editor/plugins/text' import { TextStaticRenderer } from '@editor/plugins/text/static' @@ -38,6 +39,12 @@ import { Link } from '@/components/content/link' import { isPrintMode } from '@/components/print-mode' import { MultimediaSerloStaticRenderer } from '@/serlo-editor-integration/serlo-plugin-wrappers/multimedia-serlo-static-renderer' +const DatenraumIntegrationStaticRenderer = + dynamic(() => + import('@editor/plugins/datenraum-integration/static').then( + (mod) => mod.DatenraumIntegrationStaticRenderer + ) + ) const EquationsStaticRenderer = dynamic(() => import('@editor/plugins/equations/static').then( (mod) => mod.EquationsStaticRenderer @@ -232,6 +239,11 @@ export function createRenderers(): InitRenderersArgs { return null }, }, + // experimental plugins + { + type: EditorPluginType.DatenraumIntegration, + renderer: DatenraumIntegrationStaticRenderer, + }, ], mathRenderer: (element: MathElement) => element.inline ? ( diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx index c860e4f4a7..b1463f6e10 100644 --- a/packages/editor/src/plugins/datenraum-integration/index.tsx +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -5,19 +5,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useState } from 'react' import Modal from 'react-modal' -import { EditorPluginProps, object, optional, string } from '../../plugin' - -const state = object({ - resource: optional( - object({ - url: string(), - title: string(), - description: string(), - }) - ), -}) - -type DatenraumIntegrationState = typeof state +import { state, DatenraumIntegrationState } from './state' +import { LearningResourceComponent, LearningResource } from './static' +import { EditorPluginProps } from '../../plugin' + type DatenraumIntegrationProps = EditorPluginProps export const datenraumIntegrationPlugin = { @@ -198,52 +189,3 @@ function SearchPanel({ setResults((await response.json()) as LearningResource[]) } } - -function LearningResourceComponent({ - onClick = () => {}, - resource, -}: { - resource: LearningResource - onClick?: (resource: LearningResource) => void -}) { - const { url, description, title } = resource - - return ( -
onClick(resource)} - > - -
-

{title}

-

{description}

-
-
- ) -} - -function Icon({ url }: { url: string }) { - if (url.includes('serlo')) { - return ( - Serlo - ) - } else if (url.includes('vhs')) { - return ( - Serlo - ) - } -} - -interface LearningResource { - url: string - title: string - description: string -} diff --git a/packages/editor/src/plugins/datenraum-integration/state.ts b/packages/editor/src/plugins/datenraum-integration/state.ts new file mode 100644 index 0000000000..aeef126825 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/state.ts @@ -0,0 +1,13 @@ +import { object, optional, string } from '../../plugin' + +export const state = object({ + resource: optional( + object({ + url: string(), + title: string(), + description: string(), + }) + ), +}) + +export type DatenraumIntegrationState = typeof state diff --git a/packages/editor/src/plugins/datenraum-integration/static.tsx b/packages/editor/src/plugins/datenraum-integration/static.tsx new file mode 100644 index 0000000000..d2196c7516 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/static.tsx @@ -0,0 +1,68 @@ +import { DatenraumIntegrationState } from './state' +import type { PrettyStaticState } from '../../plugin' + +export interface DatenraumIntegrationDocument { + state: PrettyStaticState +} + +export function DatenraumIntegrationStaticRenderer({ + state, +}: DatenraumIntegrationDocument) { + const { resource } = state + + return resource ? ( + window.open(resource.url, '_blank')} + /> + ) : null +} + +export function LearningResourceComponent({ + onClick = () => {}, + resource, +}: { + resource: LearningResource + onClick?: (resource: LearningResource) => void +}) { + const { url, description, title } = resource + + return ( +
onClick(resource)} + > + +
+

{title}

+

{description}

+
+
+ ) +} + +function Icon({ url }: { url: string }) { + if (url.includes('serlo')) { + return ( + Serlo + ) + } else if (url.includes('vhs')) { + return ( + Serlo + ) + } +} + +export interface LearningResource { + url: string + title: string + description: string +} From ca24da76c43515161b7138390f799f80a6b76bd1 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Tue, 21 May 2024 16:14:39 +0200 Subject: [PATCH 06/10] refactor(datenraum): split components --- .../datenraum-integration/components/icon.tsx | 19 ++ .../components/learning-resource.tsx | 32 +++ .../components/search-panel.tsx | 82 ++++++++ .../plugins/datenraum-integration/editor.tsx | 112 +++++++++++ .../plugins/datenraum-integration/index.tsx | 185 +----------------- .../plugins/datenraum-integration/static.tsx | 50 +---- .../plugins/datenraum-integration/types.ts | 0 7 files changed, 249 insertions(+), 231 deletions(-) create mode 100644 packages/editor/src/plugins/datenraum-integration/components/icon.tsx create mode 100644 packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx create mode 100644 packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx create mode 100644 packages/editor/src/plugins/datenraum-integration/editor.tsx create mode 100644 packages/editor/src/plugins/datenraum-integration/types.ts diff --git a/packages/editor/src/plugins/datenraum-integration/components/icon.tsx b/packages/editor/src/plugins/datenraum-integration/components/icon.tsx new file mode 100644 index 0000000000..6b05d74db4 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/components/icon.tsx @@ -0,0 +1,19 @@ +export function Icon({ url }: { url: string }) { + if (url.includes('serlo')) { + return ( + Serlo + ) + } else if (url.includes('vhs')) { + return ( + Serlo + ) + } +} diff --git a/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx b/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx new file mode 100644 index 0000000000..f848fe3c0e --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx @@ -0,0 +1,32 @@ +import { Icon } from './icon' + +export interface LearningResource { + url: string + title: string + description: string +} + +interface LearningResourceComponentProps { + resource: LearningResource + onClick?: (resource: LearningResource) => void +} + +export function LearningResourceComponent({ + onClick = () => {}, + resource, +}: LearningResourceComponentProps) { + const { url, description, title } = resource + + return ( +
onClick(resource)} + > + +
+

{title}

+

{description}

+
+
+ ) +} diff --git a/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx new file mode 100644 index 0000000000..ae0658c0d0 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx @@ -0,0 +1,82 @@ +import { faSpinner } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useState } from 'react' + +import { + LearningResourceComponent, + LearningResource, +} from './learning-resource' + +interface SearchPanelProps { + onSelect: (resource: LearningResource) => void +} + +export function SearchPanel({ onSelect }: SearchPanelProps) { + const [query, setQuery] = useState('') + const [results, setResults] = useState([]) + const [loading, setLoading] = useState(false) + + const handleSearch = async () => { + if (!loading) { + setLoading(true) + await search(query) + setLoading(false) + } + } + + const handleInputChange = (e: React.ChangeEvent) => { + setQuery(e.target.value) + } + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + void handleSearch() + } + } + + return ( +
+
+ + +
+ + {loading && } + + {results.map((resource, index) => ( + onSelect(resource)} + /> + ))} +
+ ) + + async function search(query: string) { + const response = await fetch( + `/api/experimental/search-datenraum?q=${query}` + ) + + if (!response.ok) { + alert('Failed to fetch search results: ' + (await response.text())) + setResults([]) + return + } + + setResults((await response.json()) as LearningResource[]) + } +} diff --git a/packages/editor/src/plugins/datenraum-integration/editor.tsx b/packages/editor/src/plugins/datenraum-integration/editor.tsx new file mode 100644 index 0000000000..249f7dc48f --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/editor.tsx @@ -0,0 +1,112 @@ +import { PluginToolbar } from '@editor/editor-ui/plugin-toolbar' +import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { faSearch } from '@fortawesome/free-solid-svg-icons' +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' +import { useState } from 'react' +import Modal from 'react-modal' + +import type { DatenraumIntegrationProps } from '.' +import { + LearningResourceComponent, + LearningResource, +} from './components/learning-resource' +import { SearchPanel } from './components/search-panel' + +export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { + const [showSearch, setShowSearch] = useState(false) + const { resource } = props.state + + return ( + <> + {renderPluginToolbar()} + {renderSearchModal()} + {resource.defined + ? renderResource({ + title: resource.title.value, + url: resource.url.value, + description: resource.description.value, + }) + : renderEmptyPlugin()} + + ) + + function renderEmptyPlugin() { + return ( +
+ +
+ ) + } + + function renderResource(resource: LearningResource) { + return ( +
+ +
+ ) + } + + function renderSearchModal() { + return ( + setShowSearch(false)} + style={{ + content: { + width: '80%', + height: '80vh', + top: '50%', + left: '50%', + bottom: 'auto', + right: 'auto', + transform: 'translate(-50%, -50%)', + }, + overlay: { zIndex: 100 }, + }} + > + + + ) + } + + function handleSelectResource(resource: LearningResource) { + const resourceState = props.state.resource + + if (resourceState.defined) { + resourceState.description.set(resource.description) + resourceState.title.set(resource.title) + resourceState.url.set(resource.url) + } else { + resourceState.create(resource) + } + + setShowSearch(false) + } + + function renderPluginToolbar() { + if (!props.focused) return null + + return ( + } + pluginSettings={ + <> + + + } + /> + ) + } +} diff --git a/packages/editor/src/plugins/datenraum-integration/index.tsx b/packages/editor/src/plugins/datenraum-integration/index.tsx index b1463f6e10..8da99615c6 100644 --- a/packages/editor/src/plugins/datenraum-integration/index.tsx +++ b/packages/editor/src/plugins/datenraum-integration/index.tsx @@ -1,191 +1,12 @@ -import { PluginToolbar } from '@editor/editor-ui/plugin-toolbar' -import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' -import { faSearch, faSpinner } from '@fortawesome/free-solid-svg-icons' -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' -import { useState } from 'react' -import Modal from 'react-modal' - +import { DatenraumIntegrationEditor } from './editor' import { state, DatenraumIntegrationState } from './state' -import { LearningResourceComponent, LearningResource } from './static' import { EditorPluginProps } from '../../plugin' -type DatenraumIntegrationProps = EditorPluginProps +export type DatenraumIntegrationProps = + EditorPluginProps export const datenraumIntegrationPlugin = { state, config: {}, Component: DatenraumIntegrationEditor, } - -function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { - const [showSearch, setShowSearch] = useState(false) - const { resource } = props.state - - return ( - <> - {renderPluginToolbar()} - {renderSearchModal()} - {resource.defined - ? renderResource({ - title: resource.title.value, - url: resource.url.value, - description: resource.description.value, - }) - : renderEmptyPlugin()} - - ) - - function renderEmptyPlugin() { - return ( -
- -
- ) - } - - function renderResource(resource: LearningResource) { - return ( -
- -
- ) - } - - function renderSearchModal() { - return ( - setShowSearch(false)} - style={{ - content: { - width: '80%', - height: '80vh', - top: '50%', - left: '50%', - bottom: 'auto', - right: 'auto', - transform: 'translate(-50%, -50%)', - }, - overlay: { zIndex: 100 }, - }} - > - - - ) - } - - function handleSelectResource(resource: LearningResource) { - const resourceState = props.state.resource - - if (resourceState.defined) { - resourceState.description.set(resource.description) - resourceState.title.set(resource.title) - resourceState.url.set(resource.url) - } else { - resourceState.create(resource) - } - - setShowSearch(false) - } - - function renderPluginToolbar() { - if (!props.focused) return null - - return ( - } - pluginSettings={ - <> - - - } - /> - ) - } -} - -function SearchPanel({ - onSelect, -}: { - onSelect: (resource: LearningResource) => void -}) { - const [query, setQuery] = useState('') - const [results, setResults] = useState([]) - const [loading, setLoading] = useState(false) - - const handleSearch = async () => { - if (!loading) { - setLoading(true) - await search(query) - setLoading(false) - } - } - - const handleInputChange = (e: React.ChangeEvent) => { - setQuery(e.target.value) - } - - const handleKeyPress = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - void handleSearch() - } - } - - return ( -
-
- - -
- - {loading && } - - {results.map((resource, index) => ( - onSelect(resource)} - /> - ))} -
- ) - - async function search(query: string) { - const response = await fetch( - `/api/experimental/search-datenraum?q=${query}` - ) - - if (!response.ok) { - alert('Failed to fetch search results: ' + (await response.text())) - setResults([]) - return - } - - setResults((await response.json()) as LearningResource[]) - } -} diff --git a/packages/editor/src/plugins/datenraum-integration/static.tsx b/packages/editor/src/plugins/datenraum-integration/static.tsx index d2196c7516..fd57b94c78 100644 --- a/packages/editor/src/plugins/datenraum-integration/static.tsx +++ b/packages/editor/src/plugins/datenraum-integration/static.tsx @@ -1,3 +1,4 @@ +import { LearningResourceComponent } from './components/learning-resource' import { DatenraumIntegrationState } from './state' import type { PrettyStaticState } from '../../plugin' @@ -17,52 +18,3 @@ export function DatenraumIntegrationStaticRenderer({ /> ) : null } - -export function LearningResourceComponent({ - onClick = () => {}, - resource, -}: { - resource: LearningResource - onClick?: (resource: LearningResource) => void -}) { - const { url, description, title } = resource - - return ( -
onClick(resource)} - > - -
-

{title}

-

{description}

-
-
- ) -} - -function Icon({ url }: { url: string }) { - if (url.includes('serlo')) { - return ( - Serlo - ) - } else if (url.includes('vhs')) { - return ( - Serlo - ) - } -} - -export interface LearningResource { - url: string - title: string - description: string -} diff --git a/packages/editor/src/plugins/datenraum-integration/types.ts b/packages/editor/src/plugins/datenraum-integration/types.ts new file mode 100644 index 0000000000..e69de29bb2 From 69a613c6b44c3d5f6da029bf7b4ca935f338b5ef Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Tue, 21 May 2024 17:57:08 +0200 Subject: [PATCH 07/10] feat(datenraum): hardcoded h5p -> serlo exercise convert --- packages/editor/src/package/editor.tsx | 4 +- .../components/learning-resource.tsx | 32 ------- .../components/search-panel.tsx | 28 +++--- .../plugins/datenraum-integration/const.ts | 77 ++++++++++++++++ .../plugins/datenraum-integration/editor.tsx | 89 +++++++++++++------ .../plugins/datenraum-integration/state.ts | 16 ++-- .../plugins/datenraum-integration/static.tsx | 11 +-- 7 files changed, 165 insertions(+), 92 deletions(-) delete mode 100644 packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx create mode 100644 packages/editor/src/plugins/datenraum-integration/const.ts diff --git a/packages/editor/src/package/editor.tsx b/packages/editor/src/package/editor.tsx index 3936ed2996..66b9ec5f5e 100644 --- a/packages/editor/src/package/editor.tsx +++ b/packages/editor/src/package/editor.tsx @@ -10,8 +10,8 @@ import { type PluginsConfig, defaultSerloEditorProps, type CustomPlugin, -} from './config.js' -import { editorData } from './editor-data.js' +} from './config' +import { editorData } from './editor-data' import { InstanceDataProvider } from '@/contexts/instance-context' import { LoggedInDataProvider } from '@/contexts/logged-in-data-context' diff --git a/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx b/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx deleted file mode 100644 index f848fe3c0e..0000000000 --- a/packages/editor/src/plugins/datenraum-integration/components/learning-resource.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Icon } from './icon' - -export interface LearningResource { - url: string - title: string - description: string -} - -interface LearningResourceComponentProps { - resource: LearningResource - onClick?: (resource: LearningResource) => void -} - -export function LearningResourceComponent({ - onClick = () => {}, - resource, -}: LearningResourceComponentProps) { - const { url, description, title } = resource - - return ( -
onClick(resource)} - > - -
-

{title}

-

{description}

-
-
- ) -} diff --git a/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx index ae0658c0d0..2a1387f438 100644 --- a/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx +++ b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx @@ -1,19 +1,15 @@ +import { H5pRenderer } from '@editor/plugins/h5p/renderer' import { faSpinner } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useState } from 'react' -import { - LearningResourceComponent, - LearningResource, -} from './learning-resource' - interface SearchPanelProps { - onSelect: (resource: LearningResource) => void + onSelect: () => void } export function SearchPanel({ onSelect }: SearchPanelProps) { const [query, setQuery] = useState('') - const [results, setResults] = useState([]) + const [showResults, setShowResults] = useState(false) const [loading, setLoading] = useState(false) const handleSearch = async () => { @@ -56,13 +52,13 @@ export function SearchPanel({ onSelect }: SearchPanelProps) { {loading && } - {results.map((resource, index) => ( - onSelect(resource)} - /> - ))} + {showResults && ( +
+
+ +
+
+ )} ) @@ -73,10 +69,10 @@ export function SearchPanel({ onSelect }: SearchPanelProps) { if (!response.ok) { alert('Failed to fetch search results: ' + (await response.text())) - setResults([]) + setShowResults(false) return } - setResults((await response.json()) as LearningResource[]) + setShowResults(true) } } diff --git a/packages/editor/src/plugins/datenraum-integration/const.ts b/packages/editor/src/plugins/datenraum-integration/const.ts new file mode 100644 index 0000000000..eae61bd306 --- /dev/null +++ b/packages/editor/src/plugins/datenraum-integration/const.ts @@ -0,0 +1,77 @@ +export const hardcodedExerciseState = { + content: { + plugin: 'rows', + state: [ + { + plugin: 'text', + state: [ + { + type: 'p', + children: [{ text: 'Trage die fehlenden Wörter ein!' }], + }, + ], + }, + ], + }, + interactive: { + plugin: 'blanksExercise', + state: { + text: { + plugin: 'text', + state: [ + { + type: 'p', + children: [ + { text: 'Ein ' }, + { + type: 'textBlank', + blankId: 'ae9a2036-c58e-4bc8-89fd-54beb4222fa1', + correctAnswers: [{ answer: 'Riesenrad' }], + acceptMathEquivalents: true, + children: [{ text: '' }], + }, + { + text: ' ist groß und rund, man findet es häufig auf großen Volksfesten. Es hat viele Gondeln.', + }, + ], + }, + { type: 'p', children: [{ text: '' }] }, + { + type: 'p', + children: [ + { text: 'Ein ' }, + { + type: 'textBlank', + blankId: '573872fa-9332-4d5f-9cce-28962d050025', + correctAnswers: [{ answer: 'Zug' }], + acceptMathEquivalents: true, + children: [{ text: '' }], + }, + { text: ' fährt auf Schienen.' }, + ], + }, + { type: 'p', children: [{ text: '' }] }, + { + type: 'p', + children: [ + { text: 'Mit einem ' }, + { + type: 'textBlank', + blankId: '6b1cb8b7-e61a-4f05-9bb3-9339aa523cf0', + correctAnswers: [ + { answer: 'Computer' }, + { answer: 'PC' }, + { answer: 'Laptop' }, + ], + acceptMathEquivalents: true, + children: [{ text: '' }], + }, + { text: ' arbeiten EntwicklerInnen.' }, + ], + }, + ], + }, + mode: 'typing', + }, + }, +} diff --git a/packages/editor/src/plugins/datenraum-integration/editor.tsx b/packages/editor/src/plugins/datenraum-integration/editor.tsx index 249f7dc48f..6c1386d506 100644 --- a/packages/editor/src/plugins/datenraum-integration/editor.tsx +++ b/packages/editor/src/plugins/datenraum-integration/editor.tsx @@ -1,32 +1,38 @@ import { PluginToolbar } from '@editor/editor-ui/plugin-toolbar' import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools' +import { + insertPluginChildBefore, + removePluginChild, + selectChildTreeOfParent, + store, + useAppDispatch, +} from '@editor/store' +import { EditorPluginType } from '@editor/types/editor-plugin-type' import { faSearch } from '@fortawesome/free-solid-svg-icons' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { useState } from 'react' import Modal from 'react-modal' import type { DatenraumIntegrationProps } from '.' -import { - LearningResourceComponent, - LearningResource, -} from './components/learning-resource' import { SearchPanel } from './components/search-panel' +import { hardcodedExerciseState } from './const' +import { H5pRenderer } from '../h5p/renderer' export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { const [showSearch, setShowSearch] = useState(false) - const { resource } = props.state + const [isConverted, setIsConverted] = useState(false) + + const dispatch = useAppDispatch() return ( <> {renderPluginToolbar()} {renderSearchModal()} - {resource.defined - ? renderResource({ - title: resource.title.value, - url: resource.url.value, - description: resource.description.value, - }) - : renderEmptyPlugin()} + {!props.state.showResource.value && !isConverted + ? renderEmptyPlugin() + : null} + {props.state.showResource.value && !isConverted ? renderResource() : null} + {renderConvertedResource()} ) @@ -43,10 +49,22 @@ export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { ) } - function renderResource(resource: LearningResource) { + function renderResource() { return (
- + +
+ ) + } + + function renderConvertedResource() { + return ( +
+ {props.state.convertedResource.render({})}
) } @@ -74,17 +92,8 @@ export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { ) } - function handleSelectResource(resource: LearningResource) { - const resourceState = props.state.resource - - if (resourceState.defined) { - resourceState.description.set(resource.description) - resourceState.title.set(resource.title) - resourceState.url.set(resource.url) - } else { - resourceState.create(resource) - } - + function handleSelectResource() { + props.state.showResource.set(true) setShowSearch(false) } @@ -93,7 +102,7 @@ export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { return ( } pluginSettings={ <> @@ -104,9 +113,37 @@ export function DatenraumIntegrationEditor(props: DatenraumIntegrationProps) { > Anderes Element auswählen + } /> ) } + + function handleConvertResource() { + const parentPlugin = selectChildTreeOfParent(store.getState(), props.id) + + if (!parentPlugin) return null + + dispatch( + insertPluginChildBefore({ + parent: parentPlugin.id, + sibling: props.id, + document: { + plugin: EditorPluginType.Exercise, + state: hardcodedExerciseState, + }, + }) + ) + + dispatch(removePluginChild({ parent: parentPlugin.id, child: props.id })) + + setIsConverted(true) + } } diff --git a/packages/editor/src/plugins/datenraum-integration/state.ts b/packages/editor/src/plugins/datenraum-integration/state.ts index aeef126825..fdb7221707 100644 --- a/packages/editor/src/plugins/datenraum-integration/state.ts +++ b/packages/editor/src/plugins/datenraum-integration/state.ts @@ -1,13 +1,13 @@ -import { object, optional, string } from '../../plugin' +import { EditorPluginType } from '@editor/types/editor-plugin-type' + +import { boolean, child, object, string } from '../../plugin' export const state = object({ - resource: optional( - object({ - url: string(), - title: string(), - description: string(), - }) - ), + showResource: boolean(false), + resource: string('https://app.lumi.education/run/J3j0eR'), + convertedResource: child({ + plugin: EditorPluginType.PasteHack, + }), }) export type DatenraumIntegrationState = typeof state diff --git a/packages/editor/src/plugins/datenraum-integration/static.tsx b/packages/editor/src/plugins/datenraum-integration/static.tsx index fd57b94c78..12d30dcd9e 100644 --- a/packages/editor/src/plugins/datenraum-integration/static.tsx +++ b/packages/editor/src/plugins/datenraum-integration/static.tsx @@ -1,6 +1,6 @@ -import { LearningResourceComponent } from './components/learning-resource' import { DatenraumIntegrationState } from './state' import type { PrettyStaticState } from '../../plugin' +import { H5pRenderer } from '../h5p/renderer' export interface DatenraumIntegrationDocument { state: PrettyStaticState @@ -9,12 +9,7 @@ export interface DatenraumIntegrationDocument { export function DatenraumIntegrationStaticRenderer({ state, }: DatenraumIntegrationDocument) { - const { resource } = state + const { showResource, resource } = state - return resource ? ( - window.open(resource.url, '_blank')} - /> - ) : null + return showResource ? : null } From e295757498f9b67d083cf50ec4107ac7fb736af4 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Tue, 21 May 2024 18:00:59 +0200 Subject: [PATCH 08/10] chore(datenraum): eslint --- apps/web/src/serlo-editor-integration/create-plugins.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/serlo-editor-integration/create-plugins.tsx b/apps/web/src/serlo-editor-integration/create-plugins.tsx index c6d253af1f..3565b8ef46 100644 --- a/apps/web/src/serlo-editor-integration/create-plugins.tsx +++ b/apps/web/src/serlo-editor-integration/create-plugins.tsx @@ -16,6 +16,7 @@ import { articlePlugin } from '@editor/plugins/article' import { audioPlugin } from '@editor/plugins/audio' import { blanksExercise } from '@editor/plugins/blanks-exercise' import { createBoxPlugin } from '@editor/plugins/box' +import { datenraumIntegrationPlugin } from '@editor/plugins/datenraum-integration' import { equationsPlugin } from '@editor/plugins/equations' import { exercisePlugin } from '@editor/plugins/exercise' import { exerciseGroupPlugin } from '@editor/plugins/exercise-group' @@ -55,7 +56,6 @@ import { shouldUseFeature } from '@/components/user/profile-experimental' import { type LoggedInData, UuidType } from '@/data-types' import { isProduction } from '@/helper/is-production' import { imagePlugin } from '@/serlo-editor-integration/image-with-serlo-config' -import {datenraumIntegrationPlugin} from '@editor/plugins/datenraum-integration' export function createPlugins({ editorStrings, From a9d6102265e426509b651ce93d50e25ac236f2e4 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Tue, 21 May 2024 18:11:42 +0200 Subject: [PATCH 09/10] chore(datenraum): typo fix --- apps/web/src/serlo-editor-integration/create-renderers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/serlo-editor-integration/create-renderers.tsx b/apps/web/src/serlo-editor-integration/create-renderers.tsx index 2b4c55ee3b..d793612c1d 100644 --- a/apps/web/src/serlo-editor-integration/create-renderers.tsx +++ b/apps/web/src/serlo-editor-integration/create-renderers.tsx @@ -5,7 +5,7 @@ import { import { AnchorStaticRenderer } from '@editor/plugins/anchor/static' import { ArticleStaticRenderer } from '@editor/plugins/article/static' import { BoxStaticRenderer } from '@editor/plugins/box/static' -import { DatenraumIntegrationDocument } from '@editor/plugins/datenraum-integraton/static' +import { DatenraumIntegrationDocument } from '@editor/plugins/datenraum-integration/static' import { RowsStaticRenderer } from '@editor/plugins/rows/static' import type { MathElement } from '@editor/plugins/text' import { TextStaticRenderer } from '@editor/plugins/text/static' From f15e5526441bad35fa8340cdad16cbe5a5856744 Mon Sep 17 00:00:00 2001 From: Vitomir Budimir Date: Wed, 22 May 2024 11:20:33 +0200 Subject: [PATCH 10/10] chore(datenraum): remove fetch --- .../components/search-panel.tsx | 22 +++++-------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx index 2a1387f438..c12dfa3a70 100644 --- a/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx +++ b/packages/editor/src/plugins/datenraum-integration/components/search-panel.tsx @@ -12,11 +12,13 @@ export function SearchPanel({ onSelect }: SearchPanelProps) { const [showResults, setShowResults] = useState(false) const [loading, setLoading] = useState(false) - const handleSearch = async () => { + const handleSearch = () => { if (!loading) { setLoading(true) - await search(query) - setLoading(false) + setTimeout(() => { + setLoading(false) + setShowResults(true) + }, 500) } } @@ -61,18 +63,4 @@ export function SearchPanel({ onSelect }: SearchPanelProps) { )} ) - - async function search(query: string) { - const response = await fetch( - `/api/experimental/search-datenraum?q=${query}` - ) - - if (!response.ok) { - alert('Failed to fetch search results: ' + (await response.text())) - setShowResults(false) - return - } - - setShowResults(true) - } }