[Refactor] Handle rename in TreeExplorer (#3099)

This commit is contained in:
Chenlei Hu
2025-03-17 12:26:26 -04:00
committed by GitHub
parent bd1be28478
commit d57d12b426
4 changed files with 60 additions and 80 deletions

View File

@@ -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 }

View File

@@ -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)

View File

@@ -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()
})
})

View File

@@ -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()