mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
improvement(folder-selection): folder deselection + selection order should match visual
This commit is contained in:
@@ -146,23 +146,77 @@ export function WorkflowList({
|
||||
const orderedWorkflowIds = useMemo(() => {
|
||||
const ids: string[] = []
|
||||
|
||||
const collectWorkflowIds = (folder: FolderTreeNode) => {
|
||||
const collectFromFolder = (folder: FolderTreeNode) => {
|
||||
const workflowsInFolder = workflowsByFolder[folder.id] || []
|
||||
for (const workflow of workflowsInFolder) {
|
||||
ids.push(workflow.id)
|
||||
const childItems: Array<{
|
||||
type: 'folder' | 'workflow'
|
||||
id: string
|
||||
sortOrder: number
|
||||
createdAt?: Date
|
||||
data: FolderTreeNode | WorkflowMetadata
|
||||
}> = []
|
||||
for (const child of folder.children) {
|
||||
childItems.push({
|
||||
type: 'folder',
|
||||
id: child.id,
|
||||
sortOrder: child.sortOrder,
|
||||
createdAt: child.createdAt,
|
||||
data: child,
|
||||
})
|
||||
}
|
||||
for (const childFolder of folder.children) {
|
||||
collectWorkflowIds(childFolder)
|
||||
for (const wf of workflowsInFolder) {
|
||||
childItems.push({
|
||||
type: 'workflow',
|
||||
id: wf.id,
|
||||
sortOrder: wf.sortOrder,
|
||||
createdAt: wf.createdAt,
|
||||
data: wf,
|
||||
})
|
||||
}
|
||||
childItems.sort(compareByOrder)
|
||||
for (const item of childItems) {
|
||||
if (item.type === 'workflow') {
|
||||
ids.push(item.id)
|
||||
} else {
|
||||
collectFromFolder(item.data as FolderTreeNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rootLevelItems: Array<{
|
||||
type: 'folder' | 'workflow'
|
||||
id: string
|
||||
sortOrder: number
|
||||
createdAt?: Date
|
||||
data: FolderTreeNode | WorkflowMetadata
|
||||
}> = []
|
||||
for (const folder of folderTree) {
|
||||
collectWorkflowIds(folder)
|
||||
rootLevelItems.push({
|
||||
type: 'folder',
|
||||
id: folder.id,
|
||||
sortOrder: folder.sortOrder,
|
||||
createdAt: folder.createdAt,
|
||||
data: folder,
|
||||
})
|
||||
}
|
||||
const rootWfs = workflowsByFolder.root || []
|
||||
for (const wf of rootWfs) {
|
||||
rootLevelItems.push({
|
||||
type: 'workflow',
|
||||
id: wf.id,
|
||||
sortOrder: wf.sortOrder,
|
||||
createdAt: wf.createdAt,
|
||||
data: wf,
|
||||
})
|
||||
}
|
||||
rootLevelItems.sort(compareByOrder)
|
||||
|
||||
const rootWorkflows = workflowsByFolder.root || []
|
||||
for (const workflow of rootWorkflows) {
|
||||
ids.push(workflow.id)
|
||||
for (const item of rootLevelItems) {
|
||||
if (item.type === 'workflow') {
|
||||
ids.push(item.id)
|
||||
} else {
|
||||
collectFromFolder(item.data as FolderTreeNode)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
@@ -185,13 +239,35 @@ export function WorkflowList({
|
||||
return ids
|
||||
}, [folderTree])
|
||||
|
||||
const workflowFolderMap = useMemo(() => {
|
||||
const map: Record<string, string> = {}
|
||||
for (const wf of regularWorkflows) {
|
||||
if (wf.folderId) {
|
||||
map[wf.id] = wf.folderId
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [regularWorkflows])
|
||||
|
||||
const folderWorkflowIds = useMemo(() => {
|
||||
const map: Record<string, string[]> = {}
|
||||
for (const [folderId, workflows] of Object.entries(workflowsByFolder)) {
|
||||
if (folderId !== 'root') {
|
||||
map[folderId] = workflows.map((w) => w.id)
|
||||
}
|
||||
}
|
||||
return map
|
||||
}, [workflowsByFolder])
|
||||
|
||||
const { handleWorkflowClick } = useWorkflowSelection({
|
||||
workflowIds: orderedWorkflowIds,
|
||||
activeWorkflowId: workflowId,
|
||||
workflowFolderMap,
|
||||
})
|
||||
|
||||
const { handleFolderClick } = useFolderSelection({
|
||||
folderIds: orderedFolderIds,
|
||||
folderWorkflowIds,
|
||||
})
|
||||
|
||||
const isWorkflowActive = useCallback(
|
||||
|
||||
@@ -6,17 +6,22 @@ interface UseFolderSelectionProps {
|
||||
* Flat array of all folder IDs in display order
|
||||
*/
|
||||
folderIds: string[]
|
||||
/**
|
||||
* Map from folder ID to the workflow IDs directly inside that folder
|
||||
*/
|
||||
folderWorkflowIds: Record<string, string[]>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing folder selection with support for single, range, and toggle selection.
|
||||
* Handles shift-click for range selection and cmd/ctrl-click for toggle selection.
|
||||
* Uses the last selected folder ID (tracked in store) as the anchor point for range selections.
|
||||
* Enforces parent-child constraint: selecting a folder deselects workflows inside it.
|
||||
*
|
||||
* @param props - Hook props
|
||||
* @returns Selection handlers
|
||||
*/
|
||||
export function useFolderSelection({ folderIds }: UseFolderSelectionProps) {
|
||||
export function useFolderSelection({ folderIds, folderWorkflowIds }: UseFolderSelectionProps) {
|
||||
const {
|
||||
selectedFolders,
|
||||
lastSelectedFolderId,
|
||||
@@ -25,6 +30,25 @@ export function useFolderSelection({ folderIds }: UseFolderSelectionProps) {
|
||||
toggleFolderSelection,
|
||||
} = useFolderStore()
|
||||
|
||||
/**
|
||||
* After a folder selection change, deselect any workflows whose parent folder is selected
|
||||
* to prevent parent-child co-selection.
|
||||
*/
|
||||
const deselectConflictingWorkflows = useCallback(() => {
|
||||
const { selectedWorkflows: workflows, selectedFolders: folders } = useFolderStore.getState()
|
||||
if (workflows.size === 0) return
|
||||
|
||||
for (const folderId of folders) {
|
||||
const wfIdsInFolder = folderWorkflowIds[folderId]
|
||||
if (!wfIdsInFolder) continue
|
||||
for (const wfId of wfIdsInFolder) {
|
||||
if (workflows.has(wfId)) {
|
||||
useFolderStore.getState().deselectWorkflow(wfId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [folderWorkflowIds])
|
||||
|
||||
/**
|
||||
* Handle folder click with support for shift-click range selection and cmd/ctrl-click toggle
|
||||
*
|
||||
@@ -34,24 +58,28 @@ export function useFolderSelection({ folderIds }: UseFolderSelectionProps) {
|
||||
*/
|
||||
const handleFolderClick = useCallback(
|
||||
(folderId: string, shiftKey: boolean, metaKey: boolean) => {
|
||||
// Cmd/Ctrl+Click: Toggle individual selection
|
||||
if (metaKey) {
|
||||
toggleFolderSelection(folderId)
|
||||
}
|
||||
// Shift+Click: Range selection from last selected folder to clicked folder
|
||||
else if (shiftKey && lastSelectedFolderId && lastSelectedFolderId !== folderId) {
|
||||
deselectConflictingWorkflows()
|
||||
} else if (shiftKey && lastSelectedFolderId && lastSelectedFolderId !== folderId) {
|
||||
selectFolderRange(folderIds, lastSelectedFolderId, folderId)
|
||||
}
|
||||
// Shift+Click without anchor: Select only this folder (establishes anchor)
|
||||
else if (shiftKey) {
|
||||
deselectConflictingWorkflows()
|
||||
} else if (shiftKey) {
|
||||
selectFolderOnly(folderId)
|
||||
}
|
||||
// Regular click: Select only this folder
|
||||
else {
|
||||
deselectConflictingWorkflows()
|
||||
} else {
|
||||
selectFolderOnly(folderId)
|
||||
deselectConflictingWorkflows()
|
||||
}
|
||||
},
|
||||
[folderIds, lastSelectedFolderId, selectFolderOnly, selectFolderRange, toggleFolderSelection]
|
||||
[
|
||||
folderIds,
|
||||
lastSelectedFolderId,
|
||||
selectFolderOnly,
|
||||
selectFolderRange,
|
||||
toggleFolderSelection,
|
||||
deselectConflictingWorkflows,
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -10,22 +10,46 @@ interface UseWorkflowSelectionProps {
|
||||
* Active workflow ID (from URL) - used as anchor for range selection
|
||||
*/
|
||||
activeWorkflowId: string | undefined
|
||||
/**
|
||||
* Map from workflow ID to its parent folder ID (only for workflows inside folders)
|
||||
*/
|
||||
workflowFolderMap: Record<string, string>
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for managing workflow selection with support for single, range, and toggle selection.
|
||||
* Handles shift-click for range selection and regular click for single selection.
|
||||
* Uses the active workflow ID as the anchor point for range selections.
|
||||
* Enforces parent-child constraint: selecting a workflow deselects its parent folder.
|
||||
*
|
||||
* @param props - Hook props
|
||||
* @returns Selection handlers
|
||||
*/
|
||||
export function useWorkflowSelection({ workflowIds, activeWorkflowId }: UseWorkflowSelectionProps) {
|
||||
export function useWorkflowSelection({
|
||||
workflowIds,
|
||||
activeWorkflowId,
|
||||
workflowFolderMap,
|
||||
}: UseWorkflowSelectionProps) {
|
||||
const { selectedWorkflows, selectOnly, selectRange, toggleWorkflowSelection } = useFolderStore()
|
||||
|
||||
/**
|
||||
* After a workflow selection change, deselect any folders that contain selected workflows
|
||||
* to prevent parent-child co-selection.
|
||||
*/
|
||||
const deselectConflictingFolders = useCallback(() => {
|
||||
const { selectedWorkflows: workflows, selectedFolders: folders } = useFolderStore.getState()
|
||||
if (folders.size === 0) return
|
||||
|
||||
for (const wfId of workflows) {
|
||||
const folderId = workflowFolderMap[wfId]
|
||||
if (folderId && folders.has(folderId)) {
|
||||
useFolderStore.getState().deselectFolder(folderId)
|
||||
}
|
||||
}
|
||||
}, [workflowFolderMap])
|
||||
|
||||
/**
|
||||
* Handle workflow click with support for shift-click range selection and cmd/ctrl-click toggle.
|
||||
* Does not clear folder selection to allow unified selection of workflows and folders.
|
||||
*
|
||||
* @param workflowId - ID of clicked workflow
|
||||
* @param shiftKey - Whether shift key was pressed
|
||||
@@ -33,24 +57,27 @@ export function useWorkflowSelection({ workflowIds, activeWorkflowId }: UseWorkf
|
||||
*/
|
||||
const handleWorkflowClick = useCallback(
|
||||
(workflowId: string, shiftKey: boolean, metaKey: boolean) => {
|
||||
// Cmd/Ctrl+Click: Toggle individual selection
|
||||
if (metaKey) {
|
||||
toggleWorkflowSelection(workflowId)
|
||||
}
|
||||
// Shift+Click: Range selection from active workflow to clicked workflow
|
||||
else if (shiftKey && activeWorkflowId && activeWorkflowId !== workflowId) {
|
||||
deselectConflictingFolders()
|
||||
} else if (shiftKey && activeWorkflowId && activeWorkflowId !== workflowId) {
|
||||
selectRange(workflowIds, activeWorkflowId, workflowId)
|
||||
}
|
||||
// Shift+Click without active workflow: Toggle selection
|
||||
else if (shiftKey) {
|
||||
deselectConflictingFolders()
|
||||
} else if (shiftKey) {
|
||||
toggleWorkflowSelection(workflowId)
|
||||
}
|
||||
// Regular click: Select only this workflow (preserves folder selection for unified multi-select)
|
||||
else {
|
||||
deselectConflictingFolders()
|
||||
} else {
|
||||
selectOnly(workflowId)
|
||||
}
|
||||
},
|
||||
[workflowIds, activeWorkflowId, selectOnly, selectRange, toggleWorkflowSelection]
|
||||
[
|
||||
workflowIds,
|
||||
activeWorkflowId,
|
||||
selectOnly,
|
||||
selectRange,
|
||||
toggleWorkflowSelection,
|
||||
deselectConflictingFolders,
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -117,7 +117,12 @@ export const useFolderStore = create<FolderState>()(
|
||||
|
||||
clearSelection: () => set({ selectedWorkflows: new Set() }),
|
||||
|
||||
selectOnly: (workflowId) => set({ selectedWorkflows: new Set([workflowId]) }),
|
||||
selectOnly: (workflowId) =>
|
||||
set({
|
||||
selectedWorkflows: new Set([workflowId]),
|
||||
selectedFolders: new Set(),
|
||||
lastSelectedFolderId: null,
|
||||
}),
|
||||
|
||||
selectRange: (workflowIds, fromId, toId) => {
|
||||
const fromIndex = workflowIds.indexOf(fromId)
|
||||
|
||||
Reference in New Issue
Block a user