From 63369000b403724225dc63d318d45119e04d241d Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 14 Jul 2021 16:04:34 +0800 Subject: [PATCH 01/14] chore: Init fieldNames --- docs/demo/field-names.md | 3 +++ examples/fieldNames.tsx | 32 ++++++++++++++++++++++++++++++++ package.json | 4 ++-- src/generate.tsx | 17 ++++++++--------- src/interface.ts | 6 ++++++ src/utils/valueUtil.ts | 34 +++++++++++++++++++++++++++++++--- tests/Select.props.spec.js | 18 +++++------------- 7 files changed, 87 insertions(+), 27 deletions(-) create mode 100644 docs/demo/field-names.md create mode 100644 examples/fieldNames.tsx diff --git a/docs/demo/field-names.md b/docs/demo/field-names.md new file mode 100644 index 00000000..5d2046a4 --- /dev/null +++ b/docs/demo/field-names.md @@ -0,0 +1,3 @@ +## FieldNames + + diff --git a/examples/fieldNames.tsx b/examples/fieldNames.tsx new file mode 100644 index 00000000..1045b725 --- /dev/null +++ b/examples/fieldNames.tsx @@ -0,0 +1,32 @@ +import '../assets/index.less'; +import React from 'react'; +import 'rc-dialog/assets/index.css'; +import TreeSelect from '../src'; + +export default () => { + return ( + + ); +}; diff --git a/package.json b/package.json index 81f932ae..22c6ab23 100644 --- a/package.json +++ b/package.json @@ -68,8 +68,8 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "^12.0.0", - "rc-tree": "^4.0.0", + "rc-select": "~13.0.0-alpha.0", + "rc-tree": "~5.0.0", "rc-util": "^5.0.5" } } diff --git a/src/generate.tsx b/src/generate.tsx index 08bafa69..deeee3c3 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -6,6 +6,7 @@ import { getLabeledValue } from 'rc-select/lib/utils/valueUtil'; import { convertDataToEntities } from 'rc-tree/lib/utils/treeUtil'; import { conductCheck } from 'rc-tree/lib/utils/conductUtil'; import type { IconType } from 'rc-tree/lib/interface'; +import omit from 'rc-util/lib/omit'; import type { FilterFunc } from 'rc-select/lib/interface/generator'; import { INTERNAL_PROPS_MARK } from 'rc-select/lib/interface/generator'; import useMergedState from 'rc-util/lib/hooks/useMergedState'; @@ -22,6 +23,7 @@ import type { LegacyDataNode, SelectSource, FlattenDataNode, + FieldNames, } from './interface'; import { flattenOptions, @@ -43,8 +45,8 @@ import { formatStrategyKeys, SHOW_ALL, SHOW_PARENT, SHOW_CHILD } from './utils/s import { fillAdditionalInfo } from './utils/legacyUtil'; import useSelectValues from './hooks/useSelectValues'; -const OMIT_PROPS = [ - 'expandedKeys', +const OMIT_PROPS: (keyof TreeSelectProps)[] = [ + 'expandedKeys' as any, 'treeData', 'treeCheckable', 'showCheckedStrategy', @@ -63,6 +65,7 @@ const OMIT_PROPS = [ 'treeMotion', 'onTreeExpand', 'onTreeLoad', + 'labelRender', 'loadData', 'treeDataSimpleMode', 'treeNodeLabelProp', @@ -81,6 +84,7 @@ export interface TreeSelectProps | 'optionLabelProp' | 'tokenSeparators' | 'filterOption' + | 'fieldNames' > { multiple?: boolean; showArrow?: boolean; @@ -99,6 +103,7 @@ export interface TreeSelectProps maxTagPlaceholder?: (omittedValues: LabelValueType[]) => React.ReactNode; + fieldNames?: FieldNames; loadData?: (dataNode: LegacyDataNode) => Promise; treeNodeFilterProp?: string; treeNodeLabelProp?: string; @@ -155,13 +160,7 @@ export default function generate(config: { filterOptions, isValueDisabled, findValueOption, - omitDOMProps: (props: object) => { - const cloneProps = { ...props }; - OMIT_PROPS.forEach(prop => { - delete cloneProps[prop]; - }); - return cloneProps; - }, + omitDOMProps: (props: TreeSelectProps) => omit(props, OMIT_PROPS), }); RefSelect.displayName = 'Select'; diff --git a/src/interface.ts b/src/interface.ts index 8c614a0a..1997f13c 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -81,3 +81,9 @@ export interface ChangeEventExtra { /** @deprecated This prop not work as react node anymore. */ allCheckedNodes: LegacyCheckedNode[]; } + +export interface FieldNames { + value?: string; + label?: string; + children?: string; +} diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 6e6f54b3..4c3a283b 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -9,6 +9,7 @@ import type { DefaultValueType, LabelValueType, LegacyDataNode, + FieldNames, } from '../interface'; import { fillLegacyProps } from './legacyUtil'; import type { SkipType } from '../hooks/useKeyValueMapping'; @@ -22,6 +23,16 @@ export function toArray(value: T | T[]): T[] { return value !== undefined ? [value] : []; } +export function fillFieldNames(fieldNames?: FieldNames) { + const { label, value, children } = fieldNames || {}; + + return { + label: label || 'label', + value: value || 'value', + children: children || 'children', + }; +} + export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] { const optionMap: Map = new Map(); @@ -65,11 +76,22 @@ function getLevel({ parent }: FlattenNode): number { /** * Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`. */ -export function flattenOptions(options: DataNode[]): FlattenDataNode[] { +export function flattenOptions( + options: DataNode[], + { fieldNames }: { fieldNames?: FieldNames } = {}, +): FlattenDataNode[] { + const { + label: fieldLabel, + value: fieldValue, + children: fieldChildren, + } = fillFieldNames(fieldNames); + // Add missing key function fillKey(list: DataNode[]): TreeDataNode[] { return (list || []).map(node => { - const { value, key, children } = node; + const { key } = node; + const value = node[fieldValue]; + const children = node[fieldChildren]; const clone = { ...node, @@ -84,7 +106,11 @@ export function flattenOptions(options: DataNode[]): FlattenDataNode[] { }); } - const flattenList = flattenTreeData(fillKey(options), true); + const flattenList = flattenTreeData(fillKey(options), true, { + title: fieldLabel, + key: fieldValue, + children: fieldChildren, + }); const cacheMap = new Map(); const flattenDateNodeList: (FlattenDataNode & { parentKey?: React.Key })[] = flattenList.map( @@ -96,6 +122,8 @@ export function flattenOptions(options: DataNode[]): FlattenDataNode[] { key, data, level: getLevel(node), + label: data[fieldLabel], + value: data[fieldValue], parentKey: node.parent?.data.key, }; diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js index d5ac5517..bef9a652 100644 --- a/tests/Select.props.spec.js +++ b/tests/Select.props.spec.js @@ -182,7 +182,7 @@ describe('TreeSelect.props', () => { // onChange - is already test above - it('onSelect', () => { + it.only('onSelect', () => { const handleSelect = jest.fn(); const wrapper = mount( createOpenSelect({ @@ -239,12 +239,9 @@ describe('TreeSelect.props', () => { dropdownStyle: style, }), ); - expect( - wrapper - .find('.test-dropdownClassName') - .first() - .props().style, - ).toEqual(expect.objectContaining(style)); + expect(wrapper.find('.test-dropdownClassName').first().props().style).toEqual( + expect.objectContaining(style), + ); }); it('notFoundContent', () => { @@ -449,12 +446,7 @@ describe('TreeSelect.props', () => { it('getPopupContainer', () => { const getPopupContainer = trigger => trigger.parentNode; const wrapper = mount(createOpenSelect({ getPopupContainer })); - expect( - wrapper - .find('Trigger') - .first() - .props().getPopupContainer, - ).toBe(getPopupContainer); + expect(wrapper.find('Trigger').first().props().getPopupContainer).toBe(getPopupContainer); }); it('set value not in the Tree', () => { From 5cf2442bb8f6fe9e7e87859d220d01d997e4cec4 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 14 Jul 2021 17:23:05 +0800 Subject: [PATCH 02/14] feat: Use fieldNames --- examples/fieldNames.tsx | 1 + src/generate.tsx | 11 +++++++++++ src/hooks/useTreeData.ts | 32 +++++++++++++++++++++++--------- src/utils/valueUtil.ts | 23 +++-------------------- tests/Select.props.spec.js | 2 +- 5 files changed, 39 insertions(+), 30 deletions(-) diff --git a/examples/fieldNames.tsx b/examples/fieldNames.tsx index 1045b725..be636993 100644 --- a/examples/fieldNames.tsx +++ b/examples/fieldNames.tsx @@ -6,6 +6,7 @@ import TreeSelect from '../src'; export default () => { return ( ((props, ref) => { const { + fieldNames, multiple, treeCheckable, treeCheckStrictly, @@ -206,12 +208,20 @@ export default function generate(config: { const mergedLabelInValue = treeCheckStrictly || labelInValue; // ======================= Tree Data ======================= + // FieldNames + const mergedFieldNames = fillFieldNames(fieldNames); + // Legacy both support `label` or `title` if not set. // We have to fallback to function to handle this const getTreeNodeTitle = (node: DataNode): React.ReactNode => { if (!treeData) { return node.title; } + + if (fieldNames?.label) { + return node[fieldNames.label]; + } + return node.label || node.title; }; @@ -232,6 +242,7 @@ export default function generate(config: { const mergedTreeData = useTreeData(treeData, children, { getLabelProp: getTreeNodeTitle, simpleMode: treeDataSimpleMode, + fieldNames: mergedFieldNames, }); const flattedOptions = useMemo(() => flattenOptions(mergedTreeData), [mergedTreeData]); diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index 9e132add..4e662774 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -1,6 +1,12 @@ import * as React from 'react'; import warning from 'rc-util/lib/warning'; -import type { DataNode, InnerDataNode, SimpleModeConfig, RawValueType } from '../interface'; +import type { + DataNode, + InnerDataNode, + SimpleModeConfig, + RawValueType, + FieldNames, +} from '../interface'; import { convertChildrenToData } from '../utils/legacyUtil'; const MAX_WARNING_TIMES = 10; @@ -13,7 +19,7 @@ function parseSimpleTreeData( const rootNodeList = []; // Fill in the map - const nodeList = treeData.map((node) => { + const nodeList = treeData.map(node => { const clone = { ...node }; const key = clone[id]; keyNodes[key] = clone; @@ -22,7 +28,7 @@ function parseSimpleTreeData( }); // Connect tree - nodeList.forEach((node) => { + nodeList.forEach(node => { const parentKey = node[pId]; const parent = keyNodes[parentKey]; @@ -47,15 +53,20 @@ function parseSimpleTreeData( function formatTreeData( treeData: DataNode[], getLabelProp: (node: DataNode) => React.ReactNode, + fieldNames: FieldNames, ): InnerDataNode[] { let warningTimes = 0; const valueSet = new Set(); + // Field names + const { value: fieldValue, children: fieldChildren } = fieldNames; + function dig(dataNodes: DataNode[]) { - return (dataNodes || []).map((node) => { - const { key, value, children, ...rest } = node; + return (dataNodes || []).map(node => { + const { key, children, ...rest } = node; - const mergedValue = 'value' in node ? value : key; + const value = node[fieldValue]; + const mergedValue = fieldValue in node ? value : key; const dataNode: InnerDataNode = { ...rest, @@ -84,8 +95,8 @@ function formatTreeData( valueSet.add(value); } - if ('children' in node) { - dataNode.children = dig(children); + if (fieldChildren in node) { + dataNode.children = dig(node[fieldChildren]); } return dataNode; @@ -105,9 +116,11 @@ export default function useTreeData( { getLabelProp, simpleMode, + fieldNames, }: { getLabelProp: (node: DataNode) => React.ReactNode; simpleMode: boolean | SimpleModeConfig; + fieldNames: FieldNames; }, ): InnerDataNode[] { const cacheRef = React.useRef<{ @@ -130,6 +143,7 @@ export default function useTreeData( }) : treeData, getLabelProp, + fieldNames, ); cacheRef.current.treeData = treeData; @@ -137,7 +151,7 @@ export default function useTreeData( cacheRef.current.formatTreeData = cacheRef.current.children === children ? cacheRef.current.formatTreeData - : formatTreeData(convertChildrenToData(children), getLabelProp); + : formatTreeData(convertChildrenToData(children), getLabelProp, fieldNames); } return cacheRef.current.formatTreeData; diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 4c3a283b..81672579 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -76,22 +76,11 @@ function getLevel({ parent }: FlattenNode): number { /** * Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`. */ -export function flattenOptions( - options: DataNode[], - { fieldNames }: { fieldNames?: FieldNames } = {}, -): FlattenDataNode[] { - const { - label: fieldLabel, - value: fieldValue, - children: fieldChildren, - } = fillFieldNames(fieldNames); - +export function flattenOptions(options: DataNode[]): FlattenDataNode[] { // Add missing key function fillKey(list: DataNode[]): TreeDataNode[] { return (list || []).map(node => { - const { key } = node; - const value = node[fieldValue]; - const children = node[fieldChildren]; + const { value, key, children } = node; const clone = { ...node, @@ -106,11 +95,7 @@ export function flattenOptions( }); } - const flattenList = flattenTreeData(fillKey(options), true, { - title: fieldLabel, - key: fieldValue, - children: fieldChildren, - }); + const flattenList = flattenTreeData(fillKey(options), true, null); const cacheMap = new Map(); const flattenDateNodeList: (FlattenDataNode & { parentKey?: React.Key })[] = flattenList.map( @@ -122,8 +107,6 @@ export function flattenOptions( key, data, level: getLevel(node), - label: data[fieldLabel], - value: data[fieldValue], parentKey: node.parent?.data.key, }; diff --git a/tests/Select.props.spec.js b/tests/Select.props.spec.js index bef9a652..724cfc14 100644 --- a/tests/Select.props.spec.js +++ b/tests/Select.props.spec.js @@ -182,7 +182,7 @@ describe('TreeSelect.props', () => { // onChange - is already test above - it.only('onSelect', () => { + it('onSelect', () => { const handleSelect = jest.fn(); const wrapper = mount( createOpenSelect({ From f289080604d0544f4c1fb5326d39d1ef68671c67 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 14 Jul 2021 19:51:20 +0800 Subject: [PATCH 03/14] fix: value injection --- package.json | 2 +- src/utils/valueUtil.ts | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 22c6ab23..938f8e88 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", - "rc-select": "~13.0.0-alpha.0", + "rc-select": "~13.0.0-alpha.1", "rc-tree": "~5.0.0", "rc-util": "^5.0.5" } diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 81672579..09ede855 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -101,10 +101,11 @@ export function flattenOptions(options: DataNode[]): FlattenDataNode[] { const flattenDateNodeList: (FlattenDataNode & { parentKey?: React.Key })[] = flattenList.map( node => { const { data } = node; - const { key } = data; + const { key, value } = data as DataNode & { value: RawValueType }; const flattenNode = { key, + value, data, level: getLevel(node), parentKey: node.parent?.data.key, From f8cd4c18b8b018fec3e5e86a45e84b0ea67aba34 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 14 Jul 2021 19:53:34 +0800 Subject: [PATCH 04/14] test: Update snapshot --- .../Select.checkable.spec.js.snap | 30 +++++++------ tests/__snapshots__/Select.spec.js.snap | 42 +++++++++---------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/tests/__snapshots__/Select.checkable.spec.js.snap b/tests/__snapshots__/Select.checkable.spec.js.snap index cb461788..470162d7 100644 --- a/tests/__snapshots__/Select.checkable.spec.js.snap +++ b/tests/__snapshots__/Select.checkable.spec.js.snap @@ -17,6 +17,7 @@ exports[`TreeSelect.checkable uncheck remove by selector not treeCheckStrictly 1 >
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
Date: Wed, 14 Jul 2021 20:46:14 +0800 Subject: [PATCH 05/14] test: Add test case --- tests/Select.fieldNames.spec.tsx | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 tests/Select.fieldNames.spec.tsx diff --git a/tests/Select.fieldNames.spec.tsx b/tests/Select.fieldNames.spec.tsx new file mode 100644 index 00000000..b640962b --- /dev/null +++ b/tests/Select.fieldNames.spec.tsx @@ -0,0 +1,45 @@ +/* eslint-disable no-undef, react/no-multi-comp, no-console */ +import React from 'react'; +import { mount } from 'enzyme'; +import type { TreeSelectProps } from '../src'; +import TreeSelect from '../src'; + +describe('TreeSelect.FieldNames', () => { + function mountTreeSelect(props?: TreeSelectProps) { + return mount( + , + ); + } + + it('render correctly', () => { + const onChange = jest.fn(); + const wrapper = mountTreeSelect({ onChange, open: true }); + wrapper.selectNode(1); + + expect(onChange).toHaveBeenCalledWith('sub_1', ['Sub 1'], expect.anything()); + }); +}); From 267d705c4cd5a59eb8dbdcbe04e539a933040592 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 15 Jul 2021 10:51:11 +0800 Subject: [PATCH 06/14] test: onSelect test driven --- tests/Select.fieldNames.spec.tsx | 44 ++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/Select.fieldNames.spec.tsx b/tests/Select.fieldNames.spec.tsx index b640962b..4af433c7 100644 --- a/tests/Select.fieldNames.spec.tsx +++ b/tests/Select.fieldNames.spec.tsx @@ -42,4 +42,48 @@ describe('TreeSelect.FieldNames', () => { expect(onChange).toHaveBeenCalledWith('sub_1', ['Sub 1'], expect.anything()); }); + + it('labelInValue', () => { + const onChange = jest.fn(); + const wrapper = mountTreeSelect({ onChange, open: true, labelInValue: true }); + wrapper.selectNode(2); + + expect(onChange).toHaveBeenCalledWith( + { label: 'Sub 2', value: 'sub_2' }, + null, + expect.anything(), + ); + }); + + it('multiple', () => { + const onChange = jest.fn(); + const wrapper = mountTreeSelect({ onChange, open: true, multiple: true }); + + wrapper.selectNode(1); + + onChange.mockReset(); + wrapper.selectNode(2); + + expect(onChange).toHaveBeenCalledWith( + ['sub_1', 'sub_2'], + ['Sub 1', 'Sub 2'], + expect.anything(), + ); + }); + + it('onSelect', () => { + const onSelect = jest.fn(); + const wrapper = mountTreeSelect({ onSelect, open: true }); + + wrapper.selectNode(0); + + expect(onSelect).toHaveBeenCalledWith('parent', { + myChildren: [ + { myLabel: 'Sub 1', myValue: 'sub_1' }, + { myLabel: 'Sub 2', myValue: 'sub_2' }, + ], + myLabel: 'Parent', + myValue: 'parent', + }); + }); }); From 9ddbb064860624da6a81341cf207b1859e7b6b13 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 15 Jul 2021 11:36:56 +0800 Subject: [PATCH 07/14] chore: Adjust internal structure --- src/hooks/useTreeData.ts | 14 +++++++------- src/interface.ts | 12 ++++++++---- src/utils/legacyUtil.tsx | 12 ++++++------ src/utils/valueUtil.ts | 28 +++++++++++++++++----------- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index 4e662774..d99825d8 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import warning from 'rc-util/lib/warning'; import type { DataNode, - InnerDataNode, + InternalDataEntity, SimpleModeConfig, RawValueType, FieldNames, @@ -54,7 +54,7 @@ function formatTreeData( treeData: DataNode[], getLabelProp: (node: DataNode) => React.ReactNode, fieldNames: FieldNames, -): InnerDataNode[] { +): InternalDataEntity[] { let warningTimes = 0; const valueSet = new Set(); @@ -63,16 +63,16 @@ function formatTreeData( function dig(dataNodes: DataNode[]) { return (dataNodes || []).map(node => { - const { key, children, ...rest } = node; + const { key } = node; const value = node[fieldValue]; const mergedValue = fieldValue in node ? value : key; - const dataNode: InnerDataNode = { - ...rest, + const dataNode: InternalDataEntity = { key: key !== null && key !== undefined ? key : mergedValue, value: mergedValue, title: getLabelProp(node), + node, }; // Check `key` & `value` and warning user @@ -122,11 +122,11 @@ export default function useTreeData( simpleMode: boolean | SimpleModeConfig; fieldNames: FieldNames; }, -): InnerDataNode[] { +): InternalDataEntity[] { const cacheRef = React.useRef<{ treeData?: DataNode[]; children?: React.ReactNode; - formatTreeData?: InnerDataNode[]; + formatTreeData?: InternalDataEntity[]; }>({}); if (treeData) { diff --git a/src/interface.ts b/src/interface.ts index 1997f13c..5640e77b 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -30,11 +30,14 @@ export interface DataNode { [prop: string]: any; } -export interface InnerDataNode extends DataNode { +export interface InternalDataEntity { key: Key; value: RawValueType; - label?: React.ReactNode; - children?: InnerDataNode[]; + title?: React.ReactNode; + children?: InternalDataEntity[]; + + /** Origin DataNode */ + node: DataNode; } export interface LegacyDataNode extends DataNode { @@ -47,8 +50,9 @@ export interface TreeDataNode extends DataNode { } export interface FlattenDataNode { - data: DataNode; + data: InternalDataEntity; key: Key; + value: RawValueType; level: number; parent?: FlattenDataNode; } diff --git a/src/utils/legacyUtil.tsx b/src/utils/legacyUtil.tsx index 4a32d3d2..878c53c2 100644 --- a/src/utils/legacyUtil.tsx +++ b/src/utils/legacyUtil.tsx @@ -5,7 +5,7 @@ import type { DataNode, LegacyDataNode, ChangeEventExtra, - InnerDataNode, + InternalDataEntity, RawValueType, LegacyCheckedNode, } from '../interface'; @@ -36,7 +36,7 @@ export function convertChildrenToData(nodes: React.ReactNode): DataNode[] { return data; }) - .filter((data) => data); + .filter(data => data); } export function fillLegacyProps(dataNode: DataNode): LegacyDataNode { @@ -66,20 +66,20 @@ export function fillAdditionalInfo( extra: ChangeEventExtra, triggerValue: RawValueType, checkedValues: RawValueType[], - treeData: InnerDataNode[], + treeData: InternalDataEntity[], showPosition: boolean, ) { let triggerNode: React.ReactNode = null; let nodeList: LegacyCheckedNode[] = null; function generateMap() { - function dig(list: InnerDataNode[], level = '0', parentIncluded = false) { + function dig(list: InternalDataEntity[], level = '0', parentIncluded = false) { return list .map((dataNode, index) => { const pos = `${level}-${index}`; const included = checkedValues.includes(dataNode.value); const children = dig(dataNode.children || [], pos, included); - const node = {children.map((child) => child.node)}; + const node = {children.map(child => child.node)}; // Link with trigger node if (triggerValue === dataNode.value) { @@ -101,7 +101,7 @@ export function fillAdditionalInfo( } return null; }) - .filter((node) => node); + .filter(node => node); } if (!nodeList) { diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 09ede855..9bef1167 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -10,6 +10,7 @@ import type { LabelValueType, LegacyDataNode, FieldNames, + InternalDataEntity, } from '../interface'; import { fillLegacyProps } from './legacyUtil'; import type { SkipType } from '../hooks/useKeyValueMapping'; @@ -38,7 +39,7 @@ export function findValueOption(values: RawValueType[], options: CompatibleDataN options.forEach(flattenItem => { const { data } = flattenItem; - optionMap.set(data.value, data); + optionMap.set(data.value, data.node); }); return values.map(val => fillLegacyProps(optionMap.get(val))); @@ -57,8 +58,9 @@ export function isCheckDisabled(node: DataNode) { return node.disabled || node.disableCheckbox || node.checkable === false; } -interface TreeDataNode { +interface TreeDataNode extends InternalDataEntity { key: Key; + children?: TreeDataNode[]; } function getLevel({ parent }: FlattenNode): number { @@ -76,13 +78,15 @@ function getLevel({ parent }: FlattenNode): number { /** * Before reuse `rc-tree` logic, we need to add key since TreeSelect use `value` instead of `key`. */ -export function flattenOptions(options: DataNode[]): FlattenDataNode[] { +export function flattenOptions(options: any): FlattenDataNode[] { + const typedOptions = options as InternalDataEntity[]; + // Add missing key - function fillKey(list: DataNode[]): TreeDataNode[] { + function fillKey(list: InternalDataEntity[]): TreeDataNode[] { return (list || []).map(node => { const { value, key, children } = node; - const clone = { + const clone: TreeDataNode = { ...node, key: 'key' in node ? key : value, }; @@ -95,20 +99,22 @@ export function flattenOptions(options: DataNode[]): FlattenDataNode[] { }); } - const flattenList = flattenTreeData(fillKey(options), true, null); + const flattenList = flattenTreeData(fillKey(typedOptions), true, null); const cacheMap = new Map(); const flattenDateNodeList: (FlattenDataNode & { parentKey?: React.Key })[] = flattenList.map( - node => { - const { data } = node; - const { key, value } = data as DataNode & { value: RawValueType }; + option => { + const { data, key, value } = option as any as Omit & { + value: RawValueType; + data: InternalDataEntity; + }; const flattenNode = { key, value, data, - level: getLevel(node), - parentKey: node.parent?.data.key, + level: getLevel(option), + parentKey: option.parent?.data.key, }; cacheMap.set(key, flattenNode); From b6b42bef436dbcf2118eaa99edb1b039a4b9d5a7 Mon Sep 17 00:00:00 2001 From: zombiej Date: Thu, 15 Jul 2021 12:32:33 +0800 Subject: [PATCH 08/14] refactor: Use internal node --- src/generate.tsx | 2 +- src/utils/valueUtil.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/generate.tsx b/src/generate.tsx index 7ccba6f6..9227fdf9 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -414,7 +414,7 @@ export default function generate(config: { ? null : eventValues.map(val => { const entity = getEntityByValue(val); - return entity ? getTreeNodeLabelProp(entity) : null; + return entity ? entity.data.title : null; }), additionalInfo, ); diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 9bef1167..6d154dd7 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -38,8 +38,8 @@ export function findValueOption(values: RawValueType[], options: CompatibleDataN const optionMap: Map = new Map(); options.forEach(flattenItem => { - const { data } = flattenItem; - optionMap.set(data.value, data.node); + const { data, value } = flattenItem; + optionMap.set(value, data.node); }); return values.map(val => fillLegacyProps(optionMap.get(val))); From cfff4da4c6f356f570aadaf8abc0dcfb02d46af0 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Jul 2021 16:11:04 +0800 Subject: [PATCH 09/14] fix: node track logic --- src/generate.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/generate.tsx b/src/generate.tsx index 9227fdf9..230bec6b 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -218,15 +218,15 @@ export default function generate(config: { return node.title; } - if (fieldNames?.label) { - return node[fieldNames.label]; + if (mergedFieldNames?.label) { + return node[mergedFieldNames.label]; } return node.label || node.title; }; const getTreeNodeLabelProp = (entity: FlattenDataNode): React.ReactNode => { - const node = entity.data; + const { node } = entity.data; if (labelRender) { return labelRender(entity); From 1e3e6bcd3dfd31b960828852498f3c6f6e0f8228 Mon Sep 17 00:00:00 2001 From: zombiej Date: Fri, 16 Jul 2021 16:45:10 +0800 Subject: [PATCH 10/14] fix: fieldNames should not default fill in render --- examples/debug.tsx | 58 ++++++++---------------------------------- src/generate.tsx | 2 +- src/utils/valueUtil.ts | 18 ++++++++++--- 3 files changed, 27 insertions(+), 51 deletions(-) diff --git a/examples/debug.tsx b/examples/debug.tsx index cea95c85..fc5086dc 100644 --- a/examples/debug.tsx +++ b/examples/debug.tsx @@ -1,54 +1,18 @@ /* eslint-disable react/no-array-index-key */ import React from 'react'; -import TreeSelect, { TreeNode } from '../src'; +import TreeSelect from '../src'; import '../assets/index.less'; -class Demo extends React.Component { - state = { - value: undefined, - }; +export default () => { + const [treeData, setTreeData] = React.useState([]); - onChange = value => { - console.log(value); - this.setState({ value }); - }; + React.useEffect(() => { + setTimeout(() => { + console.clear(); + setTreeData([{ value: 'light', title: 'bamboo' }]); + }, 1000); + }, []); - render() { - return ( - { - let current = entity; - const nodes = []; - - while (current) { - nodes.unshift(current.data.title); - current = current.parent; - } - - return nodes.join('>'); - }} - > - - - - - - - sss} key="random3" /> - - - - ); - } -} - -export default Demo; + return ; +}; diff --git a/src/generate.tsx b/src/generate.tsx index 230bec6b..d5036408 100644 --- a/src/generate.tsx +++ b/src/generate.tsx @@ -209,7 +209,7 @@ export default function generate(config: { // ======================= Tree Data ======================= // FieldNames - const mergedFieldNames = fillFieldNames(fieldNames); + const mergedFieldNames = fillFieldNames(fieldNames, true); // Legacy both support `label` or `title` if not set. // We have to fallback to function to handle this diff --git a/src/utils/valueUtil.ts b/src/utils/valueUtil.ts index 6d154dd7..b973301e 100644 --- a/src/utils/valueUtil.ts +++ b/src/utils/valueUtil.ts @@ -24,14 +24,26 @@ export function toArray(value: T | T[]): T[] { return value !== undefined ? [value] : []; } -export function fillFieldNames(fieldNames?: FieldNames) { +/** + * Fill `fieldNames` with default field names. + * + * @param fieldNames passed props + * @param skipTitle Skip if no need fill `title`. This is useful since we have 2 name as same title level + * @returns + */ +export function fillFieldNames(fieldNames?: FieldNames, skipTitle: boolean = false) { const { label, value, children } = fieldNames || {}; - return { - label: label || 'label', + const filledNames: FieldNames = { value: value || 'value', children: children || 'children', }; + + if (!skipTitle || label) { + filledNames.label = label || 'label'; + } + + return filledNames; } export function findValueOption(values: RawValueType[], options: CompatibleDataNode[]): DataNode[] { From 9b0d31100a2153dc855a1e3b5f119b652fc67cf2 Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 20 Jul 2021 18:13:04 +0800 Subject: [PATCH 11/14] test: fix test logic --- tests/Select.multiple.spec.js | 55 ++++++++--------------------------- 1 file changed, 12 insertions(+), 43 deletions(-) diff --git a/tests/Select.multiple.spec.js b/tests/Select.multiple.spec.js index d273a1d8..2fb38350 100644 --- a/tests/Select.multiple.spec.js +++ b/tests/Select.multiple.spec.js @@ -12,9 +12,7 @@ describe('TreeSelect.multiple', () => { { key: '0', value: '0', title: 'label0' }, { key: '1', value: '1', title: 'label1' }, ]; - const createSelect = props => ( - - ); + const createSelect = props => ; it('select multiple nodes', () => { const wrapper = mount(createSelect({ open: true })); @@ -33,10 +31,7 @@ describe('TreeSelect.multiple', () => { it('remove by backspace key', () => { const wrapper = mount(createSelect({ defaultValue: ['0', '1'] })); - wrapper - .find('input') - .first() - .simulate('keyDown', { which: KeyCode.BACKSPACE }); + wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE }); expect(wrapper.getSelection()).toHaveLength(1); expect(wrapper.getSelection(0).text()).toBe('label0'); }); @@ -63,15 +58,9 @@ describe('TreeSelect.multiple', () => { } } const wrapper = mount(); - wrapper - .find('input') - .first() - .simulate('keyDown', { which: KeyCode.BACKSPACE }); + wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE }); wrapper.selectNode(1); - wrapper - .find('input') - .first() - .simulate('keyDown', { which: KeyCode.BACKSPACE }); + wrapper.find('input').first().simulate('keyDown', { which: KeyCode.BACKSPACE }); expect(wrapper.getSelection()).toHaveLength(1); expect(wrapper.getSelection(0).text()).toBe('label0'); }); @@ -96,10 +85,12 @@ describe('TreeSelect.multiple', () => { wrapper.clearSelection(1); + expect(handleChange.mock.calls[0][2].allCheckedNodes[0].props).toBeTruthy(); + expect(handleChange).toHaveBeenCalledWith( ['0'], ['label0'], - expect.objectContaining({ + expect.anything({ allCheckedNodes: [ expect.objectContaining({ props: expect.objectContaining(children[0].props), @@ -120,21 +111,11 @@ describe('TreeSelect.multiple', () => { wrapper.search('0'); wrapper.selectNode(0); - expect( - wrapper - .find('input') - .first() - .props().value, - ).toBe(''); + expect(wrapper.find('input').first().props().value).toBe(''); wrapper.search('0'); wrapper.selectNode(0); - expect( - wrapper - .find('input') - .first() - .props().value, - ).toBe(''); + expect(wrapper.find('input').first().props().value).toBe(''); }); it('do not open tree when close button click', () => { @@ -238,19 +219,11 @@ describe('TreeSelect.multiple', () => { ); wrapper.selectNode(0); - expect(onChange).toHaveBeenCalledWith( - [4, 0], - expect.anything(), - expect.anything(), - ); + expect(onChange).toHaveBeenCalledWith([4, 0], expect.anything(), expect.anything()); onChange.mockReset(); wrapper.selectNode(1); - expect(onChange).toHaveBeenCalledWith( - [4, 0, 2, 3], - expect.anything(), - expect.anything(), - ); + expect(onChange).toHaveBeenCalledWith([4, 0, 2, 3], expect.anything(), expect.anything()); }); // https://github.com/ant-design/ant-design/issues/12315 @@ -271,11 +244,7 @@ describe('TreeSelect.multiple', () => { wrapper.search('sss'); wrapper.selectNode(2); - expect(onChange).toHaveBeenCalledWith( - ['leaf1', 'sss'], - expect.anything(), - expect.anything(), - ); + expect(onChange).toHaveBeenCalledWith(['leaf1', 'sss'], expect.anything(), expect.anything()); }); it('do not crash when value has empty string', () => { From 676f0f4c1dcab527b34c967ce1a71e3f50b9eadc Mon Sep 17 00:00:00 2001 From: zombiej Date: Tue, 20 Jul 2021 18:20:40 +0800 Subject: [PATCH 12/14] fix: internal labelRender logic --- tests/Select.internal.spec.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Select.internal.spec.tsx b/tests/Select.internal.spec.tsx index 8bb3ba29..5180eff3 100644 --- a/tests/Select.internal.spec.tsx +++ b/tests/Select.internal.spec.tsx @@ -21,7 +21,7 @@ describe('TreeSelect.InternalAPI', () => { const nodes = []; while (current) { - nodes.unshift(current.data.label); + nodes.unshift(current.data.node.label); current = current.parent; } From 17843bcad3a7aa752b3b03e8e1a0f23ff85a2ba9 Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 21 Jul 2021 21:17:11 +0800 Subject: [PATCH 13/14] fix: selectable logic check --- src/OptionList.tsx | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/OptionList.tsx b/src/OptionList.tsx index 32045830..1c4bef11 100644 --- a/src/OptionList.tsx +++ b/src/OptionList.tsx @@ -105,7 +105,7 @@ const OptionList: React.RefForwardingComponent< // ========================== Values ========================== const valueKeys = React.useMemo( () => - checkedKeys.map((val) => { + checkedKeys.map(val => { const entity = getEntityByValue(val); return entity ? entity.key : null; }), @@ -153,7 +153,7 @@ const OptionList: React.RefForwardingComponent< React.useEffect(() => { if (searchValue) { - setSearchExpandedKeys(flattenOptions.map((o) => o.key)); + setSearchExpandedKeys(flattenOptions.map(o => o.key)); } }, [searchValue]); @@ -167,7 +167,7 @@ const OptionList: React.RefForwardingComponent< }; // ========================== Events ========================== - const onListMouseDown: React.MouseEventHandler = (event) => { + const onListMouseDown: React.MouseEventHandler = event => { event.preventDefault(); }; @@ -190,7 +190,7 @@ const OptionList: React.RefForwardingComponent< React.useImperativeHandle(ref, () => ({ scrollTo: treeRef.current?.scrollTo, - onKeyDown: (event) => { + onKeyDown: event => { const { which } = event; switch (which) { // >>> Arrow keys @@ -203,7 +203,7 @@ const OptionList: React.RefForwardingComponent< // >>> Select item case KeyCode.ENTER: { - const { selectable, value } = activeEntity?.data || {}; + const { selectable, value } = activeEntity?.data.node || {}; if (selectable !== false) { onInternalSelect(null, { node: { key: activeKey }, @@ -281,8 +281,9 @@ const OptionList: React.RefForwardingComponent< ); }; -const RefOptionList = - React.forwardRef>(OptionList); +const RefOptionList = React.forwardRef>( + OptionList, +); RefOptionList.displayName = 'OptionList'; export default RefOptionList; From 64c6db16d8ed88c1f40e18be4dffb2be3e30103d Mon Sep 17 00:00:00 2001 From: zombiej Date: Wed, 21 Jul 2021 21:56:55 +0800 Subject: [PATCH 14/14] test: More of selection test --- src/hooks/useKeyValueMapping.ts | 9 +++-- src/hooks/useTreeData.ts | 4 ++- src/interface.ts | 2 ++ tests/Select.checkable.spec.js | 59 +++++++++++++++++++++++++-------- tests/utils.spec.js | 19 ----------- 5 files changed, 55 insertions(+), 38 deletions(-) delete mode 100644 tests/utils.spec.js diff --git a/src/hooks/useKeyValueMapping.ts b/src/hooks/useKeyValueMapping.ts index ea067e44..4882cd30 100644 --- a/src/hooks/useKeyValueMapping.ts +++ b/src/hooks/useKeyValueMapping.ts @@ -8,16 +8,15 @@ export function isDisabled(dataNode: FlattenDataNode, skipType: SkipType): boole return true; } - const { disabled, disableCheckbox } = dataNode.data; + const { disabled, disableCheckbox } = dataNode.data.node; switch (skipType) { - case 'select': - return disabled; case 'checkbox': return disabled || disableCheckbox; - } - return false; + default: + return disabled; + } } export default function useKeyValueMapping( diff --git a/src/hooks/useTreeData.ts b/src/hooks/useTreeData.ts index d99825d8..b935df07 100644 --- a/src/hooks/useTreeData.ts +++ b/src/hooks/useTreeData.ts @@ -63,12 +63,14 @@ function formatTreeData( function dig(dataNodes: DataNode[]) { return (dataNodes || []).map(node => { - const { key } = node; + const { key, disableCheckbox, disabled } = node; const value = node[fieldValue]; const mergedValue = fieldValue in node ? value : key; const dataNode: InternalDataEntity = { + disableCheckbox, + disabled, key: key !== null && key !== undefined ? key : mergedValue, value: mergedValue, title: getLabelProp(node), diff --git a/src/interface.ts b/src/interface.ts index 5640e77b..9f491eda 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -34,6 +34,8 @@ export interface InternalDataEntity { key: Key; value: RawValueType; title?: React.ReactNode; + disableCheckbox?: boolean; + disabled?: boolean; children?: InternalDataEntity[]; /** Origin DataNode */ diff --git a/tests/Select.checkable.spec.js b/tests/Select.checkable.spec.js index fad39e94..727e44ec 100644 --- a/tests/Select.checkable.spec.js +++ b/tests/Select.checkable.spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-undef, react/no-multi-comp */ +/* eslint-disable no-undef, react/no-multi-comp, max-classes-per-file */ import React from 'react'; import { mount } from 'enzyme'; import TreeSelect, { SHOW_PARENT, SHOW_ALL, TreeNode } from '../src'; @@ -231,12 +231,7 @@ describe('TreeSelect.checkable', () => { wrapper.search('foo'); wrapper.clearAll(); expect(wrapper.getSelection()).toHaveLength(0); - expect( - wrapper - .find('input') - .first() - .props().value, - ).toBe(''); + expect(wrapper.find('input').first().props().value).toBe(''); }); describe('uncheck', () => { @@ -277,7 +272,10 @@ describe('TreeSelect.checkable', () => { }); wrapper.clearSelection(1); expect(onChange).toHaveBeenCalledWith( - [{ label: '0', value: '0' }, { label: '0-0-0', value: '0-0-0' }], + [ + { label: '0', value: '0' }, + { label: '0-0-0', value: '0-0-0' }, + ], null, expect.anything(), ); @@ -616,15 +614,15 @@ describe('TreeSelect.checkable', () => { value={[{ value: 'half', halfChecked: true }]} open onChange={onChange} - treeData={[{ value: 'half', title: 'Half Check' }, { value: 'full', title: 'Full Check' }]} + treeData={[ + { value: 'half', title: 'Half Check' }, + { value: 'full', title: 'Full Check' }, + ]} />, ); function getTreeNode(index) { - return wrapper - .find('.rc-tree-select-tree-treenode') - .not('[aria-hidden]') - .at(index); + return wrapper.find('.rc-tree-select-tree-treenode').not('[aria-hidden]').at(index); } expect( @@ -651,4 +649,39 @@ describe('TreeSelect.checkable', () => { expect.anything(), ); }); + + it('disableCheckbox', () => { + const onChange = jest.fn(); + const wrapper = mount( + , + ); + + wrapper.selectNode(0); + expect(onChange).toHaveBeenCalledWith(['parent', 'sub1'], expect.anything(), expect.anything()); + + onChange.mockReset(); + wrapper.selectNode(2); + wrapper.selectNode(3); + expect(onChange).not.toHaveBeenCalled(); + + wrapper.selectNode(1); + expect(onChange).toHaveBeenCalledWith([], expect.anything(), expect.anything()); + }); }); diff --git a/tests/utils.spec.js b/tests/utils.spec.js deleted file mode 100644 index d178d31a..00000000 --- a/tests/utils.spec.js +++ /dev/null @@ -1,19 +0,0 @@ -import { isValueDisabled } from '../src/utils/valueUtil'; -import { isDisabled } from '../src/hooks/useKeyValueMapping'; - -describe('TreeSelect.util', () => { - it('isValueDisabled', () => { - const options = [{ data: { value: 'disabled', disabled: true } }, { data: { value: 'pass' } }]; - expect(isValueDisabled('disabled', options)).toBeTruthy(); - expect(isValueDisabled('pass', options)).toBeFalsy(); - expect(isValueDisabled('not-exist', options)).toBeFalsy(); - }); - - it('isDisabled', () => { - expect(isDisabled({ data: { disabled: true } }, 'select')).toBeTruthy(); - expect(isDisabled({ data: { disableCheckbox: true } }, 'select')).toBeFalsy(); - expect(isDisabled({ data: { disabled: true } }, 'checkbox')).toBeTruthy(); - expect(isDisabled({ data: { disableCheckbox: true } }, 'checkbox')).toBeTruthy(); - expect(isDisabled({ data: { disabled: true } }, null)).toBeFalsy(); - }); -});