Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(editor): replace PluginMenuItemType #4138

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions packages/editor/src/package/plugin-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,14 @@ export function getPluginMenuItems(
})
}

interface PluginMenuItem {
export interface PluginMenuItem {
type: PluginMenuType
title: string
description: string
icon: string
initialState: EditorProps['initialState']
// until we use the editor package in the frontend (only having vite for building)
// icons should be strings but are loaded as () => JSX.Element in the frontend (webpack)
icon: string | (() => JSX.Element)
}

const mysteryStrings = {
Expand Down Expand Up @@ -127,7 +129,7 @@ function getTitleAndDescription(
return { title, description }
}

export function getInitialState(
function getInitialState(
type: PluginMenuType
): [EditorProps['initialState'], EditorPluginType] {
switch (type) {
Expand Down Expand Up @@ -223,6 +225,6 @@ const iconLookup: Record<PluginMenuType, string> = {
[pluginMenuType.PagePartners]: IconFallback,
}

export function getIconString(type: PluginMenuType) {
function getIconString(type: PluginMenuType) {
return iconLookup[type]
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import IconFallback from '@editor/editor-ui/assets/plugin-icons/icon-fallback.svg'
import { EditorTooltip } from '@editor/editor-ui/editor-tooltip'
import { getInitialState, PluginMenuType } from '@editor/package/plugin-menu'
import type { PluginMenuItemType } from '@editor/plugins/rows/contexts/plugin-menu/types'
import { EditorPluginType } from '@editor/types/editor-plugin-type'
import type { EditorExerciseDocument } from '@editor/types/editor-plugins'
import type { PluginMenuItem } from '@editor/package/plugin-menu'

import { type ExerciseProps } from '..'
import { type InteractivePluginType } from '../interactive-plugin-types'
Expand All @@ -13,7 +10,7 @@ export function InteractiveExercisesSelection({
interactivePluginOptions,
interactive,
}: {
interactivePluginOptions: PluginMenuItemType[]
interactivePluginOptions: PluginMenuItem[]
interactive: ExerciseProps['state']['interactive']
}) {
const templateStrings = useEditorStrings().templatePlugins
Expand All @@ -25,16 +22,13 @@ export function InteractiveExercisesSelection({
return isFirstInLine ? 'left-0' : isLastInLine ? 'right-0' : '-left-24'
}

function handleOnClick(
pluginType: EditorPluginType,
pluginMenuType: PluginMenuType
) {
function handleOnClick(initialState: PluginMenuItem['initialState']) {
if (interactive.defined) return
const plugin = pluginType as InteractivePluginType
const [exerciseState] = getInitialState(pluginMenuType)
const pluginState = exerciseState.state as EditorExerciseDocument['state']
if (!pluginState.interactive) return
interactive.create({ plugin, state: pluginState.interactive.state })

interactive.create({
plugin: initialState.plugin as InteractivePluginType,
state: initialState.state,
})
}

return (
Expand All @@ -44,11 +38,11 @@ export function InteractiveExercisesSelection({
</p>
<div className="grid grid-cols-4 items-start gap-2 pb-10">
{interactivePluginOptions.map(
({ type, pluginType, title, icon, description }, index) => (
({ type, title, icon, description, initialState }, index) => (
<button
key={title}
data-qa={`add-exercise-${pluginType}`}
onClick={() => handleOnClick(pluginType, type)}
key={type}
data-qa={`add-exercise-${initialState.plugin}`}
onClick={() => handleOnClick(initialState)}
className="serlo-tooltip-trigger w-32 rounded-md p-1 hover:shadow-xl focus:shadow-xl"
>
<EditorTooltip
Expand Down
44 changes: 23 additions & 21 deletions packages/editor/src/plugins/exercise/editor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { AddButton } from '@editor/editor-ui'
import { EditorTooltip } from '@editor/editor-ui/editor-tooltip'
import { getPluginMenuItems } from '@editor/package/plugin-menu'
import {
type PluginMenuItem,
getPluginMenuItems,
} from '@editor/package/plugin-menu'
import { editorPlugins } from '@editor/plugin/helpers/editor-plugins'
import { EditorExerciseDocument } from '@editor/types/editor-plugins'
import { isExerciseDocument } from '@editor/types/plugin-type-guards'
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons'
import { FaIcon } from '@serlo/frontend/src/components/fa-icon'
Expand Down Expand Up @@ -30,24 +32,24 @@ export function ExerciseEditor(props: ExerciseProps) {
const exTemplateStrings = editorStrings.templatePlugins.exercise
const exPluginStrings = editorStrings.plugins.exercise

const exerciseMenuItems = useMemo(() => {
const exerciseItems = getPluginMenuItems(editorStrings).filter(
(menuItem) => {
// Initial state of interacgtive plugin menu items are wrapped with an exercise plugin
// but for this component we need the interactive plugin directly
// so we just unwrap them here:
const unwrappedMenuItems = useMemo<PluginMenuItem[]>(() => {
return getPluginMenuItems(editorStrings)
.map((menuItem) => {
if (!isExerciseDocument(menuItem.initialState)) return false
const interactivePlugin =
menuItem.initialState.state.interactive?.plugin
return interactivePlugin && editorPlugins.isSupported(interactivePlugin)
}
)

return exerciseItems.map((menuItem) => ({
type: menuItem.type,
pluginType: (menuItem.initialState as EditorExerciseDocument).state
.interactive!.plugin,
title: menuItem.title,
description: menuItem.description,
icon: menuItem.icon,
}))
const interactive = menuItem.initialState.state.interactive
if (!interactive || !editorPlugins.isSupported(interactive.plugin)) {
return false
}
const pluginMenuItem = {
...menuItem,
initialState: interactive,
}
return pluginMenuItem
})
.filter(Boolean) as unknown as PluginMenuItem[]
}, [editorStrings])

return (
Expand All @@ -67,7 +69,7 @@ export function ExerciseEditor(props: ExerciseProps) {
{focused ? (
<ExerciseToolbar
{...props}
interactivePluginOptions={exerciseMenuItems}
interactivePluginOptions={unwrappedMenuItems}
/>
) : (
<button
Expand Down Expand Up @@ -99,7 +101,7 @@ export function ExerciseEditor(props: ExerciseProps) {
</>
) : (
<InteractiveExercisesSelection
interactivePluginOptions={exerciseMenuItems}
interactivePluginOptions={unwrappedMenuItems}
interactive={interactive}
/>
)}
Expand Down
16 changes: 6 additions & 10 deletions packages/editor/src/plugins/exercise/toolbar/toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,14 @@ import { PluginToolbar, ToolbarSelect } from '@editor/editor-ui/plugin-toolbar'
import { DropdownButton } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/dropdown-button'
import { PluginDefaultTools } from '@editor/editor-ui/plugin-toolbar/plugin-tool-menu/plugin-default-tools'
import {
getInitialState,
type PluginMenuItem,
pluginMenuType,
PluginMenuType,
} from '@editor/package/plugin-menu'
import { PluginMenuItemType } from '@editor/plugins/rows/contexts/plugin-menu/types'
import { type DocumentState, selectDocument, store } from '@editor/store'
import { EditorPluginType } from '@editor/types/editor-plugin-type'
import type {
EditorBlanksExerciseDocument,
EditorExerciseDocument,
EditorScMcExerciseDocument,
} from '@editor/types/editor-plugins'
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'
Expand All @@ -25,7 +23,7 @@ export const ExerciseToolbar = ({
state,
interactivePluginOptions,
}: ExerciseProps & {
interactivePluginOptions: PluginMenuItemType[]
interactivePluginOptions: PluginMenuItem[]
}) => {
const { interactive } = state
const exTemplateStrings = useEditorStrings().templatePlugins.exercise
Expand All @@ -43,12 +41,10 @@ export const ExerciseToolbar = ({
value={currentlySelected ?? ''}
changeValue={(value, index) => {
if (interactive.defined) {
const pluginType = interactivePluginOptions[index]
.pluginType as InteractivePluginType
const exerciseState = getInitialState(value as PluginMenuType)[0]
.state as EditorExerciseDocument['state']
const pluginState = exerciseState.interactive?.state
interactive.replace(pluginType, pluginState)
const pluginInitialState =
interactivePluginOptions[index].initialState
const pluginType = pluginInitialState.plugin as InteractivePluginType
interactive.replace(pluginType, pluginInitialState.state)
}
}}
options={interactivePluginOptions.map(({ type, title }) => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import IconFallback from '@editor/editor-ui/assets/plugin-icons/icon-fallback.svg'
import { EditorTooltip } from '@editor/editor-ui/editor-tooltip'
import type { PluginMenuItem } from '@editor/package/plugin-menu'

import type { PluginMenuItemType } from '../contexts/plugin-menu/types'
import { useEditorStrings } from '@/contexts/logged-in-data-context'
import { cn } from '@/helper/cn'

Expand All @@ -17,12 +17,12 @@ export function PluginMenuItems({
itemRefs,
onInsertPlugin,
}: {
basicOptions: PluginMenuItemType[]
interactiveOptions: PluginMenuItemType[]
basicOptions: PluginMenuItem[]
interactiveOptions: PluginMenuItem[]
focusedItemIndex: number | null
setFocusedItemIndex: (index: number | null) => void
itemRefs: React.MutableRefObject<(HTMLButtonElement | null)[]>
onInsertPlugin: (pluginType: PluginMenuItemType) => void
onInsertPlugin: (pluginType: PluginMenuItem) => void
}) {
const editorStrings = useEditorStrings()

Expand Down Expand Up @@ -52,9 +52,9 @@ export function PluginMenuItems({
</>
)

function renderListItems(options: PluginMenuItemType[], offset: number) {
function renderListItems(options: PluginMenuItem[], offset: number) {
return options.map((pluginMenuItem, index) => {
const { type, pluginType, title, icon, description } = pluginMenuItem
const { type, initialState, title, icon, description } = pluginMenuItem
const currentIndex = index + offset
const selected = currentIndex === focusedItemIndex
const tooltipPosition = getTooltipPosition(index)
Expand All @@ -69,7 +69,7 @@ export function PluginMenuItems({
return (
<li key={type}>
<button
data-qa={`plugin-suggestion-${pluginType}`}
data-qa={`plugin-suggestion-${initialState.plugin}`}
ref={(el) => (itemRefs.current[currentIndex] = el)}
onClick={() => onInsertPlugin(pluginMenuItem)}
onFocus={() => setFocusedItemIndex(currentIndex)}
Expand Down
24 changes: 10 additions & 14 deletions packages/editor/src/plugins/rows/components/plugin-menu-modal.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import IconEmptyPluginsModal from '@editor/editor-ui/assets/plugin-icons/icon-question-mark.svg'
import { EditorInput } from '@editor/editor-ui/editor-input'
import { getPluginMenuItems } from '@editor/package/plugin-menu'
import {
type PluginMenuItem,
getPluginMenuItems,
} from '@editor/package/plugin-menu'
import {
PluginMenuActionTypes,
PluginMenuContext,
Expand All @@ -12,14 +15,13 @@ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react'
import { Key } from 'ts-key-enum'

import { PluginMenuItems } from './plugin-menu-items'
import { PluginMenuItemType } from '../contexts/plugin-menu/types'
import { usePluginMenuKeyboardHandler } from '../hooks/use-plugin-menu-keyboard-handler'
import { checkIsAllowedNesting } from '../utils/check-is-allowed-nesting'
import { filterOptions } from '../utils/plugin-menu'
import { ModalWithCloseButton } from '@/components/modal-with-close-button'

interface PluginMenuModalProps {
onInsertPlugin: (pluginType: PluginMenuItemType) => void
onInsertPlugin: (pluginMenuItem: PluginMenuItem) => void
}

export function PluginMenuModal({ onInsertPlugin }: PluginMenuModalProps) {
Expand Down Expand Up @@ -60,15 +62,9 @@ export function PluginMenuModal({ onInsertPlugin }: PluginMenuModalProps) {
])

const allowedMenuItems = useMemo(() => {
return menuItems
.map((menuItem) => ({
type: menuItem.type,
pluginType: menuItem.initialState.plugin as EditorPluginType,
title: menuItem.title,
description: menuItem.description,
icon: menuItem.icon,
}))
.filter(({ pluginType }) => allowedPlugins.includes(pluginType))
return menuItems.filter(({ initialState }) =>
allowedPlugins.includes(initialState.plugin)
)
}, [allowedPlugins, menuItems])

const { basicOptions, interactiveOptions, firstOption, isEmpty } =
Expand All @@ -79,11 +75,11 @@ export function PluginMenuModal({ onInsertPlugin }: PluginMenuModalProps) {
)

const basicOptions = filteredBySearchString.filter(
(option) => option.pluginType !== EditorPluginType.Exercise
({ initialState }) => initialState.plugin !== EditorPluginType.Exercise
)

const interactiveOptions = filteredBySearchString.filter(
(option) => option.pluginType === EditorPluginType.Exercise
({ initialState }) => initialState.plugin === EditorPluginType.Exercise
)

const firstOption = basicOptions.at(0) ?? interactiveOptions.at(0)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getInitialState } from '@editor/package/plugin-menu'
import type { PluginMenuItem } from '@editor/package/plugin-menu'
import {
selectEmptyTextPluginChildrenIndexes,
selectParentPluginType,
Expand All @@ -15,7 +15,6 @@ import {
PluginMenuActionTypes,
PluginMenuContext,
} from '../contexts/plugin-menu'
import { PluginMenuItemType } from '../contexts/plugin-menu/types'

export function RowsInnerEditor({ state, config, id }: RowsProps) {
// since this is only used to check if the current plugin is the child of the
Expand All @@ -36,14 +35,11 @@ export function RowsInnerEditor({ state, config, id }: RowsProps) {
})
}

function handleInsertPlugin(pluginMenuItem: PluginMenuItemType) {
const [pluginToInsert] = getInitialState(pluginMenuItem.type)
if (!pluginToInsert) return

function handleInsertPlugin(pluginMenuItem: PluginMenuItem) {
if (pluginMenuState.insertCallback) {
pluginMenuState.insertCallback(pluginToInsert)
pluginMenuState.insertCallback(pluginMenuItem.initialState)
} else {
state.insert(pluginMenuState.insertIndex, pluginToInsert)
state.insert(pluginMenuState.insertIndex, pluginMenuItem.initialState)
removeEmptyTextPluginChildren()
}

Expand Down
12 changes: 0 additions & 12 deletions packages/editor/src/plugins/rows/contexts/plugin-menu/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { PluginMenuType } from '@editor/package/plugin-menu'
import type { EditorPluginType } from '@editor/types/editor-plugin-type'
import type { AnyEditorDocument } from '@editor/types/editor-plugins'

type ActionMap<M extends { [index: string]: any }> = {
Expand Down Expand Up @@ -40,13 +38,3 @@ export interface PluginMenuState {
insertIndex: number | undefined
insertCallback: ((plugin: AnyEditorDocument) => void) | undefined
}

export interface PluginMenuItemType {
type: PluginMenuType
pluginType: EditorPluginType
title: string
description: string
// until we use the editor package in the frontend (only having vite for building)
// icons should be strings but are loaded as () => JSX.Element in the frontend (webpack)
icon: string | (() => JSX.Element)
}
7 changes: 4 additions & 3 deletions packages/editor/src/plugins/rows/utils/plugin-menu.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { PluginMenuItemType } from '../contexts/plugin-menu/types'
import type { PluginMenuItem } from '@editor/package/plugin-menu'

export function filterOptions(option: PluginMenuItemType[], text: string) {
export function filterOptions(option: PluginMenuItem[], text: string) {
if (!text.length) return option

const search = text.toLowerCase()
Expand All @@ -9,6 +9,7 @@ export function filterOptions(option: PluginMenuItemType[], text: string) {
return option.filter(
(entry) =>
entry.title.toLowerCase().includes(search) ||
entry.pluginType.toLowerCase().includes(search)
entry.type.toLowerCase().includes(search) ||
entry.initialState.plugin.toLowerCase().includes(search)
)
}