Compare commits

...

2 Commits

Author SHA1 Message Date
yyh
2f081fa6fa 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.
2026-01-18 13:24:34 +08:00
yyh
3b27d9e819 refactor(skill-editor): remove type assertions by using spread args pattern
Replace explicit parameter destructuring with spread args pattern to
eliminate `as unknown as` type assertions when composing sub-slices.
This aligns with the pattern used in the main workflow store.
2026-01-18 13:11:06 +08:00
7 changed files with 136 additions and 112 deletions

View File

@ -1,14 +1,14 @@
import type { StateCreator } from 'zustand'
import type { DirtySliceShape, SkillEditorSliceShape } from './types'
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 { DirtySliceShape } from './types'
export const createDirtySlice: StateCreator<DirtySliceShape> = (set, get) => ({
export const createDirtySlice: StateCreator<
SkillEditorSliceShape,
[],
[],
DirtySliceShape
> = (set, get) => ({
dirtyContents: new Map<string, string>(),
setDraftContent: (fileId: string, content: string) => {

View File

@ -1,15 +1,14 @@
import type { StateCreator } from 'zustand'
import type { FileOperationsMenuSliceShape, SkillEditorSliceShape } from './types'
export type FileOperationsMenuSliceShape = {
contextMenu: {
top: number
left: number
nodeId: string
} | null
setContextMenu: (menu: FileOperationsMenuSliceShape['contextMenu']) => void
}
export type { FileOperationsMenuSliceShape } from './types'
export const createFileOperationsMenuSlice: StateCreator<FileOperationsMenuSliceShape> = set => ({
export const createFileOperationsMenuSlice: StateCreator<
SkillEditorSliceShape,
[],
[],
FileOperationsMenuSliceShape
> = set => ({
contextMenu: null,
setContextMenu: (contextMenu) => {

View File

@ -1,17 +1,14 @@
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 = {
expandedFolderIds: Set<string>
setExpandedFolderIds: (ids: Set<string>) => void
toggleFolder: (folderId: string) => void
revealFile: (ancestorFolderIds: string[]) => void
setExpandedFromOpens: (opens: OpensObject) => void
getOpensObject: () => OpensObject
}
export const createFileTreeSlice: StateCreator<FileTreeSliceShape> = (set, get) => ({
export const createFileTreeSlice: StateCreator<
SkillEditorSliceShape,
[],
[],
FileTreeSliceShape
> = (set, get) => ({
expandedFolderIds: new Set<string>(),
setExpandedFolderIds: (ids: Set<string>) => {

View File

@ -1,52 +1,36 @@
import type { StateCreator } from 'zustand'
import type { DirtySliceShape } from './dirty-slice'
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 type { SkillEditorSliceShape } from './types'
import { createDirtySlice } from './dirty-slice'
import { createFileOperationsMenuSlice } from './file-operations-menu-slice'
import { createFileTreeSlice } from './file-tree-slice'
import { createMetadataSlice } from './metadata-slice'
import { createTabSlice } from './tab-slice'
export type SkillEditorSliceShape
= TabSliceShape
& FileTreeSliceShape
& DirtySliceShape
& MetadataSliceShape
& FileOperationsMenuSliceShape
& {
resetSkillEditor: () => void
}
export type { DirtySliceShape } from './dirty-slice'
export type { FileOperationsMenuSliceShape } from './file-operations-menu-slice'
export type { FileTreeSliceShape } from './file-tree-slice'
export type { MetadataSliceShape } from './metadata-slice'
export type { OpenTabOptions, TabSliceShape } from './tab-slice'
export type { SkillEditorSliceShape } from './types'
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (set, get, store) => {
// 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 metadataArgs = [set, get, store] as unknown as Parameters<StateCreator<MetadataSliceShape>>
const menuArgs = [set, get, store] as unknown as Parameters<StateCreator<FileOperationsMenuSliceShape>>
return {
...createTabSlice(...tabArgs),
...createFileTreeSlice(...fileTreeArgs),
...createDirtySlice(...dirtyArgs),
...createMetadataSlice(...metadataArgs),
...createFileOperationsMenuSlice(...menuArgs),
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...args) => ({
...createTabSlice(...args),
...createFileTreeSlice(...args),
...createDirtySlice(...args),
...createMetadataSlice(...args),
...createFileOperationsMenuSlice(...args),
resetSkillEditor: () => {
const [set] = args
set({
openTabIds: [],
activeTabId: null,
previewTabId: null,
expandedFolderIds: new Set<string>(),
dirtyContents: new Map<string, string>(),
fileMetadata: new Map<string, Record<string, any>>(),
fileMetadata: new Map<string, Record<string, unknown>>(),
dirtyMetadataIds: new Set<string>(),
contextMenu: null,
})
},
}
}
})

View File

@ -1,21 +1,18 @@
import type { StateCreator } from 'zustand'
import type { MetadataSliceShape, SkillEditorSliceShape } from './types'
export type MetadataSliceShape = {
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 type { MetadataSliceShape } from './types'
export const createMetadataSlice: StateCreator<MetadataSliceShape> = (set, get) => ({
fileMetadata: new Map<string, Record<string, any>>(),
export const createMetadataSlice: StateCreator<
SkillEditorSliceShape,
[],
[],
MetadataSliceShape
> = (set, get) => ({
fileMetadata: new Map<string, Record<string, unknown>>(),
dirtyMetadataIds: new Set<string>(),
setFileMetadata: (fileId: string, metadata: Record<string, any>) => {
setFileMetadata: (fileId: string, metadata: Record<string, unknown>) => {
const { fileMetadata } = get()
const nextMap = new Map(fileMetadata)
if (metadata)
@ -25,7 +22,7 @@ export const createMetadataSlice: StateCreator<MetadataSliceShape> = (set, get)
set({ fileMetadata: nextMap })
},
setDraftMetadata: (fileId: string, metadata: Record<string, any>) => {
setDraftMetadata: (fileId: string, metadata: Record<string, unknown>) => {
const { fileMetadata, dirtyMetadataIds } = get()
const nextMap = new Map(fileMetadata)
nextMap.set(fileId, metadata || {})

View File

@ -1,30 +1,14 @@
import type { StateCreator } from 'zustand'
import type { OpenTabOptions, SkillEditorSliceShape, TabSliceShape } from './types'
export type OpenTabOptions = {
/** true = Pinned (permanent), false/undefined = Preview (temporary) */
pinned?: boolean
}
export type { OpenTabOptions, TabSliceShape } from './types'
export type TabSliceShape = {
/** Ordered list of open tab file IDs */
openTabIds: string[]
/** Currently active tab file ID */
activeTabId: string | null
/** Current preview tab file ID (at most one) */
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) => ({
export const createTabSlice: StateCreator<
SkillEditorSliceShape,
[],
[],
TabSliceShape
> = (set, get) => ({
openTabIds: [],
activeTabId: null,
previewTabId: null,
@ -73,9 +57,9 @@ export const createTabSlice: StateCreator<TabSliceShape> = (set, get) => ({
newActiveTabId = null
}
const newPreviewTabId = previewTabId === fileId
? null
: (previewTabId && newOpenTabIds.includes(previewTabId) ? previewTabId : null)
let newPreviewTabId: string | null = null
if (previewTabId !== fileId && previewTabId && newOpenTabIds.includes(previewTabId))
newPreviewTabId = previewTabId
set({
openTabIds: newOpenTabIds,

View File

@ -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
}