mirror of https://github.com/langgenius/dify.git
feat: can drag and drop hitl block
This commit is contained in:
parent
792f28451c
commit
a9e6140dc6
|
|
@ -1,7 +1,7 @@
|
|||
'use client'
|
||||
|
||||
import type { FC } from 'react'
|
||||
import React, { useEffect } from 'react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import type {
|
||||
EditorState,
|
||||
} from 'lexical'
|
||||
|
|
@ -16,7 +16,8 @@ import { ContentEditable } from '@lexical/react/LexicalContentEditable'
|
|||
import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary'
|
||||
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
|
||||
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 ComponentPickerBlock from './plugins/component-picker-block'
|
||||
import {
|
||||
|
|
@ -165,9 +166,16 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||
} as any)
|
||||
}, [eventEmitter, historyBlock?.history])
|
||||
|
||||
const [floatingAnchorElem, setFloatingAnchorElem] = useState(null)
|
||||
|
||||
const onRef = (_floatingAnchorElem: any) => {
|
||||
if (_floatingAnchorElem !== null)
|
||||
setFloatingAnchorElem(_floatingAnchorElem)
|
||||
}
|
||||
|
||||
return (
|
||||
<LexicalComposer initialConfig={{ ...initialConfig, editable }}>
|
||||
<div className={cn('relative', wrapperClassName)}>
|
||||
<div className={cn('relative', wrapperClassName)} ref={onRef}>
|
||||
<RichTextPlugin
|
||||
contentEditable={
|
||||
<ContentEditable
|
||||
|
|
@ -271,7 +279,10 @@ const PromptEditor: FC<PromptEditorProps> = ({
|
|||
<OnBlurBlock onBlur={onBlur} onFocus={onFocus} />
|
||||
<UpdateBlock instanceId={instanceId} />
|
||||
<HistoryPlugin />
|
||||
{/* <TreeView /> */}
|
||||
{floatingAnchorElem && (
|
||||
<DraggableBlockPlugin anchorElem={floatingAnchorElem} />
|
||||
)}
|
||||
<TreeView />
|
||||
</div>
|
||||
</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 { DELETE_HITL_INPUT_BLOCK_COMMAND } from './'
|
||||
import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
|
||||
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
|
||||
|
||||
type QueryBlockComponentProps = {
|
||||
nodeKey: string
|
||||
|
|
@ -17,13 +18,49 @@ const HITLInputComponent: FC<QueryBlockComponentProps> = ({
|
|||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HITL_INPUT_BLOCK_COMMAND)
|
||||
|
||||
const [editor] = useLexicalComposerContext()
|
||||
return (
|
||||
<div
|
||||
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]
|
||||
${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}
|
||||
>
|
||||
<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> {
|
||||
__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 {
|
||||
return 'hitl-input-block'
|
||||
}
|
||||
|
|
@ -34,7 +42,7 @@ export class HITLInputNode extends DecoratorNode<React.JSX.Element> {
|
|||
|
||||
createDOM(): HTMLElement {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue