feat: multi select for file tree & clipboard support

This commit is contained in:
yyh
2026-01-20 15:23:37 +08:00
parent 331c65fd1d
commit 357489d444
13 changed files with 236 additions and 24 deletions

View File

@ -0,0 +1,38 @@
import type { StateCreator } from 'zustand'
import type { ClipboardSliceShape, SkillEditorSliceShape } from './types'
export type { ClipboardSliceShape } from './types'
export const createClipboardSlice: StateCreator<
SkillEditorSliceShape,
[],
[],
ClipboardSliceShape
> = (set, get) => ({
clipboard: null,
copyNodes: (nodeIds) => {
if (nodeIds.length === 0)
return
set({ clipboard: { operation: 'copy', nodeIds: new Set(nodeIds) } })
},
cutNodes: (nodeIds) => {
if (nodeIds.length === 0)
return
set({ clipboard: { operation: 'cut', nodeIds: new Set(nodeIds) } })
},
clearClipboard: () => {
set({ clipboard: null })
},
isCutNode: (nodeId) => {
const { clipboard } = get()
return clipboard?.operation === 'cut' && clipboard.nodeIds.has(nodeId)
},
hasClipboard: () => {
return get().clipboard !== null
},
})

View File

@ -17,6 +17,7 @@ export const createFileTreeSlice: StateCreator<
> = (set, get) => ({
expandedFolderIds: new Set<string>(),
selectedTreeNodeId: null,
selectedNodeIds: new Set<string>(),
pendingCreateNode: null,
setExpandedFolderIds: (ids: Set<string>) => {
@ -61,6 +62,21 @@ export const createFileTreeSlice: StateCreator<
set({ selectedTreeNodeId: nodeId })
},
setSelectedNodeIds: (nodeIds) => {
const lastId = nodeIds.length > 0 ? nodeIds[nodeIds.length - 1] : null
set({
selectedNodeIds: new Set(nodeIds),
selectedTreeNodeId: lastId,
})
},
clearSelection: () => {
set({
selectedNodeIds: new Set<string>(),
selectedTreeNodeId: null,
})
},
startCreateNode: (nodeType, parentId) => {
set({
pendingCreateNode: {

View File

@ -1,11 +1,13 @@
import type { StateCreator } from 'zustand'
import type { SkillEditorSliceShape } from './types'
import { createClipboardSlice } from './clipboard-slice'
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 { ClipboardSliceShape } from './clipboard-slice'
export type { DirtySliceShape } from './dirty-slice'
export type { FileOperationsMenuSliceShape } from './file-operations-menu-slice'
export type { FileTreeSliceShape } from './file-tree-slice'
@ -16,6 +18,7 @@ export type { SkillEditorSliceShape } from './types'
export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...args) => ({
...createTabSlice(...args),
...createFileTreeSlice(...args),
...createClipboardSlice(...args),
...createDirtySlice(...args),
...createMetadataSlice(...args),
...createFileOperationsMenuSlice(...args),
@ -28,7 +31,9 @@ export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...a
previewTabId: null,
expandedFolderIds: new Set<string>(),
selectedTreeNodeId: null,
selectedNodeIds: new Set<string>(),
pendingCreateNode: null,
clipboard: null,
dirtyContents: new Map<string, string>(),
fileMetadata: new Map<string, Record<string, unknown>>(),
dirtyMetadataIds: new Set<string>(),

View File

@ -32,6 +32,9 @@ export type FileTreeSliceShape = {
getOpensObject: () => OpensObject
selectedTreeNodeId: string | null
setSelectedTreeNodeId: (nodeId: string | null) => void
selectedNodeIds: Set<string>
setSelectedNodeIds: (nodeIds: string[]) => void
clearSelection: () => void
pendingCreateNode: PendingCreateNode | null
startCreateNode: (nodeType: PendingCreateNode['nodeType'], parentId: PendingCreateNode['parentId']) => void
clearCreateNode: () => void
@ -41,6 +44,22 @@ export type FileTreeSliceShape = {
setFileTreeSearchTerm: (term: string) => void
}
export type ClipboardOperation = 'copy' | 'cut'
export type ClipboardItem = {
operation: ClipboardOperation
nodeIds: Set<string>
}
export type ClipboardSliceShape = {
clipboard: ClipboardItem | null
copyNodes: (nodeIds: string[]) => void
cutNodes: (nodeIds: string[]) => void
clearClipboard: () => void
isCutNode: (nodeId: string) => boolean
hasClipboard: () => boolean
}
export type DirtySliceShape = {
dirtyContents: Map<string, string>
setDraftContent: (fileId: string, content: string) => void
@ -76,6 +95,7 @@ export type FileOperationsMenuSliceShape = {
export type SkillEditorSliceShape
= TabSliceShape
& FileTreeSliceShape
& ClipboardSliceShape
& DirtySliceShape
& MetadataSliceShape
& FileOperationsMenuSliceShape