Skip to content

Commit

Permalink
Merge pull request #3277 from serlo/feat/editor-plugin-copy-tool
Browse files Browse the repository at this point in the history
feat(editor): mvp for copy&pasting plugins
  • Loading branch information
hejtful authored Jan 15, 2024
2 parents a1e72b4 + b85667b commit e6bdec4
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 3 deletions.
6 changes: 6 additions & 0 deletions apps/web/src/components/user/profile-experimental.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ export const features = {
activeInDev: true,
hideInProduction: true,
},
editorPluginCopyTool: {
cookieName: 'useEditorPluginCopyTool',
isActive: false,
activeInDev: true,
hideInProduction: true,
},
editorAnchorLinkCopyTool: {
cookieName: 'useEditorAnchorLinkCopyTool',
isActive: false,
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/data/en/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -836,6 +836,8 @@ export const loggedInData = {
link: 'Link (%ctrlOrCmd% + K)',
noElementPasteInLists:
'Sorry, pasting elements inside of lists is not allowed.',
pastingPluginNotAllowedHere:
'Sorry, pasting this plugin here is not allowed.',
linkOverlay: {
placeholder: 'https://… or /1234',
inputLabel: 'Paste or type a link',
Expand Down Expand Up @@ -1084,6 +1086,8 @@ export const loggedInData = {
ready: 'Ready to save?',
anchorLinkWarning:
'This link will only work in the frontend and for content that has a somewhat new revision.',
pluginCopyInfo: 'You can now paste this plugin into text plugins',
pluginCopyButtonLabel: 'Copy plugin to clipboard',
},
taxonomy: {
title: 'Title',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { selectStaticDocumentWithoutIds, store } from '@editor/store'
import { faCopy } from '@fortawesome/free-solid-svg-icons'
import { shouldUseFeature } from '@serlo/frontend/src/components/user/profile-experimental'
import { useInstanceData } from '@serlo/frontend/src/contexts/instance-context'
import { useEditorStrings } from '@serlo/frontend/src/contexts/logged-in-data-context'
import { showToastNotice } from '@serlo/frontend/src/helper/show-toast-notice'
import { useCallback } from 'react'

import { DropdownButton } from './dropdown-button'

interface PluginCopyToolProps {
pluginId: string
}
/**
* experimental plugin to copy plugin's editor state to the clipboard
*/
export function PluginCopyTool({ pluginId }: PluginCopyToolProps) {
const editorStrings = useEditorStrings()
const { strings } = useInstanceData()

const handleOnClick = useCallback(() => {
const document = selectStaticDocumentWithoutIds(store.getState(), pluginId)
const rowsDocument = { plugin: 'rows', state: [document] }

void navigator.clipboard.writeText(JSON.stringify(rowsDocument))
showToastNotice(strings.share.copySuccess, 'success', 2000)
showToastNotice(
'👉 ' + editorStrings.edtrIo.pluginCopyInfo,
undefined,
4000
)
}, [pluginId, strings, editorStrings])

if (!navigator.clipboard || !shouldUseFeature('editorPluginCopyTool')) {
return null
}

return (
<DropdownButton
onClick={handleOnClick}
label={editorStrings.edtrIo.pluginCopyButtonLabel}
icon={faCopy}
className="mt-2.5 border-t pt-2.5"
dataQa="plugin-copy-tool-button"
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { useCallback, useContext } from 'react'

import { AnchorLinkCopyTool } from './anchor-link-copy-tool'
import { DropdownButton } from './dropdown-button'
import { PluginCopyTool } from './plugin-copy-tool'

interface PluginDefaultToolsProps {
pluginId: string
Expand Down Expand Up @@ -82,6 +83,7 @@ export function PluginDefaultTools({ pluginId }: PluginDefaultToolsProps) {
icon={faTrashAlt}
dataQa="remove-plugin-button"
/>
<PluginCopyTool pluginId={pluginId} />
{serloEntityId ? (
<AnchorLinkCopyTool serloEntityId={serloEntityId} pluginId={pluginId} />
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { isSelectionWithinList } from '@editor/editor-ui/plugin-toolbar/text-controls/utils/list'
import { editorPlugins } from '@editor/plugin/helpers/editor-plugins'
import { checkIsAllowedNesting } from '@editor/plugins/rows/utils/check-is-allowed-nesting'
import {
selectDocument,
selectMayManipulateSiblings,
useAppDispatch,
store,
selectAncestorPluginTypes,
} from '@editor/store'
import type { EditorRowsDocument } from '@editor/types/editor-plugins'
import { useEditorStrings } from '@serlo/frontend/src/contexts/logged-in-data-context'
import { showToastNotice } from '@serlo/frontend/src/helper/show-toast-notice'
import { useCallback } from 'react'
import { Editor as SlateEditor } from 'slate'

import { insertPlugin } from '../utils/insert-plugin'
import { shouldUseFeature } from '@/components/user/profile-experimental'

interface UseEditablePasteHandlerArgs {
editor: SlateEditor
Expand All @@ -31,14 +35,43 @@ export const useEditablePasteHandler = (args: UseEditablePasteHandlerArgs) => {
const text = event.clipboardData.getData('text')
if (!files.length && !text) return

// Exit if unable to select document data or if not allowed to manipulate siblings
// Exit if unable to select document data
const storeState = store.getState()
const document = selectDocument(storeState, id)
const mayManipulateSiblings = selectMayManipulateSiblings(storeState, id)
if (!document || !mayManipulateSiblings) return
if (!document) return
let media

// pasting editor document string and insert as plugins
if (
shouldUseFeature('editorPluginCopyTool') &&
!media &&
text.startsWith('{"plugin":"rows"')
) {
const rowsDocument = JSON.parse(text) as EditorRowsDocument
if (rowsDocument.state.length !== 1) return
const pluginDocument = rowsDocument.state.at(0)
const typesOfAncestors = selectAncestorPluginTypes(store.getState(), id)
if (!pluginDocument || typesOfAncestors === null) return

if (
mayManipulateSiblings &&
checkIsAllowedNesting(pluginDocument.plugin, typesOfAncestors)
) {
media = {
pluginType: pluginDocument.plugin,
state: pluginDocument.state,
}
} else {
event.preventDefault()
showToastNotice(textStrings.pastingPluginNotAllowedHere, 'warning')
}
}

// Exit if not allowed to manipulate siblings
if (!mayManipulateSiblings) return

// Iterate through all plugins and try to process clipboard data
let media
for (const { plugin, type } of editorPlugins.getAllWithData()) {
const state = plugin.onFiles?.(files) ?? plugin.onText?.(text)
if (state?.state) {
Expand Down

0 comments on commit e6bdec4

Please sign in to comment.