mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(web): snippet draft sync
This commit is contained in:
parent
9dd73b4d47
commit
77afc805e1
@ -201,6 +201,14 @@ describe('Nav Component', () => {
|
|||||||
expect(mockSetAppDetail).not.toHaveBeenCalled()
|
expect(mockSetAppDetail).not.toHaveBeenCalled()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should not call setAppDetail from snippets segment', () => {
|
||||||
|
vi.mocked(useSelectedLayoutSegment).mockReturnValue('snippets')
|
||||||
|
render(<Nav {...defaultProps} activeSegment={['apps', 'app', 'snippets']} />)
|
||||||
|
const link = screen.getByRole('link')
|
||||||
|
fireEvent.click(link.firstChild!)
|
||||||
|
expect(mockSetAppDetail).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
it('should show ArrowNarrowLeft on hover when curNav is provided and activated', () => {
|
it('should show ArrowNarrowLeft on hover when curNav is provided and activated', () => {
|
||||||
const curNav = navigationItems[0]
|
const curNav = navigationItems[0]
|
||||||
render(<Nav {...defaultProps} curNav={curNav} />)
|
render(<Nav {...defaultProps} curNav={curNav} />)
|
||||||
@ -238,19 +246,20 @@ describe('Nav Component', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it('should navigate when an item is selected', async () => {
|
it('should navigate when an item is selected', async () => {
|
||||||
render(<Nav {...defaultProps} curNav={curNav} />)
|
vi.mocked(useSelectedLayoutSegment).mockReturnValue('snippets')
|
||||||
|
render(<Nav {...defaultProps} activeSegment={['apps', 'app', 'snippets']} curNav={curNav} />)
|
||||||
const selectorButton = screen.getByRole('button', { name: /Item 1/i })
|
const selectorButton = screen.getByRole('button', { name: /Item 1/i })
|
||||||
|
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(selectorButton)
|
fireEvent.click(selectorButton)
|
||||||
})
|
})
|
||||||
|
mockSetAppDetail.mockClear()
|
||||||
|
|
||||||
const item2 = await screen.findByText('Item 2')
|
const item2 = await screen.findByText('Item 2')
|
||||||
await act(async () => {
|
await act(async () => {
|
||||||
fireEvent.click(item2)
|
fireEvent.click(item2)
|
||||||
})
|
})
|
||||||
|
|
||||||
expect(mockSetAppDetail).toHaveBeenCalled()
|
|
||||||
expect(mockPush).toHaveBeenCalledWith('/item2')
|
expect(mockPush).toHaveBeenCalledWith('/item2')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -51,6 +51,8 @@ const Nav = ({
|
|||||||
// Don't clear state if opening in new tab/window
|
// Don't clear state if opening in new tab/window
|
||||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
||||||
return
|
return
|
||||||
|
if (segment === 'snippets')
|
||||||
|
return
|
||||||
setAppDetail()
|
setAppDetail()
|
||||||
}}
|
}}
|
||||||
className={cn('flex h-7 cursor-pointer items-center rounded-[10px] px-2.5', isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text', curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover')}
|
className={cn('flex h-7 cursor-pointer items-center rounded-[10px] px-2.5', isActivated ? 'text-components-main-nav-nav-button-text-active' : 'text-components-main-nav-nav-button-text', curNav && isActivated && 'hover:bg-components-main-nav-nav-button-bg-active-hover')}
|
||||||
|
|||||||
@ -10,6 +10,8 @@ const mockPostWithKeepalive = vi.fn()
|
|||||||
const mockSyncDraftWorkflow = vi.fn()
|
const mockSyncDraftWorkflow = vi.fn()
|
||||||
const mockSetDraftUpdatedAt = vi.fn()
|
const mockSetDraftUpdatedAt = vi.fn()
|
||||||
const mockSetSyncWorkflowDraftHash = vi.fn()
|
const mockSetSyncWorkflowDraftHash = vi.fn()
|
||||||
|
let deferSerialCallbacks = false
|
||||||
|
let queuedSerialCallbacks: Array<() => Promise<void> | void> = []
|
||||||
|
|
||||||
let reactFlowState: {
|
let reactFlowState: {
|
||||||
getNodes: typeof mockGetNodes
|
getNodes: typeof mockGetNodes
|
||||||
@ -37,6 +39,11 @@ vi.mock('@/app/components/workflow/hooks/use-serial-async-callback', () => ({
|
|||||||
if (checkFn?.())
|
if (checkFn?.())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if (deferSerialCallbacks) {
|
||||||
|
queuedSerialCallbacks.push(() => fn(...args))
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
return fn(...args)
|
return fn(...args)
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
@ -77,6 +84,8 @@ const createInputField = (variable: string): SnippetInputField => ({
|
|||||||
describe('snippet/use-nodes-sync-draft', () => {
|
describe('snippet/use-nodes-sync-draft', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks()
|
vi.clearAllMocks()
|
||||||
|
deferSerialCallbacks = false
|
||||||
|
queuedSerialCallbacks = []
|
||||||
reactFlowState = {
|
reactFlowState = {
|
||||||
getNodes: mockGetNodes,
|
getNodes: mockGetNodes,
|
||||||
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { stable: true } }],
|
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { stable: true } }],
|
||||||
@ -121,6 +130,38 @@ describe('snippet/use-nodes-sync-draft', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should snapshot graph before queued draft sync executes', async () => {
|
||||||
|
deferSerialCallbacks = true
|
||||||
|
const { result } = renderHook(() => useNodesSyncDraft('snippet-1'))
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await result.current.doSyncWorkflowDraft()
|
||||||
|
})
|
||||||
|
|
||||||
|
mockGetNodes.mockReturnValue([
|
||||||
|
{ id: 'late-node', position: { x: 9, y: 9 }, data: { title: 'Late' } },
|
||||||
|
])
|
||||||
|
reactFlowState.edges = [{ id: 'late-edge', source: 'late-node', target: 'late-target', data: { stable: false } }]
|
||||||
|
reactFlowState.transform = [99, 88, 0.5]
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await Promise.all(queuedSerialCallbacks.map(run => run()))
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(mockSyncDraftWorkflow).toHaveBeenCalledWith({
|
||||||
|
params: { snippetId: 'snippet-1' },
|
||||||
|
body: {
|
||||||
|
graph: {
|
||||||
|
nodes: [{ id: 'node-1', position: { x: 0, y: 0 }, data: { title: 'Start' } }],
|
||||||
|
edges: [{ id: 'edge-1', source: 'node-1', target: 'node-2', data: { stable: true } }],
|
||||||
|
viewport: { x: 12, y: 24, zoom: 1.5 },
|
||||||
|
},
|
||||||
|
input_fields: [createInputField('topic')],
|
||||||
|
hash: 'draft-hash',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
it('should include the latest graph when syncing input fields', async () => {
|
it('should include the latest graph when syncing input fields', async () => {
|
||||||
const { result } = renderHook(() => useNodesSyncDraft('snippet-1'))
|
const { result } = renderHook(() => useNodesSyncDraft('snippet-1'))
|
||||||
const nextFields = [createInputField('summary')]
|
const nextFields = [createInputField('summary')]
|
||||||
|
|||||||
@ -136,21 +136,20 @@ export const useNodesSyncDraft = (snippetId: string) => {
|
|||||||
}, [getDraftSyncPayload, getNodesReadOnly, snippetId, workflowStore])
|
}, [getDraftSyncPayload, getNodesReadOnly, snippetId, workflowStore])
|
||||||
|
|
||||||
const performSync = useCallback(async (
|
const performSync = useCallback(async (
|
||||||
|
draftPayload: Omit<SnippetDraftSyncPayload, 'hash'> | null,
|
||||||
notRefreshWhenSyncError?: boolean,
|
notRefreshWhenSyncError?: boolean,
|
||||||
callback?: SyncDraftCallback,
|
callback?: SyncDraftCallback,
|
||||||
) => {
|
) => {
|
||||||
const draftPayload = getDraftSyncPayload()
|
|
||||||
if (!draftPayload)
|
if (!draftPayload)
|
||||||
return
|
return
|
||||||
|
|
||||||
await syncDraft(draftPayload, notRefreshWhenSyncError, callback)
|
await syncDraft(draftPayload, notRefreshWhenSyncError, callback)
|
||||||
}, [getDraftSyncPayload, syncDraft])
|
}, [syncDraft])
|
||||||
|
|
||||||
const performInputFieldsSync = useCallback(async (
|
const performInputFieldsSync = useCallback(async (
|
||||||
inputFields: SnippetInputField[],
|
draftPayload: Omit<SnippetDraftSyncPayload, 'hash'> | null,
|
||||||
callback?: SyncInputFieldsDraftCallback,
|
callback?: SyncInputFieldsDraftCallback,
|
||||||
) => {
|
) => {
|
||||||
const draftPayload = getDraftSyncPayload(inputFields)
|
|
||||||
if (!draftPayload)
|
if (!draftPayload)
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -165,10 +164,29 @@ export const useNodesSyncDraft = (snippetId: string) => {
|
|||||||
callback?.onRefresh?.(refreshedInputFields)
|
callback?.onRefresh?.(refreshedInputFields)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}, [getDraftSyncPayload, syncDraft])
|
}, [syncDraft])
|
||||||
|
|
||||||
const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
const syncWorkflowDraftWithPayload = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
||||||
const syncInputFieldsDraft = useSerialAsyncCallback(performInputFieldsSync)
|
const syncInputFieldsDraftWithPayload = useSerialAsyncCallback(performInputFieldsSync)
|
||||||
|
|
||||||
|
const doSyncWorkflowDraft = useCallback((
|
||||||
|
notRefreshWhenSyncError?: boolean,
|
||||||
|
callback?: SyncDraftCallback,
|
||||||
|
) => {
|
||||||
|
if (getNodesReadOnly())
|
||||||
|
return Promise.resolve()
|
||||||
|
|
||||||
|
const draftPayload = getDraftSyncPayload()
|
||||||
|
return syncWorkflowDraftWithPayload(draftPayload, notRefreshWhenSyncError, callback)
|
||||||
|
}, [getDraftSyncPayload, getNodesReadOnly, syncWorkflowDraftWithPayload])
|
||||||
|
|
||||||
|
const syncInputFieldsDraft = useCallback((
|
||||||
|
inputFields: SnippetInputField[],
|
||||||
|
callback?: SyncInputFieldsDraftCallback,
|
||||||
|
) => {
|
||||||
|
const draftPayload = getDraftSyncPayload(inputFields)
|
||||||
|
return syncInputFieldsDraftWithPayload(draftPayload, callback)
|
||||||
|
}, [getDraftSyncPayload, syncInputFieldsDraftWithPayload])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
doSyncWorkflowDraft,
|
doSyncWorkflowDraft,
|
||||||
|
|||||||
@ -498,7 +498,7 @@ export type ChildNodeTypeCount = {
|
|||||||
[key: string]: number
|
[key: string]: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRIGGER_NODE_TYPES = [
|
export const TRIGGER_NODE_TYPES = [
|
||||||
BlockEnum.TriggerSchedule,
|
BlockEnum.TriggerSchedule,
|
||||||
BlockEnum.TriggerWebhook,
|
BlockEnum.TriggerWebhook,
|
||||||
BlockEnum.TriggerPlugin,
|
BlockEnum.TriggerPlugin,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user