mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-19 11:57:31 +00:00
Compare commits
22 Commits
core/1.13
...
create_fol
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6e34cfd3 | ||
|
|
2a445f3f94 | ||
|
|
f8dcb915aa | ||
|
|
11925ce345 | ||
|
|
90053058ba | ||
|
|
b36f748a78 | ||
|
|
d57d12b426 | ||
|
|
bd1be28478 | ||
|
|
891e18af8e | ||
|
|
1610d06cd1 | ||
|
|
e3c7bbf966 | ||
|
|
0bfbbe838f | ||
|
|
c82fe80716 | ||
|
|
ad98bcb87c | ||
|
|
652ea15e8b | ||
|
|
a7a8cc633b | ||
|
|
d23aec4ceb | ||
|
|
8db088b27a | ||
|
|
7ef6e52f38 | ||
|
|
edeefe0883 | ||
|
|
c6046e47d2 | ||
|
|
e8bcccc276 |
@@ -95,18 +95,6 @@ export class WorkflowsSidebarTab extends SidebarTab {
|
||||
return this.page.locator('.workflows-sidebar-tab')
|
||||
}
|
||||
|
||||
get browseGalleryButton() {
|
||||
return this.root.locator('.browse-templates-button')
|
||||
}
|
||||
|
||||
get newBlankWorkflowButton() {
|
||||
return this.root.locator('.new-blank-workflow-button')
|
||||
}
|
||||
|
||||
get openWorkflowButton() {
|
||||
return this.root.locator('.open-workflow-button')
|
||||
}
|
||||
|
||||
async getOpenedWorkflowNames() {
|
||||
return await this.root
|
||||
.locator('.comfyui-workflows-open .node-label')
|
||||
|
||||
@@ -673,7 +673,7 @@ test.describe('Load duplicate workflow', () => {
|
||||
}) => {
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.loadWorkflow('single_ksampler')
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(1)
|
||||
})
|
||||
|
||||
@@ -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')
|
||||
@@ -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(
|
||||
@@ -397,7 +402,7 @@ test.describe('Menu', () => {
|
||||
'*Unsaved Workflow.json'
|
||||
])
|
||||
|
||||
await tab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
expect(await tab.getOpenedWorkflowNames()).toEqual([
|
||||
'*Unsaved Workflow.json',
|
||||
'*Unsaved Workflow (2).json'
|
||||
@@ -496,7 +501,7 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
test('Can save workflow as', async ({ comfyPage }) => {
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
|
||||
expect(
|
||||
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
|
||||
@@ -554,6 +559,8 @@ test.describe('Menu', () => {
|
||||
})
|
||||
|
||||
// Compare the exported workflow with the original
|
||||
delete downloadedContent.id
|
||||
delete downloadedContentZh.id
|
||||
expect(downloadedContent).toBeDefined()
|
||||
expect(downloadedContent).toEqual(downloadedContentZh)
|
||||
})
|
||||
@@ -618,7 +625,7 @@ test.describe('Menu', () => {
|
||||
|
||||
// Load blank workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
|
||||
// Switch back to the missing_nodes workflow
|
||||
await comfyPage.menu.workflowsTab.switchToWorkflow('missing_nodes')
|
||||
|
||||
@@ -57,13 +57,13 @@ test.describe('Templates', () => {
|
||||
test('Can load template workflows', async ({ comfyPage }) => {
|
||||
// Clear the workflow
|
||||
await comfyPage.menu.workflowsTab.open()
|
||||
await comfyPage.menu.workflowsTab.newBlankWorkflowButton.click()
|
||||
await comfyPage.executeCommand('Comfy.NewBlankWorkflow')
|
||||
await expect(async () => {
|
||||
expect(await comfyPage.getGraphNodesCount()).toBe(0)
|
||||
}).toPass({ timeout: 250 })
|
||||
|
||||
// Load a template
|
||||
await comfyPage.menu.workflowsTab.browseGalleryButton.click()
|
||||
await comfyPage.executeCommand('Comfy.BrowseTemplates')
|
||||
await expect(comfyPage.templates.content).toBeVisible()
|
||||
await comfyPage.templates.loadTemplate('default')
|
||||
await expect(comfyPage.templates.content).toBeHidden()
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,18 +1,18 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.13.7",
|
||||
"version": "1.14.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"version": "1.13.7",
|
||||
"version": "1.14.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.10.7",
|
||||
"@comfyorg/litegraph": "^0.10.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
@@ -466,9 +466,9 @@
|
||||
"license": "GPL-3.0-only"
|
||||
},
|
||||
"node_modules/@comfyorg/litegraph": {
|
||||
"version": "0.10.7",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.10.7.tgz",
|
||||
"integrity": "sha512-Cts4FBAk+e5RWPfmXUiBkjYzHkpy1eiW+DRCAYjGBGkobRhPZIBBHkk85vAnhMva704jSS9nsHG8c7r1/LUGyQ==",
|
||||
"version": "0.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@comfyorg/litegraph/-/litegraph-0.10.9.tgz",
|
||||
"integrity": "sha512-ubGozxdDIVNL/MYvfCAXgiaqBfIODtp0jZeN9uzWrdHwqUy9ZkLt/7/q7G4nGpNcEoShbMu7EK4VPH3WRmNQ7A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@comfyorg/comfyui-frontend",
|
||||
"private": true,
|
||||
"version": "1.13.7",
|
||||
"version": "1.14.0",
|
||||
"type": "module",
|
||||
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
|
||||
"homepage": "https://comfy.org",
|
||||
@@ -73,7 +73,7 @@
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"@atlaskit/pragmatic-drag-and-drop": "^1.3.1",
|
||||
"@comfyorg/comfyui-electron-types": "^0.4.20",
|
||||
"@comfyorg/litegraph": "^0.10.7",
|
||||
"@comfyorg/litegraph": "^0.10.9",
|
||||
"@primevue/forms": "^4.2.5",
|
||||
"@primevue/themes": "^4.2.5",
|
||||
"@sentry/vue": "^8.48.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
:class="props.class"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
v-model:selectionKeys="selectionKeys"
|
||||
:value="renderedRoots"
|
||||
:value="renderedRoot.children"
|
||||
selectionMode="single"
|
||||
:pt="{
|
||||
nodeLabel: 'tree-explorer-node-label',
|
||||
@@ -43,21 +43,24 @@ 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 type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
import {
|
||||
InjectKeyExpandedKeys,
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
type RenderedTreeExplorerNode,
|
||||
type TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { combineTrees, findNodeByKey } from '@/utils/treeUtil'
|
||||
|
||||
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
provide('expandedKeys', expandedKeys)
|
||||
provide(InjectKeyExpandedKeys, expandedKeys)
|
||||
const selectionKeys = defineModel<Record<string, boolean>>('selectionKeys')
|
||||
provide('selectionKeys', selectionKeys)
|
||||
// Tracks whether the caller has set the selectionKeys model.
|
||||
const storeSelectionKeys = selectionKeys.value !== undefined
|
||||
|
||||
const props = defineProps<{
|
||||
roots: TreeExplorerNode[]
|
||||
root: TreeExplorerNode
|
||||
class?: string
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
@@ -65,8 +68,23 @@ const emit = defineEmits<{
|
||||
(e: 'nodeDelete', node: RenderedTreeExplorerNode): void
|
||||
(e: 'contextMenu', node: RenderedTreeExplorerNode, event: MouseEvent): void
|
||||
}>()
|
||||
const renderedRoots = computed<RenderedTreeExplorerNode[]>(() => {
|
||||
return props.roots.map(fillNodeInfo)
|
||||
|
||||
const {
|
||||
newFolderNode,
|
||||
getAddFolderMenuItem,
|
||||
handleFolderCreation,
|
||||
addFolderCommand
|
||||
} = useTreeFolderOperations(
|
||||
/* expandNode */ (node: TreeExplorerNode) => {
|
||||
expandedKeys.value[node.key] = true
|
||||
}
|
||||
)
|
||||
|
||||
const renderedRoot = computed<RenderedTreeExplorerNode>(() => {
|
||||
const renderedRoot = fillNodeInfo(props.root)
|
||||
return newFolderNode.value
|
||||
? combineTrees(renderedRoot, newFolderNode.value)
|
||||
: renderedRoot
|
||||
})
|
||||
const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.getIcon) {
|
||||
@@ -81,7 +99,7 @@ const getTreeNodeIcon = (node: TreeExplorerNode) => {
|
||||
if (node.leaf) {
|
||||
return 'pi pi-file'
|
||||
}
|
||||
const isExpanded = expandedKeys.value[node.key]
|
||||
const isExpanded = expandedKeys.value?.[node.key] ?? false
|
||||
return isExpanded ? 'pi pi-folder-open' : 'pi pi-folder'
|
||||
}
|
||||
const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
@@ -95,7 +113,8 @@ const fillNodeInfo = (node: TreeExplorerNode): RenderedTreeExplorerNode => {
|
||||
children,
|
||||
type: node.leaf ? 'node' : 'folder',
|
||||
totalLeaves,
|
||||
badgeText: node.getBadgeText ? node.getBadgeText() : null
|
||||
badgeText: node.getBadgeText ? node.getBadgeText() : null,
|
||||
isEditingLabel: node.key === renameEditingNode.value?.key
|
||||
}
|
||||
}
|
||||
const onNodeContentClick = async (
|
||||
@@ -112,7 +131,6 @@ const onNodeContentClick = async (
|
||||
}
|
||||
const menu = ref(null)
|
||||
const menuTargetNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('menuTargetNode', menuTargetNode)
|
||||
const extraMenuItems = computed(() => {
|
||||
return menuTargetNode.value?.contextMenuItems
|
||||
? typeof menuTargetNode.value.contextMenuItems === 'function'
|
||||
@@ -121,7 +139,26 @@ const extraMenuItems = computed(() => {
|
||||
: []
|
||||
})
|
||||
const renameEditingNode = ref<RenderedTreeExplorerNode | null>(null)
|
||||
provide('renameEditingNode', renameEditingNode)
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleNodeLabelEdit = async (
|
||||
node: RenderedTreeExplorerNode,
|
||||
newName: string
|
||||
) => {
|
||||
await errorHandling.wrapWithErrorHandlingAsync(
|
||||
async () => {
|
||||
if (node.key === newFolderNode.value?.key) {
|
||||
await handleFolderCreation(newName)
|
||||
} else {
|
||||
await node.handleRename(newName)
|
||||
}
|
||||
},
|
||||
node.handleError,
|
||||
() => {
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
)()
|
||||
}
|
||||
provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit)
|
||||
|
||||
const { t } = useI18n()
|
||||
const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
@@ -133,6 +170,7 @@ const deleteCommand = async (node: RenderedTreeExplorerNode) => {
|
||||
}
|
||||
const menuItems = computed<MenuItem[]>(() =>
|
||||
[
|
||||
getAddFolderMenuItem(menuTargetNode.value),
|
||||
{
|
||||
label: t('g.rename'),
|
||||
icon: 'pi pi-file-edit',
|
||||
@@ -163,7 +201,6 @@ const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandling = useErrorHandling()
|
||||
const wrapCommandWithErrorHandler = (
|
||||
command: (event: MenuItemCommandEvent) => void,
|
||||
{ isAsync = false }: { isAsync: boolean }
|
||||
@@ -181,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>
|
||||
|
||||
|
||||
@@ -38,18 +38,17 @@
|
||||
<script setup lang="ts">
|
||||
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview'
|
||||
import Badge from 'primevue/badge'
|
||||
import { Ref, computed, inject, ref } from 'vue'
|
||||
import { computed, inject, ref } from 'vue'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import {
|
||||
usePragmaticDraggable,
|
||||
usePragmaticDroppable
|
||||
} from '@/composables/usePragmaticDragAndDrop'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerDragAndDropData,
|
||||
TreeExplorerNode
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
type RenderedTreeExplorerNode,
|
||||
type TreeExplorerDragAndDropData
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -77,22 +76,12 @@ const nodeBadgeText = computed<string>(() => {
|
||||
})
|
||||
const showNodeBadgeText = computed<boolean>(() => nodeBadgeText.value !== '')
|
||||
|
||||
const labelEditable = computed<boolean>(() => !!props.node.handleRename)
|
||||
const renameEditingNode =
|
||||
inject<Ref<TreeExplorerNode | null>>('renameEditingNode')
|
||||
const isEditing = computed(
|
||||
() => labelEditable.value && renameEditingNode.value?.key === props.node.key
|
||||
)
|
||||
const errorHandling = useErrorHandling()
|
||||
const handleRename = errorHandling.wrapWithErrorHandlingAsync(
|
||||
async (newName: string) => {
|
||||
await props.node.handleRename(newName)
|
||||
},
|
||||
props.node.handleError,
|
||||
() => {
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
)
|
||||
const isEditing = computed<boolean>(() => props.node.isEditingLabel)
|
||||
const handleEditLabel = inject(InjectKeyHandleEditLabelFunction)
|
||||
const handleRename = (newName: string) => {
|
||||
handleEditLabel(props.node, newName)
|
||||
}
|
||||
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const canDrop = ref(false)
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div ref="container" class="scroll-container">
|
||||
<div :style="{ height: `${(state.start / cols) * itemSize}px` }" />
|
||||
<div :style="{ height: `${(state.start / cols) * itemHeight}px` }" />
|
||||
<div :style="gridStyle">
|
||||
<div v-for="item in renderedItems" :key="item.key" data-virtual-grid-item>
|
||||
<slot name="item" :item="item"> </slot>
|
||||
@@ -8,7 +8,7 @@
|
||||
</div>
|
||||
<div
|
||||
:style="{
|
||||
height: `${((props.items.length - state.end) / cols) * itemSize}px`
|
||||
height: `${((items.length - state.end) / cols) * itemHeight}px`
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
@@ -19,22 +19,25 @@ import { useElementSize, useScroll } from '@vueuse/core'
|
||||
import { clamp, debounce } from 'lodash'
|
||||
import { type CSSProperties, computed, onBeforeUnmount, ref, watch } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
const {
|
||||
items,
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemHeight = 200,
|
||||
defaultItemWidth = 200
|
||||
} = defineProps<{
|
||||
items: (T & { key: string })[]
|
||||
gridStyle: Partial<CSSProperties>
|
||||
bufferRows?: number
|
||||
scrollThrottle?: number
|
||||
resizeDebounce?: number
|
||||
defaultItemSize?: number
|
||||
defaultItemHeight?: number
|
||||
defaultItemWidth?: number
|
||||
}>()
|
||||
const {
|
||||
bufferRows = 1,
|
||||
scrollThrottle = 64,
|
||||
resizeDebounce = 64,
|
||||
defaultItemSize = 200
|
||||
} = props
|
||||
|
||||
const itemSize = ref(defaultItemSize)
|
||||
const itemHeight = ref(defaultItemHeight)
|
||||
const itemWidth = ref(defaultItemWidth)
|
||||
const container = ref<HTMLElement | null>(null)
|
||||
const { width, height } = useElementSize(container)
|
||||
const { y: scrollY } = useScroll(container, {
|
||||
@@ -42,12 +45,10 @@ const { y: scrollY } = useScroll(container, {
|
||||
eventListenerOptions: { passive: true }
|
||||
})
|
||||
|
||||
const cols = computed(() => Math.floor(width.value / itemSize.value) || 1)
|
||||
const viewRows = computed(() => Math.ceil(height.value / itemSize.value))
|
||||
const offsetRows = computed(() => Math.floor(scrollY.value / itemSize.value))
|
||||
const isValidGrid = computed(
|
||||
() => height.value && width.value && props.items?.length
|
||||
)
|
||||
const cols = computed(() => Math.floor(width.value / itemWidth.value) || 1)
|
||||
const viewRows = computed(() => Math.ceil(height.value / itemHeight.value))
|
||||
const offsetRows = computed(() => Math.floor(scrollY.value / itemHeight.value))
|
||||
const isValidGrid = computed(() => height.value && width.value && items?.length)
|
||||
|
||||
const state = computed<{ start: number; end: number }>(() => {
|
||||
const fromRow = offsetRows.value - bufferRows
|
||||
@@ -57,18 +58,19 @@ const state = computed<{ start: number; end: number }>(() => {
|
||||
const toCol = toRow * cols.value
|
||||
|
||||
return {
|
||||
start: clamp(fromCol, 0, props.items?.length),
|
||||
end: clamp(toCol, fromCol, props.items?.length)
|
||||
start: clamp(fromCol, 0, items?.length),
|
||||
end: clamp(toCol, fromCol, items?.length)
|
||||
}
|
||||
})
|
||||
const renderedItems = computed(() =>
|
||||
isValidGrid.value ? props.items.slice(state.value.start, state.value.end) : []
|
||||
isValidGrid.value ? items.slice(state.value.start, state.value.end) : []
|
||||
)
|
||||
|
||||
const updateItemSize = () => {
|
||||
if (container.value) {
|
||||
const firstItem = container.value.querySelector('[data-virtual-grid-item]')
|
||||
itemSize.value = firstItem?.clientHeight || defaultItemSize
|
||||
itemHeight.value = firstItem?.clientHeight || defaultItemHeight
|
||||
itemWidth.value = firstItem?.clientWidth || defaultItemWidth
|
||||
}
|
||||
}
|
||||
const onResize = debounce(updateItemSize, resizeDebounce)
|
||||
|
||||
@@ -10,8 +10,10 @@ import { createI18n } from 'vue-i18n'
|
||||
|
||||
import EditableText from '@/components/common/EditableText.vue'
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import { useToastStore } from '@/stores/toastStore'
|
||||
import { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
RenderedTreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
// Create a mock i18n instance
|
||||
const i18n = createI18n({
|
||||
@@ -47,7 +49,6 @@ describe('TreeExplorerTreeNode', () => {
|
||||
props: { node: mockNode },
|
||||
global: {
|
||||
components: { EditableText, Badge },
|
||||
provide: { renameEditingNode: { value: null } },
|
||||
plugins: [createTestingPinia(), i18n]
|
||||
}
|
||||
})
|
||||
@@ -63,10 +64,14 @@ describe('TreeExplorerTreeNode', () => {
|
||||
|
||||
it('makes node label editable when renamingEditingNode matches', async () => {
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: mockNode },
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
isEditingLabel: true
|
||||
}
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
@@ -75,62 +80,25 @@ describe('TreeExplorerTreeNode', () => {
|
||||
expect(editableText.props('isEditing')).toBe(true)
|
||||
})
|
||||
|
||||
it('triggers handleRename callback when editing is finished', async () => {
|
||||
const handleRenameMock = vi.fn()
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
it('triggers handleEditLabel callback when editing is finished', async () => {
|
||||
const handleEditLabelMock = vi.fn()
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
props: {
|
||||
node: {
|
||||
...mockNode,
|
||||
isEditingLabel: true
|
||||
}
|
||||
},
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
provide: { [InjectKeyHandleEditLabelFunction]: handleEditLabelMock },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('shows error toast when handleRename promise rejects', async () => {
|
||||
const handleRenameMock = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('Rename failed'))
|
||||
const nodeWithMockRename = {
|
||||
...mockNode,
|
||||
handleRename: handleRenameMock
|
||||
}
|
||||
|
||||
const wrapper = mount(TreeExplorerTreeNode, {
|
||||
props: { node: nodeWithMockRename },
|
||||
global: {
|
||||
components: { EditableText, Badge, InputText },
|
||||
provide: { renameEditingNode: { value: { key: '1' } } },
|
||||
plugins: [createTestingPinia(), i18n, PrimeVue]
|
||||
}
|
||||
})
|
||||
|
||||
const toastStore = useToastStore()
|
||||
const addToastSpy = vi.spyOn(toastStore, 'add')
|
||||
|
||||
const editableText = wrapper.findComponent(EditableText)
|
||||
editableText.vm.$emit('edit', 'New Node Name')
|
||||
|
||||
// Wait for the promise to reject and the toast to be added
|
||||
vi.runAllTimers()
|
||||
|
||||
// Wait for any pending promises to resolve
|
||||
await new Promise(process.nextTick)
|
||||
|
||||
expect(handleRenameMock).toHaveBeenCalledOnce()
|
||||
expect(addToastSpy).toHaveBeenCalledWith({
|
||||
severity: 'error',
|
||||
summary: 'Error',
|
||||
detail: 'Rename failed'
|
||||
})
|
||||
expect(handleEditLabelMock).toHaveBeenCalledOnce()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -25,6 +25,10 @@
|
||||
v-bind="item.contentProps"
|
||||
:maximized="item.dialogComponentProps.maximized"
|
||||
/>
|
||||
|
||||
<template #footer v-if="item.footerComponent">
|
||||
<component :is="item.footerComponent" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
|
||||
114
src/components/dialog/content/ManagerProgressDialogContent.vue
Normal file
114
src/components/dialog/content/ManagerProgressDialogContent.vue
Normal file
@@ -0,0 +1,114 @@
|
||||
<template>
|
||||
<div
|
||||
class="overflow-hidden transition-all duration-300"
|
||||
:class="{
|
||||
'max-h-[500px]': isExpanded,
|
||||
'max-h-0 p-0 m-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div
|
||||
ref="sectionsContainerRef"
|
||||
class="px-6 py-4 overflow-y-auto max-h-[450px] scroll-container"
|
||||
:style="{
|
||||
scrollbarWidth: 'thin',
|
||||
scrollbarColor: 'rgba(156, 163, 175, 0.5) transparent'
|
||||
}"
|
||||
:class="{
|
||||
'max-h-[450px]': isExpanded,
|
||||
'max-h-0': !isExpanded
|
||||
}"
|
||||
>
|
||||
<div v-for="(panel, index) in taskPanels" :key="index">
|
||||
<Panel
|
||||
:expanded="expandedPanels[index] || false"
|
||||
toggleable
|
||||
class="shadow-elevation-1 rounded-lg mt-2 dark-theme:bg-black dark-theme:border-black"
|
||||
>
|
||||
<template #header>
|
||||
<div class="flex items-center justify-between w-full py-2">
|
||||
<div class="flex flex-col text-sm font-medium leading-normal">
|
||||
<span>{{ panel.taskName }}</span>
|
||||
<span v-show="expandedPanels[index]" class="text-muted">
|
||||
{{
|
||||
index === taskPanels.length - 1
|
||||
? 'In progress'
|
||||
: 'Completed ✓'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #toggleicon>
|
||||
<Button
|
||||
:icon="
|
||||
expandedPanels[index]
|
||||
? 'pi pi-chevron-down'
|
||||
: 'pi pi-chevron-right'
|
||||
"
|
||||
text
|
||||
class="text-neutral-300"
|
||||
@click="togglePanel(index)"
|
||||
/>
|
||||
</template>
|
||||
<div
|
||||
class="overflow-y-auto h-64 rounded-lg bg-black"
|
||||
:class="{
|
||||
'h-64': index !== taskPanels.length - 1,
|
||||
'flex-grow': index === taskPanels.length - 1
|
||||
}"
|
||||
>
|
||||
<div class="h-full">
|
||||
<div
|
||||
v-for="(log, logIndex) in panel.logs"
|
||||
:key="logIndex"
|
||||
class="text-neutral-400 dark-theme:text-muted"
|
||||
>
|
||||
<pre class="whitespace-pre-wrap break-words">{{ log }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useScroll, whenever } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import Panel from 'primevue/panel'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
|
||||
const { taskLogs } = useComfyManagerStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
|
||||
const taskPanels = computed(() => taskLogs)
|
||||
const isExpanded = computed(() => progressDialogContent.isExpanded)
|
||||
|
||||
const expandedPanels = ref<Record<number, boolean>>({})
|
||||
const togglePanel = (index: number) => {
|
||||
expandedPanels.value[index] = !expandedPanels.value[index]
|
||||
}
|
||||
|
||||
const sectionsContainerRef = ref<HTMLElement | null>(null)
|
||||
const { y: scrollY } = useScroll(sectionsContainerRef)
|
||||
|
||||
const scrollToBottom = () => {
|
||||
scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0
|
||||
}
|
||||
|
||||
whenever(() => isExpanded.value, scrollToBottom)
|
||||
onMounted(() => {
|
||||
expandedPanels.value = {}
|
||||
scrollToBottom()
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
progressDialogContent.collapse()
|
||||
})
|
||||
</script>
|
||||
@@ -124,7 +124,7 @@ import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { TabItem } from '@/types/comfyManagerTypes'
|
||||
import { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const DEFAULT_CARD_SIZE = 512
|
||||
const DEFAULT_CARD_SIZE = 349
|
||||
|
||||
const { t } = useI18n()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
option-value="value"
|
||||
:options="allVersionOptions"
|
||||
:highlight-on-select="false"
|
||||
class="my-3 w-full max-h-[50vh] border-none"
|
||||
class="my-3 w-full max-h-[50vh] border-none shadow-none"
|
||||
>
|
||||
<template #option="slotProps">
|
||||
<div class="flex justify-between items-center w-full p-1">
|
||||
|
||||
@@ -5,62 +5,65 @@
|
||||
'outline outline-[6px] outline-[var(--p-primary-color)]': isSelected
|
||||
}"
|
||||
:pt="{
|
||||
body: { class: 'p-0 flex flex-col h-full rounded-2xl' },
|
||||
body: { class: 'p-0 flex flex-col h-full rounded-2xl gap-0' },
|
||||
content: { class: 'flex-1 flex flex-col rounded-2xl' },
|
||||
title: { class: 'p-0 m-0' },
|
||||
title: {
|
||||
class:
|
||||
'self-stretch px-4 py-3 inline-flex justify-start items-center gap-6'
|
||||
},
|
||||
footer: { class: 'p-0 m-0' }
|
||||
}"
|
||||
>
|
||||
<template #title>
|
||||
<div class="flex justify-between p-5 pb-1 align-middle text-sm">
|
||||
<span class="flex items-start mt-2">
|
||||
<i
|
||||
class="pi pi-box text-muted text-2xl ml-1 mr-5"
|
||||
style="opacity: 0.5"
|
||||
/>
|
||||
<span class="text-lg relative top-[.25rem]">{{
|
||||
$t('manager.nodePack')
|
||||
}}</span>
|
||||
</span>
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isPackInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<PackCardHeader :node-pack="nodePack" />
|
||||
</template>
|
||||
<template #content>
|
||||
<ContentDivider />
|
||||
<div class="flex flex-1 p-5 mt-3 cursor-pointer">
|
||||
<div class="flex-shrink-0 mr-4">
|
||||
<PackIcon :node-pack="nodePack" />
|
||||
</div>
|
||||
<div class="flex flex-col flex-1 min-w-0">
|
||||
<div
|
||||
class="self-stretch px-4 py-3 inline-flex justify-start items-start cursor-pointer"
|
||||
>
|
||||
<PackIcon :node-pack="nodePack" />
|
||||
<div
|
||||
class="px-4 inline-flex flex-col justify-start items-start overflow-hidden"
|
||||
>
|
||||
<span
|
||||
class="text-lg font-bold pb-4 truncate overflow-hidden text-ellipsis"
|
||||
class="text-sm font-bold truncate overflow-hidden text-ellipsis"
|
||||
:title="nodePack.name"
|
||||
>
|
||||
{{ nodePack.name }}
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<div
|
||||
class="self-stretch inline-flex justify-center items-center gap-2.5"
|
||||
>
|
||||
<p
|
||||
v-if="nodePack.description"
|
||||
class="text-sm text-color-secondary m-0 line-clamp-3 overflow-hidden"
|
||||
class="flex-1 justify-start text-muted text-sm font-medium leading-3 break-words overflow-hidden min-h-12 line-clamp-3"
|
||||
:title="nodePack.description"
|
||||
>
|
||||
{{ nodePack.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
class="self-stretch inline-flex justify-start items-center gap-2"
|
||||
>
|
||||
<div
|
||||
v-if="nodesCount"
|
||||
class="px-2 py-1 flex justify-center text-sm items-center gap-1"
|
||||
>
|
||||
<div class="text-center justify-center font-medium leading-3">
|
||||
{{ nodesCount }} {{ $t('g.nodes') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-2 py-1 flex justify-center items-center gap-1">
|
||||
<div
|
||||
v-if="isUpdateAvailable"
|
||||
class="w-4 h-4 relative overflow-hidden"
|
||||
>
|
||||
<i class="pi pi-arrow-circle-up text-blue-600" />
|
||||
</div>
|
||||
<PackVersionBadge :node-pack="nodePack" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -76,21 +79,35 @@ import Card from 'primevue/card'
|
||||
import { computed } from 'vue'
|
||||
|
||||
import ContentDivider from '@/components/common/ContentDivider.vue'
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import PackCardFooter from '@/components/dialog/content/manager/packCard/PackCardFooter.vue'
|
||||
import PackIcon from '@/components/dialog/content/manager/packIcon/PackIcon.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
import { compareVersions, isSemVer } from '@/utils/formatUtil'
|
||||
|
||||
const { nodePack, isSelected = false } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
isSelected?: boolean
|
||||
}>()
|
||||
|
||||
const managerStore = useComfyManagerStore()
|
||||
const { isPackInstalled, getInstalledPackVersion } = useComfyManagerStore()
|
||||
|
||||
const isPackInstalled = computed(() =>
|
||||
managerStore.isPackInstalled(nodePack?.id)
|
||||
)
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const isUpdateAvailable = computed(() => {
|
||||
if (!isInstalled.value) return false
|
||||
|
||||
const latestVersion = nodePack.latest_version?.version
|
||||
if (!latestVersion) return false
|
||||
|
||||
const installedVersion = getInstalledPackVersion(nodePack.id)
|
||||
|
||||
// Don't attempt to show update available for nightly GitHub packs
|
||||
if (installedVersion && !isSemVer(installedVersion)) return false
|
||||
|
||||
return compareVersions(latestVersion, installedVersion) > 0
|
||||
})
|
||||
|
||||
// TODO: remove type assertion once comfy_nodes is added to node (pack) info type in backend
|
||||
const nodesCount = computed(() => (nodePack as any).comfy_nodes?.length)
|
||||
</script>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
<template>
|
||||
<div class="flex justify-between p-5 text-xs text-muted">
|
||||
<div
|
||||
class="flex justify-between px-5 py-4 text-xs text-muted font-medium leading-3"
|
||||
>
|
||||
<div class="flex items-center gap-2 cursor-pointer">
|
||||
<span v-if="nodePack.publisher?.name">
|
||||
{{ nodePack.publisher.name }}
|
||||
<span v-if="publisherName" class="max-w-40 truncate">
|
||||
{{ publisherName }}
|
||||
</span>
|
||||
<PackVersionBadge v-if="isInstalled" :node-pack="nodePack" />
|
||||
<span v-else-if="nodePack.latest_version">
|
||||
{{ nodePack.latest_version.version }}
|
||||
</span>
|
||||
@@ -26,14 +27,16 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackVersionBadge from '@/components/dialog/content/manager/PackVersionBadge.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
const publisherName = computed(() => {
|
||||
if (!nodePack) return null
|
||||
|
||||
const { publisher, author } = nodePack
|
||||
return publisher?.name ?? publisher?.id ?? author
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div class="w-[100%] flex justify-between items-center">
|
||||
<div class="flex justify-start items-center">
|
||||
<div class="w-1 h-6 rounded-md" />
|
||||
<div class="w-6 h-6 relative overflow-hidden">
|
||||
<i class="pi pi-box text-xl text-muted" style="opacity: 0.6" />
|
||||
</div>
|
||||
<div class="px-3 py-2 rounded-md flex justify-start items-start gap-2.5">
|
||||
<div class="text-right justify-start text-sm font-bold leading-none">
|
||||
{{ $t('manager.nodePack') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inline-flex justify-start items-center gap-3">
|
||||
<div
|
||||
v-if="nodePack.downloads"
|
||||
class="flex items-center text-sm text-muted tracking-tighter"
|
||||
>
|
||||
<i class="pi pi-download mr-2" />
|
||||
{{ $n(nodePack.downloads) }}
|
||||
</div>
|
||||
<template v-if="isInstalled">
|
||||
<PackEnableToggle :node-pack="nodePack" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<PackInstallButton :node-packs="[nodePack]" />
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
|
||||
import PackEnableToggle from '@/components/dialog/content/manager/button/PackEnableToggle.vue'
|
||||
import PackInstallButton from '@/components/dialog/content/manager/button/PackInstallButton.vue'
|
||||
import { useComfyManagerStore } from '@/stores/comfyManagerStore'
|
||||
import type { components } from '@/types/comfyRegistryTypes'
|
||||
|
||||
const { nodePack } = defineProps<{
|
||||
nodePack: components['schemas']['Node']
|
||||
}>()
|
||||
|
||||
const { isPackInstalled } = useComfyManagerStore()
|
||||
const isInstalled = computed(() => isPackInstalled(nodePack?.id))
|
||||
</script>
|
||||
107
src/components/dialog/footer/ManagerProgressFooter.vue
Normal file
107
src/components/dialog/footer/ManagerProgressFooter.vue
Normal file
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div
|
||||
class="w-full px-6 py-4 shadow-lg flex items-center justify-between"
|
||||
:class="{
|
||||
'rounded-t-none': progressDialogContent.isExpanded,
|
||||
'rounded-lg': !progressDialogContent.isExpanded
|
||||
}"
|
||||
>
|
||||
<div class="justify-center text-sm font-bold leading-none">
|
||||
<div class="flex items-center">
|
||||
<template v-if="isInProgress">
|
||||
<i class="pi pi-spin pi-spinner mr-2 text-3xl" />
|
||||
<span>{{ currentTaskName }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<i class="pi pi-check-circle mr-2 text-green-500" />
|
||||
<span class="leading-none">{{
|
||||
$t('manager.restartToApplyChanges')
|
||||
}}</span>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span v-if="isInProgress" class="text-xs font-bold text-neutral-600">
|
||||
{{ comfyManagerStore.uncompletedCount }} of
|
||||
{{ comfyManagerStore.taskLogs.length }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<Button
|
||||
v-if="!isInProgress"
|
||||
rounded
|
||||
outlined
|
||||
class="px-4 py-2 rounded-md mr-4"
|
||||
@click="handleRestart"
|
||||
>
|
||||
{{ $t('g.restart') }}
|
||||
</Button>
|
||||
<Button
|
||||
:icon="
|
||||
progressDialogContent.isExpanded
|
||||
? 'pi pi-chevron-up'
|
||||
: 'pi pi-chevron-right'
|
||||
"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
:aria-label="progressDialogContent.isExpanded ? 'Collapse' : 'Expand'"
|
||||
@click.stop="progressDialogContent.toggle"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
text
|
||||
rounded
|
||||
size="small"
|
||||
severity="secondary"
|
||||
aria-label="Close"
|
||||
@click.stop="closeDialog"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useEventListener } from '@vueuse/core'
|
||||
import Button from 'primevue/button'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { api } from '@/scripts/api'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import {
|
||||
useComfyManagerStore,
|
||||
useManagerProgressDialogStore
|
||||
} from '@/stores/comfyManagerStore'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
const { t } = useI18n()
|
||||
const dialogStore = useDialogStore()
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const comfyManagerStore = useComfyManagerStore()
|
||||
|
||||
const isInProgress = computed(() => comfyManagerStore.uncompletedCount > 0)
|
||||
|
||||
const closeDialog = () => {
|
||||
dialogStore.closeDialog({ key: 'global-manager-progress-dialog' })
|
||||
}
|
||||
|
||||
const fallbackTaskName = t('g.installing')
|
||||
const currentTaskName = computed(() => {
|
||||
if (!comfyManagerStore.taskLogs.length) return fallbackTaskName
|
||||
const task = comfyManagerStore.taskLogs.at(-1)
|
||||
return task?.taskName ?? fallbackTaskName
|
||||
})
|
||||
|
||||
const handleRestart = async () => {
|
||||
await useComfyManagerService().rebootComfyUI()
|
||||
closeDialog()
|
||||
|
||||
const onReconnect = () => {
|
||||
useCommandStore().execute('Comfy.RefreshNodeDefinitions')
|
||||
}
|
||||
useEventListener(api, 'reconnected', onReconnect, { once: true })
|
||||
}
|
||||
</script>
|
||||
29
src/components/dialog/header/ManagerProgressHeader.vue
Normal file
29
src/components/dialog/header/ManagerProgressHeader.vue
Normal file
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="progressDialogContent.isExpanded"
|
||||
class="px-4 py-2 flex items-center"
|
||||
>
|
||||
<TabMenu
|
||||
v-model:activeIndex="activeTabIndex"
|
||||
:model="tabs"
|
||||
class="w-full border-none"
|
||||
:pt="{
|
||||
menu: { class: 'border-none' },
|
||||
menuitem: { class: 'font-medium' },
|
||||
action: { class: 'px-4 py-2' }
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import TabMenu from 'primevue/tabmenu'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useManagerProgressDialogStore } from '@/stores/comfyManagerStore'
|
||||
|
||||
const progressDialogContent = useManagerProgressDialogStore()
|
||||
const activeTabIndex = ref(0)
|
||||
|
||||
const tabs = [{ label: 'Installation Queue' }, { label: 'Failed (0)' }]
|
||||
</script>
|
||||
@@ -30,7 +30,7 @@ const widgets = computed(() =>
|
||||
)
|
||||
)
|
||||
|
||||
const MARGIN = 10
|
||||
const DEFAULT_MARGIN = 10
|
||||
const updateWidgets = () => {
|
||||
const lgCanvas = canvasStore.canvas
|
||||
if (!lgCanvas) return
|
||||
@@ -49,10 +49,11 @@ const updateWidgets = () => {
|
||||
|
||||
widgetState.visible = visible
|
||||
if (visible) {
|
||||
widgetState.pos = [node.pos[0] + MARGIN, node.pos[1] + MARGIN + widget.y]
|
||||
const margin = widget.options.margin ?? DEFAULT_MARGIN
|
||||
widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]
|
||||
widgetState.size = [
|
||||
(widget.width ?? node.width) - MARGIN * 2,
|
||||
(widget.computedHeight ?? 50) - MARGIN * 2
|
||||
(widget.width ?? node.width) - margin * 2,
|
||||
(widget.computedHeight ?? 50) - margin * 2
|
||||
]
|
||||
// TODO: optimize this logic as it's O(n), where n is the number of nodes
|
||||
widgetState.zIndex = lgCanvas.graph.nodes.indexOf(node)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
<TreeExplorer
|
||||
class="model-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
:root="renderedRoot"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
|
||||
@@ -49,7 +49,7 @@
|
||||
/>
|
||||
<TreeExplorer
|
||||
class="node-lib-tree-explorer"
|
||||
:roots="renderedRoot.children"
|
||||
:root="renderedRoot"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
|
||||
@@ -5,28 +5,18 @@
|
||||
>
|
||||
<template #tool-buttons>
|
||||
<Button
|
||||
class="browse-templates-button"
|
||||
icon="pi pi-th-large"
|
||||
icon="pi pi-folder-plus"
|
||||
@click="addNewFolder"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.browseTemplates')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.BrowseTemplates')"
|
||||
v-tooltip.bottom="$t('g.newFolder')"
|
||||
/>
|
||||
<Button
|
||||
class="open-workflow-button"
|
||||
icon="pi pi-folder-open"
|
||||
icon="pi pi-refresh"
|
||||
@click="workflowStore.syncWorkflows()"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.openWorkflow')"
|
||||
text
|
||||
@click="() => commandStore.execute('Comfy.OpenWorkflow')"
|
||||
/>
|
||||
<Button
|
||||
class="new-blank-workflow-button"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
v-tooltip.bottom="$t('sideToolbar.newBlankWorkflow')"
|
||||
@click="() => commandStore.execute('Comfy.NewBlankWorkflow')"
|
||||
text
|
||||
v-tooltip.bottom="$t('g.refresh')"
|
||||
/>
|
||||
</template>
|
||||
<template #header>
|
||||
@@ -49,9 +39,7 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open).children
|
||||
"
|
||||
:root="renderTreeNode(openWorkflowsTree, WorkflowTreeType.Open)"
|
||||
:selectionKeys="selectionKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -87,11 +75,11 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
:root="
|
||||
renderTreeNode(
|
||||
bookmarkedWorkflowsTree,
|
||||
WorkflowTreeType.Bookmarks
|
||||
).children
|
||||
)
|
||||
"
|
||||
:selectionKeys="selectionKeys"
|
||||
>
|
||||
@@ -107,11 +95,10 @@
|
||||
class="ml-2"
|
||||
/>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(workflowsTree, WorkflowTreeType.Browse).children
|
||||
"
|
||||
:root="renderTreeNode(workflowsTree, WorkflowTreeType.Browse)"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
:selectionKeys="selectionKeys"
|
||||
ref="persistedWorkflowsTreeExplorerRef"
|
||||
v-if="workflowStore.persistedWorkflows.length > 0"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -128,9 +115,7 @@
|
||||
</div>
|
||||
<div class="comfyui-workflows-search-panel" v-else>
|
||||
<TreeExplorer
|
||||
:roots="
|
||||
renderTreeNode(filteredRoot, WorkflowTreeType.Browse).children
|
||||
"
|
||||
:root="renderTreeNode(filteredRoot, WorkflowTreeType.Browse)"
|
||||
v-model:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #node="{ node }">
|
||||
@@ -159,7 +144,6 @@ import SidebarTabTemplate from '@/components/sidebar/tabs/SidebarTabTemplate.vue
|
||||
import WorkflowTreeLeaf from '@/components/sidebar/tabs/workflows/WorkflowTreeLeaf.vue'
|
||||
import { useTreeExpansion } from '@/composables/useTreeExpansion'
|
||||
import { useWorkflowService } from '@/services/workflowService'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
import {
|
||||
useWorkflowBookmarkStore,
|
||||
@@ -197,7 +181,6 @@ const handleSearch = (query: string) => {
|
||||
})
|
||||
}
|
||||
|
||||
const commandStore = useCommandStore()
|
||||
const workflowStore = useWorkflowStore()
|
||||
const workflowService = useWorkflowService()
|
||||
const workspaceStore = useWorkspaceStore()
|
||||
@@ -244,7 +227,6 @@ const renderTreeNode = (
|
||||
type: WorkflowTreeType
|
||||
): TreeExplorerNode<ComfyWorkflow> => {
|
||||
const children = node.children?.map((child) => renderTreeNode(child, type))
|
||||
|
||||
const workflow: ComfyWorkflow = node.data
|
||||
|
||||
function handleClick(this: TreeExplorerNode<ComfyWorkflow>, e: MouseEvent) {
|
||||
@@ -255,7 +237,7 @@ const renderTreeNode = (
|
||||
}
|
||||
}
|
||||
|
||||
const actions = node.leaf
|
||||
const actions: Partial<TreeExplorerNode<ComfyWorkflow>> = node.leaf
|
||||
? {
|
||||
handleClick,
|
||||
async handleRename(newName: string) {
|
||||
@@ -285,7 +267,16 @@ const renderTreeNode = (
|
||||
},
|
||||
draggable: true
|
||||
}
|
||||
: { handleClick }
|
||||
: {
|
||||
handleClick,
|
||||
async handleAddFolder(folderName: string) {
|
||||
if (folderName === '') return
|
||||
|
||||
const parentPath = this.key.replace(/^root\/?/, '')
|
||||
const folderPath = parentPath + '/' + folderName
|
||||
await workflowStore.createFolder(folderPath)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
key: node.key,
|
||||
@@ -305,4 +296,11 @@ const workflowBookmarkStore = useWorkflowBookmarkStore()
|
||||
onMounted(async () => {
|
||||
await workflowBookmarkStore.loadBookmarks()
|
||||
})
|
||||
|
||||
const persistedWorkflowsTreeExplorerRef = ref<InstanceType<
|
||||
typeof TreeExplorer
|
||||
> | null>(null)
|
||||
const addNewFolder = () => {
|
||||
persistedWorkflowsTreeExplorerRef.value?.addFolderCommand?.('root')
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<TreeExplorer
|
||||
class="node-lib-bookmark-tree-explorer"
|
||||
ref="treeExplorerRef"
|
||||
:roots="renderedBookmarkedRoot.children"
|
||||
:root="renderedBookmarkedRoot"
|
||||
:expandedKeys="expandedKeys"
|
||||
>
|
||||
<template #folder="{ node }">
|
||||
@@ -40,7 +40,6 @@ import type {
|
||||
TreeExplorerDragAndDropData,
|
||||
TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
import { findNodeByKey } from '@/utils/treeUtil'
|
||||
|
||||
const props = defineProps<{
|
||||
filteredNodeDefs: ComfyNodeDefImpl[]
|
||||
@@ -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',
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
}
|
||||
|
||||
76
src/composables/tree/useTreeFolderOperations.ts
Normal file
76
src/composables/tree/useTreeFolderOperations.ts
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -590,6 +590,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
function: () => {
|
||||
dialogService.showManagerDialog()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Manager.ToggleManagerProgressDialog',
|
||||
icon: 'pi pi-spinner',
|
||||
label: 'Toggle Progress Dialog',
|
||||
versionAdded: '1.13.9',
|
||||
function: () => {
|
||||
dialogService.showManagerProgressDialog()
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
import type { NodesIndexSuggestion } from '@/services/algoliaSearchService'
|
||||
import { PackField } from '@/types/comfyManagerTypes'
|
||||
|
||||
const SEARCH_DEBOUNCE_TIME = 16
|
||||
const SEARCH_DEBOUNCE_TIME = 256
|
||||
const DEFAULT_PAGE_SIZE = 64
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Custom Nodes Manager"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Toggle Progress Dialog"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "New Blank Workflow"
|
||||
},
|
||||
|
||||
@@ -94,10 +94,12 @@
|
||||
"filter": "Filter",
|
||||
"apply": "Apply",
|
||||
"enabled": "Enabled",
|
||||
"installed": "Installed"
|
||||
"installed": "Installed",
|
||||
"restart": "Restart"
|
||||
},
|
||||
"manager": {
|
||||
"title": "Custom Nodes Manager",
|
||||
"restartToApplyChanges": "To apply changes, please restart ComfyUI",
|
||||
"loadingVersions": "Loading versions...",
|
||||
"selectVersion": "Select Version",
|
||||
"downloads": "Downloads",
|
||||
@@ -615,6 +617,7 @@
|
||||
"Interrupt": "Interrupt",
|
||||
"Load Default Workflow": "Load Default Workflow",
|
||||
"Custom Nodes Manager": "Custom Nodes Manager",
|
||||
"Toggle Progress Dialog": "Toggle Progress Dialog",
|
||||
"New": "New",
|
||||
"Clipspace": "Clipspace",
|
||||
"Open": "Open",
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Gestionnaire de Nœuds Personnalisés"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Basculer la boîte de dialogue de progression"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Nouveau flux de travail vierge"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"reportSent": "Rapport soumis",
|
||||
"reset": "Réinitialiser",
|
||||
"resetKeybindingsTooltip": "Réinitialiser les raccourcis clavier par défaut",
|
||||
"restart": "Redémarrer",
|
||||
"resultsCount": "{count} Résultats Trouvés",
|
||||
"save": "Enregistrer",
|
||||
"saving": "Enregistrement",
|
||||
@@ -397,6 +398,7 @@
|
||||
"nodePack": "Pack de Nœuds",
|
||||
"packsSelected": "Packs sélectionnés",
|
||||
"repository": "Référentiel",
|
||||
"restartToApplyChanges": "Pour appliquer les modifications, veuillez redémarrer ComfyUI",
|
||||
"searchPlaceholder": "Recherche",
|
||||
"selectVersion": "Sélectionner la version",
|
||||
"sort": {
|
||||
@@ -530,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "Basculer le panneau inférieur des journaux",
|
||||
"Toggle Model Library Sidebar": "Basculer la barre latérale de la bibliothèque de modèles",
|
||||
"Toggle Node Library Sidebar": "Basculer la barre latérale de la bibliothèque de nœuds",
|
||||
"Toggle Progress Dialog": "Basculer la boîte de dialogue de progression",
|
||||
"Toggle Queue Sidebar": "Basculer la barre latérale de la file d'attente",
|
||||
"Toggle Search Box": "Basculer la boîte de recherche",
|
||||
"Toggle Terminal Bottom Panel": "Basculer le panneau inférieur du terminal",
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "カスタムノードマネージャ"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "プログレスダイアログの切り替え"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新しい空のワークフロー"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"reportSent": "レポートが送信されました",
|
||||
"reset": "リセット",
|
||||
"resetKeybindingsTooltip": "キーバインディングをデフォルトにリセット",
|
||||
"restart": "再起動",
|
||||
"resultsCount": "{count}件の結果が見つかりました",
|
||||
"save": "保存",
|
||||
"saving": "保存中",
|
||||
@@ -397,6 +398,7 @@
|
||||
"nodePack": "ノードパック",
|
||||
"packsSelected": "選択したパック",
|
||||
"repository": "リポジトリ",
|
||||
"restartToApplyChanges": "変更を適用するには、ComfyUIを再起動してください",
|
||||
"searchPlaceholder": "検索",
|
||||
"selectVersion": "バージョンを選択",
|
||||
"sort": {
|
||||
@@ -530,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "ログパネル下部を切り替え",
|
||||
"Toggle Model Library Sidebar": "モデルライブラリサイドバーを切り替え",
|
||||
"Toggle Node Library Sidebar": "ノードライブラリサイドバーを切り替え",
|
||||
"Toggle Progress Dialog": "進行状況ダイアログの切り替え",
|
||||
"Toggle Queue Sidebar": "キューサイドバーを切り替え",
|
||||
"Toggle Search Box": "検索ボックスの切り替え",
|
||||
"Toggle Terminal Bottom Panel": "ターミナルパネル下部を切り替え",
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "사용자 정의 노드 관리자"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "프로그레스 대화 상자 전환"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "새로운 빈 워크플로"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"reportSent": "보고서 제출됨",
|
||||
"reset": "재설정",
|
||||
"resetKeybindingsTooltip": "키 바인딩을 기본값으로 재설정",
|
||||
"restart": "재시작",
|
||||
"resultsCount": "{count} 개의 결과를 찾았습니다",
|
||||
"save": "저장",
|
||||
"saving": "저장 중",
|
||||
@@ -397,6 +398,7 @@
|
||||
"nodePack": "노드 팩",
|
||||
"packsSelected": "선택한 팩",
|
||||
"repository": "저장소",
|
||||
"restartToApplyChanges": "변경 사항을 적용하려면 ComfyUI를 재시작해 주세요",
|
||||
"searchPlaceholder": "검색",
|
||||
"selectVersion": "버전 선택",
|
||||
"sort": {
|
||||
@@ -530,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "로그 하단 패널 전환",
|
||||
"Toggle Model Library Sidebar": "모델 라이브러리 사이드바 전환",
|
||||
"Toggle Node Library Sidebar": "노드 라이브러리 사이드바 전환",
|
||||
"Toggle Progress Dialog": "진행 상황 대화 상자 전환",
|
||||
"Toggle Queue Sidebar": "실행 큐 사이드바 전환",
|
||||
"Toggle Search Box": "검색 상자 전환",
|
||||
"Toggle Terminal Bottom Panel": "터미널 하단 패널 전환",
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "Менеджер Пользовательских Узлов"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "Переключить диалоговое окно прогресса"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "Новый пустой рабочий процесс"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"reportSent": "Отчёт отправлен",
|
||||
"reset": "Сбросить",
|
||||
"resetKeybindingsTooltip": "Сбросить сочетания клавиш по умолчанию",
|
||||
"restart": "Перезапустить",
|
||||
"resultsCount": "Найдено {count} результатов",
|
||||
"save": "Сохранить",
|
||||
"saving": "Сохранение",
|
||||
@@ -397,6 +398,7 @@
|
||||
"nodePack": "Пакет Узлов",
|
||||
"packsSelected": "Выбрано пакетов",
|
||||
"repository": "Репозиторий",
|
||||
"restartToApplyChanges": "Чтобы применить изменения, пожалуйста, перезапустите ComfyUI",
|
||||
"searchPlaceholder": "Поиск",
|
||||
"selectVersion": "Выберите версию",
|
||||
"sort": {
|
||||
@@ -530,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "Переключение нижней панели журналов",
|
||||
"Toggle Model Library Sidebar": "Переключение боковой панели библиотеки моделей",
|
||||
"Toggle Node Library Sidebar": "Переключение боковой панели библиотеки нод",
|
||||
"Toggle Progress Dialog": "Переключить диалоговое окно прогресса",
|
||||
"Toggle Queue Sidebar": "Переключение боковой панели очереди",
|
||||
"Toggle Search Box": "Переключить поисковую панель",
|
||||
"Toggle Terminal Bottom Panel": "Переключение нижней панели терминала",
|
||||
|
||||
@@ -128,6 +128,9 @@
|
||||
"Comfy_Manager_CustomNodesManager": {
|
||||
"label": "自定义节点管理器"
|
||||
},
|
||||
"Comfy_Manager_ToggleManagerProgressDialog": {
|
||||
"label": "切换进度对话框"
|
||||
},
|
||||
"Comfy_NewBlankWorkflow": {
|
||||
"label": "新建空白工作流"
|
||||
},
|
||||
|
||||
@@ -188,6 +188,7 @@
|
||||
"reportSent": "报告已提交",
|
||||
"reset": "重置",
|
||||
"resetKeybindingsTooltip": "将快捷键重置为默认",
|
||||
"restart": "重新启动",
|
||||
"resultsCount": "找到 {count} 个结果",
|
||||
"save": "保存",
|
||||
"saving": "正在保存",
|
||||
@@ -397,6 +398,7 @@
|
||||
"nodePack": "节点包",
|
||||
"packsSelected": "选定的包",
|
||||
"repository": "仓库",
|
||||
"restartToApplyChanges": "要应用更改,请重新启动ComfyUI",
|
||||
"searchPlaceholder": "搜索",
|
||||
"selectVersion": "选择版本",
|
||||
"sort": {
|
||||
@@ -530,6 +532,7 @@
|
||||
"Toggle Logs Bottom Panel": "切换日志底部面板",
|
||||
"Toggle Model Library Sidebar": "切换模型库侧边栏",
|
||||
"Toggle Node Library Sidebar": "切换节点库侧边栏",
|
||||
"Toggle Progress Dialog": "切换进度对话框",
|
||||
"Toggle Queue Sidebar": "切换队列侧边栏",
|
||||
"Toggle Search Box": "切换搜索框",
|
||||
"Toggle Terminal Bottom Panel": "切换终端底部面板",
|
||||
|
||||
@@ -63,6 +63,7 @@ export interface DOMWidgetOptions<V extends object | string>
|
||||
getMaxHeight?: () => number
|
||||
getHeight?: () => string | number
|
||||
onDraw?: (widget: BaseDOMWidget<V>) => void
|
||||
margin?: number
|
||||
/**
|
||||
* @deprecated Use `afterResize` instead. This callback is a legacy API
|
||||
* that fires before resize happens, but it is no longer supported. Now it
|
||||
|
||||
@@ -131,7 +131,9 @@ export const useAlgoliaSearchService = () => {
|
||||
status: algoliaNode.status,
|
||||
icon: algoliaNode.icon_url,
|
||||
latest_version: toRegistryLatestVersion(algoliaNode),
|
||||
publisher: toRegistryPublisher(algoliaNode)
|
||||
publisher: toRegistryPublisher(algoliaNode),
|
||||
// @ts-expect-error remove when comfy_nodes is added to node (pack) info
|
||||
comfy_nodes: algoliaNode.comfy_nodes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,14 @@ import ConfirmationDialogContent from '@/components/dialog/content/ConfirmationD
|
||||
import ExecutionErrorDialogContent from '@/components/dialog/content/ExecutionErrorDialogContent.vue'
|
||||
import IssueReportDialogContent from '@/components/dialog/content/IssueReportDialogContent.vue'
|
||||
import LoadWorkflowWarning from '@/components/dialog/content/LoadWorkflowWarning.vue'
|
||||
import ManagerProgressDialogContent from '@/components/dialog/content/ManagerProgressDialogContent.vue'
|
||||
import MissingModelsWarning from '@/components/dialog/content/MissingModelsWarning.vue'
|
||||
import PromptDialogContent from '@/components/dialog/content/PromptDialogContent.vue'
|
||||
import SettingDialogContent from '@/components/dialog/content/SettingDialogContent.vue'
|
||||
import ManagerDialogContent from '@/components/dialog/content/manager/ManagerDialogContent.vue'
|
||||
import ManagerHeader from '@/components/dialog/content/manager/ManagerHeader.vue'
|
||||
import ManagerProgressFooter from '@/components/dialog/footer/ManagerProgressFooter.vue'
|
||||
import ManagerProgressHeader from '@/components/dialog/header/ManagerProgressHeader.vue'
|
||||
import SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'
|
||||
import TemplateWorkflowsContent from '@/components/templates/TemplateWorkflowsContent.vue'
|
||||
import TemplateWorkflowsDialogHeader from '@/components/templates/TemplateWorkflowsDialogHeader.vue'
|
||||
@@ -121,6 +124,29 @@ export const useDialogService = () => {
|
||||
})
|
||||
}
|
||||
|
||||
function showManagerProgressDialog(options?: {
|
||||
props?: InstanceType<typeof ManagerProgressDialogContent>['$props']
|
||||
}) {
|
||||
return dialogStore.showDialog({
|
||||
key: 'global-manager-progress-dialog',
|
||||
component: ManagerProgressDialogContent,
|
||||
headerComponent: ManagerProgressHeader,
|
||||
footerComponent: ManagerProgressFooter,
|
||||
props: options?.props,
|
||||
dialogComponentProps: {
|
||||
closable: false,
|
||||
modal: false,
|
||||
position: 'bottom',
|
||||
pt: {
|
||||
root: { class: 'w-[80%] max-w-2xl mx-auto border-none' },
|
||||
content: { class: '!p-0' },
|
||||
header: { class: '!p-0 border-none' },
|
||||
footer: { class: '!p-0 border-none' }
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async function prompt({
|
||||
title,
|
||||
message,
|
||||
@@ -203,6 +229,7 @@ export const useDialogService = () => {
|
||||
showTemplateWorkflowsDialog,
|
||||
showIssueReportDialog,
|
||||
showManagerDialog,
|
||||
showManagerProgressDialog,
|
||||
prompt,
|
||||
confirm
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useCachedRequest } from '@/composables/useCachedRequest'
|
||||
import { useManagerQueue } from '@/composables/useManagerQueue'
|
||||
import { useServerLogs } from '@/composables/useServerLogs'
|
||||
import { useComfyManagerService } from '@/services/comfyManagerService'
|
||||
import { useDialogService } from '@/services/dialogService'
|
||||
import {
|
||||
InstallPackParams,
|
||||
InstalledPacksResponse,
|
||||
@@ -20,6 +21,8 @@ import {
|
||||
*/
|
||||
export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
const managerService = useComfyManagerService()
|
||||
const { showManagerProgressDialog } = useDialogService()
|
||||
|
||||
const installedPacks = ref<InstalledPacksResponse>({})
|
||||
const enabledPacksIds = ref<Set<string>>(new Set())
|
||||
const disabledPacksIds = ref<Set<string>>(new Set())
|
||||
@@ -110,6 +113,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
}
|
||||
|
||||
whenever(isStale, refreshInstalledList, { immediate: true })
|
||||
whenever(uncompletedCount, () => showManagerProgressDialog())
|
||||
|
||||
const withLogs = (task: () => Promise<null>, taskName: string) => {
|
||||
const { startListening, stopListening, logs } = useServerLogs()
|
||||
@@ -178,6 +182,11 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
enqueueTask(withLogs(task, `Disabling ${params.id}`))
|
||||
}
|
||||
|
||||
const getInstalledPackVersion = (packId: string) => {
|
||||
const pack = installedPacks.value[packId]
|
||||
return pack?.ver
|
||||
}
|
||||
|
||||
const clearLogs = () => {
|
||||
taskLogs.value = []
|
||||
}
|
||||
@@ -197,6 +206,7 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
installedPacksIds,
|
||||
isPackInstalled: isInstalledPackId,
|
||||
isPackEnabled: isEnabledPackId,
|
||||
getInstalledPackVersion,
|
||||
|
||||
// Pack actions
|
||||
installPack,
|
||||
@@ -207,3 +217,33 @@ export const useComfyManagerStore = defineStore('comfyManager', () => {
|
||||
enablePack: installPack // Enable is done via install endpoint with a disabled pack
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Store for state of the manager progress dialog content.
|
||||
* The dialog itself is managed by the dialog store. This store is used to
|
||||
* manage the visibility of the dialog's content, header, footer.
|
||||
*/
|
||||
export const useManagerProgressDialogStore = defineStore(
|
||||
'managerProgressDialog',
|
||||
() => {
|
||||
const isExpanded = ref(false)
|
||||
|
||||
const toggle = () => {
|
||||
isExpanded.value = !isExpanded.value
|
||||
}
|
||||
|
||||
const collapse = () => {
|
||||
isExpanded.value = false
|
||||
}
|
||||
|
||||
const expand = () => {
|
||||
isExpanded.value = true
|
||||
}
|
||||
return {
|
||||
isExpanded,
|
||||
toggle,
|
||||
collapse,
|
||||
expand
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -7,11 +7,24 @@ import { type Component, markRaw, ref } from 'vue'
|
||||
|
||||
import type GlobalDialog from '@/components/dialog/GlobalDialog.vue'
|
||||
|
||||
type DialogPosition =
|
||||
| 'center'
|
||||
| 'top'
|
||||
| 'bottom'
|
||||
| 'left'
|
||||
| 'right'
|
||||
| 'topleft'
|
||||
| 'topright'
|
||||
| 'bottomleft'
|
||||
| 'bottomright'
|
||||
|
||||
interface CustomDialogComponentProps {
|
||||
maximizable?: boolean
|
||||
maximized?: boolean
|
||||
onClose?: () => void
|
||||
closable?: boolean
|
||||
modal?: boolean
|
||||
position?: DialogPosition
|
||||
pt?: DialogPassThroughOptions
|
||||
}
|
||||
|
||||
@@ -25,6 +38,7 @@ interface DialogInstance {
|
||||
headerComponent?: Component
|
||||
component: Component
|
||||
contentProps: Record<string, any>
|
||||
footerComponent?: Component
|
||||
dialogComponentProps: DialogComponentProps
|
||||
}
|
||||
|
||||
@@ -32,6 +46,7 @@ export interface ShowDialogOptions {
|
||||
key?: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
@@ -66,6 +81,7 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
key: string
|
||||
title?: string
|
||||
headerComponent?: Component
|
||||
footerComponent?: Component
|
||||
component: Component
|
||||
props?: Record<string, any>
|
||||
dialogComponentProps?: DialogComponentProps
|
||||
@@ -81,6 +97,9 @@ export const useDialogStore = defineStore('dialog', () => {
|
||||
headerComponent: options.headerComponent
|
||||
? markRaw(options.headerComponent)
|
||||
: undefined,
|
||||
footerComponent: options.footerComponent
|
||||
? markRaw(options.footerComponent)
|
||||
: undefined,
|
||||
component: markRaw(options.component),
|
||||
contentProps: { ...options.props },
|
||||
dialogComponentProps: {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UserFile } from './userFileStore'
|
||||
|
||||
export class ComfyWorkflow extends UserFile {
|
||||
static readonly basePath = 'workflows/'
|
||||
static readonly folderPlaceholderFilename = 'folder.index'
|
||||
|
||||
/**
|
||||
* The change tracker for the workflow. Non-reactive raw object.
|
||||
@@ -32,7 +33,9 @@ export class ComfyWorkflow extends UserFile {
|
||||
}
|
||||
|
||||
get key() {
|
||||
return this.path.substring(ComfyWorkflow.basePath.length)
|
||||
const key = this.isFolderPlaceholder ? this.directory + '/' : this.path
|
||||
|
||||
return key.substring(ComfyWorkflow.basePath.length)
|
||||
}
|
||||
|
||||
get activeState(): ComfyWorkflowJSON | null {
|
||||
@@ -55,6 +58,13 @@ export class ComfyWorkflow extends UserFile {
|
||||
this._isModified = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the workflow is a folder placeholder.
|
||||
*/
|
||||
get isFolderPlaceholder(): boolean {
|
||||
return this.fullFilename === ComfyWorkflow.folderPlaceholderFilename
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the workflow content from remote storage. Directly returns the loaded
|
||||
* workflow if the content is already loaded.
|
||||
@@ -153,6 +163,8 @@ export interface WorkflowStore {
|
||||
getWorkflowByPath: (path: string) => ComfyWorkflow | null
|
||||
syncWorkflows: (dir?: string) => Promise<void>
|
||||
reorderWorkflows: (from: number, to: number) => void
|
||||
|
||||
createFolder: (folderPath: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const useWorkflowStore = defineStore('workflow', () => {
|
||||
@@ -418,6 +430,40 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new folder in the workflows directory
|
||||
* @param folderPath The path of the folder to create (relative to workflows/)
|
||||
* @returns Promise that resolves when the folder is created
|
||||
*/
|
||||
const createFolder = async (folderPath: string): Promise<void> => {
|
||||
isBusy.value = true
|
||||
try {
|
||||
// Ensure the path is properly formatted
|
||||
const normalizedPath = folderPath.endsWith('/')
|
||||
? folderPath.slice(0, -1)
|
||||
: folderPath
|
||||
|
||||
// Create the full path including the reserved index file
|
||||
const indexFilePath = `${ComfyWorkflow.basePath}${normalizedPath}/${ComfyWorkflow.folderPlaceholderFilename}`
|
||||
|
||||
// Create an empty file to represent the folder
|
||||
const resp = await api.storeUserData(indexFilePath, '', {
|
||||
overwrite: false,
|
||||
throwOnError: true,
|
||||
full_info: true
|
||||
})
|
||||
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Failed to create folder')
|
||||
}
|
||||
|
||||
// Sync workflows to update the file tree
|
||||
await syncWorkflows()
|
||||
} finally {
|
||||
isBusy.value = false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
activeWorkflow,
|
||||
isActive,
|
||||
@@ -439,7 +485,9 @@ export const useWorkflowStore = defineStore('workflow', () => {
|
||||
persistedWorkflows,
|
||||
modifiedWorkflows,
|
||||
getWorkflowByPath,
|
||||
syncWorkflows
|
||||
syncWorkflows,
|
||||
|
||||
createFolder
|
||||
}
|
||||
}) as unknown as () => WorkflowStore
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import type { InjectionKey, Ref } from 'vue'
|
||||
|
||||
export interface TreeExplorerNode<T = any> {
|
||||
key: string
|
||||
@@ -18,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
|
||||
@@ -58,9 +59,18 @@ export interface RenderedTreeExplorerNode<T = any> extends TreeExplorerNode<T> {
|
||||
totalLeaves: number
|
||||
/** Text to display on the leaf-count badge. Empty string means no badge. */
|
||||
badgeText?: string
|
||||
/** Whether the node label is currently being edited */
|
||||
isEditingLabel?: boolean
|
||||
}
|
||||
|
||||
export type TreeExplorerDragAndDropData<T = any> = {
|
||||
type: 'tree-explorer-node'
|
||||
data: RenderedTreeExplorerNode<T>
|
||||
}
|
||||
|
||||
export const InjectKeyHandleEditLabelFunction: InjectionKey<
|
||||
(node: RenderedTreeExplorerNode, newName: string) => void
|
||||
> = Symbol()
|
||||
|
||||
export const InjectKeyExpandedKeys: InjectionKey<Ref<Record<string, boolean>>> =
|
||||
Symbol()
|
||||
|
||||
@@ -384,3 +384,31 @@ export const downloadUrlToHfRepoUrl = (url: string): string => {
|
||||
return url
|
||||
}
|
||||
}
|
||||
|
||||
export const isSemVer = (version: string) => {
|
||||
const regex = /^(\d+)\.(\d+)\.(\d+)$/
|
||||
return regex.test(version)
|
||||
}
|
||||
|
||||
const normalizeVersion = (version: string) =>
|
||||
version
|
||||
.split(/[+.-]/)
|
||||
.map(Number)
|
||||
.filter((part) => !Number.isNaN(part))
|
||||
|
||||
export function compareVersions(versionA: string, versionB: string): number {
|
||||
versionA ??= '0.0.0'
|
||||
versionB ??= '0.0.0'
|
||||
|
||||
const aParts = normalizeVersion(versionA)
|
||||
const bParts = normalizeVersion(versionB)
|
||||
|
||||
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
||||
const aPart = aParts[i] ?? 0
|
||||
const bPart = bParts[i] ?? 0
|
||||
if (aPart < bPart) return -1
|
||||
if (aPart > bPart) return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -105,7 +105,10 @@ export function sortedTree(
|
||||
return newNode
|
||||
}
|
||||
|
||||
export const findNodeByKey = (root: TreeNode, key: string): TreeNode | null => {
|
||||
export const findNodeByKey = <T extends TreeNode>(
|
||||
root: T,
|
||||
key: string
|
||||
): T | null => {
|
||||
if (root.key === key) {
|
||||
return root
|
||||
}
|
||||
@@ -113,10 +116,46 @@ export const findNodeByKey = (root: TreeNode, key: string): TreeNode | null => {
|
||||
return null
|
||||
}
|
||||
for (const child of root.children) {
|
||||
const result = findNodeByKey(child, key)
|
||||
const result = findNodeByKey(child as T, key)
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Deep clone a tree node and its children.
|
||||
* @param node - The node to clone.
|
||||
* @returns A deep clone of the node.
|
||||
*/
|
||||
export function cloneTree<T extends TreeNode>(node: T): T {
|
||||
const clone: T = { ...node } as T
|
||||
|
||||
// Clone children recursively
|
||||
if (node.children && node.children.length > 0) {
|
||||
clone.children = node.children.map((child) => cloneTree(child as T))
|
||||
}
|
||||
|
||||
return clone
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge a subtree into the tree.
|
||||
* @param root - The root of the tree.
|
||||
* @param subtree - The subtree to merge.
|
||||
* @returns A new tree with the subtree merged.
|
||||
*/
|
||||
export const combineTrees = <T extends TreeNode>(root: T, subtree: T): T => {
|
||||
const newRoot = cloneTree(root)
|
||||
|
||||
const parentKey = subtree.key.slice(0, subtree.key.lastIndexOf('/'))
|
||||
const parent = findNodeByKey(newRoot, parentKey)
|
||||
|
||||
if (parent) {
|
||||
parent.children ??= []
|
||||
parent.children.push(cloneTree(subtree))
|
||||
}
|
||||
|
||||
return newRoot
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user