From 7e9cb50152dd87808307a21713960022c11369f1 Mon Sep 17 00:00:00 2001 From: KVOJJJin Date: Mon, 22 Jun 2026 12:51:47 +0800 Subject: [PATCH] feat(web): hide snippets (#37729) --- .../components/apps/__tests__/list.spec.tsx | 8 +- .../apps/app-list-header-filters.tsx | 7 - .../__tests__/selection-contextmenu.spec.tsx | 154 +----------------- .../block-selector/__tests__/main.spec.tsx | 1 + .../workflow/block-selector/main.tsx | 2 +- .../workflow/selection-contextmenu.tsx | 41 +---- 6 files changed, 8 insertions(+), 205 deletions(-) diff --git a/web/app/components/apps/__tests__/list.spec.tsx b/web/app/components/apps/__tests__/list.spec.tsx index f5ac48c3a46..47f328d50d6 100644 --- a/web/app/components/apps/__tests__/list.spec.tsx +++ b/web/app/components/apps/__tests__/list.spec.tsx @@ -421,18 +421,16 @@ describe('List', () => { expect(screen.getByRole('button', { name: 'common.operation.create' }))!.toBeInTheDocument() }) - it('should render sort filter before search and the snippets link', () => { + it('should render sort filter before search and hide the snippets link', () => { renderList() const sortButton = screen.getByRole('button', { name: 'Sort by Last modified' }) const searchInput = screen.getByRole('searchbox', { name: 'app.gotoAnything.actions.searchApplications' }) - const snippetsLink = screen.getByRole('link', { name: 'app.studio.viewSnippets' }) const createButton = screen.getByRole('button', { name: 'common.operation.create' }) - expect(snippetsLink).toHaveAttribute('href', '/snippets') expect(sortButton.compareDocumentPosition(searchInput) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() - expect(searchInput.compareDocumentPosition(snippetsLink) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() - expect(snippetsLink.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(searchInput.compareDocumentPosition(createButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy() + expect(screen.queryByRole('link', { name: 'app.studio.viewSnippets' })).not.toBeInTheDocument() }) it('should render app cards when apps exist', () => { diff --git a/web/app/components/apps/app-list-header-filters.tsx b/web/app/components/apps/app-list-header-filters.tsx index df32f57d7ba..ceccb7575f6 100644 --- a/web/app/components/apps/app-list-header-filters.tsx +++ b/web/app/components/apps/app-list-header-filters.tsx @@ -8,7 +8,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigge import { useTranslation } from 'react-i18next' import { SearchInput } from '@/app/components/base/search-input' import { TagFilter } from '@/features/tag-management/components/tag-filter' -import Link from '@/next/link' import { AppSortFilter } from './app-sort-filter' import { AppTypeFilter } from './app-type-filter' import CreatorsFilter from './creators-filter' @@ -71,12 +70,6 @@ export function AppListHeaderFilters({ />
- - {t('studio.viewSnippets', { ns: 'app' })} - {showCreateButton && ( ({ - value: ['snippets.create_and_modify'] as string[], -})) - -vi.mock('@/context/app-context', () => ({ - useSelector: (selector: (state: { workspacePermissionKeys: string[] }) => T): T => selector({ - workspacePermissionKeys: mockWorkspacePermissionKeys.value, - }), -})) - -vi.mock('@/app/components/snippets/hooks/use-create-snippet', async () => { - const React = await vi.importActual('react') - - return { - useCreateSnippet: () => { - const [isOpen, setIsOpen] = React.useState(false) - - return { - createSnippetMutation: { isPending: false }, - handleCloseCreateSnippetDialog: () => setIsOpen(false), - handleCreateSnippet: mockHandleCreateSnippet, - handleOpenCreateSnippetDialog: () => setIsOpen(true), - isCreateSnippetDialogOpen: isOpen, - isCreatingSnippet: false, - } - }, - } -}) - -vi.mock('@/app/components/snippets/create-snippet-dialog', () => ({ - default: (props: { - isOpen: boolean - selectedGraph?: { nodes: Node[], edges: Edge[], viewport: { x: number, y: number, zoom: number } } - inputFields?: Array<{ variable: string }> - }) => { - mockCreateSnippetDialogRender(props) - - return props.isOpen ?
: null - }, -})) vi.mock('../hooks', async () => { const actual = await vi.importActual('../hooks') @@ -142,9 +98,6 @@ describe('SelectionContextmenu', () => { mockHandleNodesCopy.mockReset() mockHandleNodesDuplicate.mockReset() mockHandleNodesDelete.mockReset() - mockHandleCreateSnippet.mockReset() - mockCreateSnippetDialogRender.mockReset() - mockWorkspacePermissionKeys.value = ['snippets.create_and_modify'] }) it('should not render when selection context menu target is absent', () => { @@ -203,41 +156,7 @@ describe('SelectionContextmenu', () => { expect(store.getState().contextMenuTarget).toBeUndefined() }) - it('should open create snippet dialog with selected graph from the top menu item', async () => { - const nodes = [ - createNode({ id: 'n1', selected: true, width: 80, height: 40 }), - createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), - createNode({ id: 'n3', selected: false, position: { x: 260, y: 0 }, width: 80, height: 40 }), - ] - const edges = [ - createEdge({ source: 'n1', target: 'n2' }), - createEdge({ source: 'n2', target: 'n3' }), - ] - const { store } = renderSelectionMenu({ nodes, edges }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - fireEvent.click(await screen.findByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })) - - expect(screen.getByTestId('create-snippet-dialog')).toBeInTheDocument() - expect(store.getState().contextMenuTarget).toBeUndefined() - - const dialogProps = mockCreateSnippetDialogRender.mock.calls.at(-1)?.[0] - expect(dialogProps.selectedGraph.nodes.map((node: Node) => node.id)).toEqual(['n1', 'n2']) - expect(dialogProps.selectedGraph.nodes.every((node: Node) => node.selected === false)).toBe(true) - expect(dialogProps.selectedGraph.edges).toHaveLength(1) - expect(dialogProps.selectedGraph.viewport).toEqual({ x: 490, y: 380, zoom: 1 }) - expect(dialogProps.selectedGraph.edges[0]).toEqual(expect.objectContaining({ - source: 'n1', - target: 'n2', - selected: false, - })) - }) - - it('should hide create snippet action without snippets create-and-modify permission', async () => { - mockWorkspacePermissionKeys.value = [] + it('should hide create snippet action for selected nodes', async () => { const nodes = [ createNode({ id: 'n1', selected: true, width: 80, height: 40 }), createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), @@ -252,76 +171,7 @@ describe('SelectionContextmenu', () => { expect(screen.getByRole('menuitem', { name: /common.copy/ })).toBeInTheDocument() }) expect(screen.queryByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })).not.toBeInTheDocument() - }) - - it('should add input fields for variable references outside of the selected graph', async () => { - const nodes = [ - createNode({ - id: 'n1', - selected: true, - width: 80, - height: 40, - data: { - prompt_template: 'Use {{#source-node.topic#}} and {{#n2.answer#}}', - query_variable_selector: ['source-node', 'topic'], - env_reference: '{{#env.API_KEY#}}', - }, - }), - createNode({ - id: 'n2', - selected: true, - position: { x: 140, y: 0 }, - width: 80, - height: 40, - }), - ] - const { store } = renderSelectionMenu({ nodes }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - fireEvent.click(await screen.findByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })) - - const dialogProps = mockCreateSnippetDialogRender.mock.calls.at(-1)?.[0] - expect(dialogProps.inputFields).toEqual([ - { - label: 'topic', - variable: 'topic', - type: PipelineInputVarType.textInput, - required: true, - }, - { - label: 'API_KEY', - variable: 'API_KEY', - type: PipelineInputVarType.textInput, - required: true, - }, - ]) - expect(dialogProps.selectedGraph.nodes[0].data.prompt_template).toBe('Use {{#start.topic#}} and {{#n2.answer#}}') - expect(dialogProps.selectedGraph.nodes[0].data.query_variable_selector).toEqual(['start', 'topic']) - expect(dialogProps.selectedGraph.nodes[0].data.env_reference).toBe('{{#start.API_KEY#}}') - }) - - it.each([ - BlockEnum.Answer, - BlockEnum.End, - BlockEnum.Start, - ])('should hide create snippet when selection contains %s node', async (nodeType) => { - const nodes = [ - createNode({ id: 'n1', selected: true, width: 80, height: 40, data: { type: nodeType } }), - createNode({ id: 'n2', selected: true, position: { x: 140, y: 0 }, width: 80, height: 40 }), - ] - const { store } = renderSelectionMenu({ nodes }) - - act(() => { - store.setState({ contextMenuTarget: { type: 'selection' } }) - }) - - await waitFor(() => { - expect(screen.getByRole('menuitem', { name: /common.copy/ })).toBeInTheDocument() - }) - expect(screen.queryByRole('menuitem', { name: /Create Snippet|snippet\.createDialogTitle/ })).not.toBeInTheDocument() + expect(screen.queryByTestId('create-snippet-dialog')).not.toBeInTheDocument() }) it('should stay hidden when only one node is selected', async () => { diff --git a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx index aaf26eedc56..fb56beda545 100644 --- a/web/app/components/workflow/block-selector/__tests__/main.spec.tsx +++ b/web/app/components/workflow/block-selector/__tests__/main.spec.tsx @@ -106,6 +106,7 @@ describe('NodeSelector', () => { await user.click(trigger) const searchInput = screen.getByPlaceholderText('workflow.tabs.searchBlock') + expect(screen.queryByText('workflow.tabs.snippets')).not.toBeInTheDocument() expect(screen.getByText('LLM')).toBeInTheDocument() expect(screen.getByText('End')).toBeInTheDocument() diff --git a/web/app/components/workflow/block-selector/main.tsx b/web/app/components/workflow/block-selector/main.tsx index 595426a262b..6678f081fe9 100644 --- a/web/app/components/workflow/block-selector/main.tsx +++ b/web/app/components/workflow/block-selector/main.tsx @@ -132,7 +132,7 @@ function NodeSelector({ const defaultAllowUserInputSelection = !hasUserInputNode && !hasTriggerNode const canSelectUserInput = allowUserInputSelection ?? defaultAllowUserInputSelection const disableStartTab = flowType === FlowType.snippet - const disableSnippetsTab = flowType === FlowType.snippet + const disableSnippetsTab = true const { activeTab, resetActiveTab, diff --git a/web/app/components/workflow/selection-contextmenu.tsx b/web/app/components/workflow/selection-contextmenu.tsx index dffbd31a5e1..0478b0e12f5 100644 --- a/web/app/components/workflow/selection-contextmenu.tsx +++ b/web/app/components/workflow/selection-contextmenu.tsx @@ -12,15 +12,11 @@ import { } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useReactFlowStore } from 'reactflow' -import { useCreateSnippetFromSelection } from '@/app/components/snippets/hooks/use-create-snippet-from-selection' -import { canCreateAndModifySnippets } from '@/app/components/snippets/utils/permission' import { useCollaborativeWorkflow } from '@/app/components/workflow/hooks/use-collaborative-workflow' -import { useSelector as useAppContextWithSelector } from '@/context/app-context' import { useNodesInteractions, useNodesReadOnly, useNodesSyncDraft } from './hooks' import { useWorkflowHistory, WorkflowHistoryEvent } from './hooks/use-workflow-history' import { ShortcutKbd } from './shortcuts/shortcut-kbd' import { useStore, useWorkflowStore } from './store' -import { BlockEnum } from './types' const AlignType = { Bottom: 'bottom', @@ -75,14 +71,6 @@ const menuSections: MenuSection[] = [ }, ] -const unsupportedSnippetNodeTypes = new Set([ - BlockEnum.Answer, - BlockEnum.End, - BlockEnum.Start, - BlockEnum.HumanInput, - BlockEnum.KnowledgeRetrieval, -]) - const getAlignableNodes = (nodes: Node[], selectedNodes: Node[]) => { const selectedNodeIds = new Set(selectedNodes.map(node => node.id)) const childNodeIds = new Set() @@ -235,7 +223,6 @@ export function SelectionContextmenu({ }) { const { t } = useTranslation() const { getNodesReadOnly } = useNodesReadOnly() - const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys) const { handleNodesCopy, handleNodesDelete, handleNodesDuplicate } = useNodesInteractions() const isSelectionContextMenu = useStore(s => s.contextMenuTarget?.type === 'selection') @@ -247,20 +234,8 @@ export function SelectionContextmenu({ const selectedNodes = useReactFlowStore(state => state.getNodes().filter(node => node.selected), ) - const edges = useReactFlowStore(state => state.edges) const { handleSyncWorkflowDraft } = useNodesSyncDraft() const { saveStateToHistory } = useWorkflowHistory() - const { - createSnippetDialog, - handleOpenCreateSnippet, - isCreateSnippetDialogOpen, - } = useCreateSnippetFromSelection({ - edges, - selectedNodes, - onClose, - }) - const canCreateSnippet = canCreateAndModifySnippets(workspacePermissionKeys) - && selectedNodes.every(node => !unsupportedSnippetNodeTypes.has(node.data.type)) const handleCopyNodes = useCallback(() => { handleNodesCopy() @@ -370,24 +345,11 @@ export function SelectionContextmenu({ }, [collaborativeWorkflow, workflowStore, selectedNodes, getNodesReadOnly, handleSyncWorkflowDraft, saveStateToHistory, onClose]) if (!isSelectionContextMenu || selectedNodes.length <= 1) - return isCreateSnippetDialogOpen ? createSnippetDialog : null + return null return ( <> - {canCreateSnippet && ( - <> - - - {t('snippet.createDialogTitle', { defaultValue: 'Create Snippet', ns: 'workflow' })} - - - - - )} ))} - {createSnippetDialog} ) }