refactor(skill): unify tree selection with VSCode-style single state
Remove redundant createTargetNodeId and use selectedTreeNodeId for both visual highlight and creation target. This simplifies the state management by having a single source of truth for tree selection, similar to VSCode's file explorer behavior where both files and folders can be selected.
This commit is contained in:
@@ -99,17 +99,18 @@ const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
|
||||
}, [storeApi])
|
||||
|
||||
const handleSelect = useCallback((nodes: NodeApi<TreeNodeData>[]) => {
|
||||
if (activeTabId) {
|
||||
storeApi.getState().setSelectedTreeNodeId(activeTabId)
|
||||
return
|
||||
}
|
||||
const selectedId = nodes[0]?.id ?? null
|
||||
storeApi.getState().setSelectedTreeNodeId(selectedId)
|
||||
storeApi.getState().setCreateTargetNodeId(selectedId)
|
||||
}, [activeTabId, storeApi])
|
||||
}, [storeApi])
|
||||
|
||||
// Clicking blank area clears selection for root-level creation
|
||||
const handleBlankAreaClick = useCallback(() => {
|
||||
storeApi.getState().setSelectedTreeNodeId(null)
|
||||
}, [storeApi])
|
||||
|
||||
const handleBlankAreaContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
storeApi.getState().setSelectedTreeNodeId(null)
|
||||
storeApi.getState().setContextMenu({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
@@ -169,6 +170,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
|
||||
// Root dropzone highlight - dashed border without layout shift
|
||||
isRootDropzone && 'relative rounded-lg bg-state-accent-hover after:pointer-events-none after:absolute after:inset-0 after:rounded-lg after:border-[1.5px] after:border-dashed after:border-state-accent-solid after:content-[\'\']',
|
||||
)}
|
||||
onClick={handleBlankAreaClick}
|
||||
onContextMenu={handleBlankAreaContextMenu}
|
||||
onDragEnter={handleRootDragEnter}
|
||||
onDragOver={handleRootDragOver}
|
||||
@@ -186,7 +188,7 @@ const FileTree: React.FC<FileTreeProps> = ({ className, searchTerm = '' }) => {
|
||||
indent={20}
|
||||
overscanCount={5}
|
||||
openByDefault={false}
|
||||
selection={activeTabId ?? selectedTreeNodeId ?? undefined}
|
||||
selection={selectedTreeNodeId ?? undefined}
|
||||
initialOpenState={initialOpensObject}
|
||||
onToggle={handleToggle}
|
||||
onSelect={handleSelect}
|
||||
|
||||
@@ -49,13 +49,12 @@ export function useTreeNodeHandlers({
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
storeApi.getState().setCreateTargetNodeId(node.data.id)
|
||||
node.select()
|
||||
node.select() // This triggers Tree's onSelect → setSelectedTreeNodeId
|
||||
if (isFolder)
|
||||
throttledToggle()
|
||||
else
|
||||
handleFileClick()
|
||||
}, [handleFileClick, isFolder, node, storeApi, throttledToggle])
|
||||
}, [handleFileClick, isFolder, node, throttledToggle])
|
||||
|
||||
const handleDoubleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
@@ -74,7 +73,8 @@ export function useTreeNodeHandlers({
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
storeApi.getState().setCreateTargetNodeId(node.data.id)
|
||||
// Select the node for highlight + creation target
|
||||
storeApi.getState().setSelectedTreeNodeId(node.data.id)
|
||||
storeApi.getState().setContextMenu({
|
||||
top: e.clientY,
|
||||
left: e.clientX,
|
||||
|
||||
@@ -65,15 +65,14 @@ const SidebarSearchAdd: FC<SidebarSearchAddProps> = ({ onSearchChange }) => {
|
||||
}, [debouncedSearchValue, onSearchChange])
|
||||
|
||||
const { data: treeData } = useSkillAssetTreeData()
|
||||
const activeTabId = useStore(s => s.activeTabId)
|
||||
const createTargetNodeId = useStore(s => s.createTargetNodeId)
|
||||
const selectedTreeNodeId = useStore(s => s.selectedTreeNodeId)
|
||||
const treeChildren = treeData?.children
|
||||
|
||||
const targetFolderId = useMemo(() => {
|
||||
if (!treeChildren)
|
||||
return 'root'
|
||||
return getTargetFolderIdFromSelection(createTargetNodeId ?? activeTabId, treeChildren)
|
||||
}, [activeTabId, createTargetNodeId, treeChildren])
|
||||
return getTargetFolderIdFromSelection(selectedTreeNodeId, treeChildren)
|
||||
}, [selectedTreeNodeId, treeChildren])
|
||||
const menuOffset = useMemo(() => ({ mainAxis: 4 }), [])
|
||||
|
||||
const {
|
||||
|
||||
@@ -17,7 +17,6 @@ export const createFileTreeSlice: StateCreator<
|
||||
> = (set, get) => ({
|
||||
expandedFolderIds: new Set<string>(),
|
||||
selectedTreeNodeId: null,
|
||||
createTargetNodeId: null,
|
||||
pendingCreateNode: null,
|
||||
|
||||
setExpandedFolderIds: (ids: Set<string>) => {
|
||||
@@ -62,10 +61,6 @@ export const createFileTreeSlice: StateCreator<
|
||||
set({ selectedTreeNodeId: nodeId })
|
||||
},
|
||||
|
||||
setCreateTargetNodeId: (nodeId) => {
|
||||
set({ createTargetNodeId: nodeId })
|
||||
},
|
||||
|
||||
startCreateNode: (nodeType, parentId) => {
|
||||
set({
|
||||
pendingCreateNode: {
|
||||
|
||||
@@ -28,7 +28,6 @@ export const createSkillEditorSlice: StateCreator<SkillEditorSliceShape> = (...a
|
||||
previewTabId: null,
|
||||
expandedFolderIds: new Set<string>(),
|
||||
selectedTreeNodeId: null,
|
||||
createTargetNodeId: null,
|
||||
pendingCreateNode: null,
|
||||
dirtyContents: new Map<string, string>(),
|
||||
fileMetadata: new Map<string, Record<string, unknown>>(),
|
||||
|
||||
@@ -30,8 +30,6 @@ export type FileTreeSliceShape = {
|
||||
getOpensObject: () => OpensObject
|
||||
selectedTreeNodeId: string | null
|
||||
setSelectedTreeNodeId: (nodeId: string | null) => void
|
||||
createTargetNodeId: string | null
|
||||
setCreateTargetNodeId: (nodeId: string | null) => void
|
||||
pendingCreateNode: PendingCreateNode | null
|
||||
startCreateNode: (nodeType: PendingCreateNode['nodeType'], parentId: PendingCreateNode['parentId']) => void
|
||||
clearCreateNode: () => void
|
||||
|
||||
Reference in New Issue
Block a user