mirror of
https://github.com/langgenius/dify.git
synced 2026-08-28 21:23:22 +08:00
feat: can drag and drop hitl block
This commit is contained in:
parent
792f28451c
commit
a9e6140dc6
@ -1,7 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { FC } from 'react'
|
import type { FC } from 'react'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import type {
|
import type {
|
||||||
EditorState,
|
EditorState,
|
||||||
} from 'lexical'
|
} from 'lexical'
|
||||||
@ -16,7 +16,8 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
|||||||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
||||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||||
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin'
|
||||||
// import TreeView from './plugins/tree-view'
|
import DraggableBlockPlugin from './plugins/draggable-plugin'
|
||||||
|
import TreeView from './plugins/tree-view'
|
||||||
import Placeholder from './plugins/placeholder'
|
import Placeholder from './plugins/placeholder'
|
||||||
import ComponentPickerBlock from './plugins/component-picker-block'
|
import ComponentPickerBlock from './plugins/component-picker-block'
|
||||||
import {
|
import {
|
||||||
@ -165,9 +166,16 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||||||
} as any)
|
} as any)
|
||||||
}, [eventEmitter, historyBlock?.history])
|
}, [eventEmitter, historyBlock?.history])
|
||||||
|
|
||||||
|
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null)
|
||||||
|
|
||||||
|
const onRef = (_floatingAnchorElem: any) => {
|
||||||
|
if (_floatingAnchorElem !== null)
|
||||||
|
setFloatingAnchorElem(_floatingAnchorElem)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||||
<div className={cn('relative', wrapperClassName)}>
|
<div className={cn('relative', wrapperClassName)} ref={onRef}>
|
||||||
<RichTextPlugin
|
<RichTextPlugin
|
||||||
contentEditable={
|
contentEditable={
|
||||||
<ContentEditable
|
<ContentEditable
|
||||||
@ -271,7 +279,10 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||||||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||||
<UpdateBlock instanceId={instanceId} />
|
<UpdateBlock instanceId={instanceId} />
|
||||||
<HistoryPlugin />
|
<HistoryPlugin />
|
||||||
{/* <TreeView /> */}
|
{floatingAnchorElem && (
|
||||||
|
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||||
|
)}
|
||||||
|
<TreeView />
|
||||||
</div>
|
</div>
|
||||||
</LexicalComposer>
|
</LexicalComposer>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" data-name="Layer 1" viewBox="0 0 24 24" fill="currentColor"><path stroke="currentColor" d="M8.5 10a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm0 7a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm7-10a2 2 0 1 0-2-2 2 2 0 0 0 2 2Zm-7-4a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm7 14a2 2 0 1 0 2 2 2 2 0 0 0-2-2Zm0-7a2 2 0 1 0 2 2 2 2 0 0 0-2-2Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 344 B |
@ -0,0 +1,40 @@
|
|||||||
|
.draggable-block-menu {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px 1px;
|
||||||
|
cursor: grab;
|
||||||
|
opacity: 0;
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
top: 0;
|
||||||
|
will-change: transform;
|
||||||
|
display: flex;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-block-menu .icon {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
opacity: 0.3;
|
||||||
|
background-image: url(./icons/draggable-block-menu.svg);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-block-menu:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-block-menu .icon:hover {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-block-target-line {
|
||||||
|
pointer-events: none;
|
||||||
|
background: deepskyblue;
|
||||||
|
height: 4px;
|
||||||
|
position: absolute;
|
||||||
|
left: -21px;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
opacity: 0;
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
@ -0,0 +1,70 @@
|
|||||||
|
import type { JSX } from 'react'
|
||||||
|
|
||||||
|
import './index.css'
|
||||||
|
|
||||||
|
import { DraggableBlockPlugin_EXPERIMENTAL } from '@lexical/react/LexicalDraggableBlockPlugin'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
|
||||||
|
const DRAGGABLE_BLOCK_MENU_CLASSNAME = 'draggable-block-menu'
|
||||||
|
|
||||||
|
function isOnMenu(element: HTMLElement): boolean {
|
||||||
|
return !!element.closest(`.${DRAGGABLE_BLOCK_MENU_CLASSNAME}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SUPPORT_DRAG_CLASS = 'support-drag'
|
||||||
|
function checkSupportDrag(element: Element | null): boolean {
|
||||||
|
if (!element) return false
|
||||||
|
|
||||||
|
if (element.classList.contains(SUPPORT_DRAG_CLASS)) return true
|
||||||
|
|
||||||
|
if (element.querySelector(`.${SUPPORT_DRAG_CLASS}`)) return true
|
||||||
|
|
||||||
|
return !!(element.closest(`.${SUPPORT_DRAG_CLASS}`))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function DraggableBlockPlugin({
|
||||||
|
anchorElem = document.body,
|
||||||
|
}: {
|
||||||
|
anchorElem?: HTMLElement;
|
||||||
|
}): JSX.Element {
|
||||||
|
const menuRef = useRef<HTMLDivElement>(null)
|
||||||
|
const targetLineRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [draggableElement, setDraggableElement] = useState<HTMLElement | null>(
|
||||||
|
null,
|
||||||
|
)
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
|
|
||||||
|
const [isSupportDrag, setIsSupportDrag] = useState(false)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const root = editor.getRootElement()
|
||||||
|
if (!root) return
|
||||||
|
|
||||||
|
const onMove = (e: MouseEvent) => {
|
||||||
|
const isSupportDrag = checkSupportDrag(e.target as Element)
|
||||||
|
setIsSupportDrag(isSupportDrag)
|
||||||
|
}
|
||||||
|
|
||||||
|
root.addEventListener('mousemove', onMove)
|
||||||
|
return () => root.removeEventListener('mousemove', onMove)
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DraggableBlockPlugin_EXPERIMENTAL
|
||||||
|
anchorElem={anchorElem}
|
||||||
|
menuRef={menuRef as any}
|
||||||
|
targetLineRef={targetLineRef as any}
|
||||||
|
menuComponent={
|
||||||
|
isSupportDrag ? <div ref={menuRef} className="icon draggable-block-menu">
|
||||||
|
<div className="icon" />
|
||||||
|
</div> : null
|
||||||
|
}
|
||||||
|
targetLineComponent={
|
||||||
|
<div ref={targetLineRef} className="draggable-block-target-line" />
|
||||||
|
}
|
||||||
|
isOnMenu={isOnMenu}
|
||||||
|
onElementChanged={setDraggableElement}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'
|
|||||||
import { useSelectOrDelete } from '../../hooks'
|
import { useSelectOrDelete } from '../../hooks'
|
||||||
import { DELETE_HITL_INPUT_BLOCK_COMMAND } from './'
|
import { DELETE_HITL_INPUT_BLOCK_COMMAND } from './'
|
||||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||||
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||||
|
|
||||||
type QueryBlockComponentProps = {
|
type QueryBlockComponentProps = {
|
||||||
nodeKey: string
|
nodeKey: string
|
||||||
@ -17,13 +18,49 @@ const HITLInputComponent: FC<QueryBlockComponentProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND)
|
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND)
|
||||||
|
const [editor] = useLexicalComposerContext()
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`
|
className={`
|
||||||
flex h-6 w-full items-center rounded-[5px] border-[1.5px] border-components-input-border-active bg-background-default-hover pl-1 pr-0.5 hover:bg-[#FFEAD5]
|
flex h-6 w-full items-center rounded-[5px] border-[1.5px] border-components-input-border-active bg-background-default-hover pl-1 pr-0.5 hover:bg-[#FFEAD5]
|
||||||
${isSelected && '!border-[#FD853A]'}
|
${isSelected && '!border-[#FD853A]'}
|
||||||
`}
|
`}
|
||||||
|
// draggable
|
||||||
|
// onDragStart={(e) => {
|
||||||
|
// e.dataTransfer.setData('application/x-lexical-drag', nodeKey)
|
||||||
|
// e.dataTransfer.effectAllowed = 'move'
|
||||||
|
// console.log(`dragging node with key: ${nodeKey}`)
|
||||||
|
// }}
|
||||||
|
// onDragOver={(e) => {
|
||||||
|
// e.preventDefault()
|
||||||
|
// e.dataTransfer.dropEffect = 'move'
|
||||||
|
// }}
|
||||||
|
// onDragEnter={(e) => {
|
||||||
|
// e.preventDefault()
|
||||||
|
// e.currentTarget.classList.add('bg-[#FFEAD5]')
|
||||||
|
// }}
|
||||||
|
// onDragLeave={(e) => {
|
||||||
|
// e.currentTarget.classList.remove('bg-[#FFEAD5]')
|
||||||
|
// }}
|
||||||
|
// onDrop={(e) => {
|
||||||
|
// e.preventDefault()
|
||||||
|
// e.currentTarget.classList.remove('bg-[#FFEAD5]')
|
||||||
|
|
||||||
|
// const draggedNodeKey = e.dataTransfer.getData('application/x-lexical-drag')
|
||||||
|
// console.log('Drop event triggered with key:', draggedNodeKey)
|
||||||
|
|
||||||
|
// if (draggedNodeKey) {
|
||||||
|
// editor.update(() => {
|
||||||
|
// const draggedNode = $getNodeByKey(draggedNodeKey)
|
||||||
|
// const dropTarget = $getNodeByKey(nodeKey)
|
||||||
|
|
||||||
|
// if (draggedNode && dropTarget && draggedNode !== dropTarget) {
|
||||||
|
// console.log('Moving node in editor')
|
||||||
|
// dropTarget.insertAfter(draggedNode)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
>
|
>
|
||||||
<UserEdit02 className='mr-1 h-[14px] w-[14px] text-[#FD853A]' />
|
<UserEdit02 className='mr-1 h-[14px] w-[14px] text-[#FD853A]' />
|
||||||
|
|||||||
@ -9,6 +9,14 @@ export type SerializedNode = SerializedLexicalNode & {
|
|||||||
export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
|
export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
|
||||||
__variableName: string
|
__variableName: string
|
||||||
|
|
||||||
|
isIsolated(): boolean {
|
||||||
|
return true // This is necessary for drag-and-drop to work correctly
|
||||||
|
}
|
||||||
|
|
||||||
|
isTopLevel(): boolean {
|
||||||
|
return true // This is necessary for drag-and-drop to work correctly
|
||||||
|
}
|
||||||
|
|
||||||
static getType(): string {
|
static getType(): string {
|
||||||
return 'hitl-input-block'
|
return 'hitl-input-block'
|
||||||
}
|
}
|
||||||
@ -34,7 +42,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
|
|||||||
|
|
||||||
createDOM(): HTMLElement {
|
createDOM(): HTMLElement {
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
div.classList.add('flex', 'items-center', 'align-middle')
|
div.classList.add('flex', 'items-center', 'align-middle', 'support-drag')
|
||||||
return div
|
return div
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user