mirror of
https://github.com/langgenius/dify.git
synced 2026-05-06 02:18:08 +08:00
refactor(skill-editor): adopt 4-generic StateCreator pattern for type-safe cross-slice access
Use explicit StateCreator<FullStore, [], [], SliceType> pattern instead of StateCreator<SliceType> for all skill-editor slices. This enables: - Type-safe cross-slice state access via get() - Explicit type contracts instead of relying on spread args behavior - Better maintainability following Lobe-chat's proven pattern Extract all type definitions to types.ts to avoid circular dependencies.
This commit is contained in:
@ -1,14 +1,14 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { DirtySliceShape, SkillEditorSliceShape } from './types'
|
||||||
|
|
||||||
export type DirtySliceShape = {
|
export type { DirtySliceShape } from './types'
|
||||||
dirtyContents: Map<string, string>
|
|
||||||
setDraftContent: (fileId: string, content: string) => void
|
|
||||||
clearDraftContent: (fileId: string) => void
|
|
||||||
isDirty: (fileId: string) => boolean
|
|
||||||
getDraftContent: (fileId: string) => string | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createDirtySlice: StateCreator<DirtySliceShape> = (set, get) => ({
|
export const createDirtySlice: StateCreator<
|
||||||
|
SkillEditorSliceShape,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
DirtySliceShape
|
||||||
|
> = (set, get) => ({
|
||||||
dirtyContents: new Map<string, string>(),
|
dirtyContents: new Map<string, string>(),
|
||||||
|
|
||||||
setDraftContent: (fileId: string, content: string) => {
|
setDraftContent: (fileId: string, content: string) => {
|
||||||
|
|||||||
@ -1,15 +1,14 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { FileOperationsMenuSliceShape, SkillEditorSliceShape } from './types'
|
||||||
|
|
||||||
export type FileOperationsMenuSliceShape = {
|
export type { FileOperationsMenuSliceShape } from './types'
|
||||||
contextMenu: {
|
|
||||||
top: number
|
|
||||||
left: number
|
|
||||||
nodeId: string
|
|
||||||
} | null
|
|
||||||
setContextMenu: (menu: FileOperationsMenuSliceShape['contextMenu']) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createFileOperationsMenuSlice: StateCreator<FileOperationsMenuSliceShape> = set => ({
|
export const createFileOperationsMenuSlice: StateCreator<
|
||||||
|
SkillEditorSliceShape,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
FileOperationsMenuSliceShape
|
||||||
|
> = set => ({
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
|
|
||||||
setContextMenu: (contextMenu) => {
|
setContextMenu: (contextMenu) => {
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { FileTreeSliceShape, OpensObject, SkillEditorSliceShape } from './types'
|
||||||
|
|
||||||
export type OpensObject = Record<string, boolean>
|
export type { FileTreeSliceShape, OpensObject } from './types'
|
||||||
|
|
||||||
export type FileTreeSliceShape = {
|
export const createFileTreeSlice: StateCreator<
|
||||||
expandedFolderIds: Set<string>
|
SkillEditorSliceShape,
|
||||||
setExpandedFolderIds: (ids: Set<string>) => void
|
[],
|
||||||
toggleFolder: (folderId: string) => void
|
[],
|
||||||
revealFile: (ancestorFolderIds: string[]) => void
|
FileTreeSliceShape
|
||||||
setExpandedFromOpens: (opens: OpensObject) => void
|
> = (set, get) => ({
|
||||||
getOpensObject: () => OpensObject
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createFileTreeSlice: StateCreator<FileTreeSliceShape> = (set, get) => ({
|
|
||||||
expandedFolderIds: new Set<string>(),
|
expandedFolderIds: new Set<string>(),
|
||||||
|
|
||||||
setExpandedFolderIds: (ids: Set<string>) => {
|
setExpandedFolderIds: (ids: Set<string>) => {
|
||||||
|
|||||||
@ -1,24 +1,17 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
import type { DirtySliceShape } from './dirty-slice'
|
import type { SkillEditorSliceShape } from './types'
|
||||||
import type { FileOperationsMenuSliceShape } from './file-operations-menu-slice'
|
|
||||||
import type { FileTreeSliceShape } from './file-tree-slice'
|
|
||||||
import type { MetadataSliceShape } from './metadata-slice'
|
|
||||||
import type { TabSliceShape } from './tab-slice'
|
|
||||||
import { createDirtySlice } from './dirty-slice'
|
import { createDirtySlice } from './dirty-slice'
|
||||||
import { createFileOperationsMenuSlice } from './file-operations-menu-slice'
|
import { createFileOperationsMenuSlice } from './file-operations-menu-slice'
|
||||||
import { createFileTreeSlice } from './file-tree-slice'
|
import { createFileTreeSlice } from './file-tree-slice'
|
||||||
import { createMetadataSlice } from './metadata-slice'
|
import { createMetadataSlice } from './metadata-slice'
|
||||||
import { createTabSlice } from './tab-slice'
|
import { createTabSlice } from './tab-slice'
|
||||||
|
|
||||||
export type SkillEditorSliceShape
|
export type { DirtySliceShape } from './dirty-slice'
|
||||||
= TabSliceShape
|
export type { FileOperationsMenuSliceShape } from './file-operations-menu-slice'
|
||||||
& FileTreeSliceShape
|
export type { FileTreeSliceShape } from './file-tree-slice'
|
||||||
& DirtySliceShape
|
export type { MetadataSliceShape } from './metadata-slice'
|
||||||
& MetadataSliceShape
|
export type { OpenTabOptions, TabSliceShape } from './tab-slice'
|
||||||
& FileOperationsMenuSliceShape
|
export type { SkillEditorSliceShape } from './types'
|
||||||
& {
|
|
||||||
resetSkillEditor: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...args) => ({
|
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...args) => ({
|
||||||
...createTabSlice(...args),
|
...createTabSlice(...args),
|
||||||
@ -35,7 +28,7 @@ export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...a
|
|||||||
previewTabId: null,
|
previewTabId: null,
|
||||||
expandedFolderIds: new Set<string>(),
|
expandedFolderIds: new Set<string>(),
|
||||||
dirtyContents: new Map<string, string>(),
|
dirtyContents: new Map<string, string>(),
|
||||||
fileMetadata: new Map<string, Record<string, any>>(),
|
fileMetadata: new Map<string, Record<string, unknown>>(),
|
||||||
dirtyMetadataIds: new Set<string>(),
|
dirtyMetadataIds: new Set<string>(),
|
||||||
contextMenu: null,
|
contextMenu: null,
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1,21 +1,18 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { MetadataSliceShape, SkillEditorSliceShape } from './types'
|
||||||
|
|
||||||
export type MetadataSliceShape = {
|
export type { MetadataSliceShape } from './types'
|
||||||
fileMetadata: Map<string, Record<string, any>>
|
|
||||||
dirtyMetadataIds: Set<string>
|
|
||||||
setFileMetadata: (fileId: string, metadata: Record<string, any>) => void
|
|
||||||
setDraftMetadata: (fileId: string, metadata: Record<string, any>) => void
|
|
||||||
clearDraftMetadata: (fileId: string) => void
|
|
||||||
clearFileMetadata: (fileId: string) => void
|
|
||||||
isMetadataDirty: (fileId: string) => boolean
|
|
||||||
getFileMetadata: (fileId: string) => Record<string, any> | undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createMetadataSlice: StateCreator<MetadataSliceShape> = (set, get) => ({
|
export const createMetadataSlice: StateCreator<
|
||||||
fileMetadata: new Map<string, Record<string, any>>(),
|
SkillEditorSliceShape,
|
||||||
|
[],
|
||||||
|
[],
|
||||||
|
MetadataSliceShape
|
||||||
|
> = (set, get) => ({
|
||||||
|
fileMetadata: new Map<string, Record<string, unknown>>(),
|
||||||
dirtyMetadataIds: new Set<string>(),
|
dirtyMetadataIds: new Set<string>(),
|
||||||
|
|
||||||
setFileMetadata: (fileId: string, metadata: Record<string, any>) => {
|
setFileMetadata: (fileId: string, metadata: Record<string, unknown>) => {
|
||||||
const { fileMetadata } = get()
|
const { fileMetadata } = get()
|
||||||
const nextMap = new Map(fileMetadata)
|
const nextMap = new Map(fileMetadata)
|
||||||
if (metadata)
|
if (metadata)
|
||||||
@ -25,7 +22,7 @@ export const createMetadataSlice: StateCreator<MetadataSliceShape> = (set, get)
|
|||||||
set({ fileMetadata: nextMap })
|
set({ fileMetadata: nextMap })
|
||||||
},
|
},
|
||||||
|
|
||||||
setDraftMetadata: (fileId: string, metadata: Record<string, any>) => {
|
setDraftMetadata: (fileId: string, metadata: Record<string, unknown>) => {
|
||||||
const { fileMetadata, dirtyMetadataIds } = get()
|
const { fileMetadata, dirtyMetadataIds } = get()
|
||||||
const nextMap = new Map(fileMetadata)
|
const nextMap = new Map(fileMetadata)
|
||||||
nextMap.set(fileId, metadata || {})
|
nextMap.set(fileId, metadata || {})
|
||||||
|
|||||||
@ -1,30 +1,14 @@
|
|||||||
import type { StateCreator } from 'zustand'
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { OpenTabOptions, SkillEditorSliceShape, TabSliceShape } from './types'
|
||||||
|
|
||||||
export type OpenTabOptions = {
|
export type { OpenTabOptions, TabSliceShape } from './types'
|
||||||
/** true = Pinned (permanent), false/undefined = Preview (temporary) */
|
|
||||||
pinned?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export type TabSliceShape = {
|
export const createTabSlice: StateCreator<
|
||||||
/** Ordered list of open tab file IDs */
|
SkillEditorSliceShape,
|
||||||
openTabIds: string[]
|
[],
|
||||||
/** Currently active tab file ID */
|
[],
|
||||||
activeTabId: string | null
|
TabSliceShape
|
||||||
/** Current preview tab file ID (at most one) */
|
> = (set, get) => ({
|
||||||
previewTabId: string | null
|
|
||||||
/** Open a file tab with optional pinned mode */
|
|
||||||
openTab: (fileId: string, options?: OpenTabOptions) => void
|
|
||||||
/** Close a tab */
|
|
||||||
closeTab: (fileId: string) => void
|
|
||||||
/** Activate an existing tab */
|
|
||||||
activateTab: (fileId: string) => void
|
|
||||||
/** Convert preview tab to pinned tab */
|
|
||||||
pinTab: (fileId: string) => void
|
|
||||||
/** Check if a tab is in preview mode */
|
|
||||||
isPreviewTab: (fileId: string) => boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createTabSlice: StateCreator<TabSliceShape> = (set, get) => ({
|
|
||||||
openTabIds: [],
|
openTabIds: [],
|
||||||
activeTabId: null,
|
activeTabId: null,
|
||||||
previewTabId: null,
|
previewTabId: null,
|
||||||
@ -73,9 +57,9 @@ export const createTabSlice: StateCreator<TabSliceShape> = (set, get) => ({
|
|||||||
newActiveTabId = null
|
newActiveTabId = null
|
||||||
}
|
}
|
||||||
|
|
||||||
const newPreviewTabId = previewTabId === fileId
|
let newPreviewTabId: string | null = null
|
||||||
? null
|
if (previewTabId !== fileId && previewTabId && newOpenTabIds.includes(previewTabId))
|
||||||
: (previewTabId && newOpenTabIds.includes(previewTabId) ? previewTabId : null)
|
newPreviewTabId = previewTabId
|
||||||
|
|
||||||
set({
|
set({
|
||||||
openTabIds: newOpenTabIds,
|
openTabIds: newOpenTabIds,
|
||||||
|
|||||||
@ -0,0 +1,63 @@
|
|||||||
|
export type OpenTabOptions = {
|
||||||
|
pinned?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TabSliceShape = {
|
||||||
|
openTabIds: string[]
|
||||||
|
activeTabId: string | null
|
||||||
|
previewTabId: string | null
|
||||||
|
openTab: (fileId: string, options?: OpenTabOptions) => void
|
||||||
|
closeTab: (fileId: string) => void
|
||||||
|
activateTab: (fileId: string) => void
|
||||||
|
pinTab: (fileId: string) => void
|
||||||
|
isPreviewTab: (fileId: string) => boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OpensObject = Record<string, boolean>
|
||||||
|
|
||||||
|
export type FileTreeSliceShape = {
|
||||||
|
expandedFolderIds: Set<string>
|
||||||
|
setExpandedFolderIds: (ids: Set<string>) => void
|
||||||
|
toggleFolder: (folderId: string) => void
|
||||||
|
revealFile: (ancestorFolderIds: string[]) => void
|
||||||
|
setExpandedFromOpens: (opens: OpensObject) => void
|
||||||
|
getOpensObject: () => OpensObject
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DirtySliceShape = {
|
||||||
|
dirtyContents: Map<string, string>
|
||||||
|
setDraftContent: (fileId: string, content: string) => void
|
||||||
|
clearDraftContent: (fileId: string) => void
|
||||||
|
isDirty: (fileId: string) => boolean
|
||||||
|
getDraftContent: (fileId: string) => string | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetadataSliceShape = {
|
||||||
|
fileMetadata: Map<string, Record<string, unknown>>
|
||||||
|
dirtyMetadataIds: Set<string>
|
||||||
|
setFileMetadata: (fileId: string, metadata: Record<string, unknown>) => void
|
||||||
|
setDraftMetadata: (fileId: string, metadata: Record<string, unknown>) => void
|
||||||
|
clearDraftMetadata: (fileId: string) => void
|
||||||
|
clearFileMetadata: (fileId: string) => void
|
||||||
|
isMetadataDirty: (fileId: string) => boolean
|
||||||
|
getFileMetadata: (fileId: string) => Record<string, unknown> | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FileOperationsMenuSliceShape = {
|
||||||
|
contextMenu: {
|
||||||
|
top: number
|
||||||
|
left: number
|
||||||
|
nodeId: string
|
||||||
|
} | null
|
||||||
|
setContextMenu: (menu: FileOperationsMenuSliceShape['contextMenu']) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SkillEditorSliceShape
|
||||||
|
= TabSliceShape
|
||||||
|
& FileTreeSliceShape
|
||||||
|
& DirtySliceShape
|
||||||
|
& MetadataSliceShape
|
||||||
|
& FileOperationsMenuSliceShape
|
||||||
|
& {
|
||||||
|
resetSkillEditor: () => void
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user