mirror of
https://github.com/langgenius/dify.git
synced 2026-04-30 16:02:00 -04:00
fix(web): snippet draft sync
This commit is contained in:
@@ -201,6 +201,14 @@ describe('Nav Component', () => {
|
||||
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', () => {
|
||||
const curNav = navigationItems[0]
|
||||
render(<Nav {...defaultProps} curNav={curNav} />)
|
||||
@@ -238,19 +246,20 @@ describe('Nav Component', () => {
|
||||
})
|
||||
|
||||
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 })
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(selectorButton)
|
||||
})
|
||||
mockSetAppDetail.mockClear()
|
||||
|
||||
const item2 = await screen.findByText('Item 2')
|
||||
await act(async () => {
|
||||
fireEvent.click(item2)
|
||||
})
|
||||
|
||||
expect(mockSetAppDetail).toHaveBeenCalled()
|
||||
expect(mockPush).toHaveBeenCalledWith('/item2')
|
||||
})
|
||||
|
||||
|
||||
@@ -51,6 +51,8 @@ const Nav = ({
|
||||
// Don't clear state if opening in new tab/window
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0)
|
||||
return
|
||||
if (segment === 'snippets')
|
||||
return
|
||||
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')}
|
||||
|
||||
@@ -10,6 +10,8 @@ const mockPostWithKeepalive = vi.fn()
|
||||
const mockSyncDraftWorkflow = vi.fn()
|
||||
const mockSetDraftUpdatedAt = vi.fn()
|
||||
const mockSetSyncWorkflowDraftHash = vi.fn()
|
||||
let deferSerialCallbacks = false
|
||||
let queuedSerialCallbacks: Array<() => Promise<void> | void> = []
|
||||
|
||||
let reactFlowState: {
|
||||
getNodes: typeof mockGetNodes
|
||||
@@ -37,6 +39,11 @@ vi.mock('@/app/components/workflow/hooks/use-serial-async-callback', () => ({
|
||||
if (checkFn?.())
|
||||
return
|
||||
|
||||
if (deferSerialCallbacks) {
|
||||
queuedSerialCallbacks.push(() => fn(...args))
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
return fn(...args)
|
||||
},
|
||||
}))
|
||||
@@ -77,6 +84,8 @@ const createInputField = (variable: string): SnippetInputField => ({
|
||||
describe('snippet/use-nodes-sync-draft', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
deferSerialCallbacks = false
|
||||
queuedSerialCallbacks = []
|
||||
reactFlowState = {
|
||||
getNodes: mockGetNodes,
|
||||
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 () => {
|
||||
const { result } = renderHook(() => useNodesSyncDraft('snippet-1'))
|
||||
const nextFields = [createInputField('summary')]
|
||||
|
||||
@@ -136,21 +136,20 @@ export const useNodesSyncDraft = (snippetId: string) => {
|
||||
}, [getDraftSyncPayload, getNodesReadOnly, snippetId, workflowStore])
|
||||
|
||||
const performSync = useCallback(async (
|
||||
draftPayload: Omit<SnippetDraftSyncPayload, 'hash'> | null,
|
||||
notRefreshWhenSyncError?: boolean,
|
||||
callback?: SyncDraftCallback,
|
||||
) => {
|
||||
const draftPayload = getDraftSyncPayload()
|
||||
if (!draftPayload)
|
||||
return
|
||||
|
||||
await syncDraft(draftPayload, notRefreshWhenSyncError, callback)
|
||||
}, [getDraftSyncPayload, syncDraft])
|
||||
}, [syncDraft])
|
||||
|
||||
const performInputFieldsSync = useCallback(async (
|
||||
inputFields: SnippetInputField[],
|
||||
draftPayload: Omit<SnippetDraftSyncPayload, 'hash'> | null,
|
||||
callback?: SyncInputFieldsDraftCallback,
|
||||
) => {
|
||||
const draftPayload = getDraftSyncPayload(inputFields)
|
||||
if (!draftPayload)
|
||||
return
|
||||
|
||||
@@ -165,10 +164,29 @@ export const useNodesSyncDraft = (snippetId: string) => {
|
||||
callback?.onRefresh?.(refreshedInputFields)
|
||||
},
|
||||
)
|
||||
}, [getDraftSyncPayload, syncDraft])
|
||||
}, [syncDraft])
|
||||
|
||||
const doSyncWorkflowDraft = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
||||
const syncInputFieldsDraft = useSerialAsyncCallback(performInputFieldsSync)
|
||||
const syncWorkflowDraftWithPayload = useSerialAsyncCallback(performSync, getNodesReadOnly)
|
||||
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 {
|
||||
doSyncWorkflowDraft,
|
||||
|
||||
@@ -498,7 +498,7 @@ export type ChildNodeTypeCount = {
|
||||
[key: string]: number
|
||||
}
|
||||
|
||||
const TRIGGER_NODE_TYPES = [
|
||||
export const TRIGGER_NODE_TYPES = [
|
||||
BlockEnum.TriggerSchedule,
|
||||
BlockEnum.TriggerWebhook,
|
||||
BlockEnum.TriggerPlugin,
|
||||
|
||||
Reference in New Issue
Block a user