mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 10:28:10 +08:00
refactor(skill): move skill editor slice to core workflow store
Move SkillEditorSlice from injection pattern to core workflow store, making it available to all workflow contexts (workflow-app, chatflow, and future rag-pipeline). - Add createSkillEditorSlice to core createWorkflowStore - Remove complex type conversion logic from workflow-app/index.tsx - Remove optional chaining (?.) and non-null assertions (!) from components - Simplify slice composition with type assertions via unknown
This commit is contained in:
@ -1,8 +1,7 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import type { WorkflowSliceShape } from './store/workflow/workflow-slice'
|
|
||||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
||||||
import type { SkillEditorSliceShape } from '@/app/components/workflow/skill/store'
|
import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store'
|
||||||
import { useSearchParams } from 'next/navigation'
|
import { useSearchParams } from 'next/navigation'
|
||||||
import { useQueryState } from 'nuqs'
|
import { useQueryState } from 'nuqs'
|
||||||
import {
|
import {
|
||||||
@ -19,7 +18,6 @@ import {
|
|||||||
WorkflowContextProvider,
|
WorkflowContextProvider,
|
||||||
} from '@/app/components/workflow/context'
|
} from '@/app/components/workflow/context'
|
||||||
import SkillMain from '@/app/components/workflow/skill/main'
|
import SkillMain from '@/app/components/workflow/skill/main'
|
||||||
import { createSkillEditorSlice } from '@/app/components/workflow/skill/store'
|
|
||||||
import { useWorkflowStore } from '@/app/components/workflow/store'
|
import { useWorkflowStore } from '@/app/components/workflow/store'
|
||||||
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
|
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
|
||||||
import {
|
import {
|
||||||
@ -248,29 +246,10 @@ const WorkflowAppWithAdditionalContext = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowAppInjectedSlice = WorkflowSliceShape & SkillEditorSliceShape
|
|
||||||
type SliceCreatorArgs = Parameters<import('zustand').StateCreator<WorkflowAppInjectedSlice>>
|
|
||||||
|
|
||||||
const injectWorkflowStoreSliceFn: import('@/app/components/workflow/store').InjectWorkflowStoreSliceFn = (
|
|
||||||
set,
|
|
||||||
get,
|
|
||||||
store,
|
|
||||||
) => {
|
|
||||||
const args: SliceCreatorArgs = [
|
|
||||||
set as SliceCreatorArgs[0],
|
|
||||||
get as SliceCreatorArgs[1],
|
|
||||||
store as SliceCreatorArgs[2],
|
|
||||||
]
|
|
||||||
return {
|
|
||||||
...createWorkflowSlice(...args),
|
|
||||||
...createSkillEditorSlice(...args),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const WorkflowAppWrapper = () => {
|
const WorkflowAppWrapper = () => {
|
||||||
return (
|
return (
|
||||||
<WorkflowContextProvider
|
<WorkflowContextProvider
|
||||||
injectWorkflowStoreSliceFn={injectWorkflowStoreSliceFn}
|
injectWorkflowStoreSliceFn={createWorkflowSlice as InjectWorkflowStoreSliceFn}
|
||||||
>
|
>
|
||||||
<WorkflowAppWithAdditionalContext />
|
<WorkflowAppWithAdditionalContext />
|
||||||
</WorkflowContextProvider>
|
</WorkflowContextProvider>
|
||||||
|
|||||||
@ -12,26 +12,26 @@ import { useSkillAssetNodeMap } from './hooks/use-skill-asset-tree'
|
|||||||
|
|
||||||
const EditorTabs: FC = () => {
|
const EditorTabs: FC = () => {
|
||||||
const { t } = useTranslation('workflow')
|
const { t } = useTranslation('workflow')
|
||||||
const openTabIds = useStore(s => s.openTabIds!)
|
const openTabIds = useStore(s => s.openTabIds)
|
||||||
const activeTabId = useStore(s => s.activeTabId!)
|
const activeTabId = useStore(s => s.activeTabId)
|
||||||
const previewTabId = useStore(s => s.previewTabId!)
|
const previewTabId = useStore(s => s.previewTabId)
|
||||||
const dirtyContents = useStore(s => s.dirtyContents!)
|
const dirtyContents = useStore(s => s.dirtyContents)
|
||||||
const storeApi = useWorkflowStore()
|
const storeApi = useWorkflowStore()
|
||||||
const { data: nodeMap } = useSkillAssetNodeMap()
|
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||||
|
|
||||||
const [pendingCloseId, setPendingCloseId] = useState<string | null>(null)
|
const [pendingCloseId, setPendingCloseId] = useState<string | null>(null)
|
||||||
|
|
||||||
const handleTabClick = useCallback((fileId: string) => {
|
const handleTabClick = useCallback((fileId: string) => {
|
||||||
storeApi.getState().activateTab?.(fileId)
|
storeApi.getState().activateTab(fileId)
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
|
|
||||||
const handleTabDoubleClick = useCallback((fileId: string) => {
|
const handleTabDoubleClick = useCallback((fileId: string) => {
|
||||||
storeApi.getState().pinTab?.(fileId)
|
storeApi.getState().pinTab(fileId)
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
|
|
||||||
const closeTab = useCallback((fileId: string) => {
|
const closeTab = useCallback((fileId: string) => {
|
||||||
storeApi.getState().closeTab?.(fileId)
|
storeApi.getState().closeTab(fileId)
|
||||||
storeApi.getState().clearDraftContent?.(fileId)
|
storeApi.getState().clearDraftContent(fileId)
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
|
|
||||||
const handleTabClose = useCallback((fileId: string) => {
|
const handleTabClose = useCallback((fileId: string) => {
|
||||||
|
|||||||
@ -49,8 +49,8 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
|||||||
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
const { data: treeData, isLoading, error } = useSkillAssetTreeData()
|
||||||
const isMutating = useIsMutating() > 0
|
const isMutating = useIsMutating() > 0
|
||||||
|
|
||||||
const expandedFolderIds = useStore(s => s.expandedFolderIds!)
|
const expandedFolderIds = useStore(s => s.expandedFolderIds)
|
||||||
const activeTabId = useStore(s => s.activeTabId!)
|
const activeTabId = useStore(s => s.activeTabId)
|
||||||
const storeApi = useWorkflowStore()
|
const storeApi = useWorkflowStore()
|
||||||
|
|
||||||
const renameNode = useRenameAppAssetNode()
|
const renameNode = useRenameAppAssetNode()
|
||||||
@ -62,12 +62,12 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
|||||||
}, [expandedFolderIds])
|
}, [expandedFolderIds])
|
||||||
|
|
||||||
const handleToggle = useCallback((id: string) => {
|
const handleToggle = useCallback((id: string) => {
|
||||||
storeApi.getState().toggleFolder?.(id)
|
storeApi.getState().toggleFolder(id)
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
|
|
||||||
const handleActivate = useCallback((node: NodeApi<TreeNodeData>) => {
|
const handleActivate = useCallback((node: NodeApi<TreeNodeData>) => {
|
||||||
if (node.data.node_type === 'file')
|
if (node.data.node_type === 'file')
|
||||||
storeApi.getState().openTab?.(node.data.id, { pinned: true })
|
storeApi.getState().openTab(node.data.id, { pinned: true })
|
||||||
else
|
else
|
||||||
node.toggle()
|
node.toggle()
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
@ -95,7 +95,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className }) => {
|
|||||||
|
|
||||||
const ancestors = getAncestorIds(activeTabId, treeData.children)
|
const ancestors = getAncestorIds(activeTabId, treeData.children)
|
||||||
if (ancestors.length > 0)
|
if (ancestors.length > 0)
|
||||||
storeApi.getState().revealFile?.(ancestors)
|
storeApi.getState().revealFile(ancestors)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const node = tree.get(activeTabId)
|
const node = tree.get(activeTabId)
|
||||||
if (node) {
|
if (node) {
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const TreeContextMenu: FC<TreeContextMenuProps> = ({ treeRef }) => {
|
|||||||
const { data: treeData } = useSkillAssetTreeData()
|
const { data: treeData } = useSkillAssetTreeData()
|
||||||
|
|
||||||
const handleClose = useCallback(() => {
|
const handleClose = useCallback(() => {
|
||||||
storeApi.getState().setContextMenu?.(null)
|
storeApi.getState().setContextMenu(null)
|
||||||
}, [storeApi])
|
}, [storeApi])
|
||||||
|
|
||||||
useClickAway(() => {
|
useClickAway(() => {
|
||||||
|
|||||||
@ -26,7 +26,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
|||||||
const { t } = useTranslation('workflow')
|
const { t } = useTranslation('workflow')
|
||||||
const isFolder = node.data.node_type === 'folder'
|
const isFolder = node.data.node_type === 'folder'
|
||||||
const isSelected = node.isSelected
|
const isSelected = node.isSelected
|
||||||
const isDirty = useStore(s => s.dirtyContents?.has(node.data.id) ?? false)
|
const isDirty = useStore(s => s.dirtyContents.has(node.data.id))
|
||||||
const contextMenuNodeId = useStore(s => s.contextMenu?.nodeId)
|
const contextMenuNodeId = useStore(s => s.contextMenu?.nodeId)
|
||||||
const hasContextMenu = contextMenuNodeId === node.data.id
|
const hasContextMenu = contextMenuNodeId === node.data.id
|
||||||
const storeApi = useWorkflowStore()
|
const storeApi = useWorkflowStore()
|
||||||
@ -41,11 +41,11 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
|||||||
)
|
)
|
||||||
|
|
||||||
const openFilePreview = useCallback(() => {
|
const openFilePreview = useCallback(() => {
|
||||||
storeApi.getState().openTab?.(node.data.id, { pinned: false })
|
storeApi.getState().openTab(node.data.id, { pinned: false })
|
||||||
}, [node.data.id, storeApi])
|
}, [node.data.id, storeApi])
|
||||||
|
|
||||||
const openFilePinned = useCallback(() => {
|
const openFilePinned = useCallback(() => {
|
||||||
storeApi.getState().openTab?.(node.data.id, { pinned: true })
|
storeApi.getState().openTab(node.data.id, { pinned: true })
|
||||||
}, [node.data.id, storeApi])
|
}, [node.data.id, storeApi])
|
||||||
|
|
||||||
const { handleClick: handleFileClick, handleDoubleClick: handleFileDoubleClick } = useDelayedClick({
|
const { handleClick: handleFileClick, handleDoubleClick: handleFileDoubleClick } = useDelayedClick({
|
||||||
@ -79,7 +79,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
|||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
storeApi.getState().setContextMenu?.({
|
storeApi.getState().setContextMenu({
|
||||||
top: e.clientY,
|
top: e.clientY,
|
||||||
left: e.clientX,
|
left: e.clientX,
|
||||||
nodeId: node.data.id,
|
nodeId: node.data.id,
|
||||||
@ -97,7 +97,7 @@ const TreeNode = ({ node, style, dragHandle }: NodeRendererProps<TreeNodeData>)
|
|||||||
if (isFolder)
|
if (isFolder)
|
||||||
node.toggle()
|
node.toggle()
|
||||||
else
|
else
|
||||||
storeApi.getState().openTab?.(node.data.id, { pinned: true })
|
storeApi.getState().openTab(node.data.id, { pinned: true })
|
||||||
}
|
}
|
||||||
}, [isFolder, node, storeApi])
|
}, [isFolder, node, storeApi])
|
||||||
|
|
||||||
|
|||||||
@ -250,14 +250,14 @@ export function useFileOperations({
|
|||||||
await deleteNode.mutateAsync({ appId, nodeId })
|
await deleteNode.mutateAsync({ appId, nodeId })
|
||||||
|
|
||||||
descendantFileIds.forEach((fileId) => {
|
descendantFileIds.forEach((fileId) => {
|
||||||
storeApi.getState().closeTab?.(fileId)
|
storeApi.getState().closeTab(fileId)
|
||||||
storeApi.getState().clearDraftContent?.(fileId)
|
storeApi.getState().clearDraftContent(fileId)
|
||||||
})
|
})
|
||||||
|
|
||||||
// Also close and clear the node itself if it's a file
|
// Also close and clear the node itself if it's a file
|
||||||
if (!isFolder) {
|
if (!isFolder) {
|
||||||
storeApi.getState().closeTab?.(nodeId)
|
storeApi.getState().closeTab(nodeId)
|
||||||
storeApi.getState().clearDraftContent?.(nodeId)
|
storeApi.getState().clearDraftContent(nodeId)
|
||||||
}
|
}
|
||||||
|
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
|
|||||||
@ -55,7 +55,7 @@ const SidebarSearchAdd: FC = () => {
|
|||||||
const [showMenu, setShowMenu] = useState(false)
|
const [showMenu, setShowMenu] = useState(false)
|
||||||
|
|
||||||
const { data: treeData } = useSkillAssetTreeData()
|
const { data: treeData } = useSkillAssetTreeData()
|
||||||
const activeTabId = useStore(s => s.activeTabId!)
|
const activeTabId = useStore(s => s.activeTabId)
|
||||||
|
|
||||||
const targetFolderId = useMemo(() => {
|
const targetFolderId = useMemo(() => {
|
||||||
if (!treeData?.children)
|
if (!treeData?.children)
|
||||||
|
|||||||
@ -34,8 +34,8 @@ const SkillDocEditor: FC = () => {
|
|||||||
const appDetail = useAppStore(s => s.appDetail)
|
const appDetail = useAppStore(s => s.appDetail)
|
||||||
const appId = appDetail?.id || ''
|
const appId = appDetail?.id || ''
|
||||||
|
|
||||||
const activeTabId = useStore(s => s.activeTabId!)
|
const activeTabId = useStore(s => s.activeTabId)
|
||||||
const dirtyContents = useStore(s => s.dirtyContents!)
|
const dirtyContents = useStore(s => s.dirtyContents)
|
||||||
const storeApi = useWorkflowStore()
|
const storeApi = useWorkflowStore()
|
||||||
const { data: nodeMap } = useSkillAssetNodeMap()
|
const { data: nodeMap } = useSkillAssetNodeMap()
|
||||||
|
|
||||||
@ -68,8 +68,8 @@ const SkillDocEditor: FC = () => {
|
|||||||
const handleEditorChange = useCallback((value: string | undefined) => {
|
const handleEditorChange = useCallback((value: string | undefined) => {
|
||||||
if (!activeTabId || !isEditable)
|
if (!activeTabId || !isEditable)
|
||||||
return
|
return
|
||||||
storeApi.getState().setDraftContent?.(activeTabId, value ?? '')
|
storeApi.getState().setDraftContent(activeTabId, value ?? '')
|
||||||
storeApi.getState().pinTab?.(activeTabId)
|
storeApi.getState().pinTab(activeTabId)
|
||||||
}, [activeTabId, isEditable, storeApi])
|
}, [activeTabId, isEditable, storeApi])
|
||||||
|
|
||||||
const handleSave = useCallback(async () => {
|
const handleSave = useCallback(async () => {
|
||||||
@ -86,7 +86,7 @@ const SkillDocEditor: FC = () => {
|
|||||||
nodeId: activeTabId,
|
nodeId: activeTabId,
|
||||||
payload: { content },
|
payload: { content },
|
||||||
})
|
})
|
||||||
storeApi.getState().clearDraftContent?.(activeTabId)
|
storeApi.getState().clearDraftContent(activeTabId)
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('api.saved', { ns: 'common' }),
|
message: t('api.saved', { ns: 'common' }),
|
||||||
|
|||||||
@ -216,13 +216,19 @@ export type SkillEditorSliceShape
|
|||||||
resetSkillEditor: () => void
|
resetSkillEditor: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape, [], [], SkillEditorSliceShape> = (set, get, store) => {
|
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (set, get, store) => {
|
||||||
const args = [set, get, store] as Parameters<StateCreator<SkillEditorSliceShape>>
|
// Type assertion via unknown to allow composition with other slices in a larger store
|
||||||
|
// This is safe because all slice creators only use set/get for their own properties
|
||||||
|
const tabArgs = [set, get, store] as unknown as Parameters<StateCreator<TabSliceShape>>
|
||||||
|
const fileTreeArgs = [set, get, store] as unknown as Parameters<StateCreator<FileTreeSliceShape>>
|
||||||
|
const dirtyArgs = [set, get, store] as unknown as Parameters<StateCreator<DirtySliceShape>>
|
||||||
|
const menuArgs = [set, get, store] as unknown as Parameters<StateCreator<FileOperationsMenuSliceShape>>
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...createTabSlice(...args),
|
...createTabSlice(...tabArgs),
|
||||||
...createFileTreeSlice(...args),
|
...createFileTreeSlice(...fileTreeArgs),
|
||||||
...createDirtySlice(...args),
|
...createDirtySlice(...dirtyArgs),
|
||||||
...createFileOperationsMenuSlice(...args),
|
...createFileOperationsMenuSlice(...menuArgs),
|
||||||
|
|
||||||
resetSkillEditor: () => {
|
resetSkillEditor: () => {
|
||||||
set({
|
set({
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import {
|
|||||||
} from 'zustand'
|
} from 'zustand'
|
||||||
import { createStore } from 'zustand/vanilla'
|
import { createStore } from 'zustand/vanilla'
|
||||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||||
|
import { createSkillEditorSlice } from '@/app/components/workflow/skill/store'
|
||||||
import { createChatVariableSlice } from './chat-variable-slice'
|
import { createChatVariableSlice } from './chat-variable-slice'
|
||||||
import { createInspectVarsSlice } from './debug/inspect-vars-slice'
|
import { createInspectVarsSlice } from './debug/inspect-vars-slice'
|
||||||
import { createEnvVariableSlice } from './env-variable-slice'
|
import { createEnvVariableSlice } from './env-variable-slice'
|
||||||
@ -41,7 +42,6 @@ import { createWorkflowSlice } from './workflow-slice'
|
|||||||
export type SliceFromInjection
|
export type SliceFromInjection
|
||||||
= Partial<WorkflowAppSliceShape>
|
= Partial<WorkflowAppSliceShape>
|
||||||
& Partial<RagPipelineSliceShape>
|
& Partial<RagPipelineSliceShape>
|
||||||
& Partial<SkillEditorSliceShape>
|
|
||||||
|
|
||||||
export type Shape
|
export type Shape
|
||||||
= ChatVariableSliceShape
|
= ChatVariableSliceShape
|
||||||
@ -57,6 +57,7 @@ export type Shape
|
|||||||
& WorkflowSliceShape
|
& WorkflowSliceShape
|
||||||
& InspectVarsSliceShape
|
& InspectVarsSliceShape
|
||||||
& LayoutSliceShape
|
& LayoutSliceShape
|
||||||
|
& SkillEditorSliceShape
|
||||||
& SliceFromInjection
|
& SliceFromInjection
|
||||||
|
|
||||||
export type InjectWorkflowStoreSliceFn = StateCreator<SliceFromInjection>
|
export type InjectWorkflowStoreSliceFn = StateCreator<SliceFromInjection>
|
||||||
@ -82,6 +83,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
|
|||||||
...createWorkflowSlice(...args),
|
...createWorkflowSlice(...args),
|
||||||
...createInspectVarsSlice(...args),
|
...createInspectVarsSlice(...args),
|
||||||
...createLayoutSlice(...args),
|
...createLayoutSlice(...args),
|
||||||
|
...createSkillEditorSlice(...args),
|
||||||
...(injectWorkflowStoreSliceFn?.(...args) || {} as SliceFromInjection),
|
...(injectWorkflowStoreSliceFn?.(...args) || {} as SliceFromInjection),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user