diff --git a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx index 4d39d5623b2..7e0cf7494e5 100644 --- a/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx +++ b/web/app/components/snippets/components/__tests__/snippet-main.spec.tsx @@ -15,7 +15,6 @@ const mockReset = vi.fn() const mockSetFields = vi.fn() const mockSetNavigationState = vi.fn() const mockPublishSnippetMutateAsync = vi.fn() -const mockUseSnippetPublishedWorkflow = vi.fn() const mockFetchInspectVars = vi.fn() const mockHandleBackupDraft = vi.fn() const mockHandleLoadBackupDraft = vi.fn() @@ -25,11 +24,7 @@ const mockHandleStartWorkflowRun = vi.fn() const mockHandleStopRun = vi.fn() const mockHandleWorkflowStartRunInWorkflow = vi.fn() const mockHandleCheckBeforePublish = vi.fn() -const mockPush = vi.hoisted(() => vi.fn()) const mockUseAvailableNodesMetaData = vi.hoisted(() => vi.fn()) -const mockWorkspacePermissionKeys = vi.hoisted(() => ({ - value: ['snippets.create_and_modify'], -})) const mockInspectVarsCrud = { hasNodeInspectVars: vi.fn(), hasSetInspectVar: vi.fn(), @@ -54,11 +49,6 @@ vi.mock('@langgenius/dify-ui/toast', () => ({ }, })) -vi.mock('@/context/app-context', () => ({ - useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => T): T => selector({ - workspacePermissionKeys: mockWorkspacePermissionKeys.value, - }), -})) let capturedHooksStore: Record | undefined let capturedWorkflowNodes: WorkflowProps['nodes'] | undefined let snippetDetailStoreState: { @@ -76,18 +66,11 @@ vi.mock('@/app/components/snippets/store', () => ({ useSnippetDetailStore: (selector: (state: typeof snippetDetailStoreState) => unknown) => selector(snippetDetailStoreState), })) -vi.mock('@/next/navigation', () => ({ - useRouter: () => ({ - push: mockPush, - }), -})) - vi.mock('@/service/use-snippet-workflows', () => ({ usePublishSnippetWorkflowMutation: () => ({ mutateAsync: mockPublishSnippetMutateAsync, isPending: false, }), - useSnippetPublishedWorkflow: () => mockUseSnippetPublishedWorkflow(), })) vi.mock('@/app/components/snippets/hooks/use-configs-map', () => ({ @@ -170,28 +153,15 @@ vi.mock('@/app/components/workflow', () => ({ vi.mock('@/app/components/snippets/components/snippet-children', () => ({ default: ({ - onCancel, - onEdit, - onExitEditingWithoutSave, onPublish, canSave, - canEdit, - isEditing, }: { canSave: boolean - canEdit: boolean - isEditing: boolean - onCancel: () => void - onEdit: () => void - onExitEditingWithoutSave: () => void onPublish: () => void }) => (
- {!isEditing && canEdit && } snippets list - -
), })) @@ -280,13 +250,6 @@ describe('SnippetMain', () => { mockDoSyncWorkflowDraft.mockResolvedValue(undefined) mockSyncInputFieldsDraft.mockResolvedValue(undefined) mockPublishSnippetMutateAsync.mockResolvedValue({ created_at: 1_744_000_000 }) - mockUseSnippetPublishedWorkflow.mockReturnValue({ - data: { - graph: payload.graph, - input_fields: payload.inputFields, - }, - refetch: vi.fn(), - }) const llmNodeMetadata = createNodeMetadata(BlockEnum.LLM) const humanInputNodeMetadata = createNodeMetadata(BlockEnum.HumanInput) const endNodeMetadata = createNodeMetadata(BlockEnum.End) @@ -321,11 +284,10 @@ describe('SnippetMain', () => { setFields: mockSetFields, setNavigationState: mockSetNavigationState, } - mockWorkspacePermissionKeys.value = ['snippets.create_and_modify'] }) describe('Initial Mode', () => { - it('should enter draft editing mode by default when there is no published workflow', () => { + it('should render the draft graph by default when there is no published workflow', () => { const draftNode = createDraftNode('draft-node') renderSnippetMain({ @@ -337,8 +299,7 @@ describe('SnippetMain', () => { expect(capturedWorkflowNodes?.map(node => node.id)).toEqual(['draft-node']) }) - it('should stay readonly without snippet create-and-modify permission', async () => { - mockWorkspacePermissionKeys.value = [] + it('should keep the snippet canvas editable and sync draft changes without permission gating', async () => { const draftNode = createDraftNode('draft-node') renderSnippetMain({ @@ -347,14 +308,17 @@ describe('SnippetMain', () => { }) expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument() + expect(mockSetNavigationState).toHaveBeenCalledWith(expect.objectContaining({ + readonly: false, + })) const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as (() => Promise) await doSyncWorkflowDraft() - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1) }) - it('should enter readonly mode with published graph by default when published workflow exists', async () => { + it('should render the draft graph even when a published workflow exists', async () => { const publishedNode = createDraftNode('published-node') const draftNode = createDraftNode('draft-node') @@ -364,35 +328,13 @@ describe('SnippetMain', () => { workflowDraftNodes: [draftNode], }) - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - expect(capturedWorkflowNodes?.map(node => node.id)).toEqual(['published-node']) + expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument() + expect(capturedWorkflowNodes?.map(node => node.id)).toEqual(['draft-node']) const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as (() => Promise) await doSyncWorkflowDraft() - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - }) - - it('should switch from readonly published graph to draft graph without forced draft sync', async () => { - const publishedNode = createDraftNode('published-node') - const draftNode = createDraftNode('draft-node') - - renderSnippetMain({ - hasPublishedWorkflow: true, - workflowNodes: [publishedNode], - workflowDraftNodes: [draftNode], - }) - - fireEvent.click(screen.getByRole('button', { name: 'edit' })) - - await waitFor(() => { - expect(capturedWorkflowNodes?.map(node => node.id)).toEqual(['draft-node']) - }) - - const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as ((notRefreshWhenSyncError?: boolean) => Promise) - await doSyncWorkflowDraft(true) - - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() + expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1) }) }) @@ -459,94 +401,13 @@ describe('SnippetMain', () => { expect(mockDoSyncWorkflowDraft).toHaveBeenCalledWith() }) - it('should sync workflow draft before routing without saving changes', async () => { + it('should sync workflow draft when the page closes', () => { renderSnippetMain({ hasInitialDraftChanges: true }) - fireEvent.click(screen.getByRole('link', { name: 'snippets list' })) - fireEvent.click(await screen.findByRole('button', { name: 'snippet.doNotSave' })) - - await waitFor(() => { - expect(mockPush).toHaveBeenCalledWith('/snippets') - }) - expect(mockDoSyncWorkflowDraft).toHaveBeenCalledWith(true) - expect(mockDoSyncWorkflowDraft.mock.invocationCallOrder[0]!).toBeLessThan(mockPush.mock.invocationCallOrder[0]!) - expect(mockHandleRestoreFromPublishedWorkflow).not.toHaveBeenCalled() - expect(mockSyncInputFieldsDraft).not.toHaveBeenCalled() - }) - - it('should sync workflow draft before exiting editing without saving changes', async () => { - renderSnippetMain({ hasInitialDraftChanges: true }) - - fireEvent.click(screen.getByRole('button', { name: 'exit without save' })) - - await waitFor(() => { - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - }) - expect(mockDoSyncWorkflowDraft).toHaveBeenCalledWith(true) - expect(mockHandleRestoreFromPublishedWorkflow).not.toHaveBeenCalled() - expect(mockSyncInputFieldsDraft).not.toHaveBeenCalled() - }) - - it('should not sync draft from workflow autosave while readonly', async () => { - renderSnippetMain({ hasInitialDraftChanges: true }) - - fireEvent.click(screen.getByRole('button', { name: 'exit without save' })) - await waitFor(() => { - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - }) - mockDoSyncWorkflowDraft.mockClear() - - const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as (() => Promise) const syncWorkflowDraftWhenPageClose = capturedHooksStore?.syncWorkflowDraftWhenPageClose as (() => void) - await doSyncWorkflowDraft() syncWorkflowDraftWhenPageClose() - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - expect(mockSyncWorkflowDraftWhenPageClose).not.toHaveBeenCalled() - }) - - it('should skip forced draft sync caused by re-entering editing mode', async () => { - renderSnippetMain({ hasInitialDraftChanges: true }) - - fireEvent.click(screen.getByRole('button', { name: 'exit without save' })) - await waitFor(() => { - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - }) - mockDoSyncWorkflowDraft.mockClear() - - fireEvent.click(screen.getByRole('button', { name: 'edit' })) - const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as ((notRefreshWhenSyncError?: boolean) => Promise) - await doSyncWorkflowDraft(true) - - expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled() - }) - - it('should use latest synced draft when re-entering editing mode', async () => { - const latestDraftNode = { - id: 'latest-node', - position: { x: 10, y: 20 }, - data: { type: BlockEnum.Code, title: 'Latest draft node' }, - } as WorkflowProps['nodes'][number] - mockDoSyncWorkflowDraft.mockResolvedValueOnce({ - graph: { - nodes: [latestDraftNode], - edges: [], - viewport: { x: 30, y: 40, zoom: 1.2 }, - }, - input_fields: [payload.inputFields[0]], - }) - renderSnippetMain({ hasInitialDraftChanges: true }) - - fireEvent.click(screen.getByRole('button', { name: 'exit without save' })) - await waitFor(() => { - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - }) - - fireEvent.click(screen.getByRole('button', { name: 'edit' })) - - await waitFor(() => { - expect(capturedWorkflowNodes?.map(node => node.id)).toContain('latest-node') - }) + expect(mockSyncWorkflowDraftWhenPageClose).toHaveBeenCalledTimes(1) }) }) @@ -635,74 +496,6 @@ describe('SnippetMain', () => { }) }) - describe('Cancel', () => { - it('should restore from the published workflow and reset published input fields', async () => { - renderSnippetMain() - - fireEvent.click(screen.getByRole('button', { name: 'cancel' })) - - await waitFor(() => { - expect(mockHandleRestoreFromPublishedWorkflow).toHaveBeenCalledWith({ - graph: payload.graph, - input_fields: payload.inputFields, - }) - expect(mockSetFields).toHaveBeenCalledWith(payload.inputFields) - expect(mockSyncInputFieldsDraft).toHaveBeenCalledWith(payload.inputFields, { - onRefresh: expect.any(Function), - }) - }) - }) - - it('should update local draft state with the published workflow after canceling changes', async () => { - const latestDraftNode = { - id: 'latest-draft-node', - position: { x: 10, y: 20 }, - data: { type: BlockEnum.Code, title: 'Latest draft node' }, - } as WorkflowProps['nodes'][number] - const publishedNode = { - id: 'published-node', - position: { x: 30, y: 40 }, - data: { type: BlockEnum.Code, title: 'Published node' }, - } as WorkflowProps['nodes'][number] - const publishedWorkflow = { - graph: { - nodes: [publishedNode], - edges: [], - viewport: { x: 0, y: 0, zoom: 1 }, - }, - input_fields: payload.inputFields, - } - mockUseSnippetPublishedWorkflow.mockReturnValue({ - data: publishedWorkflow, - refetch: vi.fn(), - }) - mockDoSyncWorkflowDraft.mockResolvedValueOnce({ - graph: { - nodes: [latestDraftNode], - edges: [], - viewport: { x: 30, y: 40, zoom: 1.2 }, - }, - input_fields: payload.inputFields, - }) - renderSnippetMain({ hasInitialDraftChanges: true }) - - fireEvent.click(screen.getByRole('button', { name: 'exit without save' })) - await waitFor(() => { - expect(screen.getByRole('button', { name: 'edit' })).toBeInTheDocument() - }) - fireEvent.click(screen.getByRole('button', { name: 'edit' })) - await waitFor(() => { - expect(capturedWorkflowNodes?.map(node => node.id)).toContain('latest-draft-node') - }) - - fireEvent.click(screen.getByRole('button', { name: 'cancel' })) - - await waitFor(() => { - expect(capturedWorkflowNodes?.map(node => node.id)).toContain('published-node') - }) - }) - }) - describe('Inspect Vars', () => { it('should pass inspect vars handlers to WorkflowWithInnerContext', () => { renderSnippetMain() diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index 73f7fa15677..bf7c68a8ba5 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -9,7 +9,6 @@ import { useCallback, useEffect, useMemo, - useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -23,9 +22,6 @@ import { initialEdges, initialNodes, } from '@/app/components/workflow/utils' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' -import { useRouter } from '@/next/navigation' -import { useSnippetPublishedWorkflow } from '@/service/use-snippet-workflows' import { useConfigsMap } from '../hooks/use-configs-map' import { useGetRunAndTraceUrl } from '../hooks/use-get-run-and-trace-url' import { useInspectVarsCrud } from '../hooks/use-inspect-vars-crud' @@ -34,10 +30,8 @@ import { useSnippetRefreshDraft } from '../hooks/use-snippet-refresh-draft' import { useSnippetRun } from '../hooks/use-snippet-run' import { useSnippetStartRun } from '../hooks/use-snippet-start-run' import { useSnippetDetailStore } from '../store' -import { canCreateAndModifySnippets } from '../utils/permission' import { useSnippetInputFieldActions } from './hooks/use-snippet-input-field-actions' import { useSnippetPublish } from './hooks/use-snippet-publish' -import SaveBeforeLeavingDialog from './save-before-leaving-dialog' import SnippetChildren from './snippet-children' type SnippetMainProps = { @@ -54,19 +48,9 @@ type SnippetMainProps = { type SnippetMainContentProps = { snippetId: string fields: SnippetInputField[] - canDiscardChanges: boolean - canEdit: boolean canSave: boolean - hasDraftChanges: boolean - isEditing: boolean onBeforePublish: () => Promise | void> - onCancel: () => void | Promise - onDiscardRoute: () => void | Promise - onEdit: () => void - onExitEditing: () => void | Promise - onExitEditingWithoutSave: () => void | Promise onSaved: (syncedDraftPayload?: Omit | void) => void - onSavedAndExitEditing: () => void } const unsupportedSnippetBlockTypes = new Set([ @@ -94,15 +78,10 @@ const SnippetMainContent = ({ snippetId, fields, canSave, - hasDraftChanges, - isEditing, onBeforePublish, - onDiscardRoute, onSaved, }: SnippetMainContentProps) => { - const { push } = useRouter() const { t } = useTranslation('snippet') - const [pendingHref, setPendingHref] = useState() const { handlePublish, isPublishing, @@ -127,166 +106,42 @@ const SnippetMainContent = ({ return didSave }, [handlePublish, onBeforePublish, onSaved, t]) - const navigateToPendingHref = useCallback((href: string) => { - const url = new URL(href, window.location.href) - if (url.origin === window.location.origin) - push(`${url.pathname}${url.search}${url.hash}`) - else - window.location.assign(url.href) - }, [push]) - - const handleDiscardAndRoute = useCallback(async () => { - if (!pendingHref) - return - - await onDiscardRoute() - navigateToPendingHref(pendingHref) - setPendingHref(undefined) - }, [navigateToPendingHref, onDiscardRoute, pendingHref]) - - const handleSaveAndRoute = useCallback(async () => { - if (!pendingHref) - return - - const didSave = await handlePublishSnippet() - if (!didSave) - return - - navigateToPendingHref(pendingHref) - setPendingHref(undefined) - }, [handlePublishSnippet, navigateToPendingHref, pendingHref]) - - useEffect(() => { - if (!isEditing || !hasDraftChanges) - return - - const handleBeforeUnload = (event: BeforeUnloadEvent) => { - event.preventDefault() - event.returnValue = '' - } - - window.addEventListener('beforeunload', handleBeforeUnload) - return () => window.removeEventListener('beforeunload', handleBeforeUnload) - }, [hasDraftChanges, isEditing]) - - useEffect(() => { - if (!isEditing || !hasDraftChanges) - return - - const handleClick = (event: MouseEvent) => { - if ( - event.defaultPrevented - || event.button !== 0 - || event.metaKey - || event.ctrlKey - || event.shiftKey - || event.altKey - ) { - return - } - - const anchor = (event.target as Element | null)?.closest?.('a[href]') - if (!(anchor instanceof HTMLAnchorElement)) - return - - if (anchor.target && anchor.target !== '_self') - return - if (anchor.hasAttribute('download')) - return - - const nextUrl = new URL(anchor.href, window.location.href) - const currentUrl = new URL(window.location.href) - if (nextUrl.href === currentUrl.href) - return - - event.preventDefault() - event.stopPropagation() - setPendingHref(nextUrl.href) - } - - document.addEventListener('click', handleClick, true) - return () => document.removeEventListener('click', handleClick, true) - }, [hasDraftChanges, isEditing]) - return ( - <> - - !open && setPendingHref(undefined)} - disabled={isPublishing} - saveDisabled={!canSave} - loading={isPublishing} - onDiscard={handleDiscardAndRoute} - onSave={handleSaveAndRoute} - /> - + ) } const SnippetMain = ({ - payload, draftPayload, - hasInitialDraftChanges, - hasPublishedWorkflow, snippetId, - nodes, - edges, - viewport, draftNodes, draftEdges, draftViewport, }: SnippetMainProps) => { - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) - const canCreateAndModifySnippet = canCreateAndModifySnippets(workspacePermissionKeys) - const [isEditingState, setIsEditingState] = useState(!hasPublishedWorkflow) - const isEditing = canCreateAndModifySnippet && isEditingState const [localDraftState, setLocalDraftState] = useState() - const [draftChangeState, setDraftChangeState] = useState({ - initial: hasInitialDraftChanges, - snippetId, - value: hasInitialDraftChanges, - }) - if (draftChangeState.snippetId !== snippetId || draftChangeState.initial !== hasInitialDraftChanges) { + const [localDraftSnippetId, setLocalDraftSnippetId] = useState(snippetId) + if (localDraftSnippetId !== snippetId) { setLocalDraftState(undefined) - setDraftChangeState({ - initial: hasInitialDraftChanges, - snippetId, - value: hasInitialDraftChanges, - }) + setLocalDraftSnippetId(snippetId) } - const hasDraftChanges = draftChangeState.value const currentCanvasNodeCount = useStore(state => state.nodes.filter(node => !node.data?._isTempNode).length) - const skipNextForcedDraftSyncRef = useRef(false) - const setHasDraftChanges = useCallback((value: boolean) => { - setDraftChangeState(prev => ({ - ...prev, - value, - })) - }, []) const effectiveDraftPayload = localDraftState?.payload ?? draftPayload const effectiveDraftNodes = localDraftState?.nodes ?? draftNodes const effectiveDraftEdges = localDraftState?.edges ?? draftEdges const effectiveDraftViewport = localDraftState?.viewport ?? draftViewport - const displayPayload = isEditing ? effectiveDraftPayload : payload - const displayNodes = isEditing ? effectiveDraftNodes : nodes - const displayEdges = isEditing ? effectiveDraftEdges : edges - const displayViewport = isEditing ? effectiveDraftViewport : viewport - const { graph, snippet } = displayPayload + const { graph, snippet } = effectiveDraftPayload const canSave = currentCanvasNodeCount > 0 const { doSyncWorkflowDraft: syncWorkflowDraft, - syncInputFieldsDraft, syncWorkflowDraftWhenPageClose, } = useNodesSyncDraft(snippetId) const workflowStore = useWorkflowStore() - const publishedWorkflowQuery = useSnippetPublishedWorkflow(snippetId) const { handleRefreshWorkflowDraft } = useSnippetRefreshDraft(snippetId) const { handleBackupDraft, @@ -316,10 +171,6 @@ const SnippetMain = ({ invalidateConversationVarValues, } = useInspectVarsCrud(snippetId) const workflowAvailableNodesMetaData = useAvailableNodesMetaData() - const { - data: publishedWorkflow, - refetch: refetchPublishedWorkflow, - } = publishedWorkflowQuery const availableNodesMetaData = useMemo(() => { const nodes = workflowAvailableNodesMetaData.nodes.filter(node => !unsupportedSnippetBlockTypes.has(node.metaData.type)) @@ -350,9 +201,9 @@ const SnippetMain = ({ }))) const { fields, - handleFieldsChange, + handleFieldsChange: handleSnippetFieldsChange, } = useSnippetInputFieldActions({ - canEdit: canCreateAndModifySnippet, + canEdit: true, snippetId, }) const { @@ -369,73 +220,45 @@ const SnippetMain = ({ }, [reset, snippetId]) useEffect(() => { - setFields(displayPayload.inputFields) - }, [displayPayload.inputFields, setFields, snippetId]) + setFields(effectiveDraftPayload.inputFields) + }, [effectiveDraftPayload.inputFields, setFields, snippetId]) useEffect(() => { - workflowStore.setState({ canvasReadOnly: !isEditing }) + workflowStore.setState({ canvasReadOnly: false }) return () => { workflowStore.setState({ canvasReadOnly: false }) } - }, [isEditing, workflowStore]) + }, [workflowStore]) useEffect(() => { workflowStore.temporal.getState().pause() workflowStore.getState().setWorkflowHistory({ - nodes: displayNodes, - edges: displayEdges, + nodes: effectiveDraftNodes, + edges: effectiveDraftEdges, workflowHistoryEvent: undefined, workflowHistoryEventMeta: undefined, }) workflowStore.temporal.getState().clear() workflowStore.temporal.getState().resume() - }, [displayEdges, displayNodes, workflowStore]) + }, [effectiveDraftEdges, effectiveDraftNodes, workflowStore]) const doSyncWorkflowDraft = useCallback(( ...args: Parameters - ) => { - if (!canCreateAndModifySnippet || !isEditing) - return Promise.resolve() + ) => syncWorkflowDraft(...args), [syncWorkflowDraft]) - const [ - notRefreshWhenSyncError, - callback, - ] = args - if (skipNextForcedDraftSyncRef.current && notRefreshWhenSyncError === true && !callback) { - skipNextForcedDraftSyncRef.current = false - return Promise.resolve() - } - - if (isEditing) - setHasDraftChanges(true) - - return syncWorkflowDraft(...args) - }, [canCreateAndModifySnippet, isEditing, setHasDraftChanges, syncWorkflowDraft]) - - const syncWorkflowDraftWhenPageCloseInEditing = useCallback(() => { - if (!canCreateAndModifySnippet || !isEditing) - return - - syncWorkflowDraftWhenPageClose() - }, [canCreateAndModifySnippet, isEditing, syncWorkflowDraftWhenPageClose]) - - const handleFieldsChangeInEditing = useCallback((nextFields: SnippetInputField[]) => { - if (!canCreateAndModifySnippet || !isEditing) - return - - handleFieldsChange(nextFields) - setHasDraftChanges(true) - }, [canCreateAndModifySnippet, handleFieldsChange, isEditing, setHasDraftChanges]) + const handleFieldsChange = useCallback((nextFields: SnippetInputField[]) => { + handleSnippetFieldsChange(nextFields) + }, [handleSnippetFieldsChange]) useEffect(() => { setNavigationState({ snippetId, snippet, - readonly: !isEditing, - onFieldsChange: handleFieldsChangeInEditing, + readonly: false, + onFieldsChange: handleFieldsChange, }) - }, [handleFieldsChangeInEditing, isEditing, setNavigationState, snippet, snippetId]) + }, [handleFieldsChange, setNavigationState, snippet, snippetId]) const updateLocalDraftFromSyncPayload = useCallback(( syncedDraftPayload?: Omit | void, @@ -469,67 +292,10 @@ const SnippetMain = ({ setFields(inputFields) }, [draftPayload, fields, setFields]) - const handleCancelChanges = useCallback(async () => { - if (!canCreateAndModifySnippet) - return - - const workflow = publishedWorkflow ?? (await refetchPublishedWorkflow()).data - if (!workflow) - return - - handleRestoreFromPublishedWorkflow(workflow as never) - - const publishedInputFields = Array.isArray(workflow.input_fields) - ? workflow.input_fields as SnippetInputField[] - : [] - updateLocalDraftFromSyncPayload({ - graph: workflow.graph, - input_fields: publishedInputFields, - }) - void syncInputFieldsDraft(publishedInputFields, { - onRefresh: setFields, - }) - setHasDraftChanges(false) - }, [canCreateAndModifySnippet, handleRestoreFromPublishedWorkflow, publishedWorkflow, refetchPublishedWorkflow, setFields, setHasDraftChanges, syncInputFieldsDraft, updateLocalDraftFromSyncPayload]) - - const handleExitEditing = useCallback(async () => { - if (!canCreateAndModifySnippet || hasDraftChanges) - return - - setIsEditingState(false) - }, [canCreateAndModifySnippet, hasDraftChanges]) - - const handleExitEditingWithoutSave = useCallback(async () => { - if (!canCreateAndModifySnippet) - return - - const syncedDraftPayload = await syncWorkflowDraft(true) - updateLocalDraftFromSyncPayload(syncedDraftPayload) - skipNextForcedDraftSyncRef.current = true - setIsEditingState(false) - }, [canCreateAndModifySnippet, syncWorkflowDraft, updateLocalDraftFromSyncPayload]) - - const handleDiscardAndRoute = useCallback(async () => { - if (!canCreateAndModifySnippet) - return - - const syncedDraftPayload = await syncWorkflowDraft(true) - updateLocalDraftFromSyncPayload(syncedDraftPayload) - skipNextForcedDraftSyncRef.current = true - }, [canCreateAndModifySnippet, syncWorkflowDraft, updateLocalDraftFromSyncPayload]) - - const handleEdit = useCallback(() => { - if (!canCreateAndModifySnippet) - return - - skipNextForcedDraftSyncRef.current = true - setIsEditingState(true) - }, [canCreateAndModifySnippet]) - const hooksStore = useMemo(() => { return { doSyncWorkflowDraft, - syncWorkflowDraftWhenPageClose: syncWorkflowDraftWhenPageCloseInEditing, + syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft, handleBackupDraft, handleLoadBackupDraft, @@ -585,41 +351,25 @@ const SnippetMain = ({ renameInspectVarName, resetConversationVar, resetToLastRunVar, - syncWorkflowDraftWhenPageCloseInEditing, + syncWorkflowDraftWhenPageClose, ]) return (
} > syncWorkflowDraft(true)} - onCancel={handleCancelChanges} - onDiscardRoute={handleDiscardAndRoute} - onEdit={handleEdit} - onExitEditing={handleExitEditing} - onExitEditingWithoutSave={handleExitEditingWithoutSave} - onSaved={(syncedDraftPayload) => { - updateLocalDraftFromSyncPayload(syncedDraftPayload) - setHasDraftChanges(false) - }} - onSavedAndExitEditing={() => { - setHasDraftChanges(false) - setIsEditingState(false) - }} + onSaved={updateLocalDraftFromSyncPayload} />