diff --git a/web/app/components/snippets/components/snippet-children.tsx b/web/app/components/snippets/components/snippet-children.tsx index 8ce66be3053..fb407038744 100644 --- a/web/app/components/snippets/components/snippet-children.tsx +++ b/web/app/components/snippets/components/snippet-children.tsx @@ -7,35 +7,17 @@ import SnippetWorkflowPanel from './workflow-panel' type SnippetChildrenProps = { snippetId: string fields: SnippetInputField[] - canDiscardChanges: boolean - canEdit?: boolean canSave: boolean - hasDraftChanges: boolean - isEditing: boolean isPublishing: boolean - onCancel: () => void - onEdit: () => void - onExitEditing: () => void | Promise - onExitEditingWithoutSave: () => void | Promise onPublish: () => void - onSaveAndExitEditing: () => void | Promise } const SnippetChildren = ({ snippetId, fields, - canDiscardChanges, - canEdit = true, canSave, - hasDraftChanges, - isEditing, isPublishing, - onCancel, - onEdit, - onExitEditing, - onExitEditingWithoutSave, onPublish, - onSaveAndExitEditing, }: SnippetChildrenProps) => { return ( <> @@ -43,18 +25,9 @@ const SnippetChildren = ({ ({ - AlertDialog: ({ children }: { children: ReactNode }) =>
{children}
, - AlertDialogActions: ({ children }: { children: ReactNode }) =>
{children}
, - AlertDialogCancelButton: ({ children }: { children: ReactNode }) => , - AlertDialogConfirmButton: ({ - children, - disabled, - onClick, - }: { - children: ReactNode - disabled?: boolean - onClick?: () => void - }) => , - AlertDialogContent: ({ children }: { children: ReactNode }) =>
{children}
, - AlertDialogDescription: ({ children }: { children: ReactNode }) =>
{children}
, - AlertDialogTitle: ({ children }: { children: ReactNode }) =>
{children}
, - AlertDialogTrigger: ({ children, render }: { children?: ReactNode, render?: ReactNode }) => render ?? , -})) - vi.mock('@/app/components/workflow/header', () => ({ default: (props: HeaderProps) => { return ( @@ -44,242 +24,71 @@ vi.mock('@/app/components/workflow/header', () => ({ })) describe('SnippetHeader', () => { - const mockCancel = vi.fn() - const mockEdit = vi.fn() - const mockExitEditing = vi.fn() - const mockExitEditingWithoutSave = vi.fn() const mockPublish = vi.fn() - const mockSaveAndExit = vi.fn() beforeEach(() => { vi.clearAllMocks() }) - // Verifies the wrapper passes the expected workflow header configuration. - describe('Rendering', () => { - it('should configure workflow header slots and hide workflow-only controls', () => { - render( - , - ) + it('should configure workflow header slots and hide workflow-only controls', () => { + render( + , + ) - const header = screen.getByTestId('workflow-header') - expect(header).toHaveAttribute('data-show-env', 'false') - expect(header).toHaveAttribute('data-show-global-variable', 'false') - expect(header).toHaveAttribute('data-history-url', '/snippets/snippet-1/workflow-runs') - expect(screen.getByText('snippet.viewOnly')).toBeInTheDocument() - expect(screen.getByRole('button', { name: /snippet\.edit/i })).toBeInTheDocument() - expect(screen.getByRole('button', { name: /snippet\.testRunButton/i })).toBeInTheDocument() - }) + const header = screen.getByTestId('workflow-header') + expect(header).toHaveAttribute('data-show-env', 'false') + expect(header).toHaveAttribute('data-show-global-variable', 'false') + expect(header).toHaveAttribute('data-history-url', '/snippets/snippet-1/workflow-runs') + expect(screen.getByRole('button', { name: /snippet\.publishButton/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /snippet\.testRunButton/i })).toBeInTheDocument() + expect(screen.queryByText('snippet.viewOnly')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: /snippet\.edit/i })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: /snippet\.exitEditing/i })).not.toBeInTheDocument() }) - // Verifies forwarded callbacks still drive the snippet-specific controls. - describe('User Interactions', () => { - it('should invoke the snippet callbacks when save and discard are clicked in editing mode', () => { - render( - , - ) + it('should publish from the primary header action', () => { + render( + , + ) - fireEvent.click(screen.getByRole('button', { name: /^snippet\.save$/i })) - fireEvent.click(screen.getByRole('button', { name: /snippet\.discardChanges/i })) + fireEvent.click(screen.getByRole('button', { name: /snippet\.publishButton/i })) - expect(mockPublish).toHaveBeenCalledTimes(1) - expect(mockCancel).toHaveBeenCalledTimes(1) - }) + expect(mockPublish).toHaveBeenCalledTimes(1) + }) - it('should disable save actions when the current graph has no nodes', () => { - render( - , - ) + it('should disable publish when the current graph has no nodes', () => { + render( + , + ) - expect(screen.getByRole('button', { name: /^snippet\.save$/i })).toBeDisabled() - expect(screen.getByRole('button', { name: /snippet\.saveAndExit/i })).toBeDisabled() - expect(screen.getByRole('button', { name: /snippet\.doNotSave/i })).not.toBeDisabled() - }) + expect(screen.getByRole('button', { name: /snippet\.publishButton/i })).toBeDisabled() + }) - it('should hide the discard draft action when there is no published workflow', () => { - render( - , - ) + it('should show publish loading state while publishing', () => { + render( + , + ) - expect(screen.queryByText('snippet.discardDraft')).not.toBeInTheDocument() - expect(screen.getByText('snippet.editingDraft')).toBeInTheDocument() - }) - - it('should enter editing mode from the readonly header action', () => { - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'snippet.edit' })) - - expect(mockEdit).toHaveBeenCalledTimes(1) - }) - - it('should exit editing immediately when there are no draft changes', () => { - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' })) - - expect(mockExitEditing).toHaveBeenCalledTimes(1) - expect(mockExitEditingWithoutSave).not.toHaveBeenCalled() - expect(mockSaveAndExit).not.toHaveBeenCalled() - }) - - it('should disable edit actions while publishing', () => { - render( - , - ) - - expect(screen.getByRole('button', { name: 'snippet.exitEditing' })).toBeDisabled() - expectLoadingButton(screen.getByRole('button', { name: /^snippet\.save$/i })) - expect(screen.getByRole('button', { name: 'snippet.doNotSave' })).toBeDisabled() - }) - - it('should discard changes from the exit confirmation dialog', async () => { - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' })) - fireEvent.click(screen.getByRole('button', { name: 'snippet.doNotSave' })) - - await waitFor(() => { - expect(mockExitEditingWithoutSave).toHaveBeenCalledTimes(1) - }) - expect(mockSaveAndExit).not.toHaveBeenCalled() - }) - - it('should save and exit from the exit confirmation dialog', async () => { - render( - , - ) - - fireEvent.click(screen.getByRole('button', { name: 'snippet.exitEditing' })) - fireEvent.click(screen.getByRole('button', { name: 'snippet.saveAndExit' })) - - await waitFor(() => { - expect(mockSaveAndExit).toHaveBeenCalledTimes(1) - }) - expect(mockExitEditingWithoutSave).not.toHaveBeenCalled() - }) + expectLoadingButton(screen.getByRole('button', { name: /snippet\.publishButton/i })) }) }) diff --git a/web/app/components/snippets/components/snippet-header/index.tsx b/web/app/components/snippets/components/snippet-header/index.tsx index b2d433d7197..a5e2aa0c5fe 100644 --- a/web/app/components/snippets/components/snippet-header/index.tsx +++ b/web/app/components/snippets/components/snippet-header/index.tsx @@ -5,128 +5,42 @@ import { Button } from '@langgenius/dify-ui/button' import { memo, useMemo, - useState, } from 'react' import { useTranslation } from 'react-i18next' import Header from '@/app/components/workflow/header' -import SaveBeforeLeavingDialog from '../save-before-leaving-dialog' -import CancelChanges from './cancel-changes' import RunMode from './run-mode' type SnippetHeaderProps = { snippetId: string - canDiscardChanges: boolean - canEdit?: boolean canSave: boolean - hasDraftChanges: boolean - isEditing: boolean isPublishing: boolean - onCancel: () => void - onEdit: () => void - onExitEditing: () => void | Promise - onExitEditingWithoutSave: () => void | Promise onPublish: () => void - onSaveAndExitEditing: () => void | Promise } -const ViewOnlyBadge = () => { - const { t } = useTranslation('snippet') - - return ( -
- {t('viewOnly')} -
- ) -} - -const EditActions = ({ - canEdit = true, +const PublishAction = ({ canSave, - hasDraftChanges, - isEditing, isPublishing, - onEdit, - onExitEditing, - onExitEditingWithoutSave, onPublish, - onSaveAndExitEditing, -}: Pick) => { +}: Pick) => { const { t } = useTranslation('snippet') - const [exitConfirmOpen, setExitConfirmOpen] = useState(false) - - if (!isEditing) { - if (!canEdit) - return null - - return ( - - ) - } return ( - <> - { - if (!canEdit) - return - - if (!hasDraftChanges) { - event.preventDefault() - void onExitEditing() - return - } - - setExitConfirmOpen(true) - }} - > - {t('exitEditing')} - - )} - disabled={isPublishing || !canEdit} - saveDisabled={!canEdit || !canSave} - loading={isPublishing} - onDiscard={async () => { - await onExitEditingWithoutSave() - setExitConfirmOpen(false) - }} - onSave={async () => { - await onSaveAndExitEditing() - setExitConfirmOpen(false) - }} - /> - - + ) } const SnippetHeader = ({ snippetId, - canDiscardChanges, - canEdit = true, canSave, - hasDraftChanges, - isEditing, isPublishing, - onCancel, - onEdit, - onExitEditing, - onExitEditingWithoutSave, onPublish, - onSaveAndExitEditing, }: SnippetHeaderProps) => { const { t } = useTranslation('snippet') const viewHistoryProps = useMemo(() => { @@ -139,21 +53,11 @@ const SnippetHeader = ({ return { normal: { components: { - title: isEditing - ? (hasDraftChanges ? : <>) - : , left: ( - ), }, @@ -174,7 +78,7 @@ const SnippetHeader = ({ viewHistoryProps, }, } - }, [canDiscardChanges, canEdit, canSave, hasDraftChanges, isEditing, isPublishing, onCancel, onEdit, onExitEditing, onExitEditingWithoutSave, onPublish, onSaveAndExitEditing, t, viewHistoryProps]) + }, [canSave, isPublishing, onPublish, t, viewHistoryProps]) return
} diff --git a/web/app/components/snippets/components/snippet-main.tsx b/web/app/components/snippets/components/snippet-main.tsx index a33300a4b62..73f7fa15677 100644 --- a/web/app/components/snippets/components/snippet-main.tsx +++ b/web/app/components/snippets/components/snippet-main.tsx @@ -93,19 +93,12 @@ const hasSnippetDraftNodes = (payload?: Omit | const SnippetMainContent = ({ snippetId, fields, - canDiscardChanges, - canEdit, canSave, hasDraftChanges, isEditing, onBeforePublish, - onCancel, onDiscardRoute, - onEdit, - onExitEditing, - onExitEditingWithoutSave, onSaved, - onSavedAndExitEditing, }: SnippetMainContentProps) => { const { push } = useRouter() const { t } = useTranslation('snippet') @@ -134,12 +127,6 @@ const SnippetMainContent = ({ return didSave }, [handlePublish, onBeforePublish, onSaved, t]) - const handleSaveAndExitEditing = useCallback(async () => { - const didSave = await handlePublishSnippet() - if (didSave) - onSavedAndExitEditing() - }, [handlePublishSnippet, onSavedAndExitEditing]) - const navigateToPendingHref = useCallback((href: string) => { const url = new URL(href, window.location.href) if (url.origin === window.location.origin) @@ -226,18 +213,9 @@ const SnippetMainContent = ({