From 1d19a2aedce5ab50df63d9e1fa1ac4a46925285a Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Mon, 10 Mar 2025 16:29:30 +0800 Subject: [PATCH 1/3] feat(app): upload image via block editor --- package.json | 3 +- src/domain/profile/repository.js | 2 +- .../views/team-profile/CustomContent.js | 21 +++++- .../profile/views/team-profile/TeamProfile.js | 2 +- .../{ => control}/block-editor/BlockEditor.js | 4 +- .../block-editor/bubble/Button.js | 0 .../block-editor/bubble/ColorSelector.js | 0 .../block-editor/bubble/LinkSelector.js | 0 .../block-editor/bubble/MathSelector.js | 0 .../block-editor/bubble/NodeSelector.js | 0 .../block-editor/bubble/Popover.js | 0 .../block-editor/bubble/Separator.js | 0 .../block-editor/bubble/TextButtons.js | 0 .../block-editor/bubble/helper.js | 0 .../block-editor/bubble/index.js | 0 .../{ => control}/block-editor/extensions.js | 8 +-- .../{ => control}/block-editor/helper.js | 21 +++++- .../{ => control}/block-editor/index.js | 0 .../{ => control}/block-editor/slash.js | 7 +- .../components/control/block-editor/upload.js | 65 +++++++++++++++++++ 20 files changed, 118 insertions(+), 15 deletions(-) rename src/shared/components/{ => control}/block-editor/BlockEditor.js (96%) rename src/shared/components/{ => control}/block-editor/bubble/Button.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/ColorSelector.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/LinkSelector.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/MathSelector.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/NodeSelector.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/Popover.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/Separator.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/TextButtons.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/helper.js (100%) rename src/shared/components/{ => control}/block-editor/bubble/index.js (100%) rename src/shared/components/{ => control}/block-editor/extensions.js (96%) rename src/shared/components/{ => control}/block-editor/helper.js (72%) rename src/shared/components/{ => control}/block-editor/index.js (100%) rename src/shared/components/{ => control}/block-editor/slash.js (98%) create mode 100644 src/shared/components/control/block-editor/upload.js diff --git a/package.json b/package.json index 980f6082..b967676e 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "next": "13.4.12", "next-auth": "^4.22.1", "nextjs-toploader": "^1.4.2", - "novel": "0.5.0", + "novel": "1.0.2", "qs": "^6.13.0", "rc-pagination": "^3.3.1", "react": "18.2.0", @@ -100,6 +100,7 @@ "survey-react-ui": "^1.9.90", "swr": "^2.2.4", "tabulator-tables": "^5.5.2", + "tiptap-markdown": "0.8.10", "turndown": "^7.2.0", "use-debounce": "^9.0.4", "validator": "^13.9.0", diff --git a/src/domain/profile/repository.js b/src/domain/profile/repository.js index 5859d6b2..4f117cee 100644 --- a/src/domain/profile/repository.js +++ b/src/domain/profile/repository.js @@ -14,7 +14,7 @@ * limitations under the License. */ -import { unwrapBlockData, wrapBlockData } from '@/components/block-editor/helper'; +import { unwrapBlockData, wrapBlockData } from '@/components/control/block-editor/helper'; import httpClient from '@/utils/http'; async function fetchWeb3BioProfile(address) { diff --git a/src/domain/profile/views/team-profile/CustomContent.js b/src/domain/profile/views/team-profile/CustomContent.js index 856820d0..2c2cc062 100644 --- a/src/domain/profile/views/team-profile/CustomContent.js +++ b/src/domain/profile/views/team-profile/CustomContent.js @@ -17,7 +17,26 @@ import clsx from 'clsx'; import { useState } from 'react'; -import BlockEditor, { getInitialBlockData, isBlockDataValid } from '@/components/block-editor'; +import BlockEditor, { setUploadHandler, getInitialBlockData, isBlockDataValid } from '@/components/control/block-editor'; + +import { upload } from '#/services/common'; + +setUploadHandler(async file => { + const formData = new FormData(); + + formData.append('file', file, file.name); + formData.append('intent', 'course_series'); + + const { code, data, message } = await upload({ file: formData }); + + return { + success: code === 200, + data: data?.user_upload_path ? `https://file-cdn.openbuild.xyz${data.user_upload_path}` : '', + code, + message, + extra: {}, + }; +}); function CustomContent({ className, data, onChange, editable }) { const [content, setContent] = useState(data); diff --git a/src/domain/profile/views/team-profile/TeamProfile.js b/src/domain/profile/views/team-profile/TeamProfile.js index c142811e..19ba8cd5 100644 --- a/src/domain/profile/views/team-profile/TeamProfile.js +++ b/src/domain/profile/views/team-profile/TeamProfile.js @@ -17,7 +17,7 @@ import { useState } from 'react'; import { useDebouncedCallback } from 'use-debounce'; -import { isBlockDataValid } from '@/components/block-editor'; +import { isBlockDataValid } from '@/components/control/block-editor/helper'; import useAppConfig from '@/hooks/useAppConfig'; import useMounted from '@/hooks/useMounted'; diff --git a/src/shared/components/block-editor/BlockEditor.js b/src/shared/components/control/block-editor/BlockEditor.js similarity index 96% rename from src/shared/components/block-editor/BlockEditor.js rename to src/shared/components/control/block-editor/BlockEditor.js index 78f20b45..ec6e9394 100644 --- a/src/shared/components/block-editor/BlockEditor.js +++ b/src/shared/components/control/block-editor/BlockEditor.js @@ -20,10 +20,10 @@ import clsx from 'clsx'; import { EditorRoot, EditorContent, EditorCommand, EditorCommandEmpty, EditorCommandList, EditorCommandItem, + ImageResizer, handleCommandNavigation, } from 'novel'; -import { ImageResizer, handleCommandNavigation } from 'novel/extensions'; -import { isFunction } from '../../utils'; +import { isFunction } from '../../../utils'; import BlockEditorBubble from './bubble'; import { defaultExtensions } from './extensions'; import { isBlockDataValid } from './helper'; diff --git a/src/shared/components/block-editor/bubble/Button.js b/src/shared/components/control/block-editor/bubble/Button.js similarity index 100% rename from src/shared/components/block-editor/bubble/Button.js rename to src/shared/components/control/block-editor/bubble/Button.js diff --git a/src/shared/components/block-editor/bubble/ColorSelector.js b/src/shared/components/control/block-editor/bubble/ColorSelector.js similarity index 100% rename from src/shared/components/block-editor/bubble/ColorSelector.js rename to src/shared/components/control/block-editor/bubble/ColorSelector.js diff --git a/src/shared/components/block-editor/bubble/LinkSelector.js b/src/shared/components/control/block-editor/bubble/LinkSelector.js similarity index 100% rename from src/shared/components/block-editor/bubble/LinkSelector.js rename to src/shared/components/control/block-editor/bubble/LinkSelector.js diff --git a/src/shared/components/block-editor/bubble/MathSelector.js b/src/shared/components/control/block-editor/bubble/MathSelector.js similarity index 100% rename from src/shared/components/block-editor/bubble/MathSelector.js rename to src/shared/components/control/block-editor/bubble/MathSelector.js diff --git a/src/shared/components/block-editor/bubble/NodeSelector.js b/src/shared/components/control/block-editor/bubble/NodeSelector.js similarity index 100% rename from src/shared/components/block-editor/bubble/NodeSelector.js rename to src/shared/components/control/block-editor/bubble/NodeSelector.js diff --git a/src/shared/components/block-editor/bubble/Popover.js b/src/shared/components/control/block-editor/bubble/Popover.js similarity index 100% rename from src/shared/components/block-editor/bubble/Popover.js rename to src/shared/components/control/block-editor/bubble/Popover.js diff --git a/src/shared/components/block-editor/bubble/Separator.js b/src/shared/components/control/block-editor/bubble/Separator.js similarity index 100% rename from src/shared/components/block-editor/bubble/Separator.js rename to src/shared/components/control/block-editor/bubble/Separator.js diff --git a/src/shared/components/block-editor/bubble/TextButtons.js b/src/shared/components/control/block-editor/bubble/TextButtons.js similarity index 100% rename from src/shared/components/block-editor/bubble/TextButtons.js rename to src/shared/components/control/block-editor/bubble/TextButtons.js diff --git a/src/shared/components/block-editor/bubble/helper.js b/src/shared/components/control/block-editor/bubble/helper.js similarity index 100% rename from src/shared/components/block-editor/bubble/helper.js rename to src/shared/components/control/block-editor/bubble/helper.js diff --git a/src/shared/components/block-editor/bubble/index.js b/src/shared/components/control/block-editor/bubble/index.js similarity index 100% rename from src/shared/components/block-editor/bubble/index.js rename to src/shared/components/control/block-editor/bubble/index.js diff --git a/src/shared/components/block-editor/extensions.js b/src/shared/components/control/block-editor/extensions.js similarity index 96% rename from src/shared/components/block-editor/extensions.js rename to src/shared/components/control/block-editor/extensions.js index 4e59b2a2..75a92d53 100644 --- a/src/shared/components/block-editor/extensions.js +++ b/src/shared/components/control/block-editor/extensions.js @@ -26,7 +26,6 @@ import { GlobalDragHandle, HighlightExtension, HorizontalRule, - MarkdownExtension, Placeholder, StarterKit, TaskItem, @@ -39,8 +38,9 @@ import { UpdatedImage, Youtube, Mathematics, -} from 'novel/extensions'; -import { UploadImagesPlugin } from 'novel/plugins'; + UploadImagesPlugin, +} from 'novel'; +import { Markdown } from 'tiptap-markdown'; //TODO I am using cx here to get tailwind autocomplete working, idk if someone else can write a regex to just capture the class key in objects const aiHighlight = AIHighlight; @@ -164,7 +164,7 @@ const mathematics = Mathematics.configure({ const characterCount = CharacterCount.configure(); -const markdownExtension = MarkdownExtension.configure({ +const markdownExtension = Markdown.configure({ html: true, tightLists: true, tightListClass: 'tight', diff --git a/src/shared/components/block-editor/helper.js b/src/shared/components/control/block-editor/helper.js similarity index 72% rename from src/shared/components/block-editor/helper.js rename to src/shared/components/control/block-editor/helper.js index 706931de..093a1dab 100644 --- a/src/shared/components/block-editor/helper.js +++ b/src/shared/components/control/block-editor/helper.js @@ -14,7 +14,21 @@ * limitations under the License. */ -import { isPlainObject } from '../../utils'; +import { isFunction, isPlainObject } from '../../../utils'; + +let uploadHandler; + +function setUploadHandler(handler) { + if (!isFunction(handler)) { + return; + } + + uploadHandler = handler; +} + +function getUploadHandler() { + return uploadHandler; +} const BLOCK_DATA_SPEC_VERSION = '0.0.1'; @@ -34,4 +48,7 @@ function wrapBlockData(data) { return { version: BLOCK_DATA_SPEC_VERSION, data }; } -export { getInitialBlockData, isBlockDataValid, wrapBlockData, unwrapBlockData }; +export { + getUploadHandler, setUploadHandler, + getInitialBlockData, isBlockDataValid, wrapBlockData, unwrapBlockData, +}; diff --git a/src/shared/components/block-editor/index.js b/src/shared/components/control/block-editor/index.js similarity index 100% rename from src/shared/components/block-editor/index.js rename to src/shared/components/control/block-editor/index.js diff --git a/src/shared/components/block-editor/slash.js b/src/shared/components/control/block-editor/slash.js similarity index 98% rename from src/shared/components/block-editor/slash.js rename to src/shared/components/control/block-editor/slash.js index a5fb929e..b8ce4e09 100644 --- a/src/shared/components/block-editor/slash.js +++ b/src/shared/components/control/block-editor/slash.js @@ -31,7 +31,9 @@ import { Columns3, Columns4, } from 'lucide-react'; -import { createSuggestionItems, Command, renderItems } from 'novel/extensions'; +import { createSuggestionItems, Command, renderItems } from 'novel'; + +import uploadFn from './upload'; function createColumnItems() { return [ @@ -141,8 +143,7 @@ const suggestionItems = createSuggestionItems([ if (input.files?.length) { const file = input.files[0]; const pos = editor.view.state.selection.from; - console.log(file, editor.view, pos); - alert('Isn\'t implemented yet.'); + uploadFn(file, editor.view, pos); } }; input.click(); diff --git a/src/shared/components/control/block-editor/upload.js b/src/shared/components/control/block-editor/upload.js new file mode 100644 index 00000000..ac5332b6 --- /dev/null +++ b/src/shared/components/control/block-editor/upload.js @@ -0,0 +1,65 @@ +/** + * Copyright 2024 OpenBuild + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createImageUpload } from 'novel'; +import { toast } from 'react-toastify'; + +import { isFunction } from '../../../utils'; +import { getUploadHandler } from './helper'; + +function onUpload(file) { + const uploadHandler = getUploadHandler(); + + if (!isFunction(uploadHandler)) { + return Promise.reject('Handler for uploading images isn\'t specified.'); + } + + const req = uploadHandler(file); + + return new Promise((resolve, reject) => { + req + .then(res => { + if (res.success) { + resolve(res.data); + } else { + throw new Error('Error uploading image. Please try again.'); + } + }) + .catch(err => { + reject(err); + return e.message; + }); + }); +} + +const uploadFn = createImageUpload({ + onUpload, + validateFn: file => { + if (!file.type.includes('image/')) { + toast.error('File type not supported.'); + return false; + } + + if (file.size / 1024 / 1024 > 20) { + toast.error('File size too big (max 20MB).'); + return false; + } + + return true; + }, +}); + +export default uploadFn; From 6e92b9d991a12f10944e5f6d2b00cc2edce4bf49 Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Mon, 10 Mar 2025 18:42:16 +0800 Subject: [PATCH 2/3] feat(app): show dropdown menu when click on block drag handle --- .../control/block-editor/BlockEditor.js | 10 ++ .../control/block-editor/DragHandle.js | 103 ++++++++++++++++++ .../control/block-editor/extensions.js | 6 +- src/shared/components/control/headlessui.js | 2 +- 4 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 src/shared/components/control/block-editor/DragHandle.js diff --git a/src/shared/components/control/block-editor/BlockEditor.js b/src/shared/components/control/block-editor/BlockEditor.js index ec6e9394..a9d6f186 100644 --- a/src/shared/components/control/block-editor/BlockEditor.js +++ b/src/shared/components/control/block-editor/BlockEditor.js @@ -22,9 +22,11 @@ import { EditorCommand, EditorCommandEmpty, EditorCommandList, EditorCommandItem, ImageResizer, handleCommandNavigation, } from 'novel'; +import { useState } from 'react'; import { isFunction } from '../../../utils'; import BlockEditorBubble from './bubble'; +import DragHandle from './DragHandle'; import { defaultExtensions } from './extensions'; import { isBlockDataValid } from './helper'; import { slashCommand, suggestionItems } from './slash'; @@ -32,6 +34,12 @@ import { slashCommand, suggestionItems } from './slash'; const extensions = [...defaultExtensions, slashCommand]; function BlockEditor({ className, data, onChange, editable = false }) { + const [cachedEditor, setCachedEditor] = useState(null); + + const handleCreate = ({ editor }) => { + setCachedEditor(editor); + }; + const handleUpdate = ({ editor }) => { isFunction(onChange) && onChange(editor.getJSON()); }; @@ -53,9 +61,11 @@ function BlockEditor({ className, data, onChange, editable = false }) { }, }} editable={editable} + onCreate={handleCreate} onUpdate={handleUpdate} slotAfter={} > + No results diff --git a/src/shared/components/control/block-editor/DragHandle.js b/src/shared/components/control/block-editor/DragHandle.js new file mode 100644 index 00000000..c8274642 --- /dev/null +++ b/src/shared/components/control/block-editor/DragHandle.js @@ -0,0 +1,103 @@ +/** + * Copyright 2024 OpenBuild + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import clsx from 'clsx'; + +import { Menu } from '../headlessui'; + +function deleteBlockNode(editor) { + return editor.chain() + .focus() + .command(({ tr }) => { + const { $from } = tr.selection; + tr.delete($from.before($from.depth), $from.after($from.depth)); + return true; + }) + .run(); +} + +function insertNewBlockBelow(editor) { + return editor.chain() + .focus() + .command(({ tr }) => { + const { $from } = tr.selection; + const insertPos = $from.after($from.depth); + tr.insert(insertPos, editor.schema.nodes.paragraph.create()); + return true; + }) + .run(); +} + +function DragHandle({ className, editor }) { + return ( + + + + + + + + + +
+ + {({ active }) => ( + + )} + + + {({ active }) => ( + + )} + +
+
+
+ ); +} + +export default DragHandle; diff --git a/src/shared/components/control/block-editor/extensions.js b/src/shared/components/control/block-editor/extensions.js index 75a92d53..1fdb9af9 100644 --- a/src/shared/components/control/block-editor/extensions.js +++ b/src/shared/components/control/block-editor/extensions.js @@ -175,6 +175,10 @@ const markdownExtension = Markdown.configure({ transformCopiedText: false, }); +const dragHandle = GlobalDragHandle.configure({ + dragHandleSelector: '.BlockEditor-dragHandle', +}); + const defaultExtensions = [ starterKit, placeholder, @@ -196,7 +200,7 @@ const defaultExtensions = [ TextStyle, Color, CustomKeymap, - GlobalDragHandle, + dragHandle, ColumnsExtension, ]; diff --git a/src/shared/components/control/headlessui.js b/src/shared/components/control/headlessui.js index a75a13dd..56945c1a 100644 --- a/src/shared/components/control/headlessui.js +++ b/src/shared/components/control/headlessui.js @@ -14,4 +14,4 @@ * limitations under the License. */ -export { Disclosure, Popover, Dialog, Listbox, Transition, Switch } from '@headlessui/react'; +export { Disclosure, Popover, Dialog, Listbox, Transition, Switch, Menu } from '@headlessui/react'; From 4930abd4461abb82d2a059f4a731a9961762f19d Mon Sep 17 00:00:00 2001 From: Ourai Lin Date: Tue, 11 Mar 2025 12:53:16 +0800 Subject: [PATCH 3/3] chore(app): remove dropdown menu of drag handle --- .../control/block-editor/BlockEditor.js | 10 +- .../control/block-editor/DragHandle.js | 103 +++++------------- 2 files changed, 26 insertions(+), 87 deletions(-) diff --git a/src/shared/components/control/block-editor/BlockEditor.js b/src/shared/components/control/block-editor/BlockEditor.js index a9d6f186..aa3c21af 100644 --- a/src/shared/components/control/block-editor/BlockEditor.js +++ b/src/shared/components/control/block-editor/BlockEditor.js @@ -22,7 +22,6 @@ import { EditorCommand, EditorCommandEmpty, EditorCommandList, EditorCommandItem, ImageResizer, handleCommandNavigation, } from 'novel'; -import { useState } from 'react'; import { isFunction } from '../../../utils'; import BlockEditorBubble from './bubble'; @@ -34,12 +33,6 @@ import { slashCommand, suggestionItems } from './slash'; const extensions = [...defaultExtensions, slashCommand]; function BlockEditor({ className, data, onChange, editable = false }) { - const [cachedEditor, setCachedEditor] = useState(null); - - const handleCreate = ({ editor }) => { - setCachedEditor(editor); - }; - const handleUpdate = ({ editor }) => { isFunction(onChange) && onChange(editor.getJSON()); }; @@ -61,11 +54,10 @@ function BlockEditor({ className, data, onChange, editable = false }) { }, }} editable={editable} - onCreate={handleCreate} onUpdate={handleUpdate} slotAfter={} > - + No results diff --git a/src/shared/components/control/block-editor/DragHandle.js b/src/shared/components/control/block-editor/DragHandle.js index c8274642..cb443c6a 100644 --- a/src/shared/components/control/block-editor/DragHandle.js +++ b/src/shared/components/control/block-editor/DragHandle.js @@ -14,89 +14,36 @@ * limitations under the License. */ -import clsx from 'clsx'; +// import { useEditor } from 'novel'; -import { Menu } from '../headlessui'; +// function insertNewBlockBelow(editor) { +// return editor.chain() +// .focus() +// .command(({ tr }) => { +// const { $from } = tr.selection; +// const insertPos = $from.after($from.depth); +// tr.insert(insertPos, editor.schema.nodes.paragraph.create()); +// return true; +// }) +// .run(); +// } -function deleteBlockNode(editor) { - return editor.chain() - .focus() - .command(({ tr }) => { - const { $from } = tr.selection; - tr.delete($from.before($from.depth), $from.after($from.depth)); - return true; - }) - .run(); -} +// function deleteBlock(editor) { +// return editor.chain() +// .focus() +// .command(({ tr }) => { +// const { $from } = tr.selection; +// tr.delete($from.before($from.depth), $from.after($from.depth)); +// return true; +// }) +// .run(); +// } -function insertNewBlockBelow(editor) { - return editor.chain() - .focus() - .command(({ tr }) => { - const { $from } = tr.selection; - const insertPos = $from.after($from.depth); - tr.insert(insertPos, editor.schema.nodes.paragraph.create()); - return true; - }) - .run(); -} +function DragHandle() { + // const { editor } = useEditor(); -function DragHandle({ className, editor }) { return ( - - - - - - - - - -
- - {({ active }) => ( - - )} - - - {({ active }) => ( - - )} - -
-
-
+
); }