From e2656c95ffe638431d42231c777e425dba68611f Mon Sep 17 00:00:00 2001 From: jsek Date: Sun, 25 Aug 2024 06:13:48 +0200 Subject: [PATCH 1/2] fix(nested): Prevent infinite loops in getPath --- packages/vuetify/src/composables/nested/nested.ts | 2 ++ packages/vuetify/src/labs/VTreeview/VTreeview.tsx | 2 ++ packages/vuetify/src/util/nested.ts | 9 +++++++++ 3 files changed, 13 insertions(+) create mode 100644 packages/vuetify/src/util/nested.ts diff --git a/packages/vuetify/src/composables/nested/nested.ts b/packages/vuetify/src/composables/nested/nested.ts index 5f80f4da090..753f159fddc 100644 --- a/packages/vuetify/src/composables/nested/nested.ts +++ b/packages/vuetify/src/composables/nested/nested.ts @@ -18,6 +18,7 @@ import { leafSingleSelectStrategy, } from './selectStrategies' import { getCurrentInstance, getUid, propsFactory } from '@/util' +import { preventLoops } from '@/util/nested' // Types import type { InjectionKey, PropType, Ref } from 'vue' @@ -182,6 +183,7 @@ export const useNested = (props: NestedProps) => { let parent: unknown = id while (parent != null) { + preventLoops(path, parent) path.unshift(parent) parent = parents.value.get(parent) } diff --git a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx index d4079447da0..237c0543a1b 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx @@ -10,6 +10,7 @@ import { useProxiedModel } from '@/composables/proxiedModel' // Utilities import { computed, provide, ref, toRaw, toRef } from 'vue' import { genericComponent, omit, propsFactory, useRender } from '@/util' +import { preventLoops } from '@/util/nested' // Types import { VTreeviewSymbol } from './shared' @@ -98,6 +99,7 @@ export const VTreeview = genericComponent( const path: unknown[] = [] let parent: unknown = id while (parent != null) { + preventLoops(path, parent) path.unshift(parent) parent = vListRef.value?.parents.get(parent) } diff --git a/packages/vuetify/src/util/nested.ts b/packages/vuetify/src/util/nested.ts new file mode 100644 index 00000000000..488e144c9ef --- /dev/null +++ b/packages/vuetify/src/util/nested.ts @@ -0,0 +1,9 @@ +export function preventLoops (path: T[] | Set | Map, itemToPush: T) { + if ( + (Array.isArray(path) && path.includes(itemToPush)) || + (path instanceof Set && path.has(itemToPush)) || + (path instanceof Map && path.has(itemToPush)) + ) { + throw new Error('[Vuetify] Could not resolve nested path because of duplicated identifiers') + } +} From 7ac1130b6fb758bc73af9ac9a08a49e4096d63ce Mon Sep 17 00:00:00 2001 From: Kael Date: Wed, 28 Aug 2024 00:59:26 +1000 Subject: [PATCH 2/2] check ids in register, remove redundant code --- .../vuetify/src/components/VList/VList.tsx | 3 +- .../src/components/VList/VListItem.tsx | 3 ++ .../vuetify/src/composables/nested/nested.ts | 19 ++++++++++-- .../vuetify/src/labs/VTreeview/VTreeview.tsx | 18 ++---------- .../src/labs/VTreeview/VTreeviewItem.tsx | 29 +++++-------------- packages/vuetify/src/util/nested.ts | 9 ------ 6 files changed, 32 insertions(+), 49 deletions(-) delete mode 100644 packages/vuetify/src/util/nested.ts diff --git a/packages/vuetify/src/components/VList/VList.tsx b/packages/vuetify/src/components/VList/VList.tsx index f5c492fd9eb..cbdc04b2cce 100644 --- a/packages/vuetify/src/components/VList/VList.tsx +++ b/packages/vuetify/src/components/VList/VList.tsx @@ -162,7 +162,7 @@ export const VList = genericComponent props.lines ? `v-list--${props.lines}-line` : undefined) const activeColor = toRef(props, 'activeColor') const baseColor = toRef(props, 'baseColor') @@ -288,6 +288,7 @@ export const VList = genericComponent()({ root, parent, openOnSelect, + id: uid, } = useNestedItem(id, false) const list = useList() const isActive = computed(() => @@ -368,6 +369,8 @@ export const VListItem = genericComponent()({ isSelected, list, select, + root, + id: uid, } }, }) diff --git a/packages/vuetify/src/composables/nested/nested.ts b/packages/vuetify/src/composables/nested/nested.ts index 753f159fddc..c5b013a3de0 100644 --- a/packages/vuetify/src/composables/nested/nested.ts +++ b/packages/vuetify/src/composables/nested/nested.ts @@ -17,8 +17,7 @@ import { leafSelectStrategy, leafSingleSelectStrategy, } from './selectStrategies' -import { getCurrentInstance, getUid, propsFactory } from '@/util' -import { preventLoops } from '@/util/nested' +import { consoleError, getCurrentInstance, getUid, propsFactory } from '@/util' // Types import type { InjectionKey, PropType, Ref } from 'vue' @@ -77,6 +76,7 @@ type NestedProvide = { activate: (id: unknown, value: boolean, event?: Event) => void select: (id: unknown, value: boolean, event?: Event) => void openOnSelect: (id: unknown, value: boolean, event?: Event) => void + getPath: (id: unknown) => unknown[] } } @@ -99,6 +99,7 @@ export const emptyNested: NestedProvide = { activated: ref(new Set()), selected: ref(new Map()), selectedValues: ref([]), + getPath: () => [], }, } @@ -183,7 +184,6 @@ export const useNested = (props: NestedProps) => { let parent: unknown = id while (parent != null) { - preventLoops(path, parent) path.unshift(parent) parent = parents.value.get(parent) } @@ -193,6 +193,8 @@ export const useNested = (props: NestedProps) => { const vm = getCurrentInstance('nested') + const nodeIds = new Set() + const nested: NestedProvide = { id: shallowRef(), root: { @@ -211,6 +213,15 @@ export const useNested = (props: NestedProps) => { return arr }), register: (id, parentId, isGroup) => { + if (nodeIds.has(id)) { + const path = getPath(id).join(' -> ') + const newPath = getPath(parentId).concat(id).join(' -> ') + consoleError(`Multiple nodes with the same ID\n\t${path}\n\t${newPath}`) + return + } else { + nodeIds.add(id) + } + parentId && id !== parentId && parents.value.set(id, parentId) isGroup && children.value.set(id, []) @@ -222,6 +233,7 @@ export const useNested = (props: NestedProps) => { unregister: id => { if (isUnmounted) return + nodeIds.delete(id) children.value.delete(id) const parent = parents.value.get(id) if (parent) { @@ -291,6 +303,7 @@ export const useNested = (props: NestedProps) => { }, children, parents, + getPath, }, } diff --git a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx index 237c0543a1b..172deddbc10 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeview.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeview.tsx @@ -10,7 +10,6 @@ import { useProxiedModel } from '@/composables/proxiedModel' // Utilities import { computed, provide, ref, toRaw, toRef } from 'vue' import { genericComponent, omit, propsFactory, useRender } from '@/util' -import { preventLoops } from '@/util/nested' // Types import { VTreeviewSymbol } from './shared' @@ -87,25 +86,14 @@ export const VTreeview = genericComponent( const search = toRef(props, 'search') const { filteredItems } = useFilter(props, flatItems, search) const visibleIds = computed(() => { - if (!search.value) { - return null - } + if (!search.value) return null + const getPath = vListRef.value?.getPath + if (!getPath) return null return new Set(filteredItems.value.flatMap(item => { return [...getPath(item.props.value), ...getChildren(item.props.value)] })) }) - function getPath (id: unknown) { - const path: unknown[] = [] - let parent: unknown = id - while (parent != null) { - preventLoops(path, parent) - path.unshift(parent) - parent = vListRef.value?.parents.get(parent) - } - return path - } - function getChildren (id: unknown) { const arr: unknown[] = [] const queue = ((vListRef.value?.children.get(id) ?? []).slice()) diff --git a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx index aaa6aa59b2b..c3a33eee6ba 100644 --- a/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx +++ b/packages/vuetify/src/labs/VTreeview/VTreeviewItem.tsx @@ -9,7 +9,6 @@ import { VProgressCircular } from '@/components/VProgressCircular' // Composables import { IconValue } from '@/composables/icons' -import { useNestedItem } from '@/composables/nested/nested' import { useLink } from '@/composables/router' // Utilities @@ -35,20 +34,11 @@ export const VTreeviewItem = genericComponent()({ setup (props, { attrs, slots, emit }) { const link = useLink(props, attrs) - const rawId = computed(() => props.value === undefined ? link.href.value : props.value) const vListItemRef = ref() - const { - activate, - isActivated, - isGroupActivator, - root, - id, - } = useNestedItem(rawId, false) - const isActivatableGroupActivator = computed(() => - (root.activatable.value) && - isGroupActivator + (vListItemRef.value?.root.activatable.value) && + vListItemRef.value?.isGroupActivator ) const isClickable = computed(() => @@ -60,15 +50,11 @@ export const VTreeviewItem = genericComponent()({ function activateItem (e: MouseEvent | KeyboardEvent) { if ( !isClickable.value || - (!isActivatableGroupActivator.value && isGroupActivator) + (!isActivatableGroupActivator.value && vListItemRef.value?.isGroupActivator) ) return - if (root.activatable.value) { - if (isActivatableGroupActivator.value) { - activate(!isActivated.value, e) - } else { - vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e) - } + if (vListItemRef.value?.root.activatable.value) { + vListItemRef.value?.activate(!vListItemRef.value?.isActivated, e) } } @@ -80,13 +66,14 @@ export const VTreeviewItem = genericComponent()({ return ( (path: T[] | Set | Map, itemToPush: T) { - if ( - (Array.isArray(path) && path.includes(itemToPush)) || - (path instanceof Set && path.has(itemToPush)) || - (path instanceof Map && path.has(itemToPush)) - ) { - throw new Error('[Vuetify] Could not resolve nested path because of duplicated identifiers') - } -}