mirror of
https://github.com/langgenius/dify.git
synced 2026-06-25 05:31:11 +08:00
feat(web): align snippet permission controls
This commit is contained in:
parent
f2c07194f8
commit
433dca0acd
@ -192,7 +192,7 @@ describe('SnippetInfoDropdown', () => {
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(screen.getByText('snippet.menu.editInfo')).toBeInTheDocument()
|
||||
expect(screen.queryByText('snippet.menu.exportSnippet')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('snippet.menu.exportSnippet')).toBeInTheDocument()
|
||||
expect(screen.queryByText('snippet.menu.deleteSnippet')).not.toBeInTheDocument()
|
||||
|
||||
unmount()
|
||||
@ -201,7 +201,7 @@ describe('SnippetInfoDropdown', () => {
|
||||
await user.click(screen.getByRole('button'))
|
||||
|
||||
expect(screen.queryByText('snippet.menu.editInfo')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('snippet.menu.exportSnippet')).toBeInTheDocument()
|
||||
expect(screen.queryByText('snippet.menu.exportSnippet')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('snippet.menu.deleteSnippet')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -244,7 +244,7 @@ describe('SnippetInfoDropdown', () => {
|
||||
describe('Export Snippet', () => {
|
||||
it('should export and download the snippet yaml', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockWorkspacePermissionKeys = ['snippets.management']
|
||||
mockWorkspacePermissionKeys = ['snippets.create_and_modify']
|
||||
mockExportMutateAsync.mockResolvedValue('yaml: content')
|
||||
|
||||
render(<SnippetInfoDropdown snippet={mockSnippet} />)
|
||||
@ -264,7 +264,7 @@ describe('SnippetInfoDropdown', () => {
|
||||
|
||||
it('should show an error toast when export fails', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockWorkspacePermissionKeys = ['snippets.management']
|
||||
mockWorkspacePermissionKeys = ['snippets.create_and_modify']
|
||||
mockExportMutateAsync.mockRejectedValue(new Error('export failed'))
|
||||
|
||||
render(<SnippetInfoDropdown snippet={mockSnippet} />)
|
||||
|
||||
@ -58,7 +58,7 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
|
||||
}, [])
|
||||
|
||||
const handleExportSnippet = React.useCallback(async () => {
|
||||
if (!canManageSnippet)
|
||||
if (!canCreateAndModifySnippet)
|
||||
return
|
||||
|
||||
setOpen(false)
|
||||
@ -70,7 +70,7 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
|
||||
catch {
|
||||
toast.error(t('exportFailed'))
|
||||
}
|
||||
}, [canManageSnippet, exportSnippetMutation, snippet.id, snippet.name, t])
|
||||
}, [canCreateAndModifySnippet, exportSnippetMutation, snippet.id, snippet.name, t])
|
||||
|
||||
const handleEditSnippet = React.useCallback(async ({ name, description }: {
|
||||
name: string
|
||||
@ -125,18 +125,20 @@ const SnippetInfoDropdown = ({ snippet }: SnippetInfoDropdownProps) => {
|
||||
popupClassName="w-[180px] p-1"
|
||||
>
|
||||
{canCreateAndModifySnippet && (
|
||||
<DropdownMenuItem className="mx-0 gap-2" onClick={handleOpenEditDialog}>
|
||||
<span aria-hidden className="i-ri-edit-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="grow">{t('menu.editInfo')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canManageSnippet && (
|
||||
<>
|
||||
<DropdownMenuItem className="mx-0 gap-2" onClick={handleOpenEditDialog}>
|
||||
<span aria-hidden className="i-ri-edit-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="grow">{t('menu.editInfo')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="mx-0 gap-2" onClick={handleExportSnippet}>
|
||||
<span aria-hidden className="i-ri-download-2-line size-4 shrink-0 text-text-tertiary" />
|
||||
<span className="grow">{t('menu.exportSnippet')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="my-1! bg-divider-subtle" />
|
||||
</>
|
||||
)}
|
||||
{canManageSnippet && (
|
||||
<>
|
||||
{canCreateAndModifySnippet && <DropdownMenuSeparator className="my-1! bg-divider-subtle" />}
|
||||
<DropdownMenuItem
|
||||
className="mx-0 gap-2"
|
||||
variant="destructive"
|
||||
|
||||
@ -359,6 +359,18 @@ describe('SnippetList', () => {
|
||||
expect(screen.queryByRole('button', { name: 'snippet.create' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('fetches snippets without create action for users with snippet management permission', () => {
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.management'])
|
||||
|
||||
renderList()
|
||||
|
||||
expect(mockUseInfiniteSnippetList).toHaveBeenCalledWith(expect.any(Object), {
|
||||
enabled: true,
|
||||
})
|
||||
expect(screen.getByRole('link', { name: /Sales Snippet/ })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('button', { name: 'snippet.create' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('does not fetch or render snippets without snippet list permissions', () => {
|
||||
mockWorkspacePermissionKeys.mockReturnValue([])
|
||||
|
||||
|
||||
@ -177,11 +177,11 @@ describe('SnippetCard', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
expect(await screen.findByRole('menuitem', { name: 'snippet.menu.editInfo' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: 'snippet.menu.exportSnippet' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('menuitem', { name: 'snippet.menu.exportSnippet' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: 'snippet.menu.deleteSnippet' })).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show export and delete with snippet management permission without edit info', async () => {
|
||||
it('should show delete with snippet management permission without create-and-modify actions', async () => {
|
||||
mockIsCurrentWorkspaceEditor.mockReturnValue(false)
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.management'])
|
||||
|
||||
@ -190,7 +190,7 @@ describe('SnippetCard', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.operation.more' }))
|
||||
|
||||
expect(screen.queryByRole('menuitem', { name: 'snippet.menu.editInfo' })).not.toBeInTheDocument()
|
||||
expect(await screen.findByRole('menuitem', { name: 'snippet.menu.exportSnippet' })).toBeInTheDocument()
|
||||
expect(screen.queryByRole('menuitem', { name: 'snippet.menu.exportSnippet' })).not.toBeInTheDocument()
|
||||
expect(await screen.findByRole('menuitem', { name: 'snippet.menu.deleteSnippet' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@ -215,7 +215,7 @@ describe('SnippetCard', () => {
|
||||
})
|
||||
|
||||
it('should export a snippet from the operations menu', async () => {
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.management'])
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.create_and_modify'])
|
||||
mockExportMutateAsync.mockResolvedValue('snippet-yaml')
|
||||
|
||||
render(<SnippetCard snippet={createSnippet()} />)
|
||||
@ -232,7 +232,7 @@ describe('SnippetCard', () => {
|
||||
})
|
||||
|
||||
it('should show an error toast when snippet export fails', async () => {
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.management'])
|
||||
mockWorkspacePermissionKeys.mockReturnValue(['snippets.create_and_modify'])
|
||||
mockExportMutateAsync.mockRejectedValue(new Error('export failed'))
|
||||
|
||||
render(<SnippetCard snippet={createSnippet()} />)
|
||||
|
||||
@ -82,7 +82,7 @@ const SnippetCard = ({
|
||||
}
|
||||
|
||||
const handleExportSnippet = async () => {
|
||||
if (!canManageSnippet)
|
||||
if (!canCreateAndModifySnippet)
|
||||
return
|
||||
|
||||
setIsOperationsMenuOpen(false)
|
||||
@ -155,13 +155,7 @@ const SnippetCard = ({
|
||||
</Link>
|
||||
|
||||
<div className="absolute right-0 bottom-1 left-0 flex h-10.5 shrink-0 items-center pt-1 pr-1.5 pb-1.5 pl-3.5">
|
||||
<div
|
||||
className="flex w-0 grow items-center gap-1"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
}}
|
||||
>
|
||||
<div className="flex w-0 grow items-center gap-1">
|
||||
<div className="mr-10.25 min-w-0 grow overflow-hidden">
|
||||
<TagSelector
|
||||
placement="bottom-start"
|
||||
@ -203,16 +197,18 @@ const SnippetCard = ({
|
||||
popupClassName="w-[216px]"
|
||||
>
|
||||
{canCreateAndModifySnippet && (
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleOpenEditDialog}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('menu.editInfo')}</span>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{canManageSnippet && (
|
||||
<>
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleOpenEditDialog}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('menu.editInfo')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="gap-2 px-3" onClick={handleExportSnippet}>
|
||||
<span className="system-sm-regular text-text-secondary">{t('menu.exportSnippet')}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{canManageSnippet && (
|
||||
<>
|
||||
{canCreateAndModifySnippet && <DropdownMenuSeparator />}
|
||||
<DropdownMenuItem
|
||||
variant="destructive"
|
||||
className="gap-2 px-3"
|
||||
|
||||
@ -25,6 +25,9 @@ const mockHandleStopRun = vi.fn()
|
||||
const mockHandleWorkflowStartRunInWorkflow = vi.fn()
|
||||
const mockHandleCheckBeforePublish = vi.fn()
|
||||
const mockUseAvailableNodesMetaData = vi.hoisted(() => vi.fn())
|
||||
const mockAppContext = vi.hoisted(() => ({
|
||||
workspacePermissionKeys: ['snippets.create_and_modify'] as string[],
|
||||
}))
|
||||
const mockInspectVarsCrud = {
|
||||
hasNodeInspectVars: vi.fn(),
|
||||
hasSetInspectVar: vi.fn(),
|
||||
@ -49,6 +52,12 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useSelector: <T,>(selector: (state: { workspacePermissionKeys: string[] }) => T): T => selector({
|
||||
workspacePermissionKeys: mockAppContext.workspacePermissionKeys,
|
||||
}),
|
||||
}))
|
||||
|
||||
let capturedHooksStore: Record<string, unknown> | undefined
|
||||
let capturedWorkflowNodes: WorkflowProps['nodes'] | undefined
|
||||
let snippetDetailStoreState: {
|
||||
@ -153,13 +162,15 @@ vi.mock('@/app/components/snippets/components/snippet-children', () => ({
|
||||
default: ({
|
||||
onPublish,
|
||||
canSave,
|
||||
canEdit,
|
||||
}: {
|
||||
canSave: boolean
|
||||
canEdit: boolean
|
||||
onPublish: () => void
|
||||
}) => (
|
||||
<div>
|
||||
<a href="/snippets">snippets list</a>
|
||||
<button type="button" disabled={!canSave} onClick={onPublish}>publish</button>
|
||||
{canEdit && <button type="button" disabled={!canSave} onClick={onPublish}>publish</button>}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -245,6 +256,7 @@ const createDraftNode = (id = 'draft-node') => ({
|
||||
describe('SnippetMain', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockAppContext.workspacePermissionKeys = ['snippets.create_and_modify']
|
||||
mockDoSyncWorkflowDraft.mockResolvedValue(undefined)
|
||||
mockSyncInputFieldsDraft.mockResolvedValue(undefined)
|
||||
mockPublishSnippetMutateAsync.mockResolvedValue({ created_at: 1_744_000_000 })
|
||||
@ -296,15 +308,17 @@ describe('SnippetMain', () => {
|
||||
expect(capturedWorkflowNodes?.map(node => node.id)).toEqual(['draft-node'])
|
||||
})
|
||||
|
||||
it('should keep the snippet canvas editable and sync draft changes without permission gating', async () => {
|
||||
it('should keep the snippet canvas editable and sync draft changes with create-and-modify permission', async () => {
|
||||
const draftNode = createDraftNode('draft-node')
|
||||
|
||||
renderSnippetMain({
|
||||
const { store } = renderSnippetMain({
|
||||
hasPublishedWorkflow: false,
|
||||
workflowDraftNodes: [draftNode],
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'edit' })).not.toBeInTheDocument()
|
||||
expect(screen.getByRole('button', { name: 'publish' })).toBeInTheDocument()
|
||||
expect(store.getState().canvasReadOnly).toBe(false)
|
||||
expect(mockSetNavigationState).toHaveBeenCalledWith(expect.objectContaining({
|
||||
readonly: false,
|
||||
}))
|
||||
@ -315,6 +329,38 @@ describe('SnippetMain', () => {
|
||||
expect(mockDoSyncWorkflowDraft).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should make the snippet canvas readonly and skip draft sync without create-and-modify permission', async () => {
|
||||
mockAppContext.workspacePermissionKeys = ['snippets.management']
|
||||
|
||||
const { store } = renderSnippetMain({
|
||||
hasPublishedWorkflow: false,
|
||||
workflowDraftNodes: [createDraftNode('draft-node')],
|
||||
})
|
||||
|
||||
expect(screen.queryByRole('button', { name: 'publish' })).not.toBeInTheDocument()
|
||||
expect(store.getState().canvasReadOnly).toBe(true)
|
||||
expect(mockSetNavigationState).toHaveBeenCalledWith(expect.objectContaining({
|
||||
readonly: true,
|
||||
}))
|
||||
|
||||
const doSyncWorkflowDraft = capturedHooksStore?.doSyncWorkflowDraft as (() => Promise<void>)
|
||||
await doSyncWorkflowDraft()
|
||||
const syncWorkflowDraftWhenPageClose = capturedHooksStore?.syncWorkflowDraftWhenPageClose as (() => void)
|
||||
syncWorkflowDraftWhenPageClose()
|
||||
snippetDetailStoreState.onFieldsChange?.([
|
||||
{
|
||||
type: PipelineInputVarType.textInput,
|
||||
label: 'Question',
|
||||
variable: 'question',
|
||||
required: false,
|
||||
},
|
||||
])
|
||||
|
||||
expect(mockDoSyncWorkflowDraft).not.toHaveBeenCalled()
|
||||
expect(mockSyncWorkflowDraftWhenPageClose).not.toHaveBeenCalled()
|
||||
expect(mockSyncInputFieldsDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render the draft graph even when a published workflow exists', async () => {
|
||||
const publishedNode = createDraftNode('published-node')
|
||||
const draftNode = createDraftNode('draft-node')
|
||||
|
||||
@ -8,6 +8,7 @@ type SnippetChildrenProps = {
|
||||
snippetId: string
|
||||
fields: SnippetInputField[]
|
||||
canSave: boolean
|
||||
canEdit: boolean
|
||||
isPublishing: boolean
|
||||
onPublish: () => void
|
||||
}
|
||||
@ -16,6 +17,7 @@ const SnippetChildren = ({
|
||||
snippetId,
|
||||
fields,
|
||||
canSave,
|
||||
canEdit,
|
||||
isPublishing,
|
||||
onPublish,
|
||||
}: SnippetChildrenProps) => {
|
||||
@ -26,6 +28,7 @@ const SnippetChildren = ({
|
||||
<SnippetHeader
|
||||
snippetId={snippetId}
|
||||
canSave={canSave}
|
||||
canEdit={canEdit}
|
||||
isPublishing={isPublishing}
|
||||
onPublish={onPublish}
|
||||
/>
|
||||
|
||||
@ -13,6 +13,7 @@ import RunMode from './run-mode'
|
||||
type SnippetHeaderProps = {
|
||||
snippetId: string
|
||||
canSave: boolean
|
||||
canEdit: boolean
|
||||
isPublishing: boolean
|
||||
onPublish: () => void
|
||||
}
|
||||
@ -39,6 +40,7 @@ const PublishAction = ({
|
||||
const SnippetHeader = ({
|
||||
snippetId,
|
||||
canSave,
|
||||
canEdit,
|
||||
isPublishing,
|
||||
onPublish,
|
||||
}: SnippetHeaderProps) => {
|
||||
@ -54,11 +56,15 @@ const SnippetHeader = ({
|
||||
normal: {
|
||||
components: {
|
||||
left: (
|
||||
<PublishAction
|
||||
canSave={canSave}
|
||||
isPublishing={isPublishing}
|
||||
onPublish={onPublish}
|
||||
/>
|
||||
canEdit
|
||||
? (
|
||||
<PublishAction
|
||||
canSave={canSave}
|
||||
isPublishing={isPublishing}
|
||||
onPublish={onPublish}
|
||||
/>
|
||||
)
|
||||
: null
|
||||
),
|
||||
},
|
||||
controls: {
|
||||
@ -78,7 +84,7 @@ const SnippetHeader = ({
|
||||
viewHistoryProps,
|
||||
},
|
||||
}
|
||||
}, [canSave, isPublishing, onPublish, t, viewHistoryProps])
|
||||
}, [canEdit, canSave, isPublishing, onPublish, t, viewHistoryProps])
|
||||
|
||||
return <Header {...headerProps} />
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
initialEdges,
|
||||
initialNodes,
|
||||
} from '@/app/components/workflow/utils'
|
||||
import { useSelector as useAppContextWithSelector } from '@/context/app-context'
|
||||
import { useSnippetDraftStore } from '../draft-store'
|
||||
import { useConfigsMap } from '../hooks/use-configs-map'
|
||||
import { useGetRunAndTraceUrl } from '../hooks/use-get-run-and-trace-url'
|
||||
@ -32,6 +33,7 @@ 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 SnippetChildren from './snippet-children'
|
||||
@ -51,6 +53,7 @@ type SnippetMainContentProps = {
|
||||
snippetId: string
|
||||
fields: SnippetInputField[]
|
||||
canSave: boolean
|
||||
canEdit: boolean
|
||||
onBeforePublish: () => Promise<Omit<SnippetDraftSyncPayload, 'hash'> | void>
|
||||
onSaved: (syncedDraftPayload?: Omit<SnippetDraftSyncPayload, 'hash'> | void) => void
|
||||
}
|
||||
@ -80,6 +83,7 @@ const SnippetMainContent = ({
|
||||
snippetId,
|
||||
fields,
|
||||
canSave,
|
||||
canEdit,
|
||||
onBeforePublish,
|
||||
onSaved,
|
||||
}: SnippetMainContentProps) => {
|
||||
@ -113,6 +117,7 @@ const SnippetMainContent = ({
|
||||
snippetId={snippetId}
|
||||
fields={fields}
|
||||
canSave={canSave}
|
||||
canEdit={canEdit}
|
||||
isPublishing={isPublishing}
|
||||
onPublish={handlePublishSnippet}
|
||||
/>
|
||||
@ -138,7 +143,9 @@ const SnippetMain = ({
|
||||
const effectiveDraftEdges = localDraftState?.edges ?? draftEdges
|
||||
const effectiveDraftViewport = localDraftState?.viewport ?? draftViewport
|
||||
const { graph, snippet } = effectiveDraftPayload
|
||||
const canSave = currentCanvasNodeCount > 0
|
||||
const workspacePermissionKeys = useAppContextWithSelector(state => state.workspacePermissionKeys)
|
||||
const canEditSnippet = canCreateAndModifySnippets(workspacePermissionKeys)
|
||||
const canSave = canEditSnippet && currentCanvasNodeCount > 0
|
||||
const {
|
||||
doSyncWorkflowDraft: syncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
@ -210,7 +217,7 @@ const SnippetMain = ({
|
||||
fields,
|
||||
handleFieldsChange: handleSnippetFieldsChange,
|
||||
} = useSnippetInputFieldActions({
|
||||
canEdit: true,
|
||||
canEdit: canEditSnippet,
|
||||
snippetId,
|
||||
})
|
||||
const {
|
||||
@ -234,12 +241,12 @@ const SnippetMain = ({
|
||||
}, [effectiveDraftPayload.inputFields, hydrateDraft, snippetId])
|
||||
|
||||
useEffect(() => {
|
||||
workflowStore.setState({ canvasReadOnly: false })
|
||||
workflowStore.setState({ canvasReadOnly: !canEditSnippet })
|
||||
|
||||
return () => {
|
||||
workflowStore.setState({ canvasReadOnly: false })
|
||||
}
|
||||
}, [workflowStore])
|
||||
}, [canEditSnippet, workflowStore])
|
||||
|
||||
useEffect(() => {
|
||||
workflowStore.temporal.getState().pause()
|
||||
@ -255,7 +262,19 @@ const SnippetMain = ({
|
||||
|
||||
const doSyncWorkflowDraft = useCallback((
|
||||
...args: Parameters<typeof syncWorkflowDraft>
|
||||
) => syncWorkflowDraft(...args), [syncWorkflowDraft])
|
||||
) => {
|
||||
if (!canEditSnippet)
|
||||
return Promise.resolve()
|
||||
|
||||
return syncWorkflowDraft(...args)
|
||||
}, [canEditSnippet, syncWorkflowDraft])
|
||||
|
||||
const handleSyncWorkflowDraftWhenPageClose = useCallback(() => {
|
||||
if (!canEditSnippet)
|
||||
return
|
||||
|
||||
syncWorkflowDraftWhenPageClose()
|
||||
}, [canEditSnippet, syncWorkflowDraftWhenPageClose])
|
||||
|
||||
const handleFieldsChange = useCallback((nextFields: SnippetInputField[]) => {
|
||||
handleSnippetFieldsChange(nextFields)
|
||||
@ -265,10 +284,10 @@ const SnippetMain = ({
|
||||
setNavigationState({
|
||||
snippetId,
|
||||
snippet,
|
||||
readonly: false,
|
||||
readonly: !canEditSnippet,
|
||||
onFieldsChange: handleFieldsChange,
|
||||
})
|
||||
}, [handleFieldsChange, setNavigationState, snippet, snippetId])
|
||||
}, [canEditSnippet, handleFieldsChange, setNavigationState, snippet, snippetId])
|
||||
|
||||
const updateLocalDraftFromSyncPayload = useCallback((
|
||||
syncedDraftPayload?: Omit<SnippetDraftSyncPayload, 'hash'> | void,
|
||||
@ -305,7 +324,7 @@ const SnippetMain = ({
|
||||
const hooksStore = useMemo(() => {
|
||||
return {
|
||||
doSyncWorkflowDraft,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
syncWorkflowDraftWhenPageClose: handleSyncWorkflowDraftWhenPageClose,
|
||||
handleRefreshWorkflowDraft,
|
||||
handleBackupDraft,
|
||||
handleLoadBackupDraft,
|
||||
@ -331,11 +350,19 @@ const SnippetMain = ({
|
||||
invalidateSysVarValues,
|
||||
resetConversationVar,
|
||||
invalidateConversationVarValues,
|
||||
accessControl: {
|
||||
canEdit: canEditSnippet,
|
||||
canComment: true,
|
||||
canRun: true,
|
||||
canImportExportDSL: canEditSnippet,
|
||||
canReleaseAndVersion: canEditSnippet,
|
||||
},
|
||||
configsMap,
|
||||
}
|
||||
}, [
|
||||
appendNodeInspectVars,
|
||||
availableNodesMetaData,
|
||||
canEditSnippet,
|
||||
configsMap,
|
||||
deleteAllInspectorVars,
|
||||
deleteInspectVar,
|
||||
@ -345,6 +372,7 @@ const SnippetMain = ({
|
||||
fetchInspectVarValue,
|
||||
fetchInspectVars,
|
||||
handleBackupDraft,
|
||||
handleSyncWorkflowDraftWhenPageClose,
|
||||
handleRefreshWorkflowDraft,
|
||||
handleLoadBackupDraft,
|
||||
handleRestoreFromPublishedWorkflow,
|
||||
@ -361,7 +389,6 @@ const SnippetMain = ({
|
||||
renameInspectVarName,
|
||||
resetConversationVar,
|
||||
resetToLastRunVar,
|
||||
syncWorkflowDraftWhenPageClose,
|
||||
])
|
||||
|
||||
return (
|
||||
@ -378,7 +405,8 @@ const SnippetMain = ({
|
||||
snippetId={snippetId}
|
||||
fields={fields}
|
||||
canSave={canSave}
|
||||
onBeforePublish={() => syncWorkflowDraft(true)}
|
||||
canEdit={canEditSnippet}
|
||||
onBeforePublish={() => doSyncWorkflowDraft(true)}
|
||||
onSaved={updateLocalDraftFromSyncPayload}
|
||||
/>
|
||||
</WorkflowWithInnerContext>
|
||||
|
||||
@ -29,7 +29,7 @@ const { mockUseQueryData, createTag } = vi.hoisted(() => ({
|
||||
}))
|
||||
|
||||
const mockWorkspacePermissionKeys = vi.hoisted(() => ({
|
||||
value: ['app.tag.manage', 'dataset.tag.manage', 'snippets.management'] as string[],
|
||||
value: ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify'] as string[],
|
||||
}))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
@ -99,7 +99,7 @@ describe('TagManagementModal', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQueryData.current = mockTags
|
||||
mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.management']
|
||||
mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify']
|
||||
vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: 0 })
|
||||
})
|
||||
|
||||
|
||||
@ -216,8 +216,8 @@ describe('TagSearchContent', () => {
|
||||
expect(screen.getByRole('option', { name: /KnowledgeDB/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders snippet management action with snippets management permission', () => {
|
||||
mockWorkspacePermissionKeys.value = ['snippets.management']
|
||||
it('renders snippet management action with snippets create-and-modify permission', () => {
|
||||
mockWorkspacePermissionKeys.value = ['snippets.create_and_modify']
|
||||
|
||||
render(
|
||||
<PanelHarness
|
||||
|
||||
@ -30,7 +30,7 @@ const { mockUseQueryData, createTag, bindTag, unBindTag } = vi.hoisted(() => {
|
||||
})
|
||||
|
||||
const mockWorkspacePermissionKeys = vi.hoisted(() => ({
|
||||
value: ['app.tag.manage', 'dataset.tag.manage', 'snippets.management'] as string[],
|
||||
value: ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify'] as string[],
|
||||
}))
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
@ -110,7 +110,7 @@ const defaultProps = {
|
||||
describe('TagSelector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.management']
|
||||
mockWorkspacePermissionKeys.value = ['app.tag.manage', 'dataset.tag.manage', 'snippets.create_and_modify']
|
||||
mockUseQueryData.current = appTags
|
||||
vi.mocked(createTag).mockResolvedValue({ id: 'new-tag', name: 'NewTag', type: 'app', binding_count: 0 })
|
||||
vi.mocked(bindTag).mockResolvedValue(undefined)
|
||||
@ -288,9 +288,9 @@ describe('TagSelector', () => {
|
||||
expect(createTag).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('opens snippet tag selector with snippets management permission', async () => {
|
||||
it('opens snippet tag selector with snippets create-and-modify permission', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockWorkspacePermissionKeys.value = ['snippets.management']
|
||||
mockWorkspacePermissionKeys.value = ['snippets.create_and_modify']
|
||||
mockUseQueryData.current = [{ id: 'snippet-tag-1', name: 'Reusable', type: 'snippet', binding_count: 1 }]
|
||||
|
||||
render(
|
||||
|
||||
@ -7,7 +7,7 @@ export const getTagManagePermissionKey = (type: TagType): PermissionKey => {
|
||||
return 'app.tag.manage'
|
||||
|
||||
if (type === 'snippet')
|
||||
return SnippetPermission.Management
|
||||
return SnippetPermission.CreateAndModify
|
||||
|
||||
return 'dataset.tag.manage'
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user