From b18e6a093881be2c7efbd061c9a93e56f9a9f716 Mon Sep 17 00:00:00 2001 From: sallerli1 Date: Thu, 12 Jan 2023 17:47:05 +0800 Subject: [PATCH] fix(pro:transfer): filtered or paginated tree data value error when tree data is filtered or paginated, target data is overidden by new keys which should be appended instead --- packages/components/utils/index.ts | 1 + .../utils/src/useTreeCheckStateResolver.ts | 282 +++++++++++++++ .../transfer/__tests__/proTransfer.spec.ts | 11 +- packages/pro/transfer/demo/BasicTable.vue | 4 +- packages/pro/transfer/demo/BasicTree.vue | 4 +- packages/pro/transfer/demo/FlattenTree.vue | 4 +- .../pro/transfer/demo/TableCustomLabel.vue | 4 +- .../pro/transfer/demo/TableMaxSelectedCnt.vue | 4 +- packages/pro/transfer/demo/TableOneWay.vue | 4 +- .../pro/transfer/demo/TablePagination.vue | 4 +- packages/pro/transfer/demo/TableRemote.vue | 5 +- packages/pro/transfer/demo/TableVirtual.vue | 8 +- .../transfer/demo/TreeCascaderStrategy.vue | 5 +- .../pro/transfer/demo/TreeCustomLabel.vue | 4 +- .../pro/transfer/demo/TreeLoadChildren.vue | 10 +- packages/pro/transfer/demo/TreeOneWay.vue | 21 +- packages/pro/transfer/demo/TreePagination.vue | 18 +- packages/pro/transfer/demo/TreeRemote.vue | 4 +- packages/pro/transfer/demo/TreeVirtual.vue | 6 +- .../src/composables/useTransferData.ts | 14 +- .../src/composables/useTransferTableProps.ts | 31 +- .../src/composables/useTransferTreeProps.ts | 77 ++-- .../src/composables/useTreeDataStrategy.ts | 328 +++--------------- .../composables/useTreeDataStrategyContext.ts | 88 +++-- .../src/composables/useTreeExpandedKeys.ts | 4 +- .../transfer/src/content/ProTransferTree.tsx | 29 +- packages/pro/transfer/src/token.ts | 13 +- packages/pro/transfer/src/types.ts | 9 +- .../src/wrapper/TreeTransferWrapper.tsx | 29 +- 29 files changed, 609 insertions(+), 416 deletions(-) create mode 100644 packages/components/utils/src/useTreeCheckStateResolver.ts diff --git a/packages/components/utils/index.ts b/packages/components/utils/index.ts index 83d41f1fa..63d43d989 100644 --- a/packages/components/utils/index.ts +++ b/packages/components/utils/index.ts @@ -10,4 +10,5 @@ export * from './src/convertTarget' export * from './src/convertVNode' export * from './src/portalTarget' export * from './src/useKey' +export * from './src/useTreeCheckStateResolver' export * from './src/zIndex' diff --git a/packages/components/utils/src/useTreeCheckStateResolver.ts b/packages/components/utils/src/useTreeCheckStateResolver.ts new file mode 100644 index 000000000..0bb8b47c2 --- /dev/null +++ b/packages/components/utils/src/useTreeCheckStateResolver.ts @@ -0,0 +1,282 @@ +/** + * @license + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE + */ + +import { type Ref, computed } from 'vue' + +import { isArray, isObject } from 'lodash-es' + +import { type TreeTypeData, type VKey, getTreeKeys, traverseTree } from '@idux/cdk/utils' + +interface GetAllCheckedKeys, C extends keyof V> { + (data: V[]): VKey[] + (defaultUnCheckedKeys: VKey[]): VKey[] + (data: V[], defaultUnCheckedKeys: VKey[]): VKey[] +} +interface GetAllUncheckedKeys, C extends keyof V> { + (data: V[]): VKey[] + (defaultCheckedKeys: VKey[]): VKey[] + (data: V[], defaultCheckedKeys: VKey[]): VKey[] +} + +export interface TreeCheckStateResolverContext, C extends keyof V> { + data: V[] | undefined + dataMap: Map + parentKeyMap: Map + depthMap: Map +} +export interface TreeCheckStateResolver, C extends keyof V> { + appendKeys: (checkedKeys: VKey[] | Set, appendedKeys: VKey[]) => VKey[] + removeKeys: (checkedKeys: VKey[] | Set, removedKeys: VKey[]) => VKey[] + + getAllCheckedKeys: GetAllCheckedKeys + getAllUncheckedKeys: GetAllUncheckedKeys +} +export type TreeCascadeStrategy = 'all' | 'child' | 'parent' | 'off' + +export function useTreeCheckStateResolver, C extends keyof V>( + data: Ref, + childrenKey: Ref, + getKey: Ref<(item: V) => VKey>, + cascadeStrategy?: Ref, +): TreeCheckStateResolver +export function useTreeCheckStateResolver, C extends keyof V>( + context: Ref>, + childrenKey: Ref, + getKey: Ref<(item: V) => VKey>, + cascadeStrategy?: Ref, +): TreeCheckStateResolver +export function useTreeCheckStateResolver, C extends keyof V>( + dataOrContext: Ref>, + childrenKey: Ref, + getKey: Ref<(item: V) => VKey>, + cascadeStrategy?: Ref, +): TreeCheckStateResolver { + const _getContext = ( + dataOrContext: V[] | TreeCheckStateResolverContext, + ): TreeCheckStateResolverContext => { + if (!isArray(dataOrContext)) { + return dataOrContext + } + + const dataMap = new Map() + const parentKeyMap = new Map() + const depthMap = new Map() + + traverseTree(dataOrContext, childrenKey.value, (item, parents) => { + const key = getKey.value(item) + const parent = parents[0] + dataMap.set(key, item) + depthMap.set(key, parents.length) + + if (parent) { + parentKeyMap.set(key, getKey.value(parent)) + } + }) + + return { + data: dataOrContext, + dataMap, + parentKeyMap, + depthMap, + } + } + + const mergedCascadeStrategy = computed(() => cascadeStrategy?.value ?? 'all') + const _context = computed(() => _getContext(dataOrContext.value)) + + const _getParents = (key: VKey, resolverContext: TreeCheckStateResolverContext) => { + const { parentKeyMap, dataMap } = resolverContext + const parents: V[] = [] + + let currentKey = key + while (parentKeyMap.has(currentKey)) { + const parentKey = parentKeyMap.get(currentKey)! + const parent = dataMap.get(parentKey) + parent && parents.push(parent) + currentKey = parentKey + } + return parents + } + const _getAllData = () => { + const { data, parentKeyMap, dataMap } = _context.value + if (data) { + return data + } + + const _data: V[] = [] + for (const key of parentKeyMap.keys()) { + if (!parentKeyMap.has(key) && dataMap.has(key)) { + _data.push(dataMap.get(key)!) + } + } + + return _data + } + + const _append = ( + checkedKeys: VKey[] | Set, + appendedKeys: VKey[], + resolverContext: TreeCheckStateResolverContext, + ) => { + if (!appendedKeys.length) { + return Array.from(checkedKeys) + } + + if (mergedCascadeStrategy.value === 'off') { + return Array.from(new Set([...checkedKeys, ...appendedKeys])) + } + + const { dataMap } = resolverContext + + const newKeySet = new Set(checkedKeys) + appendedKeys.forEach(key => { + if (!dataMap.has(key)) { + return + } + + const treeData = dataMap.get(key)! + getTreeKeys([treeData], childrenKey.value, getKey.value, mergedCascadeStrategy.value === 'child').forEach(key => { + newKeySet.add(key) + }) + }) + + if (mergedCascadeStrategy.value === 'child') { + return Array.from(newKeySet).filter(key => !dataMap.get(key)?.[childrenKey.value]?.length) + } + + appendedKeys.forEach(key => { + _getParents(key, resolverContext).forEach(parent => { + if (parent[childrenKey.value]?.every(child => newKeySet.has(getKey.value(child)))) { + newKeySet.add(key) + } + }) + }) + + if (mergedCascadeStrategy.value === 'all') { + return Array.from(newKeySet) + } + + newKeySet.forEach(key => { + if (!newKeySet.has(key)) { + return + } + + const children = dataMap.get(key)?.[childrenKey.value] + if (children) { + traverseTree(children, childrenKey.value, item => { + newKeySet.delete(getKey.value(item)) + }) + } + }) + + return Array.from(newKeySet) + } + const _remove = ( + checkedKeys: VKey[] | Set, + removedKeys: VKey[], + resolverContext: TreeCheckStateResolverContext, + ) => { + if (!removedKeys.length) { + return Array.from(checkedKeys) + } + + const newKeySet = new Set(checkedKeys) + if (mergedCascadeStrategy.value === 'off') { + removedKeys.forEach(key => { + newKeySet.delete(key) + }) + return Array.from(newKeySet) + } + + const { dataMap } = resolverContext + + const deletedKeys = new Set() + // store already deleted keys + const deleteKey = (key: VKey) => { + deletedKeys.add(key) + newKeySet.delete(key) + } + + removedKeys.forEach(key => { + if (!dataMap.has(key)) { + return + } + + getTreeKeys([dataMap.get(key)!], childrenKey.value, getKey.value).forEach(key => { + deleteKey(key) + }) + + const parents = _getParents(key, resolverContext) + + if (mergedCascadeStrategy.value === 'parent') { + const keysInChain = [key, ...parents.map(getKey.value)] + const selectedKeyIdx = keysInChain.findIndex(key => newKeySet.has(key)) + + // if one of the ancestors was selected + // replace parent node with child nodes + if (selectedKeyIdx > -1) { + // only travers chain from selected node to the bottom + parents.slice(0, selectedKeyIdx).forEach(parent => { + if (!parent[childrenKey.value]) { + return + } + + parent[childrenKey.value]!.forEach(child => { + const childKey = getKey.value(child) + // add only if the child node hasn't been deleted before + if (!deletedKeys.has(childKey)) { + newKeySet.add(childKey) + } + }) + + deleteKey(getKey.value(parent)) + }) + } + } else { + parents.forEach(parent => { + deleteKey(getKey.value(parent)) + }) + } + }) + return Array.from(newKeySet) + } + const appendKeys = (checkedKeys: VKey[] | Set, appendedKeys: VKey[]) => + _append(checkedKeys, appendedKeys, _context.value) + const removeKeys = (checkedKeys: VKey[] | Set, removedKeys: VKey[]) => + _remove(checkedKeys, removedKeys, _context.value) + + const _getAllCheckedKeys = (data: V[] | undefined, defaultUnCheckedKeys: VKey[]) => { + const _data = data ?? _getAllData() + const tempKeys = + mergedCascadeStrategy.value === 'parent' + ? _data.map(getKey.value) + : new Set(getTreeKeys(_data, childrenKey.value, getKey.value, mergedCascadeStrategy.value === 'child')) + + return _remove(tempKeys, defaultUnCheckedKeys, data ? _getContext(data) : _context.value) + } + const _getAllUncheckedKeys = (data: V[] | undefined, defaultCheckedKeys: VKey[]) => { + return _append([], defaultCheckedKeys, data ? _getContext(data) : _context.value) + } + + const getAllCheckedKeys = (data?: V[] | VKey[], defaultUnCheckedKeys?: VKey[]) => { + const dataProvided = ((data?: V[] | VKey[]): data is V[] => isObject(data?.[0]))(data) + + return _getAllCheckedKeys(dataProvided ? data : undefined, defaultUnCheckedKeys ?? (dataProvided ? [] : data ?? [])) + } + const getAllUncheckedKeys = (data?: V[] | VKey[], defaultCheckedKeys?: VKey[]) => { + const dataProvided = ((data?: V[] | VKey[]): data is V[] => isObject(data?.[0]))(data) + + return _getAllUncheckedKeys(dataProvided ? data : undefined, defaultCheckedKeys ?? (dataProvided ? [] : data ?? [])) + } + + return { + appendKeys, + removeKeys, + getAllCheckedKeys, + getAllUncheckedKeys, + } +} diff --git a/packages/pro/transfer/__tests__/proTransfer.spec.ts b/packages/pro/transfer/__tests__/proTransfer.spec.ts index aa483d7cb..b41286655 100644 --- a/packages/pro/transfer/__tests__/proTransfer.spec.ts +++ b/packages/pro/transfer/__tests__/proTransfer.spec.ts @@ -7,12 +7,13 @@ import { IxTransferList } from '@idux/components/transfer' import TransferOperations from '@idux/components/transfer/src/TransferOperations' import ProTransfer from '../src/ProTransfer' -import { ProTransferProps, TreeTransferData } from '../src/types' +import { ProTransferProps, TransferData } from '../src/types' -interface Data extends TreeTransferData<'children'> { +interface Data extends TransferData { key: string label: string disabled?: boolean + children?: Data[] } const createData = (idx: number, includeDisabled = true): Data => ({ @@ -201,14 +202,14 @@ describe('ProTransfer', () => { await sourceTree.findAll('.ix-tree-node')[0].find('input').setValue(true) await appendTrigger.trigger('click') - expect(onChange).toBeCalledWith(['1-2-2', '1', '1-1', '1-2', '1-2-1', '1-3'], ['1-2-2']) + expect(onChange).toBeCalledWith(['1-2-2', '1', '1-2', '1-2-1', '1-1', '1-3'], ['1-2-2']) - await wrapper.setProps({ value: ['1-2-2', '1', '1-1', '1-2', '1-2-1', '1-3'] }) + await wrapper.setProps({ value: ['1-2-2', '1', '1-2', '1-2-1', '1-1', '1-3'] }) await targetTree.findAll('.ix-tree-node')[0].find('input').setValue(true) await removeTrigger.trigger('click') - expect(onChange).toBeCalledWith(['1-2-2'], ['1-2-2', '1', '1-1', '1-2', '1-2-1', '1-3']) + expect(onChange).toBeCalledWith(['1-2-2'], ['1-2-2', '1', '1-2', '1-2-1', '1-1', '1-3']) }) test('table immediate work', async () => { diff --git a/packages/pro/transfer/demo/BasicTable.vue b/packages/pro/transfer/demo/BasicTable.vue index 55301e7c2..d6cebd310 100644 --- a/packages/pro/transfer/demo/BasicTable.vue +++ b/packages/pro/transfer/demo/BasicTable.vue @@ -9,11 +9,13 @@