feat: workflow interaction (#4214)
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Icon_2" d="M2.66699 4.66667H9.33366C11.5428 4.66667 13.3337 6.45753 13.3337 8.66667C13.3337 10.8758 11.5428 12.6667 9.33366 12.6667H2.66699M2.66699 4.66667L5.33366 2M2.66699 4.66667L5.33366 7.33333" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 416 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M2.4598 3.3093L6.05377 13.551C6.25503 14.1246 7.05599 14.1516 7.29552 13.593L9.08053 9.43022C9.14793 9.27295 9.27326 9.14762 9.43053 9.08022L13.5933 7.29522C14.1519 7.05569 14.1249 6.25472 13.5513 6.05346L3.30961 2.45949C2.78207 2.27437 2.27468 2.78176 2.4598 3.3093Z" stroke="#667085" stroke-width="1.5" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 474 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M11.3344 5C11.3344 4.44771 11.7821 4 12.3344 4C12.8867 4 13.3344 4.44771 13.3344 5V9.21947C13.3344 11.8597 11.1941 14 8.55387 14C6.779 14 5.15019 13.0167 4.32353 11.446L2.53767 8.05287C2.41421 7.81827 2.44145 7.53287 2.60703 7.32587L2.83481 7.04113C3.29483 6.46614 4.13389 6.37291 4.7089 6.83293L5.33441 7.33333V3.66667C5.33441 3.11438 5.78213 2.66667 6.33441 2.66667C6.88667 2.66667 7.3344 3.11438 7.3344 3.66667M11.3344 5V3.66667C11.3344 3.11438 10.8867 2.66667 10.3344 2.66667C9.78213 2.66667 9.3344 3.11438 9.3344 3.66667M11.3344 5V8M7.3344 3.66667V3C7.3344 2.44771 7.78213 2 8.3344 2C8.88667 2 9.3344 2.44771 9.3344 3V3.66667M7.3344 3.66667V7.33333M9.3344 3.66667V7.66667" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 906 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Icon_2" d="M14 14L11.1 11.1M7.33333 5.33333V9.33333M5.33333 7.33333H9.33333M12.6667 7.33333C12.6667 10.2789 10.2789 12.6667 7.33333 12.6667C4.38781 12.6667 2 10.2789 2 7.33333C2 4.38781 4.38781 2 7.33333 2C10.2789 2 12.6667 4.38781 12.6667 7.33333Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 467 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Icon_2" d="M14 14L11.1 11.1M5.33333 7.33333H9.33333M12.6667 7.33333C12.6667 10.2789 10.2789 12.6667 7.33333 12.6667C4.38781 12.6667 2 10.2789 2 7.33333C2 4.38781 4.38781 2 7.33333 2C10.2789 2 12.6667 4.38781 12.6667 7.33333Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 443 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" d="M3.53647 1.81277C2.46674 1.43738 1.43787 2.46625 1.81326 3.53598L5.40722 13.7777C5.81532 14.9407 7.43953 14.9956 7.92526 13.8628L9.70733 9.70683L13.8633 7.92476C14.9961 7.4391 14.9412 5.81484 13.7782 5.40674L3.53647 1.81277Z" fill="#155EEF"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 386 B |
@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Icon">
|
||||
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M8.04519 1.33331C7.62792 1.33331 7.28966 1.66963 7.28966 2.08449V6.59153C7.28966 6.79898 7.12053 6.96711 6.91193 6.96711C6.70333 6.96711 6.53417 6.79898 6.53417 6.59153V2.83566C6.53417 2.4208 6.19593 2.08449 5.77868 2.08449C5.36143 2.08449 5.02318 2.4208 5.02318 2.83566V7.43091C5.02318 7.58418 4.92957 7.72205 4.78663 7.77931C4.6437 7.83658 4.4801 7.80178 4.37325 7.69138L3.47554 6.76385C2.95809 6.22921 2.07117 6.32919 1.68723 6.96545L1.66699 6.99898L3.52969 11.5222C4.31291 13.4242 6.17482 14.6666 8.24186 14.6666C11.054 14.6666 13.3337 12.4 13.3337 9.60398V4.33801C13.3337 3.92315 12.9954 3.58683 12.5782 3.58683C12.1609 3.58683 11.8227 3.92315 11.8227 4.33801V7.34271C11.8227 7.55011 11.6535 7.71831 11.4449 7.71831C11.2363 7.71831 11.0672 7.55011 11.0672 7.34271V2.83566C11.0672 2.4208 10.7289 2.08449 10.3117 2.08449C9.89439 2.08449 9.55619 2.4208 9.55619 2.83566V6.96711C9.55619 7.17458 9.38706 7.34271 9.17839 7.34271C8.96979 7.34271 8.80066 7.17458 8.80066 6.96711V2.08449C8.80066 1.66963 8.46239 1.33331 8.04519 1.33331Z" fill="#155EEF"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.2 KiB |
@ -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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_2",
|
||||
"d": "M2.66699 4.66667H9.33366C11.5428 4.66667 13.3337 6.45753 13.3337 8.66667C13.3337 10.8758 11.5428 12.6667 9.33366 12.6667H2.66699M2.66699 4.66667L5.33366 2M2.66699 4.66667L5.33366 7.33333",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ReverseLeft"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ReverseLeft.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ReverseLeft'
|
||||
|
||||
export default Icon
|
@ -9,3 +9,4 @@ export { default as Collapse04 } from './Collapse04'
|
||||
export { default as FlipBackward } from './FlipBackward'
|
||||
export { default as RefreshCcw01 } from './RefreshCcw01'
|
||||
export { default as RefreshCw05 } from './RefreshCw05'
|
||||
export { default as ReverseLeft } from './ReverseLeft'
|
||||
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M2.4598 3.3093L6.05377 13.551C6.25503 14.1246 7.05599 14.1516 7.29552 13.593L9.08053 9.43022C9.14793 9.27295 9.27326 9.14762 9.43053 9.08022L13.5933 7.29522C14.1519 7.05569 14.1249 6.25472 13.5513 6.05346L3.30961 2.45949C2.78207 2.27437 2.27468 2.78176 2.4598 3.3093Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Cursor02C"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Cursor02C.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Cursor02C'
|
||||
|
||||
export default Icon
|
@ -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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M11.3344 5C11.3344 4.44771 11.7821 4 12.3344 4C12.8867 4 13.3344 4.44771 13.3344 5V9.21947C13.3344 11.8597 11.1941 14 8.55387 14C6.779 14 5.15019 13.0167 4.32353 11.446L2.53767 8.05287C2.41421 7.81827 2.44145 7.53287 2.60703 7.32587L2.83481 7.04113C3.29483 6.46614 4.13389 6.37291 4.7089 6.83293L5.33441 7.33333V3.66667C5.33441 3.11438 5.78213 2.66667 6.33441 2.66667C6.88667 2.66667 7.3344 3.11438 7.3344 3.66667M11.3344 5V3.66667C11.3344 3.11438 10.8867 2.66667 10.3344 2.66667C9.78213 2.66667 9.3344 3.11438 9.3344 3.66667M11.3344 5V8M7.3344 3.66667V3C7.3344 2.44771 7.78213 2 8.3344 2C8.88667 2 9.3344 2.44771 9.3344 3V3.66667M7.3344 3.66667V7.33333M9.3344 3.66667V7.66667",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Hand02"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Hand02.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Hand02'
|
||||
|
||||
export default Icon
|
@ -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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_2",
|
||||
"d": "M14 14L11.1 11.1M7.33333 5.33333V9.33333M5.33333 7.33333H9.33333M12.6667 7.33333C12.6667 10.2789 10.2789 12.6667 7.33333 12.6667C4.38781 12.6667 2 10.2789 2 7.33333C2 4.38781 4.38781 2 7.33333 2C10.2789 2 12.6667 4.38781 12.6667 7.33333Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ZoomIn"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ZoomIn.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ZoomIn'
|
||||
|
||||
export default Icon
|
@ -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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Icon_2",
|
||||
"d": "M14 14L11.1 11.1M5.33333 7.33333H9.33333M12.6667 7.33333C12.6667 10.2789 10.2789 12.6667 7.33333 12.6667C4.38781 12.6667 2 10.2789 2 7.33333C2 4.38781 4.38781 2 7.33333 2C10.2789 2 12.6667 4.38781 12.6667 7.33333Z",
|
||||
"stroke": "currentColor",
|
||||
"stroke-width": "1.5",
|
||||
"stroke-linecap": "round",
|
||||
"stroke-linejoin": "round"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "ZoomOut"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './ZoomOut.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'ZoomOut'
|
||||
|
||||
export default Icon
|
@ -1,7 +1,11 @@
|
||||
export { default as AlignLeft } from './AlignLeft'
|
||||
export { default as BezierCurve03 } from './BezierCurve03'
|
||||
export { default as Colors } from './Colors'
|
||||
export { default as Cursor02C } from './Cursor02C'
|
||||
export { default as Hand02 } from './Hand02'
|
||||
export { default as ImageIndentLeft } from './ImageIndentLeft'
|
||||
export { default as LeftIndent02 } from './LeftIndent02'
|
||||
export { default as LetterSpacing01 } from './LetterSpacing01'
|
||||
export { default as TypeSquare } from './TypeSquare'
|
||||
export { default as ZoomIn } from './ZoomIn'
|
||||
export { default as ZoomOut } from './ZoomOut'
|
||||
|
@ -0,0 +1,36 @@
|
||||
{
|
||||
"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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"d": "M3.53647 1.81277C2.46674 1.43738 1.43787 2.46625 1.81326 3.53598L5.40722 13.7777C5.81532 14.9407 7.43953 14.9956 7.92526 13.8628L9.70733 9.70683L13.8633 7.92476C14.9961 7.4391 14.9412 5.81484 13.7782 5.40674L3.53647 1.81277Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Cursor02C"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Cursor02C.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Cursor02C'
|
||||
|
||||
export default Icon
|
@ -0,0 +1,38 @@
|
||||
{
|
||||
"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": "Icon"
|
||||
},
|
||||
"children": [
|
||||
{
|
||||
"type": "element",
|
||||
"name": "path",
|
||||
"attributes": {
|
||||
"id": "Vector",
|
||||
"fill-rule": "evenodd",
|
||||
"clip-rule": "evenodd",
|
||||
"d": "M8.04519 1.33331C7.62792 1.33331 7.28966 1.66963 7.28966 2.08449V6.59153C7.28966 6.79898 7.12053 6.96711 6.91193 6.96711C6.70333 6.96711 6.53417 6.79898 6.53417 6.59153V2.83566C6.53417 2.4208 6.19593 2.08449 5.77868 2.08449C5.36143 2.08449 5.02318 2.4208 5.02318 2.83566V7.43091C5.02318 7.58418 4.92957 7.72205 4.78663 7.77931C4.6437 7.83658 4.4801 7.80178 4.37325 7.69138L3.47554 6.76385C2.95809 6.22921 2.07117 6.32919 1.68723 6.96545L1.66699 6.99898L3.52969 11.5222C4.31291 13.4242 6.17482 14.6666 8.24186 14.6666C11.054 14.6666 13.3337 12.4 13.3337 9.60398V4.33801C13.3337 3.92315 12.9954 3.58683 12.5782 3.58683C12.1609 3.58683 11.8227 3.92315 11.8227 4.33801V7.34271C11.8227 7.55011 11.6535 7.71831 11.4449 7.71831C11.2363 7.71831 11.0672 7.55011 11.0672 7.34271V2.83566C11.0672 2.4208 10.7289 2.08449 10.3117 2.08449C9.89439 2.08449 9.55619 2.4208 9.55619 2.83566V6.96711C9.55619 7.17458 9.38706 7.34271 9.17839 7.34271C8.96979 7.34271 8.80066 7.17458 8.80066 6.96711V2.08449C8.80066 1.66963 8.46239 1.33331 8.04519 1.33331Z",
|
||||
"fill": "currentColor"
|
||||
},
|
||||
"children": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"name": "Hand02"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
// GENERATE BY script
|
||||
// DON NOT EDIT IT MANUALLY
|
||||
|
||||
import * as React from 'react'
|
||||
import data from './Hand02.json'
|
||||
import IconBase from '@/app/components/base/icons/IconBase'
|
||||
import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
|
||||
|
||||
const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
|
||||
props,
|
||||
ref,
|
||||
) => <IconBase {...props} ref={ref} data={data as IconData} />)
|
||||
|
||||
Icon.displayName = 'Hand02'
|
||||
|
||||
export default Icon
|
@ -1,5 +1,7 @@
|
||||
export { default as Brush01 } from './Brush01'
|
||||
export { default as Citations } from './Citations'
|
||||
export { default as Colors } from './Colors'
|
||||
export { default as Cursor02C } from './Cursor02C'
|
||||
export { default as Hand02 } from './Hand02'
|
||||
export { default as Paragraph } from './Paragraph'
|
||||
export { default as TypeSquare } from './TypeSquare'
|
||||
|
@ -1,13 +1,17 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import cn from 'classnames'
|
||||
import type { OffsetOptions, Placement } from '@floating-ui/react'
|
||||
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
|
||||
export type TooltipProps = {
|
||||
position?: 'top' | 'right' | 'bottom' | 'left'
|
||||
position?: Placement
|
||||
triggerMethod?: 'hover' | 'click'
|
||||
popupContent: React.ReactNode
|
||||
children: React.ReactNode
|
||||
hideArrow?: boolean
|
||||
popupClassName?: string
|
||||
offset?: OffsetOptions
|
||||
}
|
||||
|
||||
const arrow = (
|
||||
@ -20,6 +24,8 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
popupContent,
|
||||
children,
|
||||
hideArrow,
|
||||
popupClassName,
|
||||
offset,
|
||||
}) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
@ -28,7 +34,7 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
placement={position}
|
||||
offset={10}
|
||||
offset={offset ?? 10}
|
||||
>
|
||||
<PortalToFollowElemTrigger
|
||||
onClick={() => triggerMethod === 'click' && setOpen(v => !v)}
|
||||
@ -40,7 +46,10 @@ const Tooltip: FC<TooltipProps> = ({
|
||||
<PortalToFollowElemContent
|
||||
className="z-[9999]"
|
||||
>
|
||||
<div className='relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg'>
|
||||
<div className={cn(
|
||||
'relative px-3 py-2 text-xs font-normal text-gray-700 bg-white rounded-md shadow-lg',
|
||||
popupClassName,
|
||||
)}>
|
||||
{popupContent}
|
||||
{!hideArrow && arrow}
|
||||
</div>
|
||||
|
@ -72,15 +72,13 @@ const Blocks = ({
|
||||
className='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg'
|
||||
htmlContent={(
|
||||
<div>
|
||||
<div className='flex items-center mb-2'>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mr-2'
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='text-sm text-gray-900'>{block.title}</div>
|
||||
</div>
|
||||
{nodesExtraData[block.type].about}
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-gray-900'>{block.title}</div>
|
||||
<div className='text-xs text-gray-700 leading-[18px]'>{nodesExtraData[block.type].about}</div>
|
||||
</div>
|
||||
)}
|
||||
noArrow
|
||||
@ -91,7 +89,7 @@ const Blocks = ({
|
||||
onClick={() => onSelect(block.type)}
|
||||
>
|
||||
<BlockIcon
|
||||
className='mr-2'
|
||||
className='mr-2 shrink-0'
|
||||
type={block.type}
|
||||
/>
|
||||
<div className='text-sm text-gray-900'>{block.title}</div>
|
||||
|
@ -57,16 +57,14 @@ const Blocks = ({
|
||||
className='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !bg-transparent !rounded-xl !shadow-lg'
|
||||
htmlContent={(
|
||||
<div>
|
||||
<div className='flex items-center mb-2'>
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mr-2'
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={toolWithProvider.icon}
|
||||
/>
|
||||
<div className='text-sm text-gray-900'>{tool.label[language]}</div>
|
||||
</div>
|
||||
{tool.description[language]}
|
||||
<BlockIcon
|
||||
size='md'
|
||||
className='mb-2'
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={toolWithProvider.icon}
|
||||
/>
|
||||
<div className='mb-1 text-sm leading-5 text-gray-900'>{tool.label[language]}</div>
|
||||
<div className='text-xs text-gray-700 leading-[18px]'>{tool.description[language]}</div>
|
||||
</div>
|
||||
)}
|
||||
noArrow
|
||||
@ -83,11 +81,11 @@ const Blocks = ({
|
||||
})}
|
||||
>
|
||||
<BlockIcon
|
||||
className='mr-2'
|
||||
className='mr-2 shrink-0'
|
||||
type={BlockEnum.Tool}
|
||||
toolIcon={toolWithProvider.icon}
|
||||
/>
|
||||
<div className='text-sm text-gray-900'>{tool.label[language]}</div>
|
||||
<div className='text-sm text-gray-900 truncate'>{tool.label[language]}</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
))
|
||||
@ -97,7 +95,7 @@ const Blocks = ({
|
||||
}, [onSelect, language])
|
||||
|
||||
return (
|
||||
<div className='p-1 max-h-[464px] overflow-y-auto'>
|
||||
<div className='p-1 max-w-[320px] max-h-[464px] overflow-y-auto'>
|
||||
{
|
||||
!tools.length && (
|
||||
<div className='flex items-center px-3 h-[22px] text-xs font-medium text-gray-500'>{t('workflow.tabs.noResult')}</div>
|
||||
|
81
web/app/components/workflow/candidate-node.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import {
|
||||
memo,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import {
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
useViewport,
|
||||
} from 'reactflow'
|
||||
import { useEventListener } from 'ahooks'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from './store'
|
||||
import CustomNode from './nodes'
|
||||
|
||||
const CandidateNode = () => {
|
||||
const store = useStoreApi()
|
||||
const reactflow = useReactFlow()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const candidateNode = useStore(s => s.candidateNode)
|
||||
const mousePosition = useStore(s => s.mousePosition)
|
||||
const { zoom } = useViewport()
|
||||
|
||||
useEventListener('click', (e) => {
|
||||
const { candidateNode, mousePosition } = workflowStore.getState()
|
||||
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const nodes = getNodes()
|
||||
const { x, y } = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.push({
|
||||
...candidateNode,
|
||||
data: {
|
||||
...candidateNode.data,
|
||||
_isCandidate: false,
|
||||
},
|
||||
position: {
|
||||
x,
|
||||
y,
|
||||
},
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
}
|
||||
})
|
||||
|
||||
useEventListener('contextmenu', (e) => {
|
||||
const { candidateNode } = workflowStore.getState()
|
||||
if (candidateNode) {
|
||||
e.preventDefault()
|
||||
workflowStore.setState({ candidateNode: undefined })
|
||||
}
|
||||
})
|
||||
|
||||
if (!candidateNode)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-10'
|
||||
style={{
|
||||
left: mousePosition.elementX,
|
||||
top: mousePosition.elementY,
|
||||
transform: `scale(${zoom})`,
|
||||
transformOrigin: '0 0',
|
||||
}}
|
||||
>
|
||||
<CustomNode {...candidateNode as any} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(CandidateNode)
|
@ -1,90 +1,29 @@
|
||||
import type { FC } from 'react'
|
||||
import { memo, useCallback } from 'react'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from '../store'
|
||||
import { useStore } from '../store'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
useWorkflowStartRun,
|
||||
} from '../hooks'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import { WorkflowRunningStatus } from '../types'
|
||||
import ViewHistory from './view-history'
|
||||
import {
|
||||
Play,
|
||||
StopCircle,
|
||||
} from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { Loading02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
import { MessagePlay } from '@/app/components/base/icons/src/vender/line/communication'
|
||||
|
||||
const RunMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const {
|
||||
handleStopRun,
|
||||
handleRun,
|
||||
} = useWorkflowRun()
|
||||
const {
|
||||
doSyncWorkflowDraft,
|
||||
} = useNodesSyncDraft()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { handleWorkflowStartRunInWorkflow } = useWorkflowStartRun()
|
||||
const { handleStopRun } = useWorkflowRun()
|
||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||
const isRunning = workflowRunningData?.result.status === WorkflowRunningStatus.Running
|
||||
|
||||
const handleClick = useCallback(async () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
|
||||
return
|
||||
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel) {
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
return
|
||||
}
|
||||
|
||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||
await doSyncWorkflowDraft()
|
||||
handleRun({ inputs: {}, files: [] })
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(false)
|
||||
}
|
||||
else {
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(true)
|
||||
}
|
||||
}, [
|
||||
workflowStore,
|
||||
handleRun,
|
||||
doSyncWorkflowDraft,
|
||||
store,
|
||||
featuresStore,
|
||||
handleCancelDebugAndPreviewPanel,
|
||||
])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -93,7 +32,7 @@ const RunMode = memo(() => {
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
isRunning && 'bg-primary-50 !cursor-not-allowed',
|
||||
)}
|
||||
onClick={handleClick}
|
||||
onClick={() => handleWorkflowStartRunInWorkflow()}
|
||||
>
|
||||
{
|
||||
isRunning
|
||||
@ -128,23 +67,7 @@ RunMode.displayName = 'RunMode'
|
||||
|
||||
const PreviewMode = memo(() => {
|
||||
const { t } = useTranslation()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
|
||||
const handleClick = () => {
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
|
||||
setHistoryWorkflowData(undefined)
|
||||
}
|
||||
const { handleWorkflowStartRunInChatflow } = useWorkflowStartRun()
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -152,7 +75,7 @@ const PreviewMode = memo(() => {
|
||||
'flex items-center px-1.5 h-7 rounded-md text-[13px] font-medium text-primary-600',
|
||||
'hover:bg-primary-50 cursor-pointer',
|
||||
)}
|
||||
onClick={() => handleClick()}
|
||||
onClick={() => handleWorkflowStartRunInChatflow()}
|
||||
>
|
||||
<MessagePlay className='mr-1 w-4 h-4' />
|
||||
{t('workflow.common.debugAndPreview')}
|
||||
|
@ -9,3 +9,6 @@ export * from './use-workflow-template'
|
||||
export * from './use-checklist'
|
||||
export * from './use-workflow-mode'
|
||||
export * from './use-workflow-interactions'
|
||||
export * from './use-selection-interactions'
|
||||
export * from './use-panel-interactions'
|
||||
export * from './use-workflow-start-run'
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback, useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import produce from 'immer'
|
||||
@ -11,6 +12,7 @@ import type {
|
||||
import {
|
||||
getConnectedEdges,
|
||||
getOutgoers,
|
||||
useReactFlow,
|
||||
useStoreApi,
|
||||
} from 'reactflow'
|
||||
import type { ToolDefaultValue } from '../block-selector/types'
|
||||
@ -29,6 +31,7 @@ import {
|
||||
import {
|
||||
generateNewNode,
|
||||
getNodesConnectedSourceOrTargetHandleIdsMap,
|
||||
getTopLeftNodePosition,
|
||||
} from '../utils'
|
||||
import { useNodesExtraData } from './use-nodes-data'
|
||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||
@ -41,6 +44,7 @@ export const useNodesInteractions = () => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const reactflow = useReactFlow()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const {
|
||||
@ -705,132 +709,6 @@ export const useNodesInteractions = () => {
|
||||
handleSyncWorkflowDraft()
|
||||
}, [store, handleSyncWorkflowDraft, getNodesReadOnly, t])
|
||||
|
||||
const handleNodeCopySelected = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToCopy = nodes.filter(node => node.data.selected && node.data.type !== BlockEnum.Start)
|
||||
|
||||
setClipboardElements(nodesToCopy)
|
||||
|
||||
return nodesToCopy
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodePaste = useCallback((): undefined | Node[] => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
for (const nodeToPaste of clipboardElements) {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
selected: true,
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + 10,
|
||||
y: nodeToPaste.position.y + 10,
|
||||
},
|
||||
})
|
||||
nodesToPaste.push(newNode)
|
||||
}
|
||||
|
||||
setNodes([...nodes.map((n: Node) => ({ ...n, selected: false, data: { ...n.data, selected: false } })), ...nodesToPaste])
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
|
||||
return nodesToPaste
|
||||
}, [getNodesReadOnly, handleSyncWorkflowDraft, store, t, workflowStore])
|
||||
|
||||
const handleNodeDuplicateSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
handleNodeCopySelected()
|
||||
handleNodePaste()
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodePaste])
|
||||
|
||||
const handleNodeCut = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const nodesToCut = handleNodeCopySelected()
|
||||
if (!nodesToCut)
|
||||
return
|
||||
|
||||
for (const node of nodesToCut)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeCopySelected, handleNodeDelete])
|
||||
|
||||
const handleNodeDeleteSelected = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
edges,
|
||||
} = store.getState()
|
||||
|
||||
const currentEdgeIndex = edges.findIndex(edge => edge.selected)
|
||||
|
||||
if (currentEdgeIndex > -1)
|
||||
return
|
||||
|
||||
const nodes = getNodes()
|
||||
const nodesToDelete = nodes.filter(node => node.data.selected)
|
||||
|
||||
if (!nodesToDelete)
|
||||
return
|
||||
|
||||
for (const node of nodesToDelete)
|
||||
handleNodeDelete(node.id)
|
||||
}, [getNodesReadOnly, handleNodeDelete, store, workflowStore])
|
||||
|
||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
@ -861,6 +739,173 @@ export const useNodesInteractions = () => {
|
||||
setNodes(newNodes)
|
||||
}, [store])
|
||||
|
||||
const handleNodeContextMenu = useCallback((e: MouseEvent, node: Node) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
nodeMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
nodeId: node.id,
|
||||
},
|
||||
})
|
||||
handleNodeSelect(node.id)
|
||||
}, [workflowStore, handleNodeSelect])
|
||||
|
||||
const handleNodesCopy = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
setClipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
setClipboardElements(bundledNodes)
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
setClipboardElements([selectedNode])
|
||||
}, [getNodesReadOnly, store, workflowStore])
|
||||
|
||||
const handleNodesPaste = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
clipboardElements,
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
mousePosition,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodesToPaste: Node[] = []
|
||||
const nodes = getNodes()
|
||||
|
||||
if (clipboardElements.length) {
|
||||
const { x, y } = getTopLeftNodePosition(clipboardElements)
|
||||
const { screenToFlowPosition } = reactflow
|
||||
const currentPosition = screenToFlowPosition({ x: mousePosition.pageX, y: mousePosition.pageY })
|
||||
const offsetX = currentPosition.x - x
|
||||
const offsetY = currentPosition.y - y
|
||||
clipboardElements.forEach((nodeToPaste, index) => {
|
||||
const nodeType = nodeToPaste.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType],
|
||||
...nodeToPaste.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: nodeToPaste.position.x + offsetX,
|
||||
y: nodeToPaste.position.y + offsetY,
|
||||
},
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
nodesToPaste.push(newNode)
|
||||
})
|
||||
|
||||
setNodes([...nodes, ...nodesToPaste])
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
}, [t, getNodesReadOnly, store, workflowStore, handleSyncWorkflowDraft, reactflow])
|
||||
|
||||
const handleNodesDuplicate = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode) {
|
||||
const nodeType = selectedNode.data.type
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === nodeType)
|
||||
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[nodeType as BlockEnum],
|
||||
...selectedNode.data,
|
||||
selected: false,
|
||||
_isBundled: false,
|
||||
_connectedSourceHandleIds: [],
|
||||
_connectedTargetHandleIds: [],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${nodeType}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${nodeType}`),
|
||||
},
|
||||
position: {
|
||||
x: selectedNode.position.x + selectedNode.width! + 10,
|
||||
y: selectedNode.position.y,
|
||||
},
|
||||
})
|
||||
|
||||
setNodes([...nodes, newNode])
|
||||
}
|
||||
}, [store, t, getNodesReadOnly])
|
||||
|
||||
const handleNodesDelete = useCallback(() => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
|
||||
const {
|
||||
shortcutsDisabled,
|
||||
showFeaturesPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (shortcutsDisabled || showFeaturesPanel)
|
||||
return
|
||||
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (bundledNodes.length) {
|
||||
bundledNodes.forEach(node => handleNodeDelete(node.id))
|
||||
return
|
||||
}
|
||||
|
||||
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start && node.data.type !== BlockEnum.End)
|
||||
|
||||
if (selectedNode)
|
||||
handleNodeDelete(selectedNode.id)
|
||||
}, [store, workflowStore, getNodesReadOnly, handleNodeDelete])
|
||||
|
||||
return {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
@ -875,12 +920,12 @@ export const useNodesInteractions = () => {
|
||||
handleNodeDelete,
|
||||
handleNodeChange,
|
||||
handleNodeAdd,
|
||||
handleNodeDuplicateSelected,
|
||||
handleNodeCopySelected,
|
||||
handleNodeCut,
|
||||
handleNodeDeleteSelected,
|
||||
handleNodePaste,
|
||||
handleNodeCancelRunningStatus,
|
||||
handleNodesCancelSelected,
|
||||
handleNodeContextMenu,
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
}
|
||||
}
|
||||
|
37
web/app/components/workflow/hooks/use-panel-interactions.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import { useCallback } from 'react'
|
||||
import { useWorkflowStore } from '../store'
|
||||
|
||||
export const usePanelInteractions = () => {
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handlePaneContextMenu = useCallback((e: MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const container = document.querySelector('#workflow-container')
|
||||
const { x, y } = container!.getBoundingClientRect()
|
||||
workflowStore.setState({
|
||||
panelMenu: {
|
||||
top: e.clientY - y,
|
||||
left: e.clientX - x,
|
||||
},
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handlePaneContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
panelMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
const handleNodeContextmenuCancel = useCallback(() => {
|
||||
workflowStore.setState({
|
||||
nodeMenu: undefined,
|
||||
})
|
||||
}, [workflowStore])
|
||||
|
||||
return {
|
||||
handlePaneContextMenu,
|
||||
handlePaneContextmenuCancel,
|
||||
handleNodeContextmenuCancel,
|
||||
}
|
||||
}
|
109
web/app/components/workflow/hooks/use-selection-interactions.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { MouseEvent } from 'react'
|
||||
import {
|
||||
useCallback,
|
||||
} from 'react'
|
||||
import produce from 'immer'
|
||||
import type {
|
||||
OnSelectionChangeFunc,
|
||||
} from 'reactflow'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import type { Node } from '../types'
|
||||
|
||||
export const useSelectionInteractions = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
|
||||
const handleSelectionStart = useCallback(() => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height) {
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
if (node.data._isBundled)
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
if (edge.data._isBundled)
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}
|
||||
}, [store])
|
||||
|
||||
const handleSelectionChange = useCallback<OnSelectionChangeFunc>(({ nodes: nodesInSelection, edges: edgesInSelection }) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
edges,
|
||||
setEdges,
|
||||
userSelectionRect,
|
||||
} = store.getState()
|
||||
|
||||
const nodes = getNodes()
|
||||
|
||||
if (!userSelectionRect?.width || !userSelectionRect?.height)
|
||||
return
|
||||
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const nodeInSelection = nodesInSelection.find(n => n.id === node.id)
|
||||
|
||||
if (nodeInSelection)
|
||||
node.data._isBundled = true
|
||||
else
|
||||
node.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
const newEdges = produce(edges, (draft) => {
|
||||
draft.forEach((edge) => {
|
||||
const edgeInSelection = edgesInSelection.find(e => e.id === edge.id)
|
||||
|
||||
if (edgeInSelection)
|
||||
edge.data._isBundled = true
|
||||
else
|
||||
edge.data._isBundled = false
|
||||
})
|
||||
})
|
||||
setEdges(newEdges)
|
||||
}, [store])
|
||||
|
||||
const handleSelectionDrag = useCallback((_: MouseEvent, nodesWithDrag: Node[]) => {
|
||||
const {
|
||||
getNodes,
|
||||
setNodes,
|
||||
} = store.getState()
|
||||
|
||||
workflowStore.setState({
|
||||
nodeAnimation: false,
|
||||
})
|
||||
const nodes = getNodes()
|
||||
const newNodes = produce(nodes, (draft) => {
|
||||
draft.forEach((node) => {
|
||||
const dragNode = nodesWithDrag.find(n => n.id === node.id)
|
||||
|
||||
if (dragNode)
|
||||
node.position = dragNode.position
|
||||
})
|
||||
})
|
||||
setNodes(newNodes)
|
||||
}, [store, workflowStore])
|
||||
|
||||
return {
|
||||
handleSelectionStart,
|
||||
handleSelectionChange,
|
||||
handleSelectionDrag,
|
||||
}
|
||||
}
|
88
web/app/components/workflow/hooks/use-workflow-start-run.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { useCallback } from 'react'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import {
|
||||
BlockEnum,
|
||||
WorkflowRunningStatus,
|
||||
} from '../types'
|
||||
import {
|
||||
useIsChatMode,
|
||||
useNodesSyncDraft,
|
||||
useWorkflowInteractions,
|
||||
useWorkflowRun,
|
||||
} from './index'
|
||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
||||
|
||||
export const useWorkflowStartRun = () => {
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const featuresStore = useFeaturesStore()
|
||||
const isChatMode = useIsChatMode()
|
||||
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
|
||||
const { handleRun } = useWorkflowRun()
|
||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
|
||||
const handleWorkflowStartRunInWorkflow = useCallback(async () => {
|
||||
const {
|
||||
workflowRunningData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
|
||||
return
|
||||
|
||||
const { getNodes } = store.getState()
|
||||
const nodes = getNodes()
|
||||
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
|
||||
const startVariables = startNode?.data.variables || []
|
||||
const fileSettings = featuresStore!.getState().features.file
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setShowInputsPanel,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel) {
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
return
|
||||
}
|
||||
|
||||
if (!startVariables.length && !fileSettings?.image?.enabled) {
|
||||
await doSyncWorkflowDraft()
|
||||
handleRun({ inputs: {}, files: [] })
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(false)
|
||||
}
|
||||
else {
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
setShowInputsPanel(true)
|
||||
}
|
||||
}, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft])
|
||||
|
||||
const handleWorkflowStartRunInChatflow = useCallback(async () => {
|
||||
const {
|
||||
showDebugAndPreviewPanel,
|
||||
setShowDebugAndPreviewPanel,
|
||||
setHistoryWorkflowData,
|
||||
} = workflowStore.getState()
|
||||
|
||||
if (showDebugAndPreviewPanel)
|
||||
handleCancelDebugAndPreviewPanel()
|
||||
else
|
||||
setShowDebugAndPreviewPanel(true)
|
||||
|
||||
setHistoryWorkflowData(undefined)
|
||||
}, [workflowStore, handleCancelDebugAndPreviewPanel])
|
||||
|
||||
const handleStartWorkflowRun = useCallback(() => {
|
||||
if (!isChatMode)
|
||||
handleWorkflowStartRunInWorkflow()
|
||||
else
|
||||
handleWorkflowStartRunInChatflow()
|
||||
}, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow])
|
||||
|
||||
return {
|
||||
handleStartWorkflowRun,
|
||||
handleWorkflowStartRunInWorkflow,
|
||||
handleWorkflowStartRunInChatflow,
|
||||
}
|
||||
}
|
@ -6,19 +6,24 @@ import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { setAutoFreeze } from 'immer'
|
||||
import {
|
||||
useEventListener,
|
||||
useKeyPress,
|
||||
} from 'ahooks'
|
||||
import ReactFlow, {
|
||||
Background,
|
||||
ReactFlowProvider,
|
||||
SelectionMode,
|
||||
useEdgesState,
|
||||
useNodesState,
|
||||
useOnViewportChange,
|
||||
} from 'reactflow'
|
||||
import type { Viewport } from 'reactflow'
|
||||
import type {
|
||||
Viewport,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
import './style.css'
|
||||
import type {
|
||||
@ -31,9 +36,12 @@ import {
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
usePanelInteractions,
|
||||
useSelectionInteractions,
|
||||
useWorkflow,
|
||||
useWorkflowInit,
|
||||
useWorkflowReadOnly,
|
||||
useWorkflowStartRun,
|
||||
} from './hooks'
|
||||
import Header from './header'
|
||||
import CustomNode from './nodes'
|
||||
@ -43,8 +51,15 @@ import CustomConnectionLine from './custom-connection-line'
|
||||
import Panel from './panel'
|
||||
import Features from './features'
|
||||
import HelpLine from './help-line'
|
||||
import { useStore } from './store'
|
||||
import CandidateNode from './candidate-node'
|
||||
import PanelContextmenu from './panel-contextmenu'
|
||||
import NodeContextmenu from './node-contextmenu'
|
||||
import {
|
||||
useStore,
|
||||
useWorkflowStore,
|
||||
} from './store'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from './utils'
|
||||
@ -71,9 +86,12 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
edges: originalEdges,
|
||||
viewport,
|
||||
}) => {
|
||||
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
||||
const workflowStore = useWorkflowStore()
|
||||
const [nodes, setNodes] = useNodesState(originalNodes)
|
||||
const [edges, setEdges] = useEdgesState(originalEdges)
|
||||
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const nodeAnimation = useStore(s => s.nodeAnimation)
|
||||
const {
|
||||
handleSyncWorkflowDraft,
|
||||
@ -118,6 +136,25 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
}
|
||||
}, [handleSyncWorkflowDraftWhenPageClose])
|
||||
|
||||
useEventListener('keydown', (e) => {
|
||||
if ((e.key === 'd' || e.key === 'D') && (e.ctrlKey || e.metaKey))
|
||||
e.preventDefault()
|
||||
})
|
||||
useEventListener('mousemove', (e) => {
|
||||
const containerClientRect = workflowContainerRef.current?.getBoundingClientRect()
|
||||
|
||||
if (containerClientRect) {
|
||||
workflowStore.setState({
|
||||
mousePosition: {
|
||||
pageX: e.clientX,
|
||||
pageY: e.clientY,
|
||||
elementX: e.clientX - containerClientRect.left,
|
||||
elementY: e.clientY - containerClientRect.top,
|
||||
},
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const {
|
||||
handleNodeDragStart,
|
||||
handleNodeDrag,
|
||||
@ -128,11 +165,11 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleNodeConnect,
|
||||
handleNodeConnectStart,
|
||||
handleNodeConnectEnd,
|
||||
handleNodeDuplicateSelected,
|
||||
handleNodeCopySelected,
|
||||
handleNodeCut,
|
||||
handleNodeDeleteSelected,
|
||||
handleNodePaste,
|
||||
handleNodeContextMenu,
|
||||
handleNodesCopy,
|
||||
handleNodesPaste,
|
||||
handleNodesDuplicate,
|
||||
handleNodesDelete,
|
||||
} = useNodesInteractions()
|
||||
const {
|
||||
handleEdgeEnter,
|
||||
@ -140,9 +177,18 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
handleEdgeDelete,
|
||||
handleEdgesChange,
|
||||
} = useEdgesInteractions()
|
||||
const {
|
||||
handleSelectionStart,
|
||||
handleSelectionChange,
|
||||
handleSelectionDrag,
|
||||
} = useSelectionInteractions()
|
||||
const {
|
||||
handlePaneContextMenu,
|
||||
} = usePanelInteractions()
|
||||
const {
|
||||
isValidConnection,
|
||||
} = useWorkflow()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
|
||||
useOnViewportChange({
|
||||
onEnd: () => {
|
||||
@ -150,12 +196,12 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
},
|
||||
})
|
||||
|
||||
useKeyPress(['delete', 'backspace'], handleNodeDeleteSelected)
|
||||
useKeyPress(['delete', 'backspace'], handleEdgeDelete)
|
||||
useKeyPress(['ctrl.c', 'meta.c'], handleNodeCopySelected)
|
||||
useKeyPress(['ctrl.x', 'meta.x'], handleNodeCut)
|
||||
useKeyPress(['ctrl.v', 'meta.v'], handleNodePaste)
|
||||
useKeyPress(['ctrl.alt.d', 'meta.shift.d'], handleNodeDuplicateSelected)
|
||||
useKeyPress('delete', handleNodesDelete)
|
||||
useKeyPress('delete', handleEdgeDelete)
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.c`, handleNodesCopy, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.v`, handleNodesPaste, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.d`, handleNodesDuplicate, { exactMatch: true, useCapture: true })
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('alt')}.r`, handleStartWorkflowRun, { exactMatch: true, useCapture: true })
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -165,13 +211,17 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
${workflowReadOnly && 'workflow-panel-animation'}
|
||||
${nodeAnimation && 'workflow-node-animation'}
|
||||
`}
|
||||
ref={workflowContainerRef}
|
||||
>
|
||||
<CandidateNode />
|
||||
<Header />
|
||||
<Panel />
|
||||
<Operator />
|
||||
{
|
||||
showFeaturesPanel && <Features />
|
||||
}
|
||||
<PanelContextmenu />
|
||||
<NodeContextmenu />
|
||||
<HelpLine />
|
||||
<ReactFlow
|
||||
nodeTypes={nodeTypes}
|
||||
@ -184,12 +234,17 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
onNodeMouseEnter={handleNodeEnter}
|
||||
onNodeMouseLeave={handleNodeLeave}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeContextMenu={handleNodeContextMenu}
|
||||
onConnect={handleNodeConnect}
|
||||
onConnectStart={handleNodeConnectStart}
|
||||
onConnectEnd={handleNodeConnectEnd}
|
||||
onEdgeMouseEnter={handleEdgeEnter}
|
||||
onEdgeMouseLeave={handleEdgeLeave}
|
||||
onEdgesChange={handleEdgesChange}
|
||||
onSelectionStart={handleSelectionStart}
|
||||
onSelectionChange={handleSelectionChange}
|
||||
onSelectionDrag={handleSelectionDrag}
|
||||
onPaneContextMenu={handlePaneContextMenu}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
defaultViewport={viewport}
|
||||
multiSelectionKeyCode={null}
|
||||
@ -198,11 +253,15 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||
nodesConnectable={!nodesReadOnly}
|
||||
nodesFocusable={!nodesReadOnly}
|
||||
edgesFocusable={!nodesReadOnly}
|
||||
panOnDrag={!workflowReadOnly}
|
||||
panOnDrag={controlMode === 'hand' && !workflowReadOnly}
|
||||
zoomOnPinch={!workflowReadOnly}
|
||||
zoomOnScroll={!workflowReadOnly}
|
||||
zoomOnDoubleClick={!workflowReadOnly}
|
||||
isValidConnection={isValidConnection}
|
||||
selectionKeyCode={null}
|
||||
selectionMode={SelectionMode.Partial}
|
||||
selectionOnDrag={controlMode === 'pointer' && !workflowReadOnly}
|
||||
minZoom={0.25}
|
||||
>
|
||||
<Background
|
||||
gap={[14, 14]}
|
||||
|
44
web/app/components/workflow/node-contextmenu.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import {
|
||||
memo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import { useNodes } from 'reactflow'
|
||||
import PanelOperatorPopup from './nodes/_base/components/panel-operator/panel-operator-popup'
|
||||
import type { Node } from './types'
|
||||
import { useStore } from './store'
|
||||
import { usePanelInteractions } from './hooks'
|
||||
|
||||
const PanelContextmenu = () => {
|
||||
const ref = useRef(null)
|
||||
const nodes = useNodes()
|
||||
const { handleNodeContextmenuCancel } = usePanelInteractions()
|
||||
const nodeMenu = useStore(s => s.nodeMenu)
|
||||
const currentNode = nodes.find(node => node.id === nodeMenu?.nodeId) as Node
|
||||
|
||||
useClickAway(() => {
|
||||
handleNodeContextmenuCancel()
|
||||
}, ref)
|
||||
|
||||
if (!nodeMenu || !currentNode)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute z-[9]'
|
||||
style={{
|
||||
left: nodeMenu.left,
|
||||
top: nodeMenu.top,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<PanelOperatorPopup
|
||||
id={currentNode.id}
|
||||
data={currentNode.data}
|
||||
onClosePopup={() => handleNodeContextmenuCancel()}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelContextmenu)
|
@ -1,19 +1,10 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import ChangeBlock from './change-block'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import PanelOperatorPopup from './panel-operator-popup'
|
||||
import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
@ -21,8 +12,6 @@ import {
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
type PanelOperatorProps = {
|
||||
id: string
|
||||
@ -43,35 +32,7 @@ const PanelOperator = ({
|
||||
onOpenChange,
|
||||
inNode,
|
||||
}: PanelOperatorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const edges = useEdges()
|
||||
const { handleNodeDelete } = useNodesInteractions()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const [open, setOpen] = useState(false)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}, [data, nodesExtraData, buildInTools, customTools])
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools])
|
||||
|
||||
const handleOpenChange = useCallback((newOpen: boolean) => {
|
||||
setOpen(newOpen)
|
||||
@ -100,60 +61,11 @@ const PanelOperator = ({
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-[11]'>
|
||||
<div className='w-[240px] border-[0.5px] border-gray-200 rounded-lg shadow-xl bg-white'>
|
||||
<div className='p-1'>
|
||||
{
|
||||
data.type !== BlockEnum.Start && !nodesReadOnly && (
|
||||
<ChangeBlock
|
||||
nodeId={id}
|
||||
nodeType={data.type}
|
||||
sourceHandle={edge?.sourceHandle || 'source'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<a
|
||||
href={
|
||||
language === 'zh_Hans'
|
||||
? 'https://docs.dify.ai/v/zh-hans/guides/workflow'
|
||||
: 'https://docs.dify.ai/features/workflow'
|
||||
}
|
||||
target='_blank'
|
||||
className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
>
|
||||
{t('workflow.panel.helpLink')}
|
||||
</a>
|
||||
</div>
|
||||
{
|
||||
data.type !== BlockEnum.Start && !nodesReadOnly && (
|
||||
<>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer
|
||||
hover:bg-rose-50 hover:text-red-500
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div className='px-3 py-2 text-xs text-gray-500'>
|
||||
<div className='flex items-center mb-1 h-[22px] font-medium'>
|
||||
{t('workflow.panel.about').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-1 text-gray-700 leading-[18px]'>{about}</div>
|
||||
<div className='leading-[18px]'>
|
||||
{t('workflow.panel.createdBy')} {author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<PanelOperatorPopup
|
||||
id={id}
|
||||
data={data}
|
||||
onClosePopup={() => setOpen(false)}
|
||||
/>
|
||||
</PortalToFollowElemContent>
|
||||
</PortalToFollowElem>
|
||||
)
|
||||
|
@ -0,0 +1,181 @@
|
||||
import {
|
||||
memo,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useEdges } from 'reactflow'
|
||||
import ChangeBlock from './change-block'
|
||||
import {
|
||||
canRunBySingle,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useStore } from '@/app/components/workflow/store'
|
||||
import {
|
||||
useNodeDataUpdate,
|
||||
useNodesExtraData,
|
||||
useNodesInteractions,
|
||||
useNodesReadOnly,
|
||||
useNodesSyncDraft,
|
||||
} from '@/app/components/workflow/hooks'
|
||||
import ShortcutsName from '@/app/components/workflow/shortcuts-name'
|
||||
import type { Node } from '@/app/components/workflow/types'
|
||||
import { BlockEnum } from '@/app/components/workflow/types'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
|
||||
type PanelOperatorPopupProps = {
|
||||
id: string
|
||||
data: Node['data']
|
||||
onClosePopup: () => void
|
||||
}
|
||||
const PanelOperatorPopup = ({
|
||||
id,
|
||||
data,
|
||||
onClosePopup,
|
||||
}: PanelOperatorPopupProps) => {
|
||||
const { t } = useTranslation()
|
||||
const language = useGetLanguage()
|
||||
const edges = useEdges()
|
||||
const {
|
||||
handleNodeDelete,
|
||||
handleNodesDuplicate,
|
||||
handleNodeSelect,
|
||||
handleNodesCopy,
|
||||
} = useNodesInteractions()
|
||||
const { handleNodeDataUpdate } = useNodeDataUpdate()
|
||||
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const buildInTools = useStore(s => s.buildInTools)
|
||||
const customTools = useStore(s => s.customTools)
|
||||
const edge = edges.find(edge => edge.target === id)
|
||||
const author = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].author
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.author
|
||||
}, [data, nodesExtraData, buildInTools, customTools])
|
||||
|
||||
const about = useMemo(() => {
|
||||
if (data.type !== BlockEnum.Tool)
|
||||
return nodesExtraData[data.type].about
|
||||
|
||||
if (data.provider_type === 'builtin')
|
||||
return buildInTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
|
||||
return customTools.find(toolWithProvider => toolWithProvider.id === data.provider_id)?.description[language]
|
||||
}, [data, nodesExtraData, language, buildInTools, customTools])
|
||||
|
||||
const showChangeBlock = data.type !== BlockEnum.Start && !nodesReadOnly
|
||||
|
||||
return (
|
||||
<div className='w-[240px] border-[0.5px] border-gray-200 rounded-lg shadow-xl bg-white'>
|
||||
{
|
||||
(showChangeBlock || canRunBySingle(data.type)) && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
{
|
||||
canRunBySingle(data.type) && (
|
||||
<div
|
||||
className={`
|
||||
flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer
|
||||
hover:bg-gray-50
|
||||
`}
|
||||
onClick={() => {
|
||||
handleNodeSelect(id)
|
||||
handleNodeDataUpdate({ id, data: { _isSingleRun: true } })
|
||||
handleSyncWorkflowDraft(true)
|
||||
onClosePopup()
|
||||
}}
|
||||
>
|
||||
{t('workflow.panel.runThisStep')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
showChangeBlock && (
|
||||
<ChangeBlock
|
||||
nodeId={id}
|
||||
nodeType={data.type}
|
||||
sourceHandle={edge?.sourceHandle || 'source'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.Start && data.type !== BlockEnum.End && !nodesReadOnly && (
|
||||
<>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesCopy()
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.copy')}
|
||||
<ShortcutsName keys={['ctrl', 'c']} />
|
||||
</div>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => {
|
||||
onClosePopup()
|
||||
handleNodesDuplicate()
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.duplicate')}
|
||||
<ShortcutsName keys={['ctrl', 'd']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={`
|
||||
flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer
|
||||
hover:bg-rose-50 hover:text-red-500
|
||||
`}
|
||||
onClick={() => handleNodeDelete(id)}
|
||||
>
|
||||
{t('common.operation.delete')}
|
||||
<ShortcutsName keys={['del']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div className='p-1'>
|
||||
<a
|
||||
href={
|
||||
language === 'zh_Hans'
|
||||
? 'https://docs.dify.ai/v/zh-hans/guides/workflow'
|
||||
: 'https://docs.dify.ai/features/workflow'
|
||||
}
|
||||
target='_blank'
|
||||
className='flex items-center px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
>
|
||||
{t('workflow.panel.helpLink')}
|
||||
</a>
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div className='px-3 py-2 text-xs text-gray-500'>
|
||||
<div className='flex items-center mb-1 h-[22px] font-medium'>
|
||||
{t('workflow.panel.about').toLocaleUpperCase()}
|
||||
</div>
|
||||
<div className='mb-1 text-gray-700 leading-[18px]'>{about}</div>
|
||||
<div className='leading-[18px]'>
|
||||
{t('workflow.panel.createdBy')} {author}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelOperatorPopup)
|
@ -6,6 +6,7 @@ import {
|
||||
cloneElement,
|
||||
memo,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import type { NodeProps } from '../../types'
|
||||
import {
|
||||
@ -37,27 +38,30 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
data,
|
||||
children,
|
||||
}) => {
|
||||
const nodeRef = useRef<HTMLDivElement>(null)
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const toolIcon = useToolIcon(data)
|
||||
|
||||
const showSelectedBorder = data.selected || data._isBundled
|
||||
const {
|
||||
showRunningBorder,
|
||||
showSuccessBorder,
|
||||
showFailedBorder,
|
||||
} = useMemo(() => {
|
||||
return {
|
||||
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !data.selected,
|
||||
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !data.selected,
|
||||
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !data.selected,
|
||||
showRunningBorder: data._runningStatus === NodeRunningStatus.Running && !showSelectedBorder,
|
||||
showSuccessBorder: data._runningStatus === NodeRunningStatus.Succeeded && !showSelectedBorder,
|
||||
showFailedBorder: data._runningStatus === NodeRunningStatus.Failed && !showSelectedBorder,
|
||||
}
|
||||
}, [data._runningStatus, data.selected])
|
||||
}, [data._runningStatus, showSelectedBorder])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex border-[2px] rounded-2xl
|
||||
${(data.selected && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
${(showSelectedBorder && !data._isInvalidConnection) ? 'border-primary-600' : 'border-transparent'}
|
||||
`}
|
||||
ref={nodeRef}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
@ -68,10 +72,11 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
${showSuccessBorder && '!border-[#12B76A]'}
|
||||
${showFailedBorder && '!border-[#F04438]'}
|
||||
${data._isInvalidConnection && '!border-[#F04438]'}
|
||||
${data._isBundled && '!shadow-lg'}
|
||||
`}
|
||||
>
|
||||
{
|
||||
data.type !== BlockEnum.VariableAssigner && (
|
||||
data.type !== BlockEnum.VariableAssigner && !data._isCandidate && (
|
||||
<NodeTargetHandle
|
||||
id={id}
|
||||
data={data}
|
||||
@ -81,7 +86,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && (
|
||||
data.type !== BlockEnum.IfElse && data.type !== BlockEnum.QuestionClassifier && !data._isCandidate && (
|
||||
<NodeSourceHandle
|
||||
id={id}
|
||||
data={data}
|
||||
@ -91,7 +96,7 @@ const BaseNode: FC<BaseNodeProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
!data._runningStatus && !nodesReadOnly && (
|
||||
!data._runningStatus && !nodesReadOnly && !data._isCandidate && (
|
||||
<NodeControl
|
||||
id={id}
|
||||
data={data}
|
||||
|
110
web/app/components/workflow/operator/add-block.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { OffsetOptions } from '@floating-ui/react'
|
||||
import {
|
||||
generateNewNode,
|
||||
} from '../utils'
|
||||
import {
|
||||
useNodesExtraData,
|
||||
useNodesReadOnly,
|
||||
usePanelInteractions,
|
||||
} from '../hooks'
|
||||
import { NODES_INITIAL_DATA } from '../constants'
|
||||
import { useWorkflowStore } from '../store'
|
||||
import TipPopup from './tip-popup'
|
||||
import BlockSelector from '@/app/components/workflow/block-selector'
|
||||
import { Plus } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import type {
|
||||
OnSelectBlock,
|
||||
} from '@/app/components/workflow/types'
|
||||
import {
|
||||
BlockEnum,
|
||||
} from '@/app/components/workflow/types'
|
||||
|
||||
type AddBlockProps = {
|
||||
renderTrigger?: (open: boolean) => React.ReactNode
|
||||
offset?: OffsetOptions
|
||||
}
|
||||
const AddBlock = ({
|
||||
renderTrigger,
|
||||
offset,
|
||||
}: AddBlockProps) => {
|
||||
const { t } = useTranslation()
|
||||
const store = useStoreApi()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const nodesExtraData = useNodesExtraData()
|
||||
const { nodesReadOnly } = useNodesReadOnly()
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const [open, setOpen] = useState(false)
|
||||
const availableNextNodes = nodesExtraData[BlockEnum.Start].availableNextNodes
|
||||
|
||||
const handleOpenChange = useCallback((open: boolean) => {
|
||||
setOpen(open)
|
||||
if (!open)
|
||||
handlePaneContextmenuCancel()
|
||||
}, [handlePaneContextmenuCancel])
|
||||
|
||||
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
|
||||
const {
|
||||
getNodes,
|
||||
} = store.getState()
|
||||
const nodes = getNodes()
|
||||
const nodesWithSameType = nodes.filter(node => node.data.type === type)
|
||||
const newNode = generateNewNode({
|
||||
data: {
|
||||
...NODES_INITIAL_DATA[type],
|
||||
title: nodesWithSameType.length > 0 ? `${t(`workflow.blocks.${type}`)} ${nodesWithSameType.length + 1}` : t(`workflow.blocks.${type}`),
|
||||
...(toolDefaultValue || {}),
|
||||
_isCandidate: true,
|
||||
},
|
||||
position: {
|
||||
x: 0,
|
||||
y: 0,
|
||||
},
|
||||
})
|
||||
workflowStore.setState({
|
||||
candidateNode: newNode,
|
||||
})
|
||||
}, [store, workflowStore, t])
|
||||
|
||||
const renderTriggerElement = useCallback((open: boolean) => {
|
||||
return (
|
||||
<TipPopup
|
||||
title={t('workflow.common.addBlock')}
|
||||
>
|
||||
<div className={cn(
|
||||
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
open && '!bg-black/5',
|
||||
)}>
|
||||
<Plus className='w-4 h-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
)
|
||||
}, [nodesReadOnly, t])
|
||||
|
||||
return (
|
||||
<BlockSelector
|
||||
open={open}
|
||||
onOpenChange={handleOpenChange}
|
||||
disabled={nodesReadOnly}
|
||||
onSelect={handleSelect}
|
||||
placement='top-start'
|
||||
offset={offset ?? {
|
||||
mainAxis: 4,
|
||||
crossAxis: -8,
|
||||
}}
|
||||
trigger={renderTrigger || renderTriggerElement}
|
||||
popupClassName='!min-w-[256px]'
|
||||
availableBlocksTypes={availableNextNodes}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AddBlock)
|
85
web/app/components/workflow/operator/control.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import cn from 'classnames'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../hooks'
|
||||
import { useStore } from '../store'
|
||||
import AddBlock from './add-block'
|
||||
import TipPopup from './tip-popup'
|
||||
import {
|
||||
Cursor02C,
|
||||
Hand02,
|
||||
} from '@/app/components/base/icons/src/vender/line/editor'
|
||||
import {
|
||||
Cursor02C as Cursor02CSolid,
|
||||
Hand02 as Hand02Solid,
|
||||
} from '@/app/components/base/icons/src/vender/solid/editor'
|
||||
import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
|
||||
const Control = () => {
|
||||
const { t } = useTranslation()
|
||||
const controlMode = useStore(s => s.controlMode)
|
||||
const setControlMode = useStore(s => s.setControlMode)
|
||||
const { handleLayout } = useWorkflow()
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
|
||||
const goLayout = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
handleLayout()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='flex items-center p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'>
|
||||
<AddBlock />
|
||||
<div className='mx-[3px] w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<TipPopup title={t('workflow.common.pointerMode')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center mr-[1px] w-8 h-8 rounded-lg cursor-pointer',
|
||||
controlMode === 'pointer' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={() => setControlMode('pointer')}
|
||||
>
|
||||
{
|
||||
controlMode === 'pointer' ? <Cursor02CSolid className='w-4 h-4' /> : <Cursor02C className='w-4 h-4' />
|
||||
}
|
||||
</div>
|
||||
</TipPopup>
|
||||
<TipPopup title={t('workflow.common.handMode')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer',
|
||||
controlMode === 'hand' ? 'bg-primary-50 text-primary-600' : 'hover:bg-black/5 hover:text-gray-700',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={() => setControlMode('hand')}
|
||||
>
|
||||
{
|
||||
controlMode === 'hand' ? <Hand02Solid className='w-4 h-4' /> : <Hand02 className='w-4 h-4' />
|
||||
}
|
||||
</div>
|
||||
</TipPopup>
|
||||
<div className='mx-[3px] w-[1px] h-3.5 bg-gray-200'></div>
|
||||
<TipPopup title={t('workflow.panel.organizeBlocks')}>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-center w-8 h-8 rounded-lg hover:bg-black/5 hover:text-gray-700 cursor-pointer',
|
||||
`${nodesReadOnly && '!cursor-not-allowed opacity-50'}`,
|
||||
)}
|
||||
onClick={goLayout}
|
||||
>
|
||||
<OrganizeGrid className='w-4 h-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(Control)
|
@ -1,54 +1,23 @@
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { MiniMap } from 'reactflow'
|
||||
import {
|
||||
useNodesReadOnly,
|
||||
useWorkflow,
|
||||
} from '../hooks'
|
||||
import ZoomInOut from './zoom-in-out'
|
||||
import { OrganizeGrid } from '@/app/components/base/icons/src/vender/line/layout'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
import Control from './control'
|
||||
|
||||
const Operator = () => {
|
||||
const { t } = useTranslation()
|
||||
const { handleLayout } = useWorkflow()
|
||||
const {
|
||||
nodesReadOnly,
|
||||
getNodesReadOnly,
|
||||
} = useNodesReadOnly()
|
||||
|
||||
const goLayout = () => {
|
||||
if (getNodesReadOnly())
|
||||
return
|
||||
handleLayout()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`
|
||||
absolute left-6 bottom-6 z-[9]
|
||||
`}>
|
||||
<>
|
||||
<MiniMap
|
||||
style={{
|
||||
width: 128,
|
||||
height: 80,
|
||||
width: 102,
|
||||
height: 72,
|
||||
}}
|
||||
className='!static !m-0 !w-[128px] !h-[80px] !border-[0.5px] !border-black/[0.08] !rounded-lg !shadow-lg'
|
||||
className='!absolute !left-4 !bottom-14 z-[9] !m-0 !w-[102px] !h-[72px] !border-[0.5px] !border-black/[0.08] !rounded-lg !shadow-lg'
|
||||
/>
|
||||
<div className='flex items-center mt-1 p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-lg text-gray-500'>
|
||||
<div className='flex items-center mt-1 gap-2 absolute left-4 bottom-4 z-[9]'>
|
||||
<ZoomInOut />
|
||||
<TooltipPlus popupContent={t('workflow.panel.organizeBlocks')}>
|
||||
<div
|
||||
className={`
|
||||
ml-[1px] flex items-center justify-center w-8 h-8 cursor-pointer hover:bg-black/5 rounded-lg
|
||||
${nodesReadOnly && '!cursor-not-allowed opacity-50'}
|
||||
`}
|
||||
onClick={goLayout}
|
||||
>
|
||||
<OrganizeGrid className='w-4 h-4' />
|
||||
</div>
|
||||
</TooltipPlus>
|
||||
<Control />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
|
34
web/app/components/workflow/operator/tip-popup.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { memo } from 'react'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
import TooltipPlus from '@/app/components/base/tooltip-plus'
|
||||
|
||||
type TipPopupProps = {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
shortcuts?: string[]
|
||||
}
|
||||
const TipPopup = ({
|
||||
title,
|
||||
children,
|
||||
shortcuts,
|
||||
}: TipPopupProps) => {
|
||||
return (
|
||||
<TooltipPlus
|
||||
offset={4}
|
||||
hideArrow
|
||||
popupClassName='!p-0 !bg-gray-25'
|
||||
popupContent={
|
||||
<div className='flex items-center gap-1 px-2 h-6 text-xs font-medium text-gray-700 rounded-lg border-[0.5px] border-black/5'>
|
||||
{title}
|
||||
{
|
||||
shortcuts && <ShortcutsName keys={shortcuts} className='!text-[11px]' />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</TooltipPlus>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(TipPopup)
|
@ -5,6 +5,8 @@ import {
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useKeyPress } from 'ahooks'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
useReactFlow,
|
||||
@ -14,13 +16,32 @@ import {
|
||||
useNodesSyncDraft,
|
||||
useWorkflowReadOnly,
|
||||
} from '../hooks'
|
||||
import {
|
||||
getKeyboardKeyCodeBySystem,
|
||||
getKeyboardKeyNameBySystem,
|
||||
} from '../utils'
|
||||
import ShortcutsName from '../shortcuts-name'
|
||||
import TipPopup from './tip-popup'
|
||||
import {
|
||||
PortalToFollowElem,
|
||||
PortalToFollowElemContent,
|
||||
PortalToFollowElemTrigger,
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import { SearchLg } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ChevronDown } from '@/app/components/base/icons/src/vender/line/arrows'
|
||||
import {
|
||||
ZoomIn,
|
||||
ZoomOut,
|
||||
} from '@/app/components/base/icons/src/vender/line/editor'
|
||||
|
||||
enum ZoomType {
|
||||
zoomIn = 'zoomIn',
|
||||
zoomOut = 'zoomOut',
|
||||
zoomToFit = 'zoomToFit',
|
||||
zoomTo25 = 'zoomTo25',
|
||||
zoomTo50 = 'zoomTo50',
|
||||
zoomTo75 = 'zoomTo75',
|
||||
zoomTo100 = 'zoomTo100',
|
||||
zoomTo200 = 'zoomTo200',
|
||||
}
|
||||
|
||||
const ZoomInOut: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -41,27 +62,29 @@ const ZoomInOut: FC = () => {
|
||||
const ZOOM_IN_OUT_OPTIONS = [
|
||||
[
|
||||
{
|
||||
key: 'in',
|
||||
text: t('workflow.operator.zoomIn'),
|
||||
key: ZoomType.zoomTo200,
|
||||
text: '200%',
|
||||
},
|
||||
{
|
||||
key: 'out',
|
||||
text: t('workflow.operator.zoomOut'),
|
||||
key: ZoomType.zoomTo100,
|
||||
text: '100%',
|
||||
},
|
||||
{
|
||||
key: ZoomType.zoomTo75,
|
||||
text: '75%',
|
||||
},
|
||||
{
|
||||
key: ZoomType.zoomTo50,
|
||||
text: '50%',
|
||||
},
|
||||
{
|
||||
key: ZoomType.zoomTo25,
|
||||
text: '25%',
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: 'to50',
|
||||
text: t('workflow.operator.zoomTo50'),
|
||||
},
|
||||
{
|
||||
key: 'to100',
|
||||
text: t('workflow.operator.zoomTo100'),
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
key: 'fit',
|
||||
key: ZoomType.zoomToFit,
|
||||
text: t('workflow.operator.zoomToFit'),
|
||||
},
|
||||
],
|
||||
@ -71,24 +94,99 @@ const ZoomInOut: FC = () => {
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
if (type === 'in')
|
||||
zoomIn()
|
||||
|
||||
if (type === 'out')
|
||||
zoomOut()
|
||||
|
||||
if (type === 'fit')
|
||||
if (type === ZoomType.zoomToFit)
|
||||
fitView()
|
||||
|
||||
if (type === 'to50')
|
||||
if (type === ZoomType.zoomTo25)
|
||||
zoomTo(0.25)
|
||||
|
||||
if (type === ZoomType.zoomTo50)
|
||||
zoomTo(0.5)
|
||||
|
||||
if (type === 'to100')
|
||||
if (type === ZoomType.zoomTo75)
|
||||
zoomTo(0.75)
|
||||
|
||||
if (type === ZoomType.zoomTo100)
|
||||
zoomTo(1)
|
||||
|
||||
if (type === ZoomType.zoomTo200)
|
||||
zoomTo(2)
|
||||
|
||||
handleSyncWorkflowDraft()
|
||||
}
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.1`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
fitView()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.1', (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomTo(1)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.2', (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomTo(2)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress('shift.5', (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomTo(0.5)
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.dash`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomOut()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
useKeyPress(`${getKeyboardKeyCodeBySystem('ctrl')}.equalsign`, (e) => {
|
||||
e.preventDefault()
|
||||
if (workflowReadOnly)
|
||||
return
|
||||
|
||||
zoomIn()
|
||||
handleSyncWorkflowDraft()
|
||||
}, {
|
||||
exactMatch: true,
|
||||
useCapture: true,
|
||||
})
|
||||
|
||||
const handleTrigger = useCallback(() => {
|
||||
if (getWorkflowReadOnly())
|
||||
return
|
||||
@ -108,17 +206,47 @@ const ZoomInOut: FC = () => {
|
||||
>
|
||||
<PortalToFollowElemTrigger asChild onClick={handleTrigger}>
|
||||
<div className={`
|
||||
flex items-center px-2 h-8 cursor-pointer text-[13px] hover:bg-gray-50 rounded-lg
|
||||
${open && 'bg-gray-50'}
|
||||
p-0.5 h-9 cursor-pointer text-[13px] text-gray-500 font-medium rounded-lg bg-white shadow-lg border-[0.5px] border-gray-100
|
||||
${workflowReadOnly && '!cursor-not-allowed opacity-50'}
|
||||
`}>
|
||||
<SearchLg className='mr-1 w-4 h-4' />
|
||||
<div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
|
||||
<ChevronDown className='ml-1 w-4 h-4' />
|
||||
<div className={cn(
|
||||
'flex items-center justify-between w-[98px] h-8 hover:bg-gray-50 rounded-lg',
|
||||
open && 'bg-gray-50',
|
||||
)}>
|
||||
<TipPopup
|
||||
title={t('workflow.operator.zoomOut')}
|
||||
shortcuts={['ctrl', '-']}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
zoomOut()
|
||||
}}
|
||||
>
|
||||
<ZoomOut className='w-4 h-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
<div className='w-[34px]'>{parseFloat(`${zoom * 100}`).toFixed(0)}%</div>
|
||||
<TipPopup
|
||||
title={t('workflow.operator.zoomIn')}
|
||||
shortcuts={['ctrl', '+']}
|
||||
>
|
||||
<div
|
||||
className='flex items-center justify-center w-8 h-8 rounded-lg cursor-pointer hover:bg-black/5'
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
zoomIn()
|
||||
}}
|
||||
>
|
||||
<ZoomIn className='w-4 h-4' />
|
||||
</div>
|
||||
</TipPopup>
|
||||
</div>
|
||||
</div>
|
||||
</PortalToFollowElemTrigger>
|
||||
<PortalToFollowElemContent className='z-10'>
|
||||
<div className='w-[168px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'>
|
||||
<div className='w-[145px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-lg'>
|
||||
{
|
||||
ZOOM_IN_OUT_OPTIONS.map((options, i) => (
|
||||
<Fragment key={i}>
|
||||
@ -132,10 +260,30 @@ const ZoomInOut: FC = () => {
|
||||
options.map(option => (
|
||||
<div
|
||||
key={option.key}
|
||||
className='flex items-center px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700'
|
||||
className='flex items-center justify-between px-3 h-8 rounded-lg hover:bg-gray-50 cursor-pointer text-sm text-gray-700'
|
||||
onClick={() => handleZoom(option.key)}
|
||||
>
|
||||
{option.text}
|
||||
{
|
||||
option.key === ZoomType.zoomToFit && (
|
||||
<ShortcutsName keys={[`${getKeyboardKeyNameBySystem('ctrl')}`, '1']} />
|
||||
)
|
||||
}
|
||||
{
|
||||
option.key === ZoomType.zoomTo50 && (
|
||||
<ShortcutsName keys={['shift', '5']} />
|
||||
)
|
||||
}
|
||||
{
|
||||
option.key === ZoomType.zoomTo100 && (
|
||||
<ShortcutsName keys={['shift', '1']} />
|
||||
)
|
||||
}
|
||||
{
|
||||
option.key === ZoomType.zoomTo200 && (
|
||||
<ShortcutsName keys={['shift', '2']} />
|
||||
)
|
||||
}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
|
123
web/app/components/workflow/panel-contextmenu.tsx
Normal file
@ -0,0 +1,123 @@
|
||||
import {
|
||||
memo,
|
||||
useRef,
|
||||
} from 'react'
|
||||
import cn from 'classnames'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import ShortcutsName from './shortcuts-name'
|
||||
import { useStore } from './store'
|
||||
import {
|
||||
useNodesInteractions,
|
||||
usePanelInteractions,
|
||||
useWorkflowStartRun,
|
||||
} from './hooks'
|
||||
import AddBlock from './operator/add-block'
|
||||
import { exportAppConfig } from '@/service/apps'
|
||||
import { useToastContext } from '@/app/components/base/toast'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
|
||||
const PanelContextmenu = () => {
|
||||
const { t } = useTranslation()
|
||||
const { notify } = useToastContext()
|
||||
const ref = useRef(null)
|
||||
const panelMenu = useStore(s => s.panelMenu)
|
||||
const clipboardElements = useStore(s => s.clipboardElements)
|
||||
const appDetail = useAppStore(s => s.appDetail)
|
||||
const { handleNodesPaste } = useNodesInteractions()
|
||||
const { handlePaneContextmenuCancel } = usePanelInteractions()
|
||||
const { handleStartWorkflowRun } = useWorkflowStartRun()
|
||||
|
||||
useClickAway(() => {
|
||||
handlePaneContextmenuCancel()
|
||||
}, ref)
|
||||
|
||||
const onExport = async () => {
|
||||
if (!appDetail)
|
||||
return
|
||||
try {
|
||||
const { data } = await exportAppConfig(appDetail.id)
|
||||
const a = document.createElement('a')
|
||||
const file = new Blob([data], { type: 'application/yaml' })
|
||||
a.href = URL.createObjectURL(file)
|
||||
a.download = `${appDetail.name}.yml`
|
||||
a.click()
|
||||
}
|
||||
catch (e) {
|
||||
notify({ type: 'error', message: t('app.exportFailed') })
|
||||
}
|
||||
}
|
||||
|
||||
const renderTrigger = () => {
|
||||
return (
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
>
|
||||
{t('workflow.common.addBlock')}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!panelMenu)
|
||||
return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className='absolute w-[200px] rounded-lg border-[0.5px] border-gray-200 bg-white shadow-xl z-[9]'
|
||||
style={{
|
||||
left: panelMenu.left,
|
||||
top: panelMenu.top,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<div className='p-1'>
|
||||
<AddBlock
|
||||
renderTrigger={renderTrigger}
|
||||
offset={{
|
||||
mainAxis: -36,
|
||||
crossAxis: -4,
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => {
|
||||
handleStartWorkflowRun()
|
||||
handlePaneContextmenuCancel()
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.run')}
|
||||
<ShortcutsName keys={['alt', 'r']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer',
|
||||
!clipboardElements.length ? 'opacity-50 cursor-not-allowed' : 'hover:bg-gray-50',
|
||||
)}
|
||||
onClick={() => {
|
||||
if (clipboardElements.length) {
|
||||
handleNodesPaste()
|
||||
handlePaneContextmenuCancel()
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('workflow.common.pasteHere')}
|
||||
<ShortcutsName keys={['ctrl', 'v']} />
|
||||
</div>
|
||||
</div>
|
||||
<div className='h-[1px] bg-gray-100'></div>
|
||||
<div className='p-1'>
|
||||
<div
|
||||
className='flex items-center justify-between px-3 h-8 text-sm text-gray-700 rounded-lg cursor-pointer hover:bg-gray-50'
|
||||
onClick={() => onExport()}
|
||||
>
|
||||
{t('app.export')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(PanelContextmenu)
|
32
web/app/components/workflow/shortcuts-name.tsx
Normal file
@ -0,0 +1,32 @@
|
||||
import { memo } from 'react'
|
||||
import cn from 'classnames'
|
||||
import { getKeyboardKeyNameBySystem } from './utils'
|
||||
|
||||
type ShortcutsNameProps = {
|
||||
keys: string[]
|
||||
className?: string
|
||||
}
|
||||
const ShortcutsName = ({
|
||||
keys,
|
||||
className,
|
||||
}: ShortcutsNameProps) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
'flex items-center gap-0.5 h-4 text-xs text-gray-400',
|
||||
className,
|
||||
)}>
|
||||
{
|
||||
keys.map(key => (
|
||||
<div
|
||||
key={key}
|
||||
className='capitalize'
|
||||
>
|
||||
{getKeyboardKeyNameBySystem(key)}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(ShortcutsName)
|
@ -75,6 +75,27 @@ type Shape = {
|
||||
setShortcutsDisabled: (shortcutsDisabled: boolean) => void
|
||||
showDebugAndPreviewPanel: boolean
|
||||
setShowDebugAndPreviewPanel: (showDebugAndPreviewPanel: boolean) => void
|
||||
selection: null | { x1: number; y1: number; x2: number; y2: number }
|
||||
setSelection: (selection: Shape['selection']) => void
|
||||
bundleNodeSize: { width: number; height: number } | null
|
||||
setBundleNodeSize: (bundleNodeSize: Shape['bundleNodeSize']) => void
|
||||
controlMode: 'pointer' | 'hand'
|
||||
setControlMode: (controlMode: Shape['controlMode']) => void
|
||||
candidateNode?: Node
|
||||
setCandidateNode: (candidateNode?: Node) => void
|
||||
panelMenu?: {
|
||||
top: number
|
||||
left: number
|
||||
}
|
||||
setPanelMenu: (panelMenu: Shape['panelMenu']) => void
|
||||
nodeMenu?: {
|
||||
top: number
|
||||
left: number
|
||||
nodeId: string
|
||||
}
|
||||
setNodeMenu: (nodeMenu: Shape['nodeMenu']) => void
|
||||
mousePosition: { pageX: number; pageY: number; elementX: number; elementY: number }
|
||||
setMousePosition: (mousePosition: Shape['mousePosition']) => void
|
||||
}
|
||||
|
||||
export const createWorkflowStore = () => {
|
||||
@ -126,6 +147,23 @@ export const createWorkflowStore = () => {
|
||||
setShortcutsDisabled: shortcutsDisabled => set(() => ({ shortcutsDisabled })),
|
||||
showDebugAndPreviewPanel: false,
|
||||
setShowDebugAndPreviewPanel: showDebugAndPreviewPanel => set(() => ({ showDebugAndPreviewPanel })),
|
||||
selection: null,
|
||||
setSelection: selection => set(() => ({ selection })),
|
||||
bundleNodeSize: null,
|
||||
setBundleNodeSize: bundleNodeSize => set(() => ({ bundleNodeSize })),
|
||||
controlMode: localStorage.getItem('workflow-operation-mode') === 'pointer' ? 'pointer' : 'hand',
|
||||
setControlMode: (controlMode) => {
|
||||
set(() => ({ controlMode }))
|
||||
localStorage.setItem('workflow-operation-mode', controlMode)
|
||||
},
|
||||
candidateNode: undefined,
|
||||
setCandidateNode: candidateNode => set(() => ({ candidateNode })),
|
||||
panelMenu: undefined,
|
||||
setPanelMenu: panelMenu => set(() => ({ panelMenu })),
|
||||
nodeMenu: undefined,
|
||||
setNodeMenu: nodeMenu => set(() => ({ nodeMenu })),
|
||||
mousePosition: { pageX: 0, pageY: 0, elementX: 0, elementY: 0 },
|
||||
setMousePosition: mousePosition => set(() => ({ mousePosition })),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -4,4 +4,15 @@
|
||||
|
||||
.workflow-node-animation .react-flow__node {
|
||||
transition: transform 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#workflow-container .react-flow__nodesselection-rect {
|
||||
border: 1px solid #528BFF;
|
||||
background: rgba(21, 94, 239, 0.05);
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
#workflow-container .react-flow__selection {
|
||||
border: 1px solid #528BFF;
|
||||
background: rgba(21, 94, 239, 0.05);
|
||||
}
|
@ -37,6 +37,8 @@ export type CommonNodeType<T = {}> = {
|
||||
_isSingleRun?: boolean
|
||||
_runningStatus?: NodeRunningStatus
|
||||
_singleRunningStatus?: NodeRunningStatus
|
||||
_isCandidate?: boolean
|
||||
_isBundled?: boolean
|
||||
selected?: boolean
|
||||
title: string
|
||||
desc: string
|
||||
@ -48,6 +50,7 @@ export type CommonEdgeType = {
|
||||
_connectedNodeIsHovering?: boolean
|
||||
_connectedNodeIsSelected?: boolean
|
||||
_runned?: boolean
|
||||
_isBundled?: boolean
|
||||
sourceType: BlockEnum
|
||||
targetType: BlockEnum
|
||||
}
|
||||
|
@ -361,3 +361,48 @@ export const changeNodesAndEdgesId = (nodes: Node[], edges: Edge[]) => {
|
||||
|
||||
return [newNodes, newEdges] as [Node[], Edge[]]
|
||||
}
|
||||
|
||||
export const isMac = () => {
|
||||
return navigator.userAgent.toUpperCase().includes('MAC')
|
||||
}
|
||||
|
||||
const specialKeysNameMap: Record<string, string | undefined> = {
|
||||
ctrl: '⌘',
|
||||
alt: '⌥',
|
||||
}
|
||||
|
||||
export const getKeyboardKeyNameBySystem = (key: string) => {
|
||||
if (isMac())
|
||||
return specialKeysNameMap[key] || key
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
const specialKeysCodeMap: Record<string, string | undefined> = {
|
||||
ctrl: 'meta',
|
||||
}
|
||||
|
||||
export const getKeyboardKeyCodeBySystem = (key: string) => {
|
||||
if (isMac())
|
||||
return specialKeysCodeMap[key] || key
|
||||
|
||||
return key
|
||||
}
|
||||
|
||||
export const getTopLeftNodePosition = (nodes: Node[]) => {
|
||||
let minX = Infinity
|
||||
let minY = Infinity
|
||||
|
||||
nodes.forEach((node) => {
|
||||
if (node.position.x < minX)
|
||||
minX = node.position.x
|
||||
|
||||
if (node.position.y < minY)
|
||||
minY = node.position.y
|
||||
})
|
||||
|
||||
return {
|
||||
x: minX,
|
||||
y: minY,
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,12 @@ const translation = {
|
||||
jinjaEditorPlaceholder: 'Type \'/\' or \'{\' to insert variable',
|
||||
viewOnly: 'View Only',
|
||||
showRunHistory: 'Show Run History',
|
||||
copy: 'Copy',
|
||||
duplicate: 'Duplicate',
|
||||
addBlock: 'Add Block',
|
||||
pasteHere: 'Paste Here',
|
||||
pointerMode: 'Pointer Mode',
|
||||
handMode: 'Hand Mode',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} is required',
|
||||
|
@ -52,6 +52,12 @@ const translation = {
|
||||
jinjaEditorPlaceholder: '输入 “/” 或 “{” 插入变量',
|
||||
viewOnly: '只读',
|
||||
showRunHistory: '显示运行历史',
|
||||
copy: '拷贝',
|
||||
duplicate: '复制',
|
||||
addBlock: '添加节点',
|
||||
pasteHere: '粘贴到这里',
|
||||
pointerMode: '指针模式',
|
||||
handMode: '手模式',
|
||||
},
|
||||
errorMsg: {
|
||||
fieldRequired: '{{field}} 不能为空',
|
||||
|