Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Refactor] Support handleAddFolder in TreeExplorer #3101

Merged
merged 4 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions browser_tests/menu.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ test.describe('Menu', () => {
test('Can add new bookmark folder', async ({ comfyPage }) => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.newFolderButton.click()
await comfyPage.page.keyboard.press('Enter')
const textInput = comfyPage.page.locator('.editable-text input')
await textInput.waitFor({ state: 'visible' })
await textInput.fill('New Folder')
await textInput.press('Enter')
expect(await tab.getFolder('New Folder').count()).toBe(1)
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
Expand All @@ -132,8 +135,10 @@ test.describe('Menu', () => {

await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('New Folder').click()
await comfyPage.page.keyboard.type('bar')
await comfyPage.page.keyboard.press('Enter')
const textInput = comfyPage.page.locator('.editable-text input')
await textInput.waitFor({ state: 'visible' })
await textInput.fill('bar')
await textInput.press('Enter')

expect(await tab.getFolder('bar').count()).toBe(1)
expect(
Expand Down
37 changes: 34 additions & 3 deletions src/components/common/TreeExplorer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,18 @@ import { computed, provide, ref } from 'vue'
import { useI18n } from 'vue-i18n'

import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import { useTreeFolderOperations } from '@/composables/tree/useTreeFolderOperations'
import { useErrorHandling } from '@/composables/useErrorHandling'
import {
InjectKeyExpandedKeys,
InjectKeyHandleEditLabelFunction,
type RenderedTreeExplorerNode,
type TreeExplorerNode
} from '@/types/treeExplorerTypes'
import { combineTrees, findNodeByKey } from '@/utils/treeUtil'

const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
provide(InjectKeyExpandedKeys, expandedKeys)
const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
// Tracks whether the caller has set the selectionKeys model.
const storeSelectionKeys = selectionKeys.value !== undefined
Expand All @@ -64,8 +68,23 @@ const emit = defineEmits<{
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
}>()

const {
newFolderNode,
getAddFolderMenuItem,
handleFolderCreation,
addFolderCommand
} = useTreeFolderOperations(
/* expandNode */ (node: TreeExplorerNode) => {
expandedKeys.value[node.key] = true
}
)

const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
return fillNodeInfo(props.root)
const renderedRoot = fillNodeInfo(props.root)
return newFolderNode.value
? combineTrees(renderedRoot, newFolderNode.value)
: renderedRoot
})
const getTreeNodeIcon = (node: TreeExplorerNode) => {
if (node.getIcon) {
Expand Down Expand Up @@ -127,7 +146,11 @@ const handleNodeLabelEdit = async (
) => {
await errorHandling.wrapWithErrorHandlingAsync(
async () => {
await node.handleRename(newName)
if (node.key === newFolderNode.value?.key) {
await handleFolderCreation(newName)
} else {
await node.handleRename(newName)
}
},
node.handleError,
() => {
Expand All @@ -147,6 +170,7 @@ const deleteCommand = async (node: RenderedTreeExplorerNode) => {
}
const menuItems = computed<MenuItem[]>(() =>
[
getAddFolderMenuItem(menuTargetNode.value),
{
label: t('g.rename'),
icon: 'pi pi-file-edit',
Expand Down Expand Up @@ -194,7 +218,14 @@ const wrapCommandWithErrorHandler = (

defineExpose({
renameCommand,
deleteCommand
deleteCommand,
/**
* The command to add a folder to a node via the context menu
* @param targetNodeKey - The key of the node where the folder will be added under
*/
addFolderCommand: (targetNodeKey: string) => {
addFolderCommand(findNodeByKey(renderedRoot.value, targetNodeKey))
}
})
</script>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import type {
TreeExplorerDragAndDropData,
TreeExplorerNode
} from '@/types/treeExplorerTypes'
import { findNodeByKey } from '@/utils/treeUtil'

const props = defineProps<{
filteredNodeDefs: ComfyNodeDefImpl[]
Expand Down Expand Up @@ -94,14 +93,6 @@ const { t } = useI18n()
const extraMenuItems = (
menuTargetNode: RenderedTreeExplorerNode<ComfyNodeDefImpl>
) => [
{
label: t('g.newFolder'),
icon: 'pi pi-folder-plus',
command: () => {
addNewBookmarkFolder(menuTargetNode)
},
visible: !menuTargetNode?.leaf
},
{
label: t('g.customize'),
icon: 'pi pi-palette',
Expand Down Expand Up @@ -152,6 +143,11 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
},
children: sortedChildren,
draggable: node.leaf,
handleAddFolder(newName: string) {
if (newName !== '') {
nodeBookmarkStore.addNewBookmarkFolder(this.data, newName)
}
},
renderDragPreview(container) {
const vnode = h(NodePreview, { nodeDef: node.data })
render(vnode, container)
Expand Down Expand Up @@ -197,25 +193,8 @@ const renderedBookmarkedRoot = computed<TreeExplorerNode<ComfyNodeDefImpl>>(
)

const treeExplorerRef = ref<InstanceType<typeof TreeExplorer> | null>(null)
const addNewBookmarkFolder = (
parent?: RenderedTreeExplorerNode<ComfyNodeDefImpl>
) => {
const newFolderKey =
'root/' + nodeBookmarkStore.addNewBookmarkFolder(parent?.data).slice(0, -1)
nextTick(() => {
treeExplorerRef.value?.renameCommand(
findNodeByKey(
renderedBookmarkedRoot.value,
newFolderKey
) as RenderedTreeExplorerNode
)
if (parent) {
expandedKeys.value[parent.key] = true
}
})
}
defineExpose({
addNewBookmarkFolder
addNewBookmarkFolder: () => treeExplorerRef.value?.addFolderCommand('root')
})

const showCustomizationDialog = ref(false)
Expand Down
16 changes: 11 additions & 5 deletions src/components/sidebar/tabs/nodeLibrary/NodeTreeFolder.vue
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@
</template>

<script setup lang="ts">
import { Ref, computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'
import { computed, inject, onMounted, onUnmounted, ref, watch } from 'vue'

import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
import type { BookmarkCustomization } from '@/schemas/apiSchema'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
import { ComfyNodeDefImpl } from '@/stores/nodeDefStore'
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
import {
InjectKeyExpandedKeys,
type RenderedTreeExplorerNode
} from '@/types/treeExplorerTypes'

const props = defineProps<{
const { node } = defineProps<{
node: RenderedTreeExplorerNode<ComfyNodeDefImpl>
}>()

const nodeBookmarkStore = useNodeBookmarkStore()
const customization = computed<BookmarkCustomization | undefined>(() => {
return nodeBookmarkStore.bookmarksCustomization[props.node.data.nodePath]
const nodeDef = node.data
return nodeDef
? nodeBookmarkStore.bookmarksCustomization[nodeDef.nodePath]
: undefined
})

const treeNodeElement = ref<HTMLElement | null>(null)
Expand Down Expand Up @@ -56,7 +62,7 @@ onUnmounted(() => {
}
})

const expandedKeys = inject<Ref<Record<string, boolean>>>('expandedKeys')
const expandedKeys = inject(InjectKeyExpandedKeys)
const handleItemDrop = (node: RenderedTreeExplorerNode) => {
expandedKeys.value[node.key] = true
}
Expand Down
76 changes: 76 additions & 0 deletions src/composables/tree/useTreeFolderOperations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'

import type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'

/**
* Use this to handle folder operations in a tree.
* @param expandNode - The function to expand a node.
*/
export function useTreeFolderOperations(
expandNode: (node: RenderedTreeExplorerNode) => void
) {
const { t } = useI18n()
const newFolderNode = ref<RenderedTreeExplorerNode | null>(null)
const addFolderTargetNode = ref<RenderedTreeExplorerNode | null>(null)

// Generate a unique temporary key for the new folder
const generateTempKey = (parentKey: string) => {
return `${parentKey}/new_folder_${Date.now()}`
}

// Handle folder creation after name is confirmed
const handleFolderCreation = async (newName: string) => {
if (!newFolderNode.value || !addFolderTargetNode.value) return

try {
// Call the handleAddFolder method with the new folder name
await addFolderTargetNode.value?.handleAddFolder?.(newName)
} finally {
newFolderNode.value = null
addFolderTargetNode.value = null
}
}

/**
* The command to add a folder to a node via the context menu
* @param targetNode - The node where the folder will be added under
*/
const addFolderCommand = (targetNode: RenderedTreeExplorerNode) => {
expandNode(targetNode)
newFolderNode.value = {
key: generateTempKey(targetNode.key),
label: '',
leaf: false,
children: [],
icon: 'pi pi-folder',
type: 'folder',
totalLeaves: 0,
badgeText: '',
isEditingLabel: true
}
addFolderTargetNode.value = targetNode
}

// Generate the "Add Folder" menu item
const getAddFolderMenuItem = (
targetNode: RenderedTreeExplorerNode | null
) => {
return {
label: t('g.newFolder'),
icon: 'pi pi-folder-plus',
command: () => {
if (targetNode) addFolderCommand(targetNode)
},
visible: targetNode && !targetNode.leaf && !!targetNode.handleAddFolder,
isAsync: false
}
}

return {
newFolderNode,
addFolderCommand,
getAddFolderMenuItem,
handleFolderCreation
}
}
18 changes: 4 additions & 14 deletions src/composables/useTreeExpansion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,30 +23,20 @@ export function useTreeExpansion(expandedKeys: Ref<Record<string, boolean>>) {
}

const expandNode = (node: TreeNode) => {
if (
node.key &&
typeof node.key === 'string' &&
node.children &&
node.children.length
) {
if (node.key && typeof node.key === 'string' && !node.leaf) {
expandedKeys.value[node.key] = true

for (const child of node.children) {
for (const child of node.children ?? []) {
expandNode(child)
}
}
}

const collapseNode = (node: TreeNode) => {
if (
node.key &&
typeof node.key === 'string' &&
node.children &&
node.children.length
) {
if (node.key && typeof node.key === 'string' && !node.leaf) {
delete expandedKeys.value[node.key]

for (const child of node.children) {
for (const child of node.children ?? []) {
collapseNode(child)
}
}
Expand Down
12 changes: 5 additions & 7 deletions src/stores/nodeBookmarkStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,12 @@ export const useNodeBookmarkStore = defineStore('nodeBookmark', () => {
)
}

const addNewBookmarkFolder = (parent?: ComfyNodeDefImpl) => {
const addNewBookmarkFolder = (
parent: ComfyNodeDefImpl | undefined,
folderName: string
) => {
const parentPath = parent ? parent.nodePath : ''
let newFolderPath = parentPath + 'New Folder/'
let suffix = 1
while (bookmarks.value.some((b: string) => b.startsWith(newFolderPath))) {
newFolderPath = parentPath + `New Folder ${suffix}/`
suffix++
}
const newFolderPath = parentPath + folderName + '/'
addBookmark(newFolderPath)
return newFolderPath
}
Expand Down
11 changes: 7 additions & 4 deletions src/types/treeExplorerTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { MenuItem } from 'primevue/menuitem'
import type { InjectionKey } from 'vue'
import type { InjectionKey, Ref } from 'vue'

export interface TreeExplorerNode<T = any> {
key: string
Expand All @@ -19,10 +19,10 @@ export interface TreeExplorerNode<T = any> {
) => void | Promise<void>
/** Function to handle deleting the node */
handleDelete?: (this: TreeExplorerNode<T>) => void | Promise<void>
/** Function to handle adding a child node */
handleAddChild?: (
/** Function to handle adding a folder */
handleAddFolder?: (
this: TreeExplorerNode<T>,
child: TreeExplorerNode<T>
folderName: string
) => void | Promise<void>
/** Whether the node is draggable */
draggable?: boolean
Expand Down Expand Up @@ -71,3 +71,6 @@ export type TreeExplorerDragAndDropData<T = any> = {
export const InjectKeyHandleEditLabelFunction: InjectionKey<
(node: RenderedTreeExplorerNode, newName: string) => void
> = Symbol()

export const InjectKeyExpandedKeys: InjectionKey<Ref<Record<string, boolean>>> =
Symbol()
Loading