mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-10 10:00:08 +00:00
[Refactor] Handle rename in TreeExplorer (#3099)
This commit is contained in:
@@ -44,9 +44,10 @@ import { useI18n } from 'vue-i18n'
|
||||
|
||||
import TreeExplorerTreeNode from '@/components/common/TreeExplorerTreeNode.vue'
|
||||
import { useErrorHandling } from '@/composables/useErrorHandling'
|
||||
import type {
|
||||
RenderedTreeExplorerNode,
|
||||
TreeExplorerNode
|
||||
import {
|
||||
InjectKeyHandleEditLabelFunction,
|
||||
type RenderedTreeExplorerNode,
|
||||
type TreeExplorerNode
|
||||
} from '@/types/treeExplorerTypes'
|
||||
|
||||
const expandedKeys = defineModel<Record<string, boolean>>('expandedKeys')
|
||||
@@ -95,7 +96,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 (
|
||||
@@ -121,7 +123,22 @@ 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 () => {
|
||||
await node.handleRename(newName)
|
||||
},
|
||||
node.handleError,
|
||||
() => {
|
||||
renameEditingNode.value = null
|
||||
}
|
||||
)()
|
||||
}
|
||||
provide(InjectKeyHandleEditLabelFunction, handleNodeLabelEdit)
|
||||
|
||||
const { t } = useI18n()
|
||||
const renameCommand = (node: RenderedTreeExplorerNode) => {
|
||||
@@ -163,7 +180,6 @@ const handleContextMenu = (e: MouseEvent, node: RenderedTreeExplorerNode) => {
|
||||
}
|
||||
}
|
||||
|
||||
const errorHandling = useErrorHandling()
|
||||
const wrapCommandWithErrorHandler = (
|
||||
command: (event: MenuItemCommandEvent) => void,
|
||||
{ isAsync = false }: { isAsync: boolean }
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { MenuItem } from 'primevue/menuitem'
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
export interface TreeExplorerNode<T = any> {
|
||||
key: string
|
||||
@@ -58,9 +59,15 @@ 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()
|
||||
|
||||
Reference in New Issue
Block a user