fix(web): snippet draft sync

This commit is contained in:
JzoNg
2026-04-28 14:42:51 +08:00
parent 9dd73b4d47
commit 77afc805e1
5 changed files with 80 additions and 10 deletions

View File

@@ -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')
})

View File

@@ -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')}

View File

@@ -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')]

View File

@@ -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,

View File

@@ -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,