From c28d709d7f303809e8c392e9176a579d8aecc2d4 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Fri, 14 Jun 2024 17:08:11 +0800 Subject: [PATCH] feat: workflow add note node (#5164) --- .../assets/vender/line/editor/bold-01.svg | 5 + .../vender/line/editor/dotpoints-01.svg | 5 + .../assets/vender/line/editor/italic-01.svg | 3 + .../vender/line/editor/strikethrough-01.svg | 5 + .../assets/vender/line/editor/title-case.svg | 8 + .../vender/line/files/sticker-square.svg | 5 + .../assets/vender/line/general/Workflow.zip | Bin 478 -> 0 bytes .../assets/vender/line/general/link-01.svg | 5 + .../vender/line/general/link-broken-01.svg | 10 + .../icons/src/vender/line/editor/Bold01.json | 39 +++ .../icons/src/vender/line/editor/Bold01.tsx | 16 + .../src/vender/line/editor/Dotpoints01.json | 39 +++ .../src/vender/line/editor/Dotpoints01.tsx | 16 + .../src/vender/line/editor/Italic01.json | 29 ++ .../icons/src/vender/line/editor/Italic01.tsx | 16 + .../vender/line/editor/Strikethrough01.json | 39 +++ .../vender/line/editor/Strikethrough01.tsx | 16 + .../src/vender/line/editor/TitleCase.json | 53 ++++ .../src/vender/line/editor/TitleCase.tsx | 16 + .../icons/src/vender/line/editor/index.ts | 5 + .../src/vender/line/files/StickerSquare.json | 39 +++ .../src/vender/line/files/StickerSquare.tsx | 16 + .../base/icons/src/vender/line/files/index.ts | 1 + .../icons/src/vender/line/general/Link01.json | 39 +++ .../icons/src/vender/line/general/Link01.tsx | 16 + .../src/vender/line/general/LinkBroken01.json | 66 ++++ .../src/vender/line/general/LinkBroken01.tsx | 16 + .../icons/src/vender/line/general/index.ts | 2 + .../components/workflow/candidate-node.tsx | 19 +- web/app/components/workflow/constants.ts | 1 + .../workflow/hooks/use-checklist.ts | 36 ++- .../workflow/hooks/use-node-data-update.ts | 3 +- .../workflow/hooks/use-nodes-interactions.ts | 21 +- .../components/workflow/hooks/use-workflow.ts | 9 +- web/app/components/workflow/index.tsx | 8 +- .../nodes/_base/components/node-resizer.tsx | 15 +- .../components/workflow/nodes/constants.ts | 2 + web/app/components/workflow/nodes/index.tsx | 28 +- .../workflow/note-node/constants.ts | 42 +++ .../components/workflow/note-node/hooks.ts | 29 ++ .../components/workflow/note-node/index.tsx | 127 ++++++++ .../note-node/note-editor/context.tsx | 65 ++++ .../workflow/note-node/note-editor/editor.tsx | 62 ++++ .../workflow/note-node/note-editor/index.tsx | 3 + .../plugins/format-detector-plugin/hooks.ts | 78 +++++ .../plugins/format-detector-plugin/index.tsx | 9 + .../plugins/link-editor-plugin/component.tsx | 152 +++++++++ .../plugins/link-editor-plugin/hooks.ts | 115 +++++++ .../plugins/link-editor-plugin/index.tsx | 25 ++ .../workflow/note-node/note-editor/store.ts | 72 +++++ .../note-node/note-editor/theme/index.ts | 17 + .../note-node/note-editor/theme/theme.css | 24 ++ .../note-editor/toolbar/color-picker.tsx | 105 +++++++ .../note-node/note-editor/toolbar/command.tsx | 81 +++++ .../note-node/note-editor/toolbar/divider.tsx | 7 + .../toolbar/font-size-selector.tsx | 86 +++++ .../note-node/note-editor/toolbar/hooks.ts | 147 +++++++++ .../note-node/note-editor/toolbar/index.tsx | 48 +++ .../note-editor/toolbar/operator.tsx | 107 +++++++ .../workflow/note-node/note-editor/utils.ts | 21 ++ .../components/workflow/note-node/types.ts | 17 + .../components/workflow/operator/control.tsx | 28 +- web/app/components/workflow/operator/hooks.ts | 41 +++ .../components/workflow/panel-contextmenu.tsx | 12 + web/app/components/workflow/utils.ts | 10 +- web/i18n/en-US/workflow.ts | 19 ++ web/i18n/zh-Hans/workflow.ts | 19 ++ web/package.json | 4 +- web/yarn.lock | 295 ++++++++++-------- 69 files changed, 2370 insertions(+), 164 deletions(-) create mode 100644 web/app/components/base/icons/assets/vender/line/editor/bold-01.svg create mode 100644 web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg create mode 100644 web/app/components/base/icons/assets/vender/line/editor/italic-01.svg create mode 100644 web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg create mode 100644 web/app/components/base/icons/assets/vender/line/editor/title-case.svg create mode 100644 web/app/components/base/icons/assets/vender/line/files/sticker-square.svg delete mode 100644 web/app/components/base/icons/assets/vender/line/general/Workflow.zip create mode 100644 web/app/components/base/icons/assets/vender/line/general/link-01.svg create mode 100644 web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg create mode 100644 web/app/components/base/icons/src/vender/line/editor/Bold01.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/Bold01.tsx create mode 100644 web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx create mode 100644 web/app/components/base/icons/src/vender/line/editor/Italic01.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/Italic01.tsx create mode 100644 web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx create mode 100644 web/app/components/base/icons/src/vender/line/editor/TitleCase.json create mode 100644 web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx create mode 100644 web/app/components/base/icons/src/vender/line/files/StickerSquare.json create mode 100644 web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx create mode 100644 web/app/components/base/icons/src/vender/line/general/Link01.json create mode 100644 web/app/components/base/icons/src/vender/line/general/Link01.tsx create mode 100644 web/app/components/base/icons/src/vender/line/general/LinkBroken01.json create mode 100644 web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx create mode 100644 web/app/components/workflow/note-node/constants.ts create mode 100644 web/app/components/workflow/note-node/hooks.ts create mode 100644 web/app/components/workflow/note-node/index.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/context.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/editor.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/index.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts create mode 100644 web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts create mode 100644 web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/store.ts create mode 100644 web/app/components/workflow/note-node/note-editor/theme/index.ts create mode 100644 web/app/components/workflow/note-node/note-editor/theme/theme.css create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/command.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/index.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx create mode 100644 web/app/components/workflow/note-node/note-editor/utils.ts create mode 100644 web/app/components/workflow/note-node/types.ts create mode 100644 web/app/components/workflow/operator/hooks.ts diff --git a/web/app/components/base/icons/assets/vender/line/editor/bold-01.svg b/web/app/components/base/icons/assets/vender/line/editor/bold-01.svg new file mode 100644 index 00000000000000..5e9e20816bf029 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/bold-01.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg b/web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg new file mode 100644 index 00000000000000..dddb80780affae --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/dotpoints-01.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/editor/italic-01.svg b/web/app/components/base/icons/assets/vender/line/editor/italic-01.svg new file mode 100644 index 00000000000000..216506a903eac6 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/italic-01.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg b/web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg new file mode 100644 index 00000000000000..5935b4dd7dda82 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/strikethrough-01.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/editor/title-case.svg b/web/app/components/base/icons/assets/vender/line/editor/title-case.svg new file mode 100644 index 00000000000000..c3a13dd7ef8fd5 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/editor/title-case.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/files/sticker-square.svg b/web/app/components/base/icons/assets/vender/line/files/sticker-square.svg new file mode 100644 index 00000000000000..0104e7ce8a82e9 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/files/sticker-square.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/general/Workflow.zip b/web/app/components/base/icons/assets/vender/line/general/Workflow.zip deleted file mode 100644 index 05631f93b04db33a2cb267793513d60df912c8ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmWIWW@h1HU|`^2@SYYG!8vj9F)v02hC(I=21%f3cz#iKT26ksesV@?a<*=AW>IoZ zs$OwfdTG$XzGel1z2CJt_P&wpV&dAh(s!F}?k3-Fd!{dQT$K}R>g8=;?|5{X?>DB; z_deY7nGyN@I+MiI*GA12O$(;1()cP7Kh@;+vuOtfS{U396sm`YO0Jn5=iLt;MS=U#^qgW=N^SGFMjFxOL3MIlk^*9mr5Va1P$NID!)=VgxiIWD7Ob6VUtmQ^g&K|O2DbV zzvmveaA|o^v4>M4lgVOJZuHNVa*@J$24>F<5A`g48Eg0OX5FD?KJ|oa)^_`D^A|kt z?)SVu>$=_6f4q+CcR$lH-f<-5^v&JsVRdC1^HzFY4U)OdQ+9UgIr$g!-LB5#f1}Ud zYFkwK>h05IAD-6#%~Z)#nk({af2id3LsPs?U%WnhXT_&->5Y+p7z4Z+nM4?HM*%Pv ZkO0mI3GilR1F2&KLU$nD2#iGr1^}H_#GL>D diff --git a/web/app/components/base/icons/assets/vender/line/general/link-01.svg b/web/app/components/base/icons/assets/vender/line/general/link-01.svg new file mode 100644 index 00000000000000..60fccd84f2d400 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/general/link-01.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg b/web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg new file mode 100644 index 00000000000000..47a0560db12ec0 --- /dev/null +++ b/web/app/components/base/icons/assets/vender/line/general/link-broken-01.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/web/app/components/base/icons/src/vender/line/editor/Bold01.json b/web/app/components/base/icons/src/vender/line/editor/Bold01.json new file mode 100644 index 00000000000000..6a807dacdfd575 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Bold01.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon_2", + "d": "M4.5 7.99996H9.83333C11.3061 7.99996 12.5 6.80605 12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H4.5V7.99996ZM4.5 7.99996H10.5C11.9728 7.99996 13.1667 9.19387 13.1667 10.6666C13.1667 12.1394 11.9728 13.3333 10.5 13.3333H4.5V7.99996Z", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "Bold01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/Bold01.tsx b/web/app/components/base/icons/src/vender/line/editor/Bold01.tsx new file mode 100644 index 00000000000000..5fb2c4b6203b45 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Bold01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Bold01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Bold01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json b/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json new file mode 100644 index 00000000000000..926824bddac19b --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon_2", + "d": "M14.5 8.00004L6.5 8.00004M14.5 4.00004L6.5 4.00004M14.5 12L6.5 12M3.83333 8.00004C3.83333 8.36823 3.53486 8.66671 3.16667 8.66671C2.79848 8.66671 2.5 8.36823 2.5 8.00004C2.5 7.63185 2.79848 7.33337 3.16667 7.33337C3.53486 7.33337 3.83333 7.63185 3.83333 8.00004ZM3.83333 4.00004C3.83333 4.36823 3.53486 4.66671 3.16667 4.66671C2.79848 4.66671 2.5 4.36823 2.5 4.00004C2.5 3.63185 2.79848 3.33337 3.16667 3.33337C3.53486 3.33337 3.83333 3.63185 3.83333 4.00004ZM3.83333 12C3.83333 12.3682 3.53486 12.6667 3.16667 12.6667C2.79848 12.6667 2.5 12.3682 2.5 12C2.5 11.6319 2.79848 11.3334 3.16667 11.3334C3.53486 11.3334 3.83333 11.6319 3.83333 12Z", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "Dotpoints01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx b/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx new file mode 100644 index 00000000000000..34cdbb71449a18 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Dotpoints01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Dotpoints01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Dotpoints01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/Italic01.json b/web/app/components/base/icons/src/vender/line/editor/Italic01.json new file mode 100644 index 00000000000000..d5fff5ec270c33 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Italic01.json @@ -0,0 +1,29 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M13.1666 2.66663H7.16659M9.83325 13.3333H3.83325M10.4999 2.66663L6.49992 13.3333", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + "name": "Italic01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/Italic01.tsx b/web/app/components/base/icons/src/vender/line/editor/Italic01.tsx new file mode 100644 index 00000000000000..4522c8dc9d215f --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Italic01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Italic01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Italic01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json b/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json new file mode 100644 index 00000000000000..fda981321ad565 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon_2", + "d": "M4.5 10.6666C4.5 12.1394 5.69391 13.3333 7.16667 13.3333H9.83333C11.3061 13.3333 12.5 12.1394 12.5 10.6666C12.5 9.19387 11.3061 7.99996 9.83333 7.99996M12.5 5.33329C12.5 3.86053 11.3061 2.66663 9.83333 2.66663H7.16667C5.69391 2.66663 4.5 3.86053 4.5 5.33329M2.5 7.99996H14.5", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "Strikethrough01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx b/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx new file mode 100644 index 00000000000000..7aee626ceecf04 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/Strikethrough01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Strikethrough01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Strikethrough01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/TitleCase.json b/web/app/components/base/icons/src/vender/line/editor/TitleCase.json new file mode 100644 index 00000000000000..6f7e587ee575b9 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/TitleCase.json @@ -0,0 +1,53 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Vector" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "d": "M2.0922 12.4865C2.57616 12.4865 2.84839 12.2445 3.01778 11.6638L3.47754 10.3026H6.62933L7.0891 11.6819C7.25243 12.2506 7.52463 12.4865 8.03887 12.4865C8.5712 12.4865 8.95232 12.1295 8.95232 11.6275C8.95232 11.4459 8.92208 11.2827 8.83743 11.0467L6.44179 4.54954C6.18167 3.83569 5.7582 3.52112 5.04436 3.52112C4.35471 3.52112 3.9252 3.84779 3.67112 4.55559L1.28762 11.0467C1.20897 11.2705 1.16663 11.4762 1.16663 11.6275C1.16663 12.1538 1.52355 12.4865 2.0922 12.4865ZM3.8768 8.88703L5.00806 5.31177H5.05041L6.20586 8.88703H3.8768Z", + "fill": "currentColor" + }, + "children": [] + }, + { + "type": "element", + "name": "path", + "attributes": { + "d": "M12.1068 12.4744C12.9174 12.4744 13.7281 12.0691 14.091 11.3795H14.1273V11.7122C14.1636 12.2324 14.4963 12.4986 14.9742 12.4986C15.4764 12.4986 15.8333 12.1961 15.8333 11.6093V7.91309C15.8333 6.60636 14.7504 5.74734 13.0868 5.74734C11.7438 5.74734 10.7033 6.22525 10.4008 6.99957C10.3403 7.13269 10.3101 7.25973 10.3101 7.39885C10.3101 7.79813 10.6186 8.07638 11.0481 8.07638C11.3324 8.07638 11.5563 7.9675 11.7499 7.74973C12.1431 7.24157 12.4697 7.06613 13.0081 7.06613C13.6736 7.06613 14.0971 7.41701 14.0971 8.02198V8.4515L12.4637 8.54823C10.8424 8.64503 9.93506 9.32864 9.93506 10.5083C9.93506 11.6759 10.8727 12.4744 12.1068 12.4744ZM12.6876 11.1979C12.0947 11.1979 11.6954 10.8955 11.6954 10.4115C11.6954 9.95176 12.0705 9.65528 12.7299 9.60695L14.0971 9.52224V9.99408C14.0971 10.6958 13.4619 11.1979 12.6876 11.1979Z", + "fill": "currentColor" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "TitleCase" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx b/web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx new file mode 100644 index 00000000000000..a1e073e48e3852 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/editor/TitleCase.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './TitleCase.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'TitleCase' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/editor/index.ts b/web/app/components/base/icons/src/vender/line/editor/index.ts index aafadaf5d2846c..7cab0bc584c7c4 100644 --- a/web/app/components/base/icons/src/vender/line/editor/index.ts +++ b/web/app/components/base/icons/src/vender/line/editor/index.ts @@ -1,11 +1,16 @@ export { default as AlignLeft } from './AlignLeft' export { default as BezierCurve03 } from './BezierCurve03' +export { default as Bold01 } from './Bold01' export { default as Colors } from './Colors' export { default as Cursor02C } from './Cursor02C' +export { default as Dotpoints01 } from './Dotpoints01' export { default as Hand02 } from './Hand02' export { default as ImageIndentLeft } from './ImageIndentLeft' +export { default as Italic01 } from './Italic01' export { default as LeftIndent02 } from './LeftIndent02' export { default as LetterSpacing01 } from './LetterSpacing01' +export { default as Strikethrough01 } from './Strikethrough01' +export { default as TitleCase } from './TitleCase' export { default as TypeSquare } from './TypeSquare' export { default as ZoomIn } from './ZoomIn' export { default as ZoomOut } from './ZoomOut' diff --git a/web/app/components/base/icons/src/vender/line/files/StickerSquare.json b/web/app/components/base/icons/src/vender/line/files/StickerSquare.json new file mode 100644 index 00000000000000..92d721912ccfdc --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/files/StickerSquare.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "16", + "height": "16", + "viewBox": "0 0 16 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "sticker-square" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon", + "d": "M8.66667 2.33333V4.13333C8.66667 5.25344 8.66667 5.81349 8.88465 6.24131C9.0764 6.61764 9.38236 6.9236 9.75869 7.11535C10.1865 7.33333 10.7466 7.33333 11.8667 7.33333H13.6667M14 8.65882V10.8C14 11.9201 14 12.4802 13.782 12.908C13.5903 13.2843 13.2843 13.5903 12.908 13.782C12.4802 14 11.9201 14 10.8 14H5.2C4.0799 14 3.51984 14 3.09202 13.782C2.71569 13.5903 2.40973 13.2843 2.21799 12.908C2 12.4802 2 11.9201 2 10.8V5.2C2 4.0799 2 3.51984 2.21799 3.09202C2.40973 2.71569 2.71569 2.40973 3.09202 2.21799C3.51984 2 4.0799 2 5.2 2H7.34118C7.83036 2 8.07496 2 8.30513 2.05526C8.5092 2.10425 8.70429 2.18506 8.88324 2.29472C9.08507 2.4184 9.25802 2.59135 9.60393 2.93726L13.0627 6.39608C13.4086 6.74198 13.5816 6.91493 13.7053 7.11676C13.8149 7.29571 13.8957 7.4908 13.9447 7.69487C14 7.92505 14 8.16964 14 8.65882Z", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "StickerSquare" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx b/web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx new file mode 100644 index 00000000000000..925edcdc10ee22 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/files/StickerSquare.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './StickerSquare.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'StickerSquare' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/files/index.ts b/web/app/components/base/icons/src/vender/line/files/index.ts index 4c0ddc22895903..71b4890ce8a73b 100644 --- a/web/app/components/base/icons/src/vender/line/files/index.ts +++ b/web/app/components/base/icons/src/vender/line/files/index.ts @@ -9,3 +9,4 @@ export { default as FilePlus02 } from './FilePlus02' export { default as FileText } from './FileText' export { default as FileUpload } from './FileUpload' export { default as Folder } from './Folder' +export { default as StickerSquare } from './StickerSquare' diff --git a/web/app/components/base/icons/src/vender/line/general/Link01.json b/web/app/components/base/icons/src/vender/line/general/Link01.json new file mode 100644 index 00000000000000..b49b5776a07209 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/Link01.json @@ -0,0 +1,39 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "17", + "height": "16", + "viewBox": "0 0 17 16", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Icon" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon_2", + "d": "M8.97167 12.2427L8.02886 13.1855C6.72711 14.4872 4.61656 14.4872 3.31481 13.1855C2.01306 11.8837 2.01306 9.77317 3.31481 8.47142L4.25762 7.52861M12.7429 8.47142L13.6857 7.52861C14.9875 6.22687 14.9875 4.11632 13.6857 2.81457C12.384 1.51282 10.2734 1.51282 8.97167 2.81457L8.02886 3.75738M6.16693 10.3333L10.8336 5.66667", + "stroke": "currentColor", + "stroke-width": "1.5", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + } + ] + }, + "name": "Link01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/general/Link01.tsx b/web/app/components/base/icons/src/vender/line/general/Link01.tsx new file mode 100644 index 00000000000000..ddddab1bd87e96 --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/Link01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './Link01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'Link01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/LinkBroken01.json b/web/app/components/base/icons/src/vender/line/general/LinkBroken01.json new file mode 100644 index 00000000000000..92a1234744718a --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/LinkBroken01.json @@ -0,0 +1,66 @@ +{ + "icon": { + "type": "element", + "isRootNode": true, + "name": "svg", + "attributes": { + "width": "12", + "height": "12", + "viewBox": "0 0 12 12", + "fill": "none", + "xmlns": "http://www.w3.org/2000/svg" + }, + "children": [ + { + "type": "element", + "name": "g", + "attributes": { + "id": "Left Icon", + "clip-path": "url(#clip0_6246_47371)" + }, + "children": [ + { + "type": "element", + "name": "path", + "attributes": { + "id": "Icon", + "d": "M4.5 2V1M7.5 10V11M2 4.5H1M10 7.5H11M2.45711 2.45711L1.75 1.75M9.54289 9.54289L10.25 10.25M6 8.82843L4.93934 9.88909C4.15829 10.6701 2.89196 10.6701 2.11091 9.88909C1.32986 9.10804 1.32986 7.84171 2.11091 7.06066L3.17157 6M8.82843 6L9.88909 4.93934C10.6701 4.15829 10.6701 2.89196 9.88909 2.11091C9.10804 1.32986 7.84171 1.32986 7.06066 2.11091L6 3.17157", + "stroke": "currentColor", + "stroke-width": "1.25", + "stroke-linecap": "round", + "stroke-linejoin": "round" + }, + "children": [] + } + ] + }, + { + "type": "element", + "name": "defs", + "attributes": {}, + "children": [ + { + "type": "element", + "name": "clipPath", + "attributes": { + "id": "clip0_6246_47371" + }, + "children": [ + { + "type": "element", + "name": "rect", + "attributes": { + "width": "12", + "height": "12", + "fill": "white" + }, + "children": [] + } + ] + } + ] + } + ] + }, + "name": "LinkBroken01" +} \ No newline at end of file diff --git a/web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx b/web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx new file mode 100644 index 00000000000000..10e1cee8eeb8dd --- /dev/null +++ b/web/app/components/base/icons/src/vender/line/general/LinkBroken01.tsx @@ -0,0 +1,16 @@ +// GENERATE BY script +// DON NOT EDIT IT MANUALLY + +import * as React from 'react' +import data from './LinkBroken01.json' +import IconBase from '@/app/components/base/icons/IconBase' +import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase' + +const Icon = React.forwardRef, Omit>(( + props, + ref, +) => ) + +Icon.displayName = 'LinkBroken01' + +export default Icon diff --git a/web/app/components/base/icons/src/vender/line/general/index.ts b/web/app/components/base/icons/src/vender/line/general/index.ts index 8369117eedf262..350e54dbe15fa5 100644 --- a/web/app/components/base/icons/src/vender/line/general/index.ts +++ b/web/app/components/base/icons/src/vender/line/general/index.ts @@ -14,7 +14,9 @@ export { default as Edit05 } from './Edit05' export { default as Hash02 } from './Hash02' export { default as HelpCircle } from './HelpCircle' export { default as InfoCircle } from './InfoCircle' +export { default as Link01 } from './Link01' export { default as Link03 } from './Link03' +export { default as LinkBroken01 } from './LinkBroken01' export { default as LinkExternal01 } from './LinkExternal01' export { default as LinkExternal02 } from './LinkExternal02' export { default as Loading02 } from './Loading02' diff --git a/web/app/components/workflow/candidate-node.tsx b/web/app/components/workflow/candidate-node.tsx index 1987e6227bea53..8528e61daaaab4 100644 --- a/web/app/components/workflow/candidate-node.tsx +++ b/web/app/components/workflow/candidate-node.tsx @@ -12,7 +12,11 @@ import { useStore, useWorkflowStore, } from './store' +import { useNodesInteractions } from './hooks' +import { CUSTOM_NODE } from './constants' import CustomNode from './nodes' +import CustomNoteNode from './note-node' +import { CUSTOM_NOTE_NODE } from './note-node/constants' const CandidateNode = () => { const store = useStoreApi() @@ -21,6 +25,7 @@ const CandidateNode = () => { const candidateNode = useStore(s => s.candidateNode) const mousePosition = useStore(s => s.mousePosition) const { zoom } = useViewport() + const { handleNodeSelect } = useNodesInteractions() useEventListener('click', (e) => { const { candidateNode, mousePosition } = workflowStore.getState() @@ -49,6 +54,9 @@ const CandidateNode = () => { }) setNodes(newNodes) workflowStore.setState({ candidateNode: undefined }) + + if (candidateNode.type === CUSTOM_NOTE_NODE) + handleNodeSelect(candidateNode.id) } }) @@ -73,7 +81,16 @@ const CandidateNode = () => { transformOrigin: '0 0', }} > - + { + candidateNode.type === CUSTOM_NODE && ( + + ) + } + { + candidateNode.type === CUSTOM_NOTE_NODE && ( + + ) + } ) } diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 918649a26bbe3b..a6f313e98e4434 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -391,3 +391,4 @@ export const PARAMETER_EXTRACTOR_COMMON_STRUCT: Var[] = [ ] export const WORKFLOW_DATA_UPDATE = 'WORKFLOW_DATA_UPDATE' +export const CUSTOM_NODE = 'custom' diff --git a/web/app/components/workflow/hooks/use-checklist.ts b/web/app/components/workflow/hooks/use-checklist.ts index 818505d0fe444d..142f96ed2a15b9 100644 --- a/web/app/components/workflow/hooks/use-checklist.ts +++ b/web/app/components/workflow/hooks/use-checklist.ts @@ -14,7 +14,10 @@ import { getToolCheckParams, getValidTreeNodes, } from '../utils' -import { MAX_TREE_DEEPTH } from '../constants' +import { + CUSTOM_NODE, + MAX_TREE_DEEPTH, +} from '../constants' import type { ToolNodeType } from '../nodes/tool/types' import { useIsChatMode } from './use-workflow' import { useNodesExtraData } from './use-nodes-data' @@ -33,7 +36,7 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { const needWarningNodes = useMemo(() => { const list = [] - const { validNodes } = getValidTreeNodes(nodes, edges) + const { validNodes } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges) for (let i = 0; i < nodes.length; i++) { const node = nodes[i] @@ -53,17 +56,20 @@ export const useChecklist = (nodes: Node[], edges: Edge[]) => { if (provider_type === CollectionType.workflow) toolIcon = workflowTools.find(tool => tool.id === node.data.provider_id)?.icon } - const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid) - - if (errorMessage || !validNodes.find(n => n.id === node.id)) { - list.push({ - id: node.id, - type: node.data.type, - title: node.data.title, - toolIcon, - unConnected: !validNodes.find(n => n.id === node.id), - errorMessage, - }) + + if (node.type === CUSTOM_NODE) { + const { errorMessage } = nodesExtraData[node.data.type].checkValid(node.data, t, moreDataForCheckValid) + + if (errorMessage || !validNodes.find(n => n.id === node.id)) { + list.push({ + id: node.id, + type: node.data.type, + title: node.data.title, + toolIcon, + unConnected: !validNodes.find(n => n.id === node.id), + errorMessage, + }) + } } } @@ -107,11 +113,11 @@ export const useChecklistBeforePublish = () => { getNodes, edges, } = store.getState() - const nodes = getNodes() + const nodes = getNodes().filter(node => node.type === CUSTOM_NODE) const { validNodes, maxDepth, - } = getValidTreeNodes(nodes, edges) + } = getValidTreeNodes(nodes.filter(node => node.type === CUSTOM_NODE), edges) if (maxDepth > MAX_TREE_DEEPTH) { notify({ type: 'error', message: t('workflow.common.maxTreeDepth', { depth: MAX_TREE_DEEPTH }) }) diff --git a/web/app/components/workflow/hooks/use-node-data-update.ts b/web/app/components/workflow/hooks/use-node-data-update.ts index 4cb4d6b26bcf94..c59c858184120d 100644 --- a/web/app/components/workflow/hooks/use-node-data-update.ts +++ b/web/app/components/workflow/hooks/use-node-data-update.ts @@ -22,7 +22,8 @@ export const useNodeDataUpdate = () => { const newNodes = produce(getNodes(), (draft) => { const currentNode = draft.find(node => node.id === id)! - currentNode.data = { ...currentNode?.data, ...data } + if (currentNode) + currentNode.data = { ...currentNode.data, ...data } }) setNodes(newNodes) }, [store]) diff --git a/web/app/components/workflow/hooks/use-nodes-interactions.ts b/web/app/components/workflow/hooks/use-nodes-interactions.ts index 64cba1c790dfcb..c63d662e013271 100644 --- a/web/app/components/workflow/hooks/use-nodes-interactions.ts +++ b/web/app/components/workflow/hooks/use-nodes-interactions.ts @@ -38,6 +38,7 @@ import { getNodesConnectedSourceOrTargetHandleIdsMap, getTopLeftNodePosition, } from '../utils' +import { CUSTOM_NOTE_NODE } from '../note-node/constants' import type { IterationNodeType } from '../nodes/iteration/types' import type { VariableAssignerNodeType } from '../nodes/variable-assigner/types' import { useNodeIterationInteractions } from '../nodes/iteration/use-interactions' @@ -71,7 +72,7 @@ export const useNodesInteractions = () => { if (getNodesReadOnly()) return - if (node.data.isIterationStart) + if (node.data.isIterationStart || node.type === CUSTOM_NOTE_NODE) return dragNodeStartPosition.current = { x: node.position.x, y: node.position.y } @@ -143,6 +144,9 @@ export const useNodesInteractions = () => { if (getNodesReadOnly()) return + if (node.type === CUSTOM_NOTE_NODE) + return + const { getNodes, setNodes, @@ -193,10 +197,13 @@ export const useNodesInteractions = () => { setEdges(newEdges) }, [store, workflowStore, getNodesReadOnly]) - const handleNodeLeave = useCallback(() => { + const handleNodeLeave = useCallback((_, node) => { if (getNodesReadOnly()) return + if (node.type === CUSTOM_NOTE_NODE) + return + const { setEnteringNodePayload, } = workflowStore.getState() @@ -298,6 +305,9 @@ export const useNodesInteractions = () => { if (targetNode?.data.isIterationStart) return + if (sourceNode?.type === CUSTOM_NOTE_NODE || targetNode?.type === CUSTOM_NOTE_NODE) + return + const needDeleteEdges = edges.filter((edge) => { if ( (edge.source === source && edge.sourceHandle === sourceHandle) @@ -361,6 +371,9 @@ export const useNodesInteractions = () => { const { getNodes } = store.getState() const node = getNodes().find(n => n.id === nodeId)! + if (node.type === CUSTOM_NOTE_NODE) + return + if (node.data.type === BlockEnum.VariableAggregator || node.data.type === BlockEnum.VariableAssigner) { if (handleType === 'target') return @@ -975,6 +988,9 @@ export const useNodesInteractions = () => { }, [store]) const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => { + if (node.type === CUSTOM_NOTE_NODE) + return + e.preventDefault() const container = document.querySelector('#workflow-container') const { x, y } = container!.getBoundingClientRect() @@ -1051,6 +1067,7 @@ export const useNodesInteractions = () => { const nodeType = nodeToPaste.data.type const newNode = generateNewNode({ + type: nodeToPaste.type, data: { ...NODES_INITIAL_DATA[nodeType], ...nodeToPaste.data, diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index d7759eb998d2c3..fc995ed976dad8 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -34,8 +34,10 @@ import { useWorkflowStore, } from '../store' import { + CUSTOM_NODE, SUPPORT_OUTPUT_VARS_NODE, } from '../constants' +import { CUSTOM_NOTE_NODE } from '../note-node/constants' import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' import { useNodesExtraData } from './use-nodes-data' import { useWorkflowTemplate } from './use-workflow-template' @@ -88,7 +90,7 @@ export const useWorkflow = () => { const rankMap = {} as Record nodes.forEach((node) => { - if (!node.parentId) { + if (!node.parentId && node.type === CUSTOM_NODE) { const rank = layout.node(node.id).rank! if (!rankMap[rank]) { @@ -103,7 +105,7 @@ export const useWorkflow = () => { const newNodes = produce(nodes, (draft) => { draft.forEach((node) => { - if (!node.parentId) { + if (!node.parentId && node.type === CUSTOM_NODE) { const nodeWithPosition = layout.node(node.id) node.position = { @@ -345,6 +347,9 @@ export const useWorkflow = () => { if (targetNode.data.isIterationStart) return false + if (sourceNode.type === CUSTOM_NOTE_NODE || targetNode.type === CUSTOM_NOTE_NODE) + return false + if (sourceNode && targetNode) { const sourceNodeAvailableNextNodes = nodesExtraData[sourceNode.data.type].availableNextNodes const targetNodeAvailablePrevNodes = [...nodesExtraData[targetNode.data.type].availablePrevNodes, BlockEnum.Start] diff --git a/web/app/components/workflow/index.tsx b/web/app/components/workflow/index.tsx index e9ae8dd84fc8c1..23807e36ffe903 100644 --- a/web/app/components/workflow/index.tsx +++ b/web/app/components/workflow/index.tsx @@ -46,6 +46,8 @@ import { } from './hooks' import Header from './header' import CustomNode from './nodes' +import CustomNoteNode from './note-node' +import { CUSTOM_NOTE_NODE } from './note-node/constants' import Operator from './operator' import CustomEdge from './custom-edge' import CustomConnectionLine from './custom-connection-line' @@ -66,6 +68,7 @@ import { initialNodes, } from './utils' import { + CUSTOM_NODE, ITERATION_CHILDREN_Z_INDEX, WORKFLOW_DATA_UPDATE, } from './constants' @@ -76,10 +79,11 @@ import { useEventEmitterContextContext } from '@/context/event-emitter' import Confirm from '@/app/components/base/confirm/common' const nodeTypes = { - custom: CustomNode, + [CUSTOM_NODE]: CustomNode, + [CUSTOM_NOTE_NODE]: CustomNoteNode, } const edgeTypes = { - custom: CustomEdge, + [CUSTOM_NODE]: CustomEdge, } type WorkflowProps = { diff --git a/web/app/components/workflow/nodes/_base/components/node-resizer.tsx b/web/app/components/workflow/nodes/_base/components/node-resizer.tsx index 377f5f4c2d8d21..9de94845810887 100644 --- a/web/app/components/workflow/nodes/_base/components/node-resizer.tsx +++ b/web/app/components/workflow/nodes/_base/components/node-resizer.tsx @@ -19,10 +19,18 @@ const Icon = () => { type NodeResizerProps = { nodeId: string nodeData: CommonNodeType + icon?: JSX.Element + minWidth?: number + minHeight?: number + maxWidth?: number } const NodeResizer = ({ nodeId, nodeData, + icon = , + minWidth = 272, + minHeight = 176, + maxWidth, }: NodeResizerProps) => { const { handleNodeResize } = useNodesInteractions() @@ -39,10 +47,11 @@ const NodeResizer = ({ position='bottom-right' className='!border-none !bg-transparent' onResize={handleResize} - minWidth={272} - minHeight={176} + minWidth={minWidth} + minHeight={minHeight} + maxWidth={maxWidth} > -
+
{icon}
) diff --git a/web/app/components/workflow/nodes/constants.ts b/web/app/components/workflow/nodes/constants.ts index c67ce757ab2ce2..a97aa086edac9f 100644 --- a/web/app/components/workflow/nodes/constants.ts +++ b/web/app/components/workflow/nodes/constants.ts @@ -64,3 +64,5 @@ export const PanelComponentMap: Record> = { [BlockEnum.ParameterExtractor]: ParameterExtractorPanel, [BlockEnum.Iteration]: IterationPanel, } + +export const CUSTOM_NODE_TYPE = 'custom' diff --git a/web/app/components/workflow/nodes/index.tsx b/web/app/components/workflow/nodes/index.tsx index a79651af7007fe..bebc140414fd9b 100644 --- a/web/app/components/workflow/nodes/index.tsx +++ b/web/app/components/workflow/nodes/index.tsx @@ -1,6 +1,10 @@ -import { memo } from 'react' +import { + memo, + useMemo, +} from 'react' import type { NodeProps } from 'reactflow' import type { Node } from '../types' +import { CUSTOM_NODE } from '../constants' import { NodeComponentMap, PanelComponentMap, @@ -23,14 +27,24 @@ const CustomNode = (props: NodeProps) => { CustomNode.displayName = 'CustomNode' export const Panel = memo((props: Node) => { + const nodeClass = props.type const nodeData = props.data - const PanelComponent = PanelComponentMap[nodeData.type] + const PanelComponent = useMemo(() => { + if (nodeClass === CUSTOM_NODE) + return PanelComponentMap[nodeData.type] - return ( - - - - ) + return () => null + }, [nodeClass, nodeData.type]) + + if (nodeClass === CUSTOM_NODE) { + return ( + + + + ) + } + + return null }) Panel.displayName = 'Panel' diff --git a/web/app/components/workflow/note-node/constants.ts b/web/app/components/workflow/note-node/constants.ts new file mode 100644 index 00000000000000..efd1e01b3c202a --- /dev/null +++ b/web/app/components/workflow/note-node/constants.ts @@ -0,0 +1,42 @@ +import { NoteTheme } from './types' + +export const CUSTOM_NOTE_NODE = 'custom-note' + +export const THEME_MAP: Record = { + [NoteTheme.blue]: { + outer: '#2E90FA', + title: '#D1E9FF', + bg: '#EFF8FF', + border: '#84CAFF', + }, + [NoteTheme.cyan]: { + outer: '#06AED4', + title: '#CFF9FE', + bg: '#ECFDFF', + border: '#67E3F9', + }, + [NoteTheme.green]: { + outer: '#16B364', + title: '#D3F8DF', + bg: '#EDFCF2', + border: '#73E2A3', + }, + [NoteTheme.yellow]: { + outer: '#EAAA08', + title: '#FEF7C3', + bg: '#FEFBE8', + border: '#FDE272', + }, + [NoteTheme.pink]: { + outer: '#EE46BC', + title: '#FCE7F6', + bg: '#FDF2FA', + border: '#FAA7E0', + }, + [NoteTheme.violet]: { + outer: '#875BF7', + title: '#ECE9FE', + bg: '#F5F3FF', + border: '#C3B5FD', + }, +} diff --git a/web/app/components/workflow/note-node/hooks.ts b/web/app/components/workflow/note-node/hooks.ts new file mode 100644 index 00000000000000..7606951726cfea --- /dev/null +++ b/web/app/components/workflow/note-node/hooks.ts @@ -0,0 +1,29 @@ +import { useCallback } from 'react' +import type { EditorState } from 'lexical' +import { useNodeDataUpdate } from '../hooks' +import type { NoteTheme } from './types' + +export const useNote = (id: string) => { + const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + + const handleThemeChange = useCallback((theme: NoteTheme) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { theme } }) + }, [handleNodeDataUpdateWithSyncDraft, id]) + + const handleEditorChange = useCallback((editorState: EditorState) => { + if (!editorState?.isEmpty()) + handleNodeDataUpdateWithSyncDraft({ id, data: { text: JSON.stringify(editorState) } }) + else + handleNodeDataUpdateWithSyncDraft({ id, data: { text: '' } }) + }, [handleNodeDataUpdateWithSyncDraft, id]) + + const handleShowAuthorChange = useCallback((showAuthor: boolean) => { + handleNodeDataUpdateWithSyncDraft({ id, data: { showAuthor } }) + }, [handleNodeDataUpdateWithSyncDraft, id]) + + return { + handleThemeChange, + handleEditorChange, + handleShowAuthorChange, + } +} diff --git a/web/app/components/workflow/note-node/index.tsx b/web/app/components/workflow/note-node/index.tsx new file mode 100644 index 00000000000000..850c6b730a67f6 --- /dev/null +++ b/web/app/components/workflow/note-node/index.tsx @@ -0,0 +1,127 @@ +import { + memo, + useCallback, + useRef, +} from 'react' +import cn from 'classnames' +import { useTranslation } from 'react-i18next' +import { useClickAway } from 'ahooks' +import type { NodeProps } from 'reactflow' +import NodeResizer from '../nodes/_base/components/node-resizer' +import { + useNodeDataUpdate, + useNodesInteractions, +} from '../hooks' +import { useStore } from '../store' +import { + NoteEditor, + NoteEditorContextProvider, + NoteEditorToolbar, +} from './note-editor' +import { THEME_MAP } from './constants' +import { useNote } from './hooks' +import type { NoteNodeType } from './types' + +const Icon = () => { + return ( + + + + ) +} + +const NoteNode = ({ + id, + data, +}: NodeProps) => { + const { t } = useTranslation() + const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) + const ref = useRef(null) + const theme = data.theme + const { + handleThemeChange, + handleEditorChange, + handleShowAuthorChange, + } = useNote(id) + const { + handleNodesCopy, + handleNodesDuplicate, + handleNodeDelete, + } = useNodesInteractions() + const { handleNodeDataUpdateWithSyncDraft } = useNodeDataUpdate() + + const handleDeleteNode = useCallback(() => { + handleNodeDelete(id) + }, [id, handleNodeDelete]) + + useClickAway(() => { + handleNodeDataUpdateWithSyncDraft({ id, data: { selected: false } }) + }, ref) + + return ( +
+ + <> + } + minWidth={240} + maxWidth={640} + minHeight={88} + /> +
+ { + data.selected && ( +
+ +
+ ) + } +
+
+ +
+
+ { + data.showAuthor && ( +
+ {data.author} +
+ ) + } + +
+
+ ) +} + +export default memo(NoteNode) diff --git a/web/app/components/workflow/note-node/note-editor/context.tsx b/web/app/components/workflow/note-node/note-editor/context.tsx new file mode 100644 index 00000000000000..0d892b3ae680e5 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/context.tsx @@ -0,0 +1,65 @@ +'use client' + +import { + createContext, + memo, + useRef, +} from 'react' +import { LexicalComposer } from '@lexical/react/LexicalComposer' +import { LinkNode } from '@lexical/link' +import { + ListItemNode, + ListNode, +} from '@lexical/list' +import { createNoteEditorStore } from './store' +import theme from './theme' + +type NoteEditorStore = ReturnType +const NoteEditorContext = createContext(null) + +type NoteEditorContextProviderProps = { + value: string + children: JSX.Element | string | (JSX.Element | string)[] +} +export const NoteEditorContextProvider = memo(({ + value, + children, +}: NoteEditorContextProviderProps) => { + const storeRef = useRef() + + if (!storeRef.current) + storeRef.current = createNoteEditorStore() + + let initialValue = null + try { + initialValue = JSON.parse(value) + } + catch (e) { + + } + + const initialConfig = { + namespace: 'note-editor', + nodes: [ + LinkNode, + ListNode, + ListItemNode, + ], + editorState: !initialValue?.root.children.length ? null : JSON.stringify(initialValue), + onError: (error: Error) => { + throw error + }, + theme, + } + + return ( + + + {children} + + + ) +}) +NoteEditorContextProvider.displayName = 'NoteEditorContextProvider' + +export default NoteEditorContext diff --git a/web/app/components/workflow/note-node/note-editor/editor.tsx b/web/app/components/workflow/note-node/note-editor/editor.tsx new file mode 100644 index 00000000000000..189cc78c42e54e --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/editor.tsx @@ -0,0 +1,62 @@ +'use client' + +import { + memo, + useCallback, +} from 'react' +import type { EditorState } from 'lexical' +import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin' +import { ContentEditable } from '@lexical/react/LexicalContentEditable' +import { ClickableLinkPlugin } from '@lexical/react/LexicalClickableLinkPlugin' +import { LinkPlugin } from '@lexical/react/LexicalLinkPlugin' +import { ListPlugin } from '@lexical/react/LexicalListPlugin' +import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary' +import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin' +import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin' +import LinkEditorPlugin from './plugins/link-editor-plugin' +import FormatDetectorPlugin from './plugins/format-detector-plugin' +// import TreeView from '@/app/components/base/prompt-editor/plugins/tree-view' +import Placeholder from '@/app/components/base/prompt-editor/plugins/placeholder' + +type EditorProps = { + placeholder?: string + onChange?: (editorState: EditorState) => void + containerElement: HTMLDivElement | null +} +const Editor = ({ + placeholder = 'write you note...', + onChange, + containerElement, +}: EditorProps) => { + const handleEditorChange = useCallback((editorState: EditorState) => { + onChange?.(editorState) + }, [onChange]) + + return ( +
+ + +
+ } + placeholder={} + ErrorBoundary={LexicalErrorBoundary} + /> + + + + + + + + {/* */} + + ) +} + +export default memo(Editor) diff --git a/web/app/components/workflow/note-node/note-editor/index.tsx b/web/app/components/workflow/note-node/note-editor/index.tsx new file mode 100644 index 00000000000000..f3c7364e8e0691 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/index.tsx @@ -0,0 +1,3 @@ +export { NoteEditorContextProvider } from './context' +export { default as NoteEditor } from './editor' +export { default as NoteEditorToolbar } from './toolbar' diff --git a/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts new file mode 100644 index 00000000000000..bc7e855c3bc941 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/hooks.ts @@ -0,0 +1,78 @@ +import { + useCallback, + useEffect, +} from 'react' +import { + $getSelection, + $isRangeSelection, +} from 'lexical' +import { mergeRegister } from '@lexical/utils' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import type { LinkNode } from '@lexical/link' +import { $isLinkNode } from '@lexical/link' +import { $isListItemNode } from '@lexical/list' +import { getSelectedNode } from '../../utils' +import { useNoteEditorStore } from '../../store' + +export const useFormatDetector = () => { + const [editor] = useLexicalComposerContext() + const noteEditorStore = useNoteEditorStore() + + const handleFormat = useCallback(() => { + editor.getEditorState().read(() => { + if (editor.isComposing()) + return + + const selection = $getSelection() + + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection) + const { + setSelectedIsBold, + setSelectedIsItalic, + setSelectedIsStrikeThrough, + setSelectedLinkUrl, + setSelectedIsLink, + setSelectedIsBullet, + } = noteEditorStore.getState() + setSelectedIsBold(selection.hasFormat('bold')) + setSelectedIsItalic(selection.hasFormat('italic')) + setSelectedIsStrikeThrough(selection.hasFormat('strikethrough')) + const parent = node.getParent() + if ($isLinkNode(parent) || $isLinkNode(node)) { + const linkUrl = ($isLinkNode(parent) ? parent : node as LinkNode).getURL() + setSelectedLinkUrl(linkUrl) + setSelectedIsLink(true) + } + else { + setSelectedLinkUrl('') + setSelectedIsLink(false) + } + + if ($isListItemNode(parent) || $isListItemNode(node)) + setSelectedIsBullet(true) + else + setSelectedIsBullet(false) + } + }) + }, [editor, noteEditorStore]) + + useEffect(() => { + document.addEventListener('selectionchange', handleFormat) + return () => { + document.removeEventListener('selectionchange', handleFormat) + } + }, [handleFormat]) + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(() => { + handleFormat() + }), + ) + }, [editor, handleFormat]) + + return { + handleFormat, + } +} diff --git a/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx new file mode 100644 index 00000000000000..3a2585c4b5ab6f --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/plugins/format-detector-plugin/index.tsx @@ -0,0 +1,9 @@ +import { useFormatDetector } from './hooks' + +const FormatDetectorPlugin = () => { + useFormatDetector() + + return null +} + +export default FormatDetectorPlugin diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx new file mode 100644 index 00000000000000..5819bc8bde715d --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/component.tsx @@ -0,0 +1,152 @@ +import { + memo, + useEffect, + useState, +} from 'react' +import { escape } from 'lodash-es' +import { + FloatingPortal, + flip, + offset, + shift, + useFloating, +} from '@floating-ui/react' +import { useTranslation } from 'react-i18next' +import { useClickAway } from 'ahooks' +import cn from 'classnames' +import { useStore } from '../../store' +import { useLink } from './hooks' +import Button from '@/app/components/base/button' +import { + Edit03, + LinkBroken01, + LinkExternal01, +} from '@/app/components/base/icons/src/vender/line/general' + +type LinkEditorComponentProps = { + containerElement: HTMLDivElement | null +} +const LinkEditorComponent = ({ + containerElement, +}: LinkEditorComponentProps) => { + const { t } = useTranslation() + const { + handleSaveLink, + handleUnlink, + } = useLink() + const selectedLinkUrl = useStore(s => s.selectedLinkUrl) + const linkAnchorElement = useStore(s => s.linkAnchorElement) + const linkOperatorShow = useStore(s => s.linkOperatorShow) + const setLinkAnchorElement = useStore(s => s.setLinkAnchorElement) + const setLinkOperatorShow = useStore(s => s.setLinkOperatorShow) + const [url, setUrl] = useState(selectedLinkUrl) + const { refs, floatingStyles, elements } = useFloating({ + placement: 'top', + middleware: [ + offset(4), + shift(), + flip(), + ], + }) + + useClickAway(() => { + setLinkAnchorElement() + }, linkAnchorElement) + + useEffect(() => { + setUrl(selectedLinkUrl) + }, [selectedLinkUrl]) + + useEffect(() => { + if (linkAnchorElement) + refs.setReference(linkAnchorElement) + }, [linkAnchorElement, refs]) + + return ( + <> + { + elements.reference && ( + +
+ { + !linkOperatorShow && ( + <> + setUrl(e.target.value)} + placeholder={t('workflow.nodes.note.editor.enterUrl') || ''} + autoFocus + /> + + + ) + } + { + linkOperatorShow && ( + <> + + +
+ {t('workflow.nodes.note.editor.openLink')} +
+
+ {escape(url)} +
+
+
+
{ + e.stopPropagation() + setLinkOperatorShow(false) + }} + > + + {t('common.operation.edit')} +
+
+ + {t('workflow.nodes.note.editor.unlink')} +
+ + ) + } +
+
+ ) + } + + ) +} + +export default memo(LinkEditorComponent) diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts new file mode 100644 index 00000000000000..8be8b551963b67 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/hooks.ts @@ -0,0 +1,115 @@ +import { + useCallback, + useEffect, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + CLICK_COMMAND, + COMMAND_PRIORITY_LOW, +} from 'lexical' +import { + mergeRegister, +} from '@lexical/utils' +import { + TOGGLE_LINK_COMMAND, +} from '@lexical/link' +import { escape } from 'lodash-es' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useNoteEditorStore } from '../../store' +import { urlRegExp } from '../../utils' +import { useToastContext } from '@/app/components/base/toast' + +export const useOpenLink = () => { + const [editor] = useLexicalComposerContext() + const noteEditorStore = useNoteEditorStore() + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(() => { + setTimeout(() => { + const { + selectedLinkUrl, + selectedIsLink, + setLinkAnchorElement, + setLinkOperatorShow, + } = noteEditorStore.getState() + + if (selectedIsLink) { + setLinkAnchorElement(true) + + if (selectedLinkUrl) + setLinkOperatorShow(true) + else + setLinkOperatorShow(false) + } + else { + setLinkAnchorElement() + setLinkOperatorShow(false) + } + }) + }), + editor.registerCommand( + CLICK_COMMAND, + (payload) => { + setTimeout(() => { + const { + selectedLinkUrl, + selectedIsLink, + setLinkAnchorElement, + setLinkOperatorShow, + } = noteEditorStore.getState() + + if (selectedIsLink) { + if ((payload.metaKey || payload.ctrlKey) && selectedLinkUrl) { + window.open(selectedLinkUrl, '_blank') + return true + } + setLinkAnchorElement(true) + + if (selectedLinkUrl) + setLinkOperatorShow(true) + else + setLinkOperatorShow(false) + } + else { + setLinkAnchorElement() + setLinkOperatorShow(false) + } + }) + return false + }, + COMMAND_PRIORITY_LOW, + ), + ) + }, [editor, noteEditorStore]) +} + +export const useLink = () => { + const { t } = useTranslation() + const [editor] = useLexicalComposerContext() + const noteEditorStore = useNoteEditorStore() + const { notify } = useToastContext() + + const handleSaveLink = useCallback((url: string) => { + if (url && !urlRegExp.test(url)) { + notify({ type: 'error', message: t('workflow.nodes.note.editor.invalidUrl') }) + return + } + editor.dispatchCommand(TOGGLE_LINK_COMMAND, escape(url)) + + const { setLinkAnchorElement } = noteEditorStore.getState() + setLinkAnchorElement() + }, [editor, noteEditorStore, notify, t]) + + const handleUnlink = useCallback(() => { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) + + const { setLinkAnchorElement } = noteEditorStore.getState() + setLinkAnchorElement() + }, [editor, noteEditorStore]) + + return { + handleSaveLink, + handleUnlink, + } +} diff --git a/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx new file mode 100644 index 00000000000000..a5b3df6504ae95 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/plugins/link-editor-plugin/index.tsx @@ -0,0 +1,25 @@ +import { + memo, +} from 'react' +import { useStore } from '../../store' +import { useOpenLink } from './hooks' +import LinkEditorComponent from './component' + +type LinkEditorPluginProps = { + containerElement: HTMLDivElement | null +} +const LinkEditorPlugin = ({ + containerElement, +}: LinkEditorPluginProps) => { + useOpenLink() + const linkAnchorElement = useStore(s => s.linkAnchorElement) + + if (!linkAnchorElement) + return null + + return ( + + ) +} + +export default memo(LinkEditorPlugin) diff --git a/web/app/components/workflow/note-node/note-editor/store.ts b/web/app/components/workflow/note-node/note-editor/store.ts new file mode 100644 index 00000000000000..3507bb7c0cb177 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/store.ts @@ -0,0 +1,72 @@ +import { useContext } from 'react' +import { + useStore as useZustandStore, +} from 'zustand' +import { createStore } from 'zustand/vanilla' +import NoteEditorContext from './context' + +type Shape = { + linkAnchorElement: HTMLElement | null + setLinkAnchorElement: (open?: boolean) => void + linkOperatorShow: boolean + setLinkOperatorShow: (linkOperatorShow: boolean) => void + selectedIsBold: boolean + setSelectedIsBold: (selectedIsBold: boolean) => void + selectedIsItalic: boolean + setSelectedIsItalic: (selectedIsItalic: boolean) => void + selectedIsStrikeThrough: boolean + setSelectedIsStrikeThrough: (selectedIsStrikeThrough: boolean) => void + selectedLinkUrl: string + setSelectedLinkUrl: (selectedLinkUrl: string) => void + selectedIsLink: boolean + setSelectedIsLink: (selectedIsLink: boolean) => void + selectedIsBullet: boolean + setSelectedIsBullet: (selectedIsBullet: boolean) => void +} + +export const createNoteEditorStore = () => { + return createStore(set => ({ + linkAnchorElement: null, + setLinkAnchorElement: (open) => { + if (open) { + setTimeout(() => { + const nativeSelection = window.getSelection() + + if (nativeSelection?.focusNode) { + const parent = nativeSelection.focusNode.parentElement + set(() => ({ linkAnchorElement: parent })) + } + }) + } + else { + set(() => ({ linkAnchorElement: null })) + } + }, + linkOperatorShow: false, + setLinkOperatorShow: linkOperatorShow => set(() => ({ linkOperatorShow })), + selectedIsBold: false, + setSelectedIsBold: selectedIsBold => set(() => ({ selectedIsBold })), + selectedIsItalic: false, + setSelectedIsItalic: selectedIsItalic => set(() => ({ selectedIsItalic })), + selectedIsStrikeThrough: false, + setSelectedIsStrikeThrough: selectedIsStrikeThrough => set(() => ({ selectedIsStrikeThrough })), + selectedLinkUrl: '', + setSelectedLinkUrl: selectedLinkUrl => set(() => ({ selectedLinkUrl })), + selectedIsLink: false, + setSelectedIsLink: selectedIsLink => set(() => ({ selectedIsLink })), + selectedIsBullet: false, + setSelectedIsBullet: selectedIsBullet => set(() => ({ selectedIsBullet })), + })) +} + +export function useStore(selector: (state: Shape) => T): T { + const store = useContext(NoteEditorContext) + if (!store) + throw new Error('Missing NoteEditorContext.Provider in the tree') + + return useZustandStore(store, selector) +} + +export const useNoteEditorStore = () => { + return useContext(NoteEditorContext)! +} diff --git a/web/app/components/workflow/note-node/note-editor/theme/index.ts b/web/app/components/workflow/note-node/note-editor/theme/index.ts new file mode 100644 index 00000000000000..42069d2359bbdd --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/theme/index.ts @@ -0,0 +1,17 @@ +import type { EditorThemeClasses } from 'lexical' + +import './theme.css' + +const theme: EditorThemeClasses = { + paragraph: 'note-editor-theme_paragraph', + list: { + ul: 'note-editor-theme_list-ul', + listitem: 'note-editor-theme_list-li', + }, + link: 'note-editor-theme_link', + text: { + strikethrough: 'note-editor-theme_text-strikethrough', + }, +} + +export default theme diff --git a/web/app/components/workflow/note-node/note-editor/theme/theme.css b/web/app/components/workflow/note-node/note-editor/theme/theme.css new file mode 100644 index 00000000000000..8b04d85bb5a944 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/theme/theme.css @@ -0,0 +1,24 @@ +.note-editor-theme_paragraph { + font-size: 12px; +} + +.note-editor-theme_list-ul { + font-size: 12px; + margin: 0; + padding: 0; + list-style: disc; +} + +.note-editor-theme_list-li { + margin-left: 18px; + margin-right: 8px; +} + +.note-editor-theme_link { + text-decoration: underline; + cursor: pointer; +} + +.note-editor-theme_text-strikethrough { + text-decoration: line-through; +} \ No newline at end of file diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx new file mode 100644 index 00000000000000..429188a89b5637 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/color-picker.tsx @@ -0,0 +1,105 @@ +import { + memo, + useState, +} from 'react' +import cn from 'classnames' +import { NoteTheme } from '../../types' +import { THEME_MAP } from '../../constants' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' + +export const COLOR_LIST = [ + { + key: NoteTheme.blue, + inner: THEME_MAP[NoteTheme.blue].title, + outer: THEME_MAP[NoteTheme.blue].outer, + }, + { + key: NoteTheme.cyan, + inner: THEME_MAP[NoteTheme.cyan].title, + outer: THEME_MAP[NoteTheme.cyan].outer, + }, + { + key: NoteTheme.green, + inner: THEME_MAP[NoteTheme.green].title, + outer: THEME_MAP[NoteTheme.green].outer, + }, + { + key: NoteTheme.yellow, + inner: THEME_MAP[NoteTheme.yellow].title, + outer: THEME_MAP[NoteTheme.yellow].outer, + }, + { + key: NoteTheme.pink, + inner: THEME_MAP[NoteTheme.pink].title, + outer: THEME_MAP[NoteTheme.pink].outer, + }, + { + key: NoteTheme.violet, + inner: THEME_MAP[NoteTheme.violet].title, + outer: THEME_MAP[NoteTheme.violet].outer, + }, +] + +export type ColorPickerProps = { + theme: NoteTheme + onThemeChange: (theme: NoteTheme) => void +} +const ColorPicker = ({ + theme, + onThemeChange, +}: ColorPickerProps) => { + const [open, setOpen] = useState(false) + + return ( + + setOpen(!open)}> +
+
+
+
+ +
+ { + COLOR_LIST.map(color => ( +
{ + e.stopPropagation() + onThemeChange(color.key) + setOpen(false) + }} + > +
+
+
+ )) + } +
+
+
+ ) +} + +export default memo(ColorPicker) diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx new file mode 100644 index 00000000000000..96ad5e8d92b86c --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/command.tsx @@ -0,0 +1,81 @@ +import { + memo, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import { useStore } from '../store' +import { useCommand } from './hooks' +import { Link01 } from '@/app/components/base/icons/src/vender/line/general' +import { + Bold01, + Dotpoints01, + Italic01, + Strikethrough01, +} from '@/app/components/base/icons/src/vender/line/editor' +import TooltipPlus from '@/app/components/base/tooltip-plus' + +type CommandProps = { + type: 'bold' | 'italic' | 'strikethrough' | 'link' | 'bullet' +} +const Command = ({ + type, +}: CommandProps) => { + const { t } = useTranslation() + const selectedIsBold = useStore(s => s.selectedIsBold) + const selectedIsItalic = useStore(s => s.selectedIsItalic) + const selectedIsStrikeThrough = useStore(s => s.selectedIsStrikeThrough) + const selectedIsLink = useStore(s => s.selectedIsLink) + const selectedIsBullet = useStore(s => s.selectedIsBullet) + const { handleCommand } = useCommand() + + const icon = useMemo(() => { + switch (type) { + case 'bold': + return + case 'italic': + return + case 'strikethrough': + return + case 'link': + return + case 'bullet': + return + } + }, [type, selectedIsBold, selectedIsItalic, selectedIsStrikeThrough, selectedIsLink, selectedIsBullet]) + + const tip = useMemo(() => { + switch (type) { + case 'bold': + return t('workflow.nodes.note.editor.bold') + case 'italic': + return t('workflow.nodes.note.editor.italic') + case 'strikethrough': + return t('workflow.nodes.note.editor.strikethrough') + case 'link': + return t('workflow.nodes.note.editor.link') + case 'bullet': + return t('workflow.nodes.note.editor.bulletList') + } + }, [type, t]) + + return ( + +
handleCommand(type)} + > + {icon} +
+
+ ) +} + +export default memo(Command) diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx new file mode 100644 index 00000000000000..aefdb46b0aae82 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/divider.tsx @@ -0,0 +1,7 @@ +const Divider = () => { + return ( +
+ ) +} + +export default Divider diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx new file mode 100644 index 00000000000000..c6284a9a74b72d --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/font-size-selector.tsx @@ -0,0 +1,86 @@ +import { memo } from 'react' +import cn from 'classnames' +import { useTranslation } from 'react-i18next' +import { useFontSize } from './hooks' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import { TitleCase } from '@/app/components/base/icons/src/vender/line/editor' +import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows' +import { Check } from '@/app/components/base/icons/src/vender/line/general' + +const FontSizeSelector = () => { + const { t } = useTranslation() + const FONT_SIZE_LIST = [ + { + key: '12px', + value: t('workflow.nodes.note.editor.small'), + }, + { + key: '14px', + value: t('workflow.nodes.note.editor.medium'), + }, + { + key: '16px', + value: t('workflow.nodes.note.editor.large'), + }, + ] + const { + fontSizeSelectorShow, + handleOpenFontSizeSelector, + fontSize, + handleFontSize, + } = useFontSize() + + return ( + + handleOpenFontSizeSelector(!fontSizeSelectorShow)}> +
+ + {FONT_SIZE_LIST.find(font => font.key === fontSize)?.value || t('workflow.nodes.note.editor.small')} + +
+
+ +
+ { + FONT_SIZE_LIST.map(font => ( +
{ + e.stopPropagation() + handleFontSize(font.key) + handleOpenFontSizeSelector(false) + }} + > +
+ {font.value} +
+ { + fontSize === font.key && ( + + ) + } +
+ )) + } +
+
+
+ ) +} + +export default memo(FontSizeSelector) diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts b/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts new file mode 100644 index 00000000000000..8ed942d8d62e08 --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/hooks.ts @@ -0,0 +1,147 @@ +import { + useCallback, + useEffect, + useState, +} from 'react' +import { + $createParagraphNode, + $getSelection, + $isRangeSelection, + $setSelection, + COMMAND_PRIORITY_CRITICAL, + FORMAT_TEXT_COMMAND, + SELECTION_CHANGE_COMMAND, +} from 'lexical' +import { + $getSelectionStyleValueForProperty, + $patchStyleText, + $setBlocksType, +} from '@lexical/selection' +import { INSERT_UNORDERED_LIST_COMMAND } from '@lexical/list' +import { mergeRegister } from '@lexical/utils' +import { + $isLinkNode, + TOGGLE_LINK_COMMAND, +} from '@lexical/link' +import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext' +import { useNoteEditorStore } from '../store' +import { getSelectedNode } from '../utils' + +export const useCommand = () => { + const [editor] = useLexicalComposerContext() + const noteEditorStore = useNoteEditorStore() + + const handleCommand = useCallback((type: string) => { + if (type === 'bold') + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'bold') + + if (type === 'italic') + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'italic') + + if (type === 'strikethrough') + editor.dispatchCommand(FORMAT_TEXT_COMMAND, 'strikethrough') + + if (type === 'link') { + editor.update(() => { + const selection = $getSelection() + + if ($isRangeSelection(selection)) { + const node = getSelectedNode(selection) + const parent = node.getParent() + const { setLinkAnchorElement } = noteEditorStore.getState() + + if ($isLinkNode(parent) || $isLinkNode(node)) { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, null) + setLinkAnchorElement() + } + else { + editor.dispatchCommand(TOGGLE_LINK_COMMAND, '') + setLinkAnchorElement(true) + } + } + }) + } + + if (type === 'bullet') { + const { selectedIsBullet } = noteEditorStore.getState() + + if (selectedIsBullet) { + editor.update(() => { + const selection = $getSelection() + if ($isRangeSelection(selection)) + $setBlocksType(selection, () => $createParagraphNode()) + }) + } + else { + editor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined) + } + } + }, [editor, noteEditorStore]) + + return { + handleCommand, + } +} + +export const useFontSize = () => { + const [editor] = useLexicalComposerContext() + const [fontSize, setFontSize] = useState('12px') + const [fontSizeSelectorShow, setFontSizeSelectorShow] = useState(false) + + const handleFontSize = useCallback((fontSize: string) => { + editor.update(() => { + const selection = $getSelection() + + if ($isRangeSelection(selection)) + $patchStyleText(selection, { 'font-size': fontSize }) + }) + }, [editor]) + + const handleOpenFontSizeSelector = useCallback((newFontSizeSelectorShow: boolean) => { + if (newFontSizeSelectorShow) { + editor.update(() => { + const selection = $getSelection() + + if ($isRangeSelection(selection)) + $setSelection(selection.clone()) + }) + } + setFontSizeSelectorShow(newFontSizeSelectorShow) + }, [editor]) + + useEffect(() => { + return mergeRegister( + editor.registerUpdateListener(() => { + editor.getEditorState().read(() => { + const selection = $getSelection() + + if ($isRangeSelection(selection)) { + const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px') + setFontSize(fontSize) + } + }) + }), + editor.registerCommand( + SELECTION_CHANGE_COMMAND, + () => { + const selection = $getSelection() + + if ($isRangeSelection(selection)) { + const fontSize = $getSelectionStyleValueForProperty(selection, 'font-size', '12px') + setFontSize(fontSize) + } + + return false + }, + COMMAND_PRIORITY_CRITICAL, + ), + ) + }, [editor]) + + return { + fontSize, + handleFontSize, + fontSizeSelectorShow, + handleOpenFontSizeSelector, + } +} diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx new file mode 100644 index 00000000000000..98ee0cdf0e5c2e --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/index.tsx @@ -0,0 +1,48 @@ +import { memo } from 'react' +import Divider from './divider' +import type { ColorPickerProps } from './color-picker' +import ColorPicker from './color-picker' +import FontSizeSelector from './font-size-selector' +import Command from './command' +import type { OperatorProps } from './operator' +import Operator from './operator' + +type ToolbarProps = ColorPickerProps & OperatorProps +const Toolbar = ({ + theme, + onThemeChange, + onCopy, + onDuplicate, + onDelete, + showAuthor, + onShowAuthorChange, +}: ToolbarProps) => { + return ( +
+ + + + +
+ + + + + +
+ + +
+ ) +} + +export default memo(Toolbar) diff --git a/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx new file mode 100644 index 00000000000000..7cd27a00a19abf --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/toolbar/operator.tsx @@ -0,0 +1,107 @@ +import { + memo, + useState, +} from 'react' +import { useTranslation } from 'react-i18next' +import cn from 'classnames' +import ShortcutsName from '@/app/components/workflow/shortcuts-name' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general' +import Switch from '@/app/components/base/switch' + +export type OperatorProps = { + onCopy: () => void + onDuplicate: () => void + onDelete: () => void + showAuthor: boolean + onShowAuthorChange: (showAuthor: boolean) => void +} +const Operator = ({ + onCopy, + onDelete, + onDuplicate, + showAuthor, + onShowAuthorChange, +}: OperatorProps) => { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + + return ( + + setOpen(!open)}> +
+ +
+
+ +
+
+
{ + onCopy() + setOpen(false) + }} + > + {t('workflow.common.copy')} + +
+
{ + onDuplicate() + setOpen(false) + }} + > + {t('workflow.common.duplicate')} + +
+
+
+
+
e.stopPropagation()} + > +
{t('workflow.nodes.note.editor.showAuthor')}
+ +
+
+
+
+
{ + onDelete() + setOpen(false) + }} + > + {t('common.operation.delete')} + +
+
+
+
+
+ ) +} + +export default memo(Operator) diff --git a/web/app/components/workflow/note-node/note-editor/utils.ts b/web/app/components/workflow/note-node/note-editor/utils.ts new file mode 100644 index 00000000000000..b9ce2d33b314da --- /dev/null +++ b/web/app/components/workflow/note-node/note-editor/utils.ts @@ -0,0 +1,21 @@ +import { $isAtNodeEnd } from '@lexical/selection' +import type { ElementNode, RangeSelection, TextNode } from 'lexical' + +export function getSelectedNode( + selection: RangeSelection, +): TextNode | ElementNode { + const anchor = selection.anchor + const focus = selection.focus + const anchorNode = selection.anchor.getNode() + const focusNode = selection.focus.getNode() + if (anchorNode === focusNode) + return anchorNode + + const isBackward = selection.isBackward() + if (isBackward) + return $isAtNodeEnd(focus) ? anchorNode : focusNode + else + return $isAtNodeEnd(anchor) ? anchorNode : focusNode +} + +export const urlRegExp = /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[-;:&=+$,\w]+@)?[A-Za-z0-9.-]+|(?:www.|[-;:&=+$,\w]+@)[A-Za-z0-9.-]+)((?:\/[+~%/.\w-_]*)?\??(?:[-+=&;%@.\w_]*)#?(?:[\w]*))?)/ diff --git a/web/app/components/workflow/note-node/types.ts b/web/app/components/workflow/note-node/types.ts new file mode 100644 index 00000000000000..ad68bd0f10867b --- /dev/null +++ b/web/app/components/workflow/note-node/types.ts @@ -0,0 +1,17 @@ +import type { CommonNodeType } from '../types' + +export enum NoteTheme { + blue = 'blue', + cyan = 'cyan', + green = 'green', + yellow = 'yellow', + pink = 'pink', + violet = 'violet', +} + +export type NoteNodeType = CommonNodeType & { + text: string + theme: NoteTheme + author: string + showAuthor: boolean +} diff --git a/web/app/components/workflow/operator/control.tsx b/web/app/components/workflow/operator/control.tsx index 8903239e90bf7f..02d9b7cc6b7d99 100644 --- a/web/app/components/workflow/operator/control.tsx +++ b/web/app/components/workflow/operator/control.tsx @@ -1,4 +1,8 @@ -import { memo, useCallback } from 'react' +import type { MouseEvent } from 'react' +import { + memo, + useCallback, +} from 'react' import { useTranslation } from 'react-i18next' import cn from 'classnames' import { useKeyPress } from 'ahooks' @@ -11,6 +15,7 @@ import { isEventTargetInputArea } from '../utils' import { useStore } from '../store' import AddBlock from './add-block' import TipPopup from './tip-popup' +import { useOperator } from './hooks' import { Cursor02C, Hand02, @@ -20,12 +25,14 @@ import { Hand02 as Hand02Solid, } from '@/app/components/base/icons/src/vender/solid/editor' import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout' +import { StickerSquare } from '@/app/components/base/icons/src/vender/line/files' const Control = () => { const { t } = useTranslation() const controlMode = useStore(s => s.controlMode) const setControlMode = useStore(s => s.setControlMode) const { handleLayout } = useWorkflow() + const { handleAddNote } = useOperator() const { nodesReadOnly, getNodesReadOnly, @@ -75,9 +82,28 @@ const Control = () => { handleLayout() } + const addNote = (e: MouseEvent) => { + if (getNodesReadOnly()) + return + + e.stopPropagation() + handleAddNote() + } + return (
+ +
+ +
+
{ + const workflowStore = useWorkflowStore() + const { userProfile } = useAppContext() + + const handleAddNote = useCallback(() => { + const newNode = generateNewNode({ + type: CUSTOM_NOTE_NODE, + data: { + title: '', + desc: '', + type: '' as any, + text: '', + theme: NoteTheme.blue, + author: userProfile?.name || '', + showAuthor: true, + width: 240, + height: 88, + _isCandidate: true, + } as NoteNodeType, + position: { + x: 0, + y: 0, + }, + }) + workflowStore.setState({ + candidateNode: newNode, + }) + }, [workflowStore, userProfile]) + + return { + handleAddNote, + } +} diff --git a/web/app/components/workflow/panel-contextmenu.tsx b/web/app/components/workflow/panel-contextmenu.tsx index eeae51c8d1aff4..a5e63fda4ec601 100644 --- a/web/app/components/workflow/panel-contextmenu.tsx +++ b/web/app/components/workflow/panel-contextmenu.tsx @@ -13,6 +13,7 @@ import { useWorkflowStartRun, } from './hooks' import AddBlock from './operator/add-block' +import { useOperator } from './operator/hooks' import { exportAppConfig } from '@/service/apps' import { useToastContext } from '@/app/components/base/toast' import { useStore as useAppStore } from '@/app/components/app/store' @@ -27,6 +28,7 @@ const PanelContextmenu = () => { const { handleNodesPaste } = useNodesInteractions() const { handlePaneContextmenuCancel } = usePanelInteractions() const { handleStartWorkflowRun } = useWorkflowStartRun() + const { handleAddNote } = useOperator() useClickAway(() => { handlePaneContextmenuCancel() @@ -78,6 +80,16 @@ const PanelContextmenu = () => { crossAxis: -4, }} /> +
{ + e.stopPropagation() + handleAddNote() + handlePaneContextmenuCancel() + }} + > + {t('workflow.nodes.note.addNote')} +
{ diff --git a/web/app/components/workflow/utils.ts b/web/app/components/workflow/utils.ts index 60e9f69ddf4515..4ad9c6591c8631 100644 --- a/web/app/components/workflow/utils.ts +++ b/web/app/components/workflow/utils.ts @@ -17,6 +17,7 @@ import type { } from './types' import { BlockEnum } from './types' import { + CUSTOM_NODE, ITERATION_NODE_Z_INDEX, NODE_WIDTH_X_OFFSET, START_INITIAL_POSITION, @@ -105,7 +106,8 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => { }, {} as Record) return nodes.map((node) => { - node.type = 'custom' + if (!node.type) + node.type = CUSTOM_NODE const connectedEdges = getConnectedEdges([node], edges) node.data._connectedSourceHandleIds = connectedEdges.filter(edge => edge.source === node.id).map(edge => edge.sourceHandle || 'source') @@ -189,7 +191,7 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => { export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => { const dagreGraph = new dagre.graphlib.Graph() dagreGraph.setDefaultEdgeLabel(() => ({})) - const nodes = cloneDeep(originNodes).filter(node => !node.parentId) + const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE) const edges = cloneDeep(originEdges).filter(edge => !edge.data?.isInIteration) dagreGraph.setGraph({ rankdir: 'LR', @@ -280,10 +282,10 @@ export const getNodesConnectedSourceOrTargetHandleIdsMap = (changes: ConnectedSo return nodesConnectedSourceOrTargetHandleIdsMap } -export const generateNewNode = ({ data, position, id, zIndex, ...rest }: Omit & { id?: string }) => { +export const generateNewNode = ({ data, position, id, zIndex, type, ...rest }: Omit & { id?: string }) => { return { id: id || `${Date.now()}`, - type: 'custom', + type: type || CUSTOM_NODE, data, position, targetPosition: Position.Left, diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 9ac975f8a22c2d..5d0edcf6ce95b3 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -412,6 +412,25 @@ const translation = { iteration_other: '{{count}} Iterations', currentIteration: 'Current Iteration', }, + note: { + addNote: 'Add Note', + editor: { + placeholder: 'Write your note...', + small: 'Small', + medium: 'Medium', + large: 'Large', + bold: 'Bold', + italic: 'Italic', + strikethrough: 'Strikethrough', + link: 'Link', + openLink: 'Open', + unlink: 'Unlink', + enterUrl: 'Enter URL...', + invalidUrl: 'Invalid URL', + bulletList: 'Bullet List', + showAuthor: 'Show Author', + }, + }, }, tracing: { stopBy: 'Stop by {{user}}', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 83b933bc89fcad..1fbaf38cc5e6fe 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -412,6 +412,25 @@ const translation = { iteration_other: '{{count}}个迭代', currentIteration: '当前迭代', }, + note: { + addNote: '添加注释', + editor: { + placeholder: '输入注释...', + small: '小', + medium: '中', + large: '大', + bold: '加粗', + italic: '斜体', + strikethrough: '删除线', + link: '链接', + openLink: '打开', + unlink: '取消链接', + enterUrl: '输入链接...', + invalidUrl: '无效的链接', + bulletList: '列表', + showAuthor: '显示作者', + }, + }, }, tracing: { stopBy: '由{{user}}终止', diff --git a/web/package.json b/web/package.json index 81f9f83a3d4130..7ff952ae05e93c 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,7 @@ "@headlessui/react": "^1.7.13", "@heroicons/react": "^2.0.16", "@hookform/resolvers": "^3.3.4", - "@lexical/react": "^0.12.2", + "@lexical/react": "^0.16.0", "@mdx-js/loader": "^2.3.0", "@mdx-js/react": "^2.3.0", "@monaco-editor/react": "^4.6.0", @@ -47,7 +47,7 @@ "js-cookie": "^3.0.1", "katex": "^0.16.10", "lamejs": "^1.2.1", - "lexical": "^0.12.2", + "lexical": "^0.16.0", "lodash-es": "^4.17.21", "mermaid": "10.4.0", "negotiator": "^0.6.3", diff --git a/web/yarn.lock b/web/yarn.lock index e75fa8d0686142..d8aa078e6a3b14 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -414,159 +414,206 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@lexical/clipboard@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/clipboard/-/clipboard-0.12.2.tgz" - integrity sha512-RldmfZquuJJJCJ5WquCyoJ1/eZ+AnNgdksqvd+G+Yn/GyJl/+O3dnHM0QVaDSPvh/PynLFcCtz/57ySLo2kQxQ== +"@lexical/clipboard@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/clipboard/-/clipboard-0.16.0.tgz#3ae0d87a56bd3518de077e45b0c1bbba2f356193" + integrity sha512-eYMJ6jCXpWBVC05Mu9HLMysrBbfi++xFfsm+Yo7A6kYGrqYUhpXqjJkYnw1xdZYL3bV73Oe4ByVJuq42GU+Mqw== dependencies: - "@lexical/html" "0.12.2" - "@lexical/list" "0.12.2" - "@lexical/selection" "0.12.2" - "@lexical/utils" "0.12.2" + "@lexical/html" "0.16.0" + "@lexical/list" "0.16.0" + "@lexical/selection" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/code@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/code/-/code-0.12.2.tgz" - integrity sha512-w2JeJdnMUtYnC/Fx78sL3iJBt9Ug8pFSDOcI9ay/BkMQFQV8oqq1iyuLLBBJSG4FAM8b2DXrVdGklRQ+jTfTVw== +"@lexical/code@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/code/-/code-0.16.0.tgz#225030342e3c361e5541c750033323007a947880" + integrity sha512-1EKCBSFV745UI2zn5v75sKcvVdmd+y2JtZhw8CItiQkRnBLv4l4d/RZYy+cKOuXJGsoBrKtxXn5sl7HebwQbPw== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" prismjs "^1.27.0" -"@lexical/dragon@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/dragon/-/dragon-0.12.2.tgz" - integrity sha512-Mt8NLzTOt+VgQtc2DKDbHBwKeRlvKqbLqRIMYUVk60gol+YV7NpVBsP1PAMuYYjrTQLhlckBSC32H1SUHZRavA== +"@lexical/devtools-core@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/devtools-core/-/devtools-core-0.16.0.tgz#326c8e2995ce6e6e9e1fc4654ee2affbecdbd46d" + integrity sha512-Jt8p0J0UoMHf3UMh3VdyrXbLLwpEZuMqihTmbPRpwo+YQ6NGQU35QgwY2K0DpPAThpxL/Cm7uaFqGOy8Kjrhqw== + dependencies: + "@lexical/html" "0.16.0" + "@lexical/link" "0.16.0" + "@lexical/mark" "0.16.0" + "@lexical/table" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/hashtag@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/hashtag/-/hashtag-0.12.2.tgz" - integrity sha512-2vYzIu5Ldf+eYdUrNA2m80c3N3MF3vJ0fIJzpl5QyX8OdViggEWl1bh+lKtw1Ju0H0CUyDIXdDLZ2apW3WDkTA== +"@lexical/dragon@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/dragon/-/dragon-0.16.0.tgz#de083903701af2bb5264309b565d613c3eec06a0" + integrity sha512-Yr29SFZzOPs+S6UrEZaXnnso1fJGVfZOXVJQZbyzlspqJpSHXVH7InOXYHWN6JSWQ8Hs/vU3ksJXwqz+0TCp2g== dependencies: - "@lexical/utils" "0.12.2" + lexical "0.16.0" -"@lexical/history@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/history/-/history-0.12.2.tgz" - integrity sha512-PM/EDjnUyBPMWh1UiYb7T+FLbvTk14HwUWLXvZxn72S6Kj8ExH/PfLbWZWLCFL8RfzvbP407VwfSN8S0bF5H6g== +"@lexical/hashtag@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/hashtag/-/hashtag-0.16.0.tgz#ea0187060a114678753adaf0a15aad59d4f49a71" + integrity sha512-2EdAvxYVYqb0nv6vgxCRgE8ip7yez5p0y0oeUyxmdbcfZdA+Jl90gYH3VdevmZ5Bk3wE0/fIqiLD+Bb5smqjCQ== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/html@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/html/-/html-0.12.2.tgz" - integrity sha512-LWUO6OKhDtDZa9X1spHAqzsp+4EF01exis4cz5H9y2sHi7EofogXnRCadZ+fa07NVwPVTZWsStkk5qdSe/NEzg== +"@lexical/history@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/history/-/history-0.16.0.tgz#f83f2e331957208c5c8186d98f2f84681d936cec" + integrity sha512-xwFxgDZGviyGEqHmgt6A6gPhsyU/yzlKRk9TBUVByba3khuTknlJ1a80H5jb+OYcrpiElml7iVuGYt+oC7atCA== dependencies: - "@lexical/selection" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/link@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/link/-/link-0.12.2.tgz" - integrity sha512-etOIONa7uyRDmwg8GN52kDlf8thD2Zk1LOFLeocHWz1V8fe3i2unGUek5s/rNPkc6ynpPpNsHdN1VEghOLCCmw== +"@lexical/html@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/html/-/html-0.16.0.tgz#98477ed0dee4c7d910608f4e4de3fbd5eeecdffe" + integrity sha512-okxn3q/1qkUpCZNEFRI39XeJj4YRjb6prm3WqZgP4d39DI1W24feeTZJjYRCW+dc3NInwFaolU3pNA2MGkjRtg== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/selection" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/list@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/list/-/list-0.12.2.tgz" - integrity sha512-3CyWtYQC+IlK4cK/oiD8Uz1gSXD8UcKGOF2vVsDXkMU06O6zvHNmHZOnVJqA0JVNgZAoR9dMR1fi2xd4iuCAiw== +"@lexical/link@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/link/-/link-0.16.0.tgz#f137ab3071206ed3c3a8b8a302ed66b084399ed1" + integrity sha512-ppvJSh/XGqlzbeymOiwcXJcUcrqgQqTK2QXTBAZq7JThtb0WsJxYd2CSLSN+Ycu23prnwqOqILcU0+34+gAVFw== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/mark@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/mark/-/mark-0.12.2.tgz" - integrity sha512-ub+37PDfmThsqAWipRTrwqpgE+83ckqJ5C3mKQUBZvhZfVZW1rEUXZnKjFh2Q3eZK6iT7zVgoVJWJS9ZgEEyag== +"@lexical/list@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/list/-/list-0.16.0.tgz#ed97733633492e89c68ad51a1d455b63ce5aa1c0" + integrity sha512-nBx/DMM7nCgnOzo1JyNnVaIrk/Xi5wIPNi8jixrEV6w9Om2K6dHutn/79Xzp2dQlNGSLHEDjky6N2RyFgmXh0g== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/markdown@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/markdown/-/markdown-0.12.2.tgz" - integrity sha512-F2jTFtBp7Q+yoA11BeUOEcxhROzW+HUhUGdsn20pSLhuxsWRj3oUuryWFeNKFofpzTCVoqU6dwpaMNMI2mL/sQ== +"@lexical/mark@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/mark/-/mark-0.16.0.tgz#e87d92845c8bd231ef47106c5d44e7e10d2a3934" + integrity sha512-WMR4nqygSgIQ6Vdr5WAzohxBGjH+m44dBNTbWTGZGVlRvPzvBT6tieCoxFqpceIq/ko67HGTCNoFj2cMKVwgIA== dependencies: - "@lexical/code" "0.12.2" - "@lexical/link" "0.12.2" - "@lexical/list" "0.12.2" - "@lexical/rich-text" "0.12.2" - "@lexical/text" "0.12.2" - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/offset@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/offset/-/offset-0.12.2.tgz" - integrity sha512-rZLZXfOBmpmM8A2UZsX3cr/CQYw5F/ou67AbaKI0WImb5sjnIgICZqzu9VFUnkKlVNUurEpplV3UG3D1YYh1OQ== +"@lexical/markdown@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/markdown/-/markdown-0.16.0.tgz#fd2d2759d9d5554d9899c3e1fb30a868bfa162a2" + integrity sha512-7HQLFrBbpY68mcq4A6C1qIGmjgA+fAByditi2WRe7tD2eoIKb/B5baQAnDKis0J+m5kTaCBmdlT6csSzyOPzeQ== + dependencies: + "@lexical/code" "0.16.0" + "@lexical/link" "0.16.0" + "@lexical/list" "0.16.0" + "@lexical/rich-text" "0.16.0" + "@lexical/text" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" + +"@lexical/offset@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/offset/-/offset-0.16.0.tgz#bb3bc695ed403db0795f095330c68cdc5cbbec4b" + integrity sha512-4TqPEC2qA7sgO8Tm65nOWnhJ8dkl22oeuGv9sUB+nhaiRZnw3R45mDelg23r56CWE8itZnvueE7TKvV+F3OXtQ== + dependencies: + lexical "0.16.0" -"@lexical/overflow@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/overflow/-/overflow-0.12.2.tgz" - integrity sha512-UgE5j3ukO6qRFRpH4T7m/DvnodE9nCtImD7QinyGdsTa0hi5xlRnl0FUo605vH+vz7xEsUNAGwQXYPX9Sc/vig== +"@lexical/overflow@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/overflow/-/overflow-0.16.0.tgz#31b791f7f7005ea4b160f3ae8083a2b3de05cfdc" + integrity sha512-a7gtIRxleEuMN9dj2yO4CdezBBfIr9Mq+m7G5z62+xy7VL7cfMfF+xWjy3EmDYDXS4vOQgAXAUgO4oKz2AKGhQ== + dependencies: + lexical "0.16.0" -"@lexical/plain-text@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/plain-text/-/plain-text-0.12.2.tgz" - integrity sha512-Lcg6+ngRnX70//kz34azYhID3bvW66HSHCfu5UPhCXT+vQ/Jkd/InhRKajBwWXpaJxMM1huoi3sjzVDb3luNtw== +"@lexical/plain-text@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/plain-text/-/plain-text-0.16.0.tgz#b903bfb59fb6629ded24194e1bef451df3383393" + integrity sha512-BK7/GSOZUHRJTbNPkpb9a/xN9z+FBCdunTsZhnOY8pQ7IKws3kuMO2Tk1zXfTd882ZNAxFdDKNdLYDSeufrKpw== + dependencies: + "@lexical/clipboard" "0.16.0" + "@lexical/selection" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/react@^0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/react/-/react-0.12.2.tgz" - integrity sha512-ZBUvf5xmhiYWBw8pPrhYmLAEwFWrbF/cd15y76TUKD9l/2zDwwPs6nJQxBzfz3ei65r2/nnavLDV8W3QfvxfUA== - dependencies: - "@lexical/clipboard" "0.12.2" - "@lexical/code" "0.12.2" - "@lexical/dragon" "0.12.2" - "@lexical/hashtag" "0.12.2" - "@lexical/history" "0.12.2" - "@lexical/link" "0.12.2" - "@lexical/list" "0.12.2" - "@lexical/mark" "0.12.2" - "@lexical/markdown" "0.12.2" - "@lexical/overflow" "0.12.2" - "@lexical/plain-text" "0.12.2" - "@lexical/rich-text" "0.12.2" - "@lexical/selection" "0.12.2" - "@lexical/table" "0.12.2" - "@lexical/text" "0.12.2" - "@lexical/utils" "0.12.2" - "@lexical/yjs" "0.12.2" +"@lexical/react@^0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/react/-/react-0.16.0.tgz#0bd3ae63ceb5ad8b77e8c0e8ba7df1a0369462f0" + integrity sha512-WKFQbI0/m1YkLjL5t90YLJwjGcl5QRe6mkfm3ljQuL7Ioj3F92ZN/J2gHFVJ9iC8/lJs6Zzw6oFjiP8hQxJf9Q== + dependencies: + "@lexical/clipboard" "0.16.0" + "@lexical/code" "0.16.0" + "@lexical/devtools-core" "0.16.0" + "@lexical/dragon" "0.16.0" + "@lexical/hashtag" "0.16.0" + "@lexical/history" "0.16.0" + "@lexical/link" "0.16.0" + "@lexical/list" "0.16.0" + "@lexical/mark" "0.16.0" + "@lexical/markdown" "0.16.0" + "@lexical/overflow" "0.16.0" + "@lexical/plain-text" "0.16.0" + "@lexical/rich-text" "0.16.0" + "@lexical/selection" "0.16.0" + "@lexical/table" "0.16.0" + "@lexical/text" "0.16.0" + "@lexical/utils" "0.16.0" + "@lexical/yjs" "0.16.0" + lexical "0.16.0" react-error-boundary "^3.1.4" -"@lexical/rich-text@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/rich-text/-/rich-text-0.12.2.tgz" - integrity sha512-igsEuv7CwBOAj5c8jeE41cnx6zkhI/Bkbu4W7shT6S6lNA/3cnyZpAMlgixwyK5RoqjGRCT+IJK5l6yBxQfNkw== +"@lexical/rich-text@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/rich-text/-/rich-text-0.16.0.tgz#5b9ea6ceb1ea034fa7adf1770bd7fa6af1571d1d" + integrity sha512-AGTD6yJZ+kj2TNah1r7/6vyufs6fZANeSvv9x5eG+WjV4uyUJYkd1qR8C5gFZHdkyr+bhAcsAXvS039VzAxRrQ== + dependencies: + "@lexical/clipboard" "0.16.0" + "@lexical/selection" "0.16.0" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/selection@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/selection/-/selection-0.12.2.tgz" - integrity sha512-h+g3oOnihHKIyLTyG6uLCEVR/DmUEVdCcZO1iAoGsuW7nwWiWNPWj6oZ3Cw5J1Mk5u62DHnkkVDQsVSZbAwmtg== +"@lexical/selection@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/selection/-/selection-0.16.0.tgz#8e09edb1e555e79c646a0105beab58ac21fc7158" + integrity sha512-trT9gQVJ2j6AwAe7tHJ30SRuxCpV6yR9LFtggxphHsXSvJYnoHC0CXh1TF2jHl8Gd5OsdWseexGLBE4Y0V3gwQ== + dependencies: + lexical "0.16.0" -"@lexical/table@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/table/-/table-0.12.2.tgz" - integrity sha512-tiAmTq6RKHDVER9v589Ajm9/RL+WTF1WschrH6HHVCtil6cfJfTJeJ+MF45+XEzB9fkqy2LfrScAfWxqLjVePA== +"@lexical/table@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/table/-/table-0.16.0.tgz#68592afbb0f9c0d9bf42bebaae626b8129fc470d" + integrity sha512-A66K779kxdr0yH2RwT2itsMnkzyFLFNPXyiWGLobCH8ON4QPuBouZvjbRHBe8Pe64yJ0c1bRDxSbTqUi9Wt3Gg== dependencies: - "@lexical/utils" "0.12.2" + "@lexical/utils" "0.16.0" + lexical "0.16.0" -"@lexical/text@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/text/-/text-0.12.2.tgz" - integrity sha512-HyuIGuQvVi5djJKKBf+jYEBjK+0Eo9cKHf6WS7dlFozuCZvcCQEJkFy2yceWOwIVk+f2kptVQ5uO7aiZHExH2A== +"@lexical/text@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/text/-/text-0.16.0.tgz#fc4789591f8aaa4a33bc1814280bc8725fd036a9" + integrity sha512-9ilaOhuNIIGHKC8g8j3K/mEvJ09af9B6RKbm3GNoRcf/WNHD4dEFWNTEvgo/3zCzAS8EUBI6UINmfQQWlMjdIQ== + dependencies: + lexical "0.16.0" -"@lexical/utils@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/utils/-/utils-0.12.2.tgz" - integrity sha512-xW4y4l2Yd37+qLwkBvBGyzsKCA9wnh1ljphBJeR2vreT193i2gaIwuku2ZKlER14VHw4192qNJF7vUoAEmwurQ== +"@lexical/utils@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/utils/-/utils-0.16.0.tgz#6ad5785c53347aed5b39c980240c09b21c4a7469" + integrity sha512-GWmFEmd7o3GHqJBaEwzuZQbfTNI3Gg8ReGuHMHABgrkhZ8j2NggoRBlxsQLG0f7BewfTMVwbye22yBPq78775w== dependencies: - "@lexical/list" "0.12.2" - "@lexical/selection" "0.12.2" - "@lexical/table" "0.12.2" + "@lexical/list" "0.16.0" + "@lexical/selection" "0.16.0" + "@lexical/table" "0.16.0" + lexical "0.16.0" -"@lexical/yjs@0.12.2": - version "0.12.2" - resolved "https://registry.npmjs.org/@lexical/yjs/-/yjs-0.12.2.tgz" - integrity sha512-OPJhkJD1Mp9W80mfLzASTB3OFWFMzJteUYA+eSyDgiX9zNi1VGxAqmIITTkDvnCMa+qvw4EfhGeGezpjx6Og4A== +"@lexical/yjs@0.16.0": + version "0.16.0" + resolved "https://registry.yarnpkg.com/@lexical/yjs/-/yjs-0.16.0.tgz#e27bec25c12e90f7768b980da08f2d2d9919d25b" + integrity sha512-YIJr87DfAXTwoVHDjR7cci//hr4r/a61Nn95eo2JNwbTqQo65Gp8rwJivqVxNfvKZmRdwHTKgvdEDoBmI/tGog== dependencies: - "@lexical/offset" "0.12.2" + "@lexical/offset" "0.16.0" + lexical "0.16.0" "@mdx-js/loader@^2.3.0": version "2.3.0" @@ -4287,10 +4334,10 @@ levn@^0.4.1: prelude-ls "^1.2.1" type-check "~0.4.0" -lexical@^0.12.2: - version "0.12.2" - resolved "https://registry.npmjs.org/lexical/-/lexical-0.12.2.tgz" - integrity sha512-Kxavd+ETjxtVwG/hvPd6WZfXD44sLOKe9Vlkwxy7lBQ1qZArS+rZfs+u5iXwXe6tX9f2PIM0u3RHsrCEDDE0fw== +lexical@0.16.0, lexical@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/lexical/-/lexical-0.16.0.tgz#0515d4003cbfba5a5e0e3e50f32f65076a6b89e2" + integrity sha512-Skn45Qhriazq4fpAtwnAB11U//GKc4vjzx54xsV3TkDLDvWpbL4Z9TNRwRoN3g7w8AkWnqjeOSODKkrjgfRSrg== lilconfig@2.1.0, lilconfig@^2.0.5, lilconfig@^2.1.0: version "2.1.0"