mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 10:38:32 +08:00
refactor(web): normalize z-index layering (#38796)
This commit is contained in:
parent
3defd50709
commit
1246aa7f3a
@ -523,20 +523,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config/agent/agent-setting/index.tsx": {
|
||||
"jsx-a11y/click-events-have-key-events": {
|
||||
"count": 1
|
||||
},
|
||||
"jsx-a11y/no-static-element-interactions": {
|
||||
"count": 1
|
||||
},
|
||||
"react/set-state-in-effect": {
|
||||
"count": 1
|
||||
},
|
||||
"ts/no-explicit-any": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"web/app/components/app/configuration/config/agent/agent-tools/index.tsx": {
|
||||
"jsx-a11y/mouse-events-have-key-events": {
|
||||
"count": 2
|
||||
|
||||
@ -8,7 +8,7 @@ import AgentSettingButton from '../agent-setting-button'
|
||||
|
||||
let latestAgentSettingProps: any
|
||||
vi.mock('../agent/agent-setting', () => ({
|
||||
default: (props: any) => {
|
||||
AgentSetting: (props: any) => {
|
||||
latestAgentSettingProps = props
|
||||
return (
|
||||
<div data-testid="agent-setting">
|
||||
|
||||
@ -5,7 +5,7 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import AgentSetting from './agent/agent-setting'
|
||||
import { AgentSetting } from './agent/agent-setting'
|
||||
|
||||
type Props = Readonly<{
|
||||
isFunctionCall: boolean
|
||||
|
||||
@ -1,16 +1,7 @@
|
||||
import type { AgentConfig } from '@/models/debug'
|
||||
import { act, fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { MAX_ITERATIONS_NUM } from '@/config'
|
||||
import AgentSetting from '../index'
|
||||
|
||||
vi.mock('ahooks', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('ahooks')>()
|
||||
return {
|
||||
...actual,
|
||||
useClickAway: vi.fn(),
|
||||
}
|
||||
})
|
||||
import { AgentSetting } from '../index'
|
||||
|
||||
vi.mock('@langgenius/dify-ui/slider', () => ({
|
||||
Slider: (props: { className?: string, min?: number, max?: number, value: number, onValueChange: (value: number) => void }) => (
|
||||
@ -65,8 +56,8 @@ describe('AgentSetting', () => {
|
||||
})
|
||||
|
||||
it('should update iteration via slider and number input', () => {
|
||||
const { container } = renderModal()
|
||||
const slider = container.querySelector('.slider') as HTMLInputElement
|
||||
renderModal()
|
||||
const slider = screen.getByRole('slider')
|
||||
const numberInput = screen.getByRole('spinbutton')
|
||||
|
||||
fireEvent.change(slider, { target: { value: '7' } })
|
||||
@ -94,6 +85,34 @@ describe('AgentSetting', () => {
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call onCancel when Escape is pressed', () => {
|
||||
const { onCancel } = renderModal()
|
||||
|
||||
fireEvent.keyDown(document, { key: 'Escape' })
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onCancel when the backdrop is clicked', () => {
|
||||
const { onCancel } = renderModal()
|
||||
const dialog = screen.getByRole('dialog')
|
||||
|
||||
fireEvent.pointerDown(document.body, { target: document.body })
|
||||
fireEvent.mouseDown(document.body, { target: document.body })
|
||||
fireEvent.click(document.body, { target: document.body })
|
||||
|
||||
expect(dialog).toBeInTheDocument()
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onCancel when close button clicked', () => {
|
||||
const { onCancel } = renderModal()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.close' }))
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should call onSave with updated payload', async () => {
|
||||
const { onSave } = renderModal()
|
||||
const numberInput = screen.getByRole('spinbutton')
|
||||
|
||||
@ -1,13 +1,10 @@
|
||||
'use client'
|
||||
import type { FC } from 'react'
|
||||
import type { AgentConfig } from '@/models/debug'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { Dialog, DialogCloseButton, DialogContent, DialogTitle } from '@langgenius/dify-ui/dialog'
|
||||
import { Fieldset, FieldsetLegend } from '@langgenius/dify-ui/fieldset'
|
||||
import { Slider } from '@langgenius/dify-ui/slider'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useClickAway } from 'ahooks'
|
||||
import * as React from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
|
||||
import { Unblur } from '@/app/components/base/icons/src/vender/solid/education'
|
||||
@ -19,60 +16,43 @@ type Props = Readonly<{
|
||||
payload: AgentConfig
|
||||
isFunctionCall: boolean
|
||||
onCancel: () => void
|
||||
onSave: (payload: any) => void
|
||||
onSave: (payload: AgentConfig) => void
|
||||
}>
|
||||
|
||||
const maxIterationsMin = 1
|
||||
|
||||
const AgentSetting: FC<Props> = ({
|
||||
export function AgentSetting({
|
||||
isChatModel,
|
||||
payload,
|
||||
isFunctionCall,
|
||||
onCancel,
|
||||
onSave,
|
||||
}) => {
|
||||
}: Props) {
|
||||
const { t } = useTranslation()
|
||||
const [tempPayload, setTempPayload] = useState(payload)
|
||||
const ref = useRef(null)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const maximumIterationsLabel = t($ => $['agent.setting.maximumIterations.name'], { ns: 'appDebug' })
|
||||
|
||||
useClickAway(() => {
|
||||
if (mounted)
|
||||
onCancel()
|
||||
}, ref)
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
|
||||
const handleSave = () => {
|
||||
onSave(tempPayload)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-100 flex justify-end overflow-hidden p-2"
|
||||
style={{
|
||||
backgroundColor: 'rgba(16, 24, 40, 0.20)',
|
||||
<Dialog
|
||||
open
|
||||
onOpenChange={(open) => {
|
||||
if (!open)
|
||||
onCancel()
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className="flex h-full w-[640px] flex-col overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg shadow-xl"
|
||||
>
|
||||
<DialogContent className="top-2 right-2 bottom-2 left-auto flex h-auto max-h-none w-[640px] max-w-[calc(100vw-1rem)] translate-x-0 translate-y-0 flex-col overflow-hidden rounded-xl p-0">
|
||||
<div className="flex h-14 shrink-0 items-center justify-between border-b border-divider-regular pr-5 pl-6">
|
||||
<div className="flex flex-col text-base font-semibold text-text-primary">
|
||||
<div className="leading-6">{t($ => $['agent.setting.name'], { ns: 'appDebug' })}</div>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
onClick={onCancel}
|
||||
className="flex size-6 cursor-pointer items-center justify-center"
|
||||
>
|
||||
<RiCloseLine className="size-4 text-text-tertiary" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogTitle className="text-base leading-6 font-semibold text-text-primary">
|
||||
{t($ => $['agent.setting.name'], { ns: 'appDebug' })}
|
||||
</DialogTitle>
|
||||
<DialogCloseButton
|
||||
className="static z-auto size-6 shrink-0"
|
||||
aria-label={t($ => $['operation.close'], { ns: 'common' })}
|
||||
/>
|
||||
</div>
|
||||
{/* Body */}
|
||||
<div
|
||||
@ -158,20 +138,21 @@ const AgentSetting: FC<Props> = ({
|
||||
className="sticky bottom-0 z-5 flex w-full justify-end border-t border-divider-regular bg-background-section-burn px-6 py-4"
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="mr-2"
|
||||
>
|
||||
{t($ => $['operation.cancel'], { ns: 'common' })}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="primary"
|
||||
onClick={handleSave}
|
||||
>
|
||||
{t($ => $['operation.save'], { ns: 'common' })}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
export default React.memo(AgentSetting)
|
||||
|
||||
@ -540,7 +540,7 @@ const Flowchart = (props: FlowchartProps) => {
|
||||
|
||||
{svgString && (
|
||||
<div className={themeClasses.mermaidDiv} style={{ objectFit: 'cover' }} onClick={handlePreviewClick}>
|
||||
<div className="absolute bottom-2 left-2 z-100">
|
||||
<div className="absolute bottom-2 left-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
|
||||
@ -227,18 +227,6 @@ export default function AccountSetting({
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative flex min-h-0 w-[824px]">
|
||||
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="large"
|
||||
className="px-2"
|
||||
aria-label={t($ => $['operation.close'], { ns: 'common' })}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<span className="i-ri-close-line size-5" />
|
||||
</Button>
|
||||
<div className="mt-1 system-2xs-medium-uppercase text-text-tertiary">ESC</div>
|
||||
</div>
|
||||
<ScrollArea
|
||||
ref={scrollContainerRef}
|
||||
className="h-full min-h-0 flex-1 bg-components-panel-bg"
|
||||
@ -254,6 +242,18 @@ export default function AccountSetting({
|
||||
<div className="mt-1 system-sm-regular wrap-break-word whitespace-normal text-text-tertiary">{activeItem?.description}</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="fixed top-6 right-6 flex shrink-0 flex-col items-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="large"
|
||||
className="px-2"
|
||||
aria-label={t($ => $['operation.close'], { ns: 'common' })}
|
||||
onClick={handleClose}
|
||||
>
|
||||
<span className="i-ri-close-line size-5" />
|
||||
</Button>
|
||||
<div className="mt-1 system-2xs-medium-uppercase text-text-tertiary">ESC</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-4 pt-6 sm:px-8">
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PROVIDER && (
|
||||
|
||||
@ -29,9 +29,9 @@ const MenuDialog = ({
|
||||
}}
|
||||
>
|
||||
<DialogContent
|
||||
backdropClassName={cn('z-40 bg-transparent', backdropClassName)}
|
||||
backdropClassName={cn('bg-transparent', backdropClassName)}
|
||||
className={cn(
|
||||
'top-0 left-0 z-40 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
|
||||
'top-0 left-0 h-full max-h-none w-full max-w-none translate-x-0 translate-y-0 scale-100 overflow-hidden rounded-none border-none bg-background-sidenav-bg p-0 shadow-none backdrop-blur-md transition-opacity data-ending-style:scale-100 data-starting-style:scale-100',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
||||
@ -13,7 +13,6 @@ const ConfigurationButton = ({ modelProvider, handleOpenModal }: ConfigurationBu
|
||||
return (
|
||||
<Button
|
||||
size="small"
|
||||
className="z-100"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleOpenModal(modelProvider, ConfigurationMethodEnum.predefinedModel, undefined)
|
||||
|
||||
@ -54,7 +54,7 @@ const StatusIndicators = ({ needsConfiguration, modelProvider, inModelList, disa
|
||||
</div>
|
||||
)}
|
||||
{linkText && linkHref && (
|
||||
<div className="z-100 cursor-pointer body-xs-regular text-text-accent">
|
||||
<div className="cursor-pointer body-xs-regular text-text-accent">
|
||||
<Link
|
||||
href={linkHref}
|
||||
onClick={(e) => {
|
||||
|
||||
@ -60,7 +60,7 @@ const UpdateDSLModal = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
|
||||
<div className="absolute top-0 left-0 size-full bg-toast-warning-bg opacity-40" />
|
||||
<div className="pointer-events-none absolute top-0 left-0 size-full bg-toast-warning-bg opacity-40" />
|
||||
<div className="flex items-start justify-center p-1">
|
||||
<RiAlertFill className="size-4 shrink-0 text-text-warning-secondary" />
|
||||
</div>
|
||||
@ -70,7 +70,7 @@ const UpdateDSLModal = ({
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
className="z-1000"
|
||||
className="relative"
|
||||
onClick={onBackup}
|
||||
>
|
||||
<RiFileDownloadLine className="size-3.5 text-components-button-secondary-text" />
|
||||
|
||||
@ -41,7 +41,12 @@ export default function IntegrationsSettingModal({
|
||||
)}
|
||||
>
|
||||
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
|
||||
<div className="fixed top-6 right-6 z-9999 flex flex-col items-center">
|
||||
<IntegrationsPage
|
||||
section={section}
|
||||
onSectionChange={onSectionChange}
|
||||
onSwitchToMarketplace={handleSwitchToMarketplace}
|
||||
/>
|
||||
<div className="fixed top-6 right-6 flex shrink-0 flex-col items-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="large"
|
||||
@ -53,11 +58,6 @@ export default function IntegrationsSettingModal({
|
||||
</Button>
|
||||
<div className="mt-1 system-2xs-medium-uppercase text-text-tertiary">ESC</div>
|
||||
</div>
|
||||
<IntegrationsPage
|
||||
section={section}
|
||||
onSectionChange={onSectionChange}
|
||||
onSwitchToMarketplace={handleSwitchToMarketplace}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</MenuDialog>
|
||||
|
||||
@ -141,6 +141,7 @@ describe('CustomEdge', () => {
|
||||
expect(screen.getByTestId('block-selector').parentElement).toHaveStyle({
|
||||
transform: 'translate(-50%, -50%) translate(24px, 48px)',
|
||||
opacity: '0.7',
|
||||
zIndex: '1001',
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByTestId('block-selector'))
|
||||
|
||||
@ -15,8 +15,6 @@ describe('SyncingDataModal', () => {
|
||||
},
|
||||
})
|
||||
|
||||
const overlay = container.firstElementChild
|
||||
expect(overlay).toHaveClass('absolute', 'inset-0')
|
||||
expect(overlay).toHaveClass('z-9999')
|
||||
expect(container.firstElementChild).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { NESTED_ELEMENT_Z_INDEX } from '../../../constants'
|
||||
import { useInsertSnippet } from '../use-insert-snippet'
|
||||
|
||||
type TestNode = {
|
||||
@ -6,7 +7,9 @@ type TestNode = {
|
||||
position: { x: number, y: number }
|
||||
selected?: boolean
|
||||
parentId?: string
|
||||
zIndex?: number
|
||||
data: {
|
||||
type?: string
|
||||
selected?: boolean
|
||||
_children?: { nodeId: string, nodeType: string }[]
|
||||
_connectedSourceHandleIds?: string[]
|
||||
@ -20,6 +23,13 @@ type TestEdge = {
|
||||
sourceHandle?: string
|
||||
target: string
|
||||
targetHandle?: string
|
||||
zIndex?: number
|
||||
data?: {
|
||||
isInIteration?: boolean
|
||||
iteration_id?: string
|
||||
isInLoop?: boolean
|
||||
loop_id?: string
|
||||
}
|
||||
}
|
||||
|
||||
const mockFetchQuery = vi.fn()
|
||||
@ -93,14 +103,16 @@ describe('useInsertSnippet', () => {
|
||||
nodes: [
|
||||
{
|
||||
id: 'snippet-node-1',
|
||||
zIndex: 1,
|
||||
position: { x: 10, y: 20 },
|
||||
data: { selected: false, _children: [{ nodeId: 'snippet-node-2', nodeType: 'code' }] },
|
||||
data: { type: 'iteration', selected: false, _children: [{ nodeId: 'snippet-node-2', nodeType: 'code' }] },
|
||||
},
|
||||
{
|
||||
id: 'snippet-node-2',
|
||||
parentId: 'snippet-node-1',
|
||||
zIndex: 1002,
|
||||
position: { x: 30, y: 40 },
|
||||
data: { selected: false },
|
||||
data: { type: 'code', selected: false },
|
||||
},
|
||||
],
|
||||
edges: [
|
||||
@ -110,6 +122,7 @@ describe('useInsertSnippet', () => {
|
||||
sourceHandle: 'source',
|
||||
target: 'snippet-node-2',
|
||||
targetHandle: 'target',
|
||||
zIndex: 1002,
|
||||
data: {},
|
||||
},
|
||||
],
|
||||
@ -132,12 +145,15 @@ describe('useInsertSnippet', () => {
|
||||
expect(nextNodes).toHaveLength(3)
|
||||
expect(nextNodes[1]!.id).not.toBe('snippet-node-1')
|
||||
expect(nextNodes[2]!.parentId).toBe(nextNodes[1]!.id)
|
||||
expect(nextNodes[1]!.zIndex).toBe(0)
|
||||
expect(nextNodes[2]!.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(nextNodes[1]!.data._children![0]!.nodeId).toBe(nextNodes[2]!.id)
|
||||
|
||||
const nextEdges = mockSetEdges.mock.calls[0]![0] as TestEdge[]
|
||||
expect(nextEdges).toHaveLength(2)
|
||||
expect(nextEdges[1]!.source).toBe(nextNodes[1]!.id)
|
||||
expect(nextEdges[1]!.target).toBe(nextNodes[2]!.id)
|
||||
expect(nextEdges[1]!.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
|
||||
expect(mockSaveStateToHistory).toHaveBeenCalledWith('NodePaste', {
|
||||
nodeId: nextNodes[1]!.id,
|
||||
@ -148,18 +164,32 @@ describe('useInsertSnippet', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('should connect inserted snippet nodes to the requested edge position', async () => {
|
||||
it.each(['iteration', 'loop'] as const)('should connect inserted snippet nodes inside the %s container', async (containerType) => {
|
||||
mockGetNodes.mockReturnValue([
|
||||
{
|
||||
id: 'container-node',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
type: containerType,
|
||||
selected: false,
|
||||
_children: [
|
||||
{ nodeId: 'prev-node', nodeType: 'code' },
|
||||
{ nodeId: 'next-node', nodeType: 'code' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'prev-node',
|
||||
parentId: 'container-node',
|
||||
position: { x: 0, y: 0 },
|
||||
width: 240,
|
||||
data: { type: 'start', selected: true, _connectedSourceHandleIds: ['source'] },
|
||||
data: { type: 'code', selected: true, _connectedSourceHandleIds: ['source'] },
|
||||
},
|
||||
{
|
||||
id: 'next-node',
|
||||
parentId: 'container-node',
|
||||
position: { x: 300, y: 0 },
|
||||
data: { type: 'answer', selected: false, _connectedTargetHandleIds: ['target'] },
|
||||
data: { type: 'code', selected: false, _connectedTargetHandleIds: ['target'] },
|
||||
},
|
||||
])
|
||||
mockEdges = [
|
||||
@ -170,8 +200,8 @@ describe('useInsertSnippet', () => {
|
||||
target: 'next-node',
|
||||
targetHandle: 'target',
|
||||
data: {
|
||||
sourceType: 'start',
|
||||
targetType: 'answer',
|
||||
sourceType: 'code',
|
||||
targetType: 'code',
|
||||
},
|
||||
},
|
||||
]
|
||||
@ -221,6 +251,8 @@ describe('useInsertSnippet', () => {
|
||||
const insertedExit = nextNodes.find(node => node.id !== 'prev-node' && node.id !== 'next-node' && node.id.includes('snippet-exit'))!
|
||||
const shiftedNextNode = nextNodes.find(node => node.id === 'next-node')!
|
||||
expect(insertedEntry.position).toEqual({ x: 300, y: 0 })
|
||||
expect(insertedEntry.parentId).toBe('container-node')
|
||||
expect(insertedExit.parentId).toBe('container-node')
|
||||
expect(shiftedNextNode.position.x).toBe(600)
|
||||
expect(nextNodes.find(node => node.id === 'prev-node')!.data._connectedSourceHandleIds).toEqual(['source'])
|
||||
expect(insertedEntry.data._connectedTargetHandleIds).toEqual(['target'])
|
||||
@ -248,6 +280,18 @@ describe('useInsertSnippet', () => {
|
||||
targetHandle: 'target',
|
||||
}),
|
||||
]))
|
||||
const incomingEdge = nextEdges.find(edge => edge.source === 'prev-node' && edge.target === insertedEntry.id)
|
||||
const insertedInternalEdge = nextEdges.find(edge => edge.source === insertedEntry.id && edge.target === insertedExit.id)
|
||||
const outgoingEdge = nextEdges.find(edge => edge.source === insertedExit.id && edge.target === 'next-node')
|
||||
expect(incomingEdge?.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(insertedInternalEdge?.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(outgoingEdge?.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(insertedInternalEdge?.data).toEqual(expect.objectContaining({
|
||||
isInIteration: containerType === 'iteration',
|
||||
iteration_id: containerType === 'iteration' ? 'container-node' : undefined,
|
||||
isInLoop: containerType === 'loop',
|
||||
loop_id: containerType === 'loop' ? 'container-node' : undefined,
|
||||
}))
|
||||
expect(mockIncrementSnippetUseCount).toHaveBeenCalledWith({
|
||||
params: { snippetId: 'snippet-1' },
|
||||
})
|
||||
|
||||
@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'
|
||||
import { useStoreApi } from 'reactflow'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useIncrementSnippetUseCountMutation } from '@/service/use-snippets'
|
||||
import { CUSTOM_EDGE, ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX, NODE_WIDTH_X_OFFSET, X_OFFSET } from '../../constants'
|
||||
import { CUSTOM_EDGE, NESTED_ELEMENT_Z_INDEX, NODE_WIDTH_X_OFFSET, X_OFFSET } from '../../constants'
|
||||
import { useNodesSyncDraft, useWorkflowHistory, WorkflowHistoryEvent } from '../../hooks'
|
||||
import { BlockEnum } from '../../types'
|
||||
import { getNodesConnectedSourceOrTargetHandleIdsMap } from '../../utils'
|
||||
@ -212,9 +212,7 @@ const createBoundaryEdges = ({
|
||||
const parentNode = getParentNode(currentNodes, insertPayload)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = parentNode?.data.type === BlockEnum.Loop
|
||||
const zIndex = parentNode
|
||||
? isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
: 0
|
||||
const zIndex = parentNode ? NESTED_ELEMENT_Z_INDEX : 0
|
||||
const incomingEdges: Edge[] = []
|
||||
const outgoingEdges: Edge[] = []
|
||||
|
||||
@ -315,6 +313,7 @@ export const useInsertSnippet = () => {
|
||||
changes,
|
||||
[...currentNodes, ...remappedGraph.nodes],
|
||||
)
|
||||
const remappedNodesById = new Map(remappedGraph.nodes.map(node => [node.id, node]))
|
||||
const firstEntryNode = entryNodes.find(canConnectToTarget) ?? entryNodes[0]
|
||||
const clearedNodes = currentNodes.map(node => ({
|
||||
...node,
|
||||
@ -354,14 +353,15 @@ export const useInsertSnippet = () => {
|
||||
const shouldMoveIntoParent = !!parentNode && rootNodeIds.has(node.id)
|
||||
const isInIteration = parentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = parentNode?.data.type === BlockEnum.Loop
|
||||
const snippetParentNode = node.parentId ? remappedNodesById.get(node.parentId) : undefined
|
||||
const isNestedInSnippet = snippetParentNode?.data.type === BlockEnum.Iteration
|
||||
|| snippetParentNode?.data.type === BlockEnum.Loop
|
||||
|
||||
return {
|
||||
...node,
|
||||
parentId: shouldMoveIntoParent ? parentNode.id : node.parentId,
|
||||
extent: shouldMoveIntoParent ? parentNode.extent : node.extent,
|
||||
zIndex: shouldMoveIntoParent
|
||||
? isInIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
: node.zIndex,
|
||||
zIndex: shouldMoveIntoParent || isNestedInSnippet ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
data: {
|
||||
...node.data,
|
||||
...(nodesConnectedSourceOrTargetHandleIdsMap[node.id] ?? {}),
|
||||
@ -372,8 +372,31 @@ export const useInsertSnippet = () => {
|
||||
},
|
||||
}
|
||||
})
|
||||
const nextNodes = [...clearedNodes, ...insertedNodes]
|
||||
const finalNodesById = new Map(nextNodes.map(node => [node.id, node]))
|
||||
const insertedEdges = remappedGraph.edges.map((edge) => {
|
||||
const sourceNode = finalNodesById.get(edge.source)
|
||||
const targetNode = finalNodesById.get(edge.target)
|
||||
const sourceParentNode = sourceNode?.parentId ? finalNodesById.get(sourceNode.parentId) : undefined
|
||||
const targetParentNode = targetNode?.parentId ? finalNodesById.get(targetNode.parentId) : undefined
|
||||
const nestedParentNode = [sourceParentNode, targetParentNode].find(node => node?.data.type === BlockEnum.Iteration || node?.data.type === BlockEnum.Loop)
|
||||
const isInIteration = nestedParentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = nestedParentNode?.data.type === BlockEnum.Loop
|
||||
|
||||
setNodes([...clearedNodes, ...insertedNodes])
|
||||
return {
|
||||
...edge,
|
||||
data: {
|
||||
...edge.data,
|
||||
isInIteration,
|
||||
iteration_id: isInIteration ? nestedParentNode.id : undefined,
|
||||
isInLoop,
|
||||
loop_id: isInLoop ? nestedParentNode.id : undefined,
|
||||
},
|
||||
zIndex: nestedParentNode ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
})
|
||||
|
||||
setNodes(nextNodes)
|
||||
setEdges([
|
||||
...edges
|
||||
.filter(edge => edge.id !== currentEdge?.id)
|
||||
@ -384,7 +407,7 @@ export const useInsertSnippet = () => {
|
||||
_connectedNodeIsSelected: false,
|
||||
},
|
||||
})),
|
||||
...remappedGraph.edges,
|
||||
...insertedEdges,
|
||||
...boundaryEdges,
|
||||
])
|
||||
saveStateToHistory(WorkflowHistoryEvent.NodePaste, {
|
||||
|
||||
@ -135,7 +135,7 @@ export const CommentInput: FC<CommentInputProps> = memo(({
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute z-60 w-96',
|
||||
'absolute z-40 w-96',
|
||||
disabled && 'pointer-events-none opacity-80',
|
||||
)}
|
||||
style={{
|
||||
|
||||
@ -14,7 +14,7 @@ export const CommentCursor: FC = memo(() => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="pointer-events-none absolute z-50 flex size-6 items-center justify-center"
|
||||
className="pointer-events-none absolute z-30 flex size-6 items-center justify-center"
|
||||
style={{
|
||||
left: mousePosition.elementX,
|
||||
top: mousePosition.elementY,
|
||||
|
||||
@ -622,7 +622,7 @@ const MentionInputInner = forwardRef<HTMLTextAreaElement, MentionInputProps>(({
|
||||
|
||||
{showMentionDropdown && filteredMentionUsers.length > 0 && typeof document !== 'undefined' && createPortal(
|
||||
<div
|
||||
className="fixed z-9999 max-h-[248px] w-[280px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg/95 shadow-lg backdrop-blur-[10px]"
|
||||
className="fixed z-50 max-h-[248px] w-[280px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg/95 shadow-lg backdrop-blur-[10px]"
|
||||
style={{
|
||||
left: dropdownPosition.x,
|
||||
[dropdownPosition.placement === 'top' ? 'bottom' : 'top']: dropdownPosition.placement === 'top'
|
||||
|
||||
@ -414,7 +414,7 @@ export const CommentThread: FC<CommentThreadProps> = memo(({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="absolute z-50 w-[360px] max-w-[360px]"
|
||||
className="absolute z-30 w-[360px] max-w-[360px]"
|
||||
style={{
|
||||
left: canvasPosition.x + 40,
|
||||
top: canvasPosition.y,
|
||||
|
||||
@ -9,8 +9,8 @@ export const X_OFFSET = 60
|
||||
export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET
|
||||
export const Y_OFFSET = 39
|
||||
export const START_INITIAL_POSITION = { x: 80, y: 282 }
|
||||
export const ITERATION_NODE_Z_INDEX = 1
|
||||
export const ITERATION_CHILDREN_Z_INDEX = 1002
|
||||
// React Flow elevates selected nodes by 1000, so nested graph elements use the next layer.
|
||||
export const NESTED_ELEMENT_Z_INDEX = 1001
|
||||
export const ITERATION_PADDING = {
|
||||
top: 65,
|
||||
right: 16,
|
||||
@ -18,8 +18,6 @@ export const ITERATION_PADDING = {
|
||||
left: 16,
|
||||
}
|
||||
|
||||
export const LOOP_NODE_Z_INDEX = 1
|
||||
export const LOOP_CHILDREN_Z_INDEX = 1002
|
||||
export const LOOP_PADDING = {
|
||||
top: 65,
|
||||
right: 16,
|
||||
|
||||
@ -3,7 +3,6 @@ import type {
|
||||
Edge,
|
||||
OnSelectBlock,
|
||||
} from './types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { intersection } from 'es-toolkit/array'
|
||||
import {
|
||||
memo,
|
||||
@ -19,7 +18,7 @@ import {
|
||||
} from 'reactflow'
|
||||
import { ErrorHandleTypeEnum } from '@/app/components/workflow/nodes/_base/components/error-handle/types'
|
||||
import BlockSelector from './block-selector'
|
||||
import { ITERATION_CHILDREN_Z_INDEX, LOOP_CHILDREN_Z_INDEX } from './constants'
|
||||
import { NESTED_ELEMENT_Z_INDEX } from './constants'
|
||||
import CustomEdgeLinearGradientRender from './custom-edge-linear-gradient-render'
|
||||
import {
|
||||
useAvailableBlocks,
|
||||
@ -143,17 +142,15 @@ const CustomEdge = ({
|
||||
/>
|
||||
<EdgeLabelRenderer>
|
||||
<div
|
||||
className={cn(
|
||||
'nopan nodrag',
|
||||
'transition-opacity duration-150',
|
||||
data.isInIteration && `z-[${ITERATION_CHILDREN_Z_INDEX}]`,
|
||||
data.isInLoop && `z-[${LOOP_CHILDREN_Z_INDEX}]`,
|
||||
)}
|
||||
className="nopan nodrag transition-opacity duration-150"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
transform: `translate(-50%, -50%) translate(${labelX}px, ${labelY}px)`,
|
||||
pointerEvents: isTriggerVisible ? 'all' : 'none',
|
||||
opacity: isTriggerVisible ? (data._waitingRun ? 0.7 : 1) : 0,
|
||||
zIndex: data.isInIteration || data.isInLoop
|
||||
? NESTED_ELEMENT_Z_INDEX
|
||||
: undefined,
|
||||
}}
|
||||
onMouseEnter={() => setIsTriggerHovered(true)}
|
||||
onMouseLeave={() => setIsTriggerHovered(false)}
|
||||
|
||||
@ -1112,6 +1112,7 @@ describe('useNodesInteractions', () => {
|
||||
createNode({
|
||||
id: nodeId,
|
||||
position: { x: 120, y: 120 },
|
||||
zIndex: 1002,
|
||||
data: {
|
||||
type: containerType,
|
||||
title: containerType === BlockEnum.Iteration ? 'Iteration' : 'Loop',
|
||||
@ -1136,6 +1137,7 @@ describe('useNodesInteractions', () => {
|
||||
|
||||
expect(newContainer).toBeDefined()
|
||||
expect(newContainer?.parentId).toBeUndefined()
|
||||
expect(newContainer?.zIndex).toBe(0)
|
||||
expect(newContainer?.data.isInIteration).toBeFalsy()
|
||||
expect(newContainer?.data.isInLoop).toBeFalsy()
|
||||
})
|
||||
|
||||
@ -27,10 +27,9 @@ import { consoleQuery } from '@/service/client'
|
||||
import { collaborationManager } from '../collaboration/core/collaboration-manager'
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
ITERATION_PADDING,
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_PADDING,
|
||||
NESTED_ELEMENT_Z_INDEX,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
X_OFFSET,
|
||||
Y_OFFSET,
|
||||
@ -597,11 +596,7 @@ export const useNodesInteractions = () => {
|
||||
isInLoop,
|
||||
loop_id: isInLoop ? targetNode?.parentId : undefined,
|
||||
},
|
||||
zIndex: targetNode?.parentId
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: targetNode?.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap
|
||||
= getNodesConnectedSourceOrTargetHandleIdsMap(
|
||||
@ -998,11 +993,11 @@ export const useNodesInteractions = () => {
|
||||
newNode.data.isInLoop = isInLoop
|
||||
if (isInIteration) {
|
||||
newNode.data.iteration_id = parentNode.id
|
||||
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
if (isInLoop) {
|
||||
newNode.data.loop_id = parentNode.id
|
||||
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
if (
|
||||
isInIteration
|
||||
@ -1042,11 +1037,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isInLoop ? prevNode!.parentId : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: prevNode!.parentId
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: prevNode!.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1156,11 +1147,11 @@ export const useNodesInteractions = () => {
|
||||
newNode.data.isInLoop = isInLoop
|
||||
if (isInIteration) {
|
||||
newNode.data.iteration_id = parentNode.id
|
||||
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
if (isInLoop) {
|
||||
newNode.data.loop_id = parentNode.id
|
||||
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
}
|
||||
|
||||
@ -1188,11 +1179,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isInLoop ? nextNode.parentId : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: nextNode.parentId
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: nextNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1309,11 +1296,11 @@ export const useNodesInteractions = () => {
|
||||
newNode.data.isInLoop = isInLoop
|
||||
if (isInIteration) {
|
||||
newNode.data.iteration_id = parentNode.id
|
||||
newNode.zIndex = ITERATION_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
if (isInLoop) {
|
||||
newNode.data.loop_id = parentNode.id
|
||||
newNode.zIndex = LOOP_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
}
|
||||
}
|
||||
|
||||
@ -1339,11 +1326,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isInLoop ? prevNode.parentId : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: prevNode.parentId
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: prevNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1382,11 +1365,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isNextNodeInLoop ? nextNode.parentId : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: nextNode.parentId
|
||||
? isNextNodeInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: nextNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
}
|
||||
const nodesConnectedSourceOrTargetHandleIdsMap
|
||||
@ -1627,11 +1606,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isInLoop ? targetNodeForEdge.parentId : undefined,
|
||||
_connectedNodeIsSelected: false,
|
||||
},
|
||||
zIndex: targetNodeForEdge.parentId
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: targetNodeForEdge.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
})
|
||||
}
|
||||
|
||||
@ -1667,11 +1642,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: newNodeIsInLoop ? newCurrentNode.parentId : undefined,
|
||||
_connectedNodeIsSelected: false,
|
||||
},
|
||||
zIndex: newCurrentNode.parentId
|
||||
? newNodeIsInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: LOOP_CHILDREN_Z_INDEX
|
||||
: 0,
|
||||
zIndex: newCurrentNode.parentId ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
})
|
||||
}
|
||||
|
||||
@ -2075,7 +2046,7 @@ export const useNodesInteractions = () => {
|
||||
y: nodeToPaste.position.y + offsetY,
|
||||
},
|
||||
extent: nodeToPaste.extent,
|
||||
zIndex: nodeToPaste.zIndex,
|
||||
zIndex: 0,
|
||||
})
|
||||
newNode.id = newNode.id + index
|
||||
|
||||
@ -2145,7 +2116,7 @@ export const useNodesInteractions = () => {
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNode.id,
|
||||
extent: child.extent,
|
||||
zIndex: ITERATION_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
})
|
||||
newChild.id = `${newNode.id}${newChild.id + childIndex}`
|
||||
idMapping[child.id] = newChild.id
|
||||
@ -2245,7 +2216,7 @@ export const useNodesInteractions = () => {
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNode.id,
|
||||
extent: child.extent,
|
||||
zIndex: LOOP_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
})
|
||||
newChild.id = `${newNode.id}${newChild.id + childIndex}`
|
||||
idMapping[child.id] = newChild.id
|
||||
@ -2290,7 +2261,7 @@ export const useNodesInteractions = () => {
|
||||
newNode.data.loop_id = !isIteration ? selectedContainerNode.id : undefined
|
||||
|
||||
newNode.parentId = selectedContainerNode.id
|
||||
newNode.zIndex = isIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX
|
||||
newNode.zIndex = NESTED_ELEMENT_Z_INDEX
|
||||
newNode.positionAbsolute = {
|
||||
x: newNode.position.x,
|
||||
y: newNode.position.y,
|
||||
@ -2348,13 +2319,7 @@ export const useNodesInteractions = () => {
|
||||
loop_id: isInLoop ? parentNode?.id : undefined,
|
||||
_connectedNodeIsSelected: false,
|
||||
},
|
||||
zIndex: parentNode
|
||||
? isInIteration
|
||||
? ITERATION_CHILDREN_Z_INDEX
|
||||
: isInLoop
|
||||
? LOOP_CHILDREN_Z_INDEX
|
||||
: 0
|
||||
: 0,
|
||||
zIndex: parentNode && (isInIteration || isInLoop) ? NESTED_ELEMENT_Z_INDEX : 0,
|
||||
}
|
||||
edgesToPaste.push(newEdge)
|
||||
}
|
||||
|
||||
@ -73,7 +73,6 @@ import { CommentThread } from './comment/thread'
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
CUSTOM_NODE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
WORKFLOW_DATA_UPDATE,
|
||||
} from './constants'
|
||||
import CustomConnectionLine from './custom-connection-line'
|
||||
@ -618,13 +617,12 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
<div
|
||||
id="workflow-container"
|
||||
className={cn(
|
||||
'relative h-full w-full min-w-[960px] overflow-hidden',
|
||||
'relative isolate h-full w-full min-w-[960px] overflow-hidden',
|
||||
workflowReadOnly && 'workflow-panel-animation',
|
||||
nodeAnimation && 'workflow-node-animation',
|
||||
)}
|
||||
ref={workflowContainerRef}
|
||||
>
|
||||
<SyncingDataModal />
|
||||
<CandidateNode />
|
||||
<CommentManager />
|
||||
<div
|
||||
@ -750,8 +748,6 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
onPaneContextMenu={handlePaneContextMenu}
|
||||
onSelectionContextMenu={handleSelectionContextMenu}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
// NOTE: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same?
|
||||
connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
|
||||
defaultViewport={viewport}
|
||||
multiSelectionKeyCode={null}
|
||||
deleteKeyCode={null}
|
||||
@ -785,6 +781,7 @@ export const Workflow: FC<WorkflowProps> = memo(({
|
||||
)}
|
||||
</ReactFlow>
|
||||
</WorkflowContextmenu>
|
||||
<SyncingDataModal />
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
@ -151,12 +151,10 @@ const CodeEditor: FC<Props> = ({
|
||||
{isShowVarPicker && createPortal(
|
||||
<div
|
||||
ref={popupRef}
|
||||
className="w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg"
|
||||
className="fixed z-50 w-[228px] space-y-1 rounded-lg border border-components-panel-border bg-components-panel-bg p-1 shadow-lg"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
top: popupPosition.y,
|
||||
left: popupPosition.x,
|
||||
zIndex: 100,
|
||||
}}
|
||||
>
|
||||
<VarReferenceVars
|
||||
|
||||
@ -85,7 +85,7 @@ describe('iteration interaction helpers', () => {
|
||||
position: { x: 12, y: 24 },
|
||||
positionAbsolute: { x: 12, y: 24 },
|
||||
extent: 'parent',
|
||||
zIndex: 7,
|
||||
zIndex: 1002,
|
||||
data: { type: BlockEnum.Code, title: 'Original', desc: 'child', selected: true },
|
||||
})
|
||||
|
||||
@ -99,7 +99,7 @@ describe('iteration interaction helpers', () => {
|
||||
|
||||
expect(result).toEqual(expect.objectContaining({
|
||||
parentId: 'iteration-2',
|
||||
zIndex: 7,
|
||||
zIndex: 1001,
|
||||
data: expect.objectContaining({
|
||||
title: 'blocks.code 3',
|
||||
iteration_id: 'iteration-2',
|
||||
|
||||
@ -5,6 +5,7 @@ import type {
|
||||
} from '../../types'
|
||||
import {
|
||||
ITERATION_PADDING,
|
||||
NESTED_ELEMENT_Z_INDEX,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../iteration-start/constants'
|
||||
|
||||
@ -108,6 +109,6 @@ export const buildIterationChildCopy = ({
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: child.zIndex,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,7 +87,7 @@ describe('loop interaction helpers', () => {
|
||||
expect(result.newId).toBe('loop-23')
|
||||
expect(result.params).toEqual(expect.objectContaining({
|
||||
parentId: 'loop-2',
|
||||
zIndex: 1002,
|
||||
zIndex: 1001,
|
||||
data: expect.objectContaining({
|
||||
title: 'Code 3',
|
||||
isInLoop: true,
|
||||
|
||||
@ -3,8 +3,8 @@ import type {
|
||||
Node,
|
||||
} from '../../types'
|
||||
import {
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_PADDING,
|
||||
NESTED_ELEMENT_Z_INDEX,
|
||||
} from '../../constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../loop-start/constants'
|
||||
|
||||
@ -99,7 +99,7 @@ export const buildLoopChildCopy = ({
|
||||
positionAbsolute: child.positionAbsolute,
|
||||
parentId: newNodeId,
|
||||
extent: child.extent,
|
||||
zIndex: LOOP_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -24,7 +24,7 @@
|
||||
}
|
||||
|
||||
#workflow-container .react-flow__node-custom-note {
|
||||
z-index: -1000 !important;
|
||||
z-index: -1 !important;
|
||||
}
|
||||
|
||||
#workflow-container .react-flow__attribution {
|
||||
|
||||
@ -7,7 +7,7 @@ const SyncingDataModal = () => {
|
||||
return null
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 z-9999">
|
||||
<div className="absolute inset-0 z-50">
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -220,7 +220,7 @@ const UpdateDSLModal = ({
|
||||
</button>
|
||||
</div>
|
||||
<div className="relative mb-2 flex grow gap-0.5 overflow-hidden rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-2 shadow-xs">
|
||||
<div className="absolute top-0 left-0 size-full bg-toast-warning-bg opacity-40" />
|
||||
<div className="pointer-events-none absolute top-0 left-0 size-full bg-toast-warning-bg opacity-40" />
|
||||
<div className="flex items-start justify-center p-1">
|
||||
<RiAlertFill className="size-4 shrink-0 text-text-warning-secondary" />
|
||||
</div>
|
||||
@ -230,7 +230,7 @@ const UpdateDSLModal = ({
|
||||
<Button
|
||||
size="small"
|
||||
variant="secondary"
|
||||
className="z-1000"
|
||||
className="relative"
|
||||
onClick={onBackup}
|
||||
>
|
||||
<RiFileDownloadLine className="size-3.5 text-components-button-secondary-text" />
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { IterationNodeType } from '../../nodes/iteration/types'
|
||||
import type { LoopNodeType } from '../../nodes/loop/types'
|
||||
import type { CommonNodeType, Node } from '../../types'
|
||||
import { CUSTOM_NODE, ITERATION_CHILDREN_Z_INDEX, ITERATION_NODE_Z_INDEX, LOOP_CHILDREN_Z_INDEX, LOOP_NODE_Z_INDEX } from '../../constants'
|
||||
import { CUSTOM_NODE, NESTED_ELEMENT_Z_INDEX } from '../../constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../../nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '../../nodes/loop-start/constants'
|
||||
import { CUSTOM_SIMPLE_NODE } from '../../simple-node/constants'
|
||||
@ -18,6 +18,13 @@ import {
|
||||
hasRetryNode,
|
||||
} from '../node'
|
||||
|
||||
describe('nested workflow node layering', () => {
|
||||
it('should keep nested elements above React Flow selected nodes', () => {
|
||||
expect(NESTED_ELEMENT_Z_INDEX).toBe(1001)
|
||||
expect(NESTED_ELEMENT_Z_INDEX).toBeGreaterThan(1000)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNodeCatalogType', () => {
|
||||
it('should use Agent V2 catalog type for graph agent nodes with the Agent V2 discriminator', () => {
|
||||
expect(getNodeCatalogType({
|
||||
@ -67,22 +74,22 @@ describe('generateNewNode', () => {
|
||||
expect(newNode.id).toBe('custom-id')
|
||||
})
|
||||
|
||||
it('should set ITERATION_NODE_Z_INDEX for iteration nodes', () => {
|
||||
it('should use the default layer for iteration nodes', () => {
|
||||
const { newNode } = generateNewNode({
|
||||
data: { title: 'Iter', desc: '', type: BlockEnum.Iteration } as CommonNodeType,
|
||||
position: { x: 0, y: 0 },
|
||||
})
|
||||
|
||||
expect(newNode.zIndex).toBe(ITERATION_NODE_Z_INDEX)
|
||||
expect(newNode.zIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should set LOOP_NODE_Z_INDEX for loop nodes', () => {
|
||||
it('should use the default layer for loop nodes', () => {
|
||||
const { newNode } = generateNewNode({
|
||||
data: { title: 'Loop', desc: '', type: BlockEnum.Loop } as CommonNodeType,
|
||||
position: { x: 0, y: 0 },
|
||||
})
|
||||
|
||||
expect(newNode.zIndex).toBe(LOOP_NODE_Z_INDEX)
|
||||
expect(newNode.zIndex).toBe(0)
|
||||
})
|
||||
|
||||
it('should create an iteration start node for iteration type', () => {
|
||||
@ -139,7 +146,7 @@ describe('getIterationStartNode', () => {
|
||||
expect(node.parentId).toBe('parent-iter')
|
||||
expect(node.selectable).toBe(false)
|
||||
expect(node.draggable).toBe(false)
|
||||
expect(node.zIndex).toBe(ITERATION_CHILDREN_Z_INDEX)
|
||||
expect(node.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(node.position).toEqual({ x: 24, y: 68 })
|
||||
})
|
||||
})
|
||||
@ -155,7 +162,7 @@ describe('getLoopStartNode', () => {
|
||||
expect(node.parentId).toBe('parent-loop')
|
||||
expect(node.selectable).toBe(false)
|
||||
expect(node.draggable).toBe(false)
|
||||
expect(node.zIndex).toBe(LOOP_CHILDREN_Z_INDEX)
|
||||
expect(node.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(node.position).toEqual({ x: 24, y: 68 })
|
||||
})
|
||||
})
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
Edge,
|
||||
Node,
|
||||
} from '@/app/components/workflow/types'
|
||||
import { CUSTOM_NODE, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_MAX } from '@/app/components/workflow/constants'
|
||||
import { CUSTOM_NODE, DEFAULT_RETRY_INTERVAL, DEFAULT_RETRY_MAX, NESTED_ELEMENT_Z_INDEX } from '@/app/components/workflow/constants'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||
import { BlockEnum, ErrorHandleMode } from '@/app/components/workflow/types'
|
||||
@ -186,6 +186,37 @@ describe('preprocessNodesAndEdges', () => {
|
||||
})
|
||||
|
||||
describe('initialNodes', () => {
|
||||
it('should normalize legacy container and nested node layers regardless of node order', () => {
|
||||
const nodes = [
|
||||
createNode({
|
||||
id: 'child-1',
|
||||
parentId: 'iter-1',
|
||||
zIndex: 1002,
|
||||
data: { type: BlockEnum.Code, title: '', desc: '' },
|
||||
}),
|
||||
createNode({
|
||||
id: 'iter-1',
|
||||
selected: true,
|
||||
zIndex: 1,
|
||||
data: { type: BlockEnum.Iteration, title: '', desc: '' },
|
||||
}),
|
||||
createNode({
|
||||
id: 'root-1',
|
||||
zIndex: 1002,
|
||||
data: { type: BlockEnum.Code, title: '', desc: '' },
|
||||
}),
|
||||
]
|
||||
|
||||
const result = initialNodes(nodes, [])
|
||||
|
||||
const parentNode = result.find(node => node.id === 'iter-1')!
|
||||
const childNode = result.find(node => node.id === 'child-1')!
|
||||
expect(parentNode.zIndex).toBe(0)
|
||||
expect(childNode.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(result.find(node => node.id === 'root-1')!.zIndex).toBe(0)
|
||||
expect(parentNode.zIndex! + 1000).toBeLessThan(childNode.zIndex!)
|
||||
})
|
||||
|
||||
it('should set positions when first node has no position', () => {
|
||||
const nodes = [
|
||||
createNode({ id: 'n1', data: { type: BlockEnum.Start, title: '', desc: '' } }),
|
||||
@ -564,6 +595,44 @@ describe('initialNodes', () => {
|
||||
})
|
||||
|
||||
describe('initialEdges', () => {
|
||||
it('should normalize legacy nested and root edge layers', () => {
|
||||
const nodes = [
|
||||
createNode({ id: 'iter-1', data: { type: BlockEnum.Iteration, title: '', desc: '' } }),
|
||||
createNode({ id: 'child-1', parentId: 'iter-1', data: { type: BlockEnum.Code, title: '', desc: '' } }),
|
||||
createNode({ id: 'child-2', parentId: 'iter-1', data: { type: BlockEnum.Code, title: '', desc: '' } }),
|
||||
createNode({ id: 'root-1', data: { type: BlockEnum.Code, title: '', desc: '' } }),
|
||||
]
|
||||
const edges = [
|
||||
createEdge({ id: 'nested-edge', source: 'child-1', target: 'child-2', zIndex: 1002 }),
|
||||
createEdge({ id: 'boundary-edge', source: 'child-2', target: 'root-1', zIndex: 1002 }),
|
||||
createEdge({ id: 'root-edge', source: 'root-1', target: 'iter-1', zIndex: 1002 }),
|
||||
]
|
||||
|
||||
const result = initialEdges(edges, nodes)
|
||||
|
||||
expect(result.find(edge => edge.id === 'nested-edge')!.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(result.find(edge => edge.id === 'boundary-edge')!.zIndex).toBe(NESTED_ELEMENT_Z_INDEX)
|
||||
expect(result.find(edge => edge.id === 'root-edge')!.zIndex).toBe(0)
|
||||
expect(result.find(edge => edge.id === 'nested-edge')!.data).toEqual(expect.objectContaining({
|
||||
isInIteration: true,
|
||||
iteration_id: 'iter-1',
|
||||
isInLoop: false,
|
||||
loop_id: undefined,
|
||||
}))
|
||||
expect(result.find(edge => edge.id === 'boundary-edge')!.data).toEqual(expect.objectContaining({
|
||||
isInIteration: true,
|
||||
iteration_id: 'iter-1',
|
||||
isInLoop: false,
|
||||
loop_id: undefined,
|
||||
}))
|
||||
expect(result.find(edge => edge.id === 'root-edge')!.data).toEqual(expect.objectContaining({
|
||||
isInIteration: false,
|
||||
iteration_id: undefined,
|
||||
isInLoop: false,
|
||||
loop_id: undefined,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should set edge type to custom', () => {
|
||||
const nodes = [
|
||||
createNode({ id: 'a', data: { type: BlockEnum.Start, title: '', desc: '' } }),
|
||||
|
||||
@ -10,10 +10,7 @@ import {
|
||||
import { CUSTOM_SIMPLE_NODE } from '@/app/components/workflow/simple-node/constants'
|
||||
import {
|
||||
CUSTOM_NODE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
ITERATION_NODE_Z_INDEX,
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
LOOP_NODE_Z_INDEX,
|
||||
NESTED_ELEMENT_Z_INDEX,
|
||||
} from '../constants'
|
||||
import { isAgentV2NodeData } from '../nodes/agent-v2/types'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
||||
@ -38,7 +35,7 @@ export function generateNewNode({ data, position, id, zIndex, type, ...rest }: O
|
||||
position,
|
||||
targetPosition: Position.Left,
|
||||
sourcePosition: Position.Right,
|
||||
zIndex: data.type === BlockEnum.Iteration ? ITERATION_NODE_Z_INDEX : (data.type === BlockEnum.Loop ? LOOP_NODE_Z_INDEX : zIndex),
|
||||
zIndex: data.type === BlockEnum.Iteration || data.type === BlockEnum.Loop ? 0 : zIndex,
|
||||
...rest,
|
||||
} as Node
|
||||
|
||||
@ -81,7 +78,7 @@ export function getIterationStartNode(iterationId: string): Node {
|
||||
x: 24,
|
||||
y: 68,
|
||||
},
|
||||
zIndex: ITERATION_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
parentId: iterationId,
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
@ -102,7 +99,7 @@ export function getLoopStartNode(loopId: string): Node {
|
||||
x: 24,
|
||||
y: 68,
|
||||
},
|
||||
zIndex: LOOP_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
parentId: loopId,
|
||||
selectable: false,
|
||||
draggable: false,
|
||||
|
||||
@ -20,8 +20,7 @@ import {
|
||||
CUSTOM_NODE,
|
||||
DEFAULT_RETRY_INTERVAL,
|
||||
DEFAULT_RETRY_MAX,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
LOOP_CHILDREN_Z_INDEX,
|
||||
NESTED_ELEMENT_Z_INDEX,
|
||||
NODE_WIDTH_X_OFFSET,
|
||||
START_INITIAL_POSITION,
|
||||
} from '../constants'
|
||||
@ -176,7 +175,7 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
loop_id: isInLoop ? startNode!.parentId : undefined,
|
||||
_connectedNodeIsSelected: true,
|
||||
},
|
||||
zIndex: isIteration ? ITERATION_CHILDREN_Z_INDEX : LOOP_CHILDREN_Z_INDEX,
|
||||
zIndex: NESTED_ELEMENT_Z_INDEX,
|
||||
}
|
||||
})
|
||||
nodes.forEach((node) => {
|
||||
@ -196,6 +195,7 @@ export const preprocessNodesAndEdges = (nodes: Node[], edges: Edge[]) => {
|
||||
export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
const { nodes, edges } = preprocessNodesAndEdges(cloneDeep(originNodes), cloneDeep(originEdges))
|
||||
const firstNode = nodes[0]
|
||||
const nodesById = new Map(nodes.map(node => [node.id, node]))
|
||||
|
||||
if (!firstNode?.position) {
|
||||
nodes.forEach((node, index) => {
|
||||
@ -217,6 +217,11 @@ export const initialNodes = (originNodes: Node[], originEdges: Edge[]) => {
|
||||
}, {} as Record<string, { nodeId: string, nodeType: BlockEnum }[]>)
|
||||
|
||||
return nodes.map((node) => {
|
||||
const parentNode = node.parentId ? nodesById.get(node.parentId) : undefined
|
||||
const isNested = parentNode?.data.type === BlockEnum.Iteration || parentNode?.data.type === BlockEnum.Loop
|
||||
|
||||
node.zIndex = isNested ? NESTED_ELEMENT_Z_INDEX : 0
|
||||
|
||||
if (!node.type)
|
||||
node.type = CUSTOM_NODE
|
||||
|
||||
@ -356,6 +361,24 @@ export const initialEdges = (originEdges: Edge[], originNodes: Node[]) => {
|
||||
} as any
|
||||
}
|
||||
|
||||
const sourceNode = nodesMap[edge.source]
|
||||
const targetNode = nodesMap[edge.target]
|
||||
const sourceParentNode = sourceNode?.parentId ? nodesMap[sourceNode.parentId] : undefined
|
||||
const targetParentNode = targetNode?.parentId ? nodesMap[targetNode.parentId] : undefined
|
||||
const nestedParentNode = [sourceParentNode, targetParentNode].find(node => node?.data.type === BlockEnum.Iteration || node?.data.type === BlockEnum.Loop)
|
||||
const isInIteration = nestedParentNode?.data.type === BlockEnum.Iteration
|
||||
const isInLoop = nestedParentNode?.data.type === BlockEnum.Loop
|
||||
edge.data = {
|
||||
...edge.data,
|
||||
isInIteration,
|
||||
iteration_id: isInIteration ? nestedParentNode.id : undefined,
|
||||
isInLoop,
|
||||
loop_id: isInLoop ? nestedParentNode.id : undefined,
|
||||
} as Edge['data']
|
||||
edge.zIndex = nestedParentNode
|
||||
? NESTED_ELEMENT_Z_INDEX
|
||||
: 0
|
||||
|
||||
return edge
|
||||
})
|
||||
}
|
||||
|
||||
@ -25,7 +25,6 @@ import ReactFlow, {
|
||||
import {
|
||||
CUSTOM_EDGE,
|
||||
CUSTOM_NODE,
|
||||
ITERATION_CHILDREN_Z_INDEX,
|
||||
} from '@/app/components/workflow/constants'
|
||||
import CustomConnectionLine from '@/app/components/workflow/custom-connection-line'
|
||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||
@ -113,7 +112,6 @@ const WorkflowPreview = ({
|
||||
edges={edgesData}
|
||||
onEdgesChange={onEdgesChange}
|
||||
connectionLineComponent={CustomConnectionLine}
|
||||
connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }}
|
||||
defaultViewport={viewport}
|
||||
multiSelectionKeyCode={null}
|
||||
deleteKeyCode={null}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user