diff --git a/packages/antd/docs/components/SelectTable.md b/packages/antd/docs/components/SelectTable.md index e813c9196bd..f68c6b938ef 100644 --- a/packages/antd/docs/components/SelectTable.md +++ b/packages/antd/docs/components/SelectTable.md @@ -604,7 +604,8 @@ export default () => ( | Property name | Type | Description | Default value | | ------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | mode | `'multiple' \| 'single'` | Set mode of SelectTable | `'multiple'` | -| optionAsValue | boolean | use `option` as value | false | +| valueType | `'all' \| 'parent' \| 'child' \| 'path'` | value type, Only applies when checkStrictly is set to `false` | `'all'` | +| optionAsValue | boolean | use `option` as value, Only applies when valueType is not set to `'path'` | false | | showSearch | boolean | show `Search` component | false | | searchProps | object | `Search` component props | - | | primaryKey | `string \| (record) => string` | Row's unique key | `'key'` | diff --git a/packages/antd/docs/components/SelectTable.zh-CN.md b/packages/antd/docs/components/SelectTable.zh-CN.md index 150b396998a..15ba288a745 100644 --- a/packages/antd/docs/components/SelectTable.zh-CN.md +++ b/packages/antd/docs/components/SelectTable.zh-CN.md @@ -600,7 +600,8 @@ export default () => ( | 属性名 | 类型 | 描述 | 默认值 | | ------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------ | | mode | `'multiple' \| 'single'` | 设置 SelectTable 模式为单选或多选 | `'multiple'` | -| optionAsValue | boolean | 使用表格行数据作为值 | false | +| valueType | `'all' \| 'parent' \| 'child' \| 'path'` | 返回值类型,checkStrictly 设置为 `false` 时有效 | `'all'` | +| optionAsValue | boolean | 使用表格行数据作为值,valueType 值为 `'path'` 时无效 | false | | showSearch | boolean | 是否显示搜索组件 | false | | searchProps | object | Search 组件属性 | - | | primaryKey | `string \| (record) => string` | 表格行 key 的取值 | `'key'` | diff --git a/packages/antd/src/select-table/index.tsx b/packages/antd/src/select-table/index.tsx index 2e93d8142c1..c342f6b4bc9 100644 --- a/packages/antd/src/select-table/index.tsx +++ b/packages/antd/src/select-table/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { observer, useFieldSchema, useField, Schema } from '@formily/react' import cls from 'classnames' import { isArr, isBool, isFn } from '@formily/shared' @@ -8,6 +8,7 @@ import { SearchProps } from 'antd/lib/input' import { useFilterOptions } from './useFilterOptions' import { useFlatOptions } from './useFlatOptions' import { useSize } from './useSize' +import { getUISelected, getOutputData } from './utils' import { usePrefixCls } from '../__builtins__' const { Search } = Input @@ -24,6 +25,7 @@ export interface ISelectTableProps extends TableProps { mode?: 'multiple' | 'single' dataSource?: any[] optionAsValue?: boolean + valueType: 'all' | 'parent' | 'child' | 'path' showSearch?: boolean searchProps?: SearchProps primaryKey?: string | ((record: any) => string) @@ -79,6 +81,7 @@ export const SelectTable: ComposedSelectTable = observer((props) => { mode, dataSource: propsDataSource, optionAsValue, + valueType, showSearch, filterOption, filterSort, @@ -92,7 +95,6 @@ export const SelectTable: ComposedSelectTable = observer((props) => { ...otherTableProps } = props const prefixCls = usePrefixCls('formily-select-table', props) - const [selected, setSelected] = useState() const [searchValue, setSearchValue] = useState() const field = useField() as any const loading = isBool(props.loading) ? props.loading : field.loading @@ -105,12 +107,26 @@ export const SelectTable: ComposedSelectTable = observer((props) => { props?.size ) const primaryKey = isFn(rowKey) ? '__formily_key__' : rowKey + const columns = useColumns() + + // dataSource let dataSource = isArr(propsDataSource) ? propsDataSource : field.dataSource dataSource = isFn(rowKey) ? addPrimaryKey(dataSource, rowKey, primaryKey) : dataSource const flatDataSource = useFlatOptions(dataSource) - const columns = useColumns() + + // selected keys for Table UI + const selected = getUISelected( + value, + flatDataSource, + primaryKey, + valueType, + optionAsValue, + mode, + rowSelection?.checkStrictly, + rowKey + ) // Filter dataSource By Search const filteredDataSource = useFilterOptions( @@ -130,7 +146,7 @@ export const SelectTable: ComposedSelectTable = observer((props) => { // readPretty Value const readPrettyDataSource = useMemo( () => - orderedFilteredDataSource.filter((item) => + orderedFilteredDataSource?.filter((item) => selected?.includes(item?.[primaryKey]) ), [orderedFilteredDataSource, selected, primaryKey] @@ -146,16 +162,16 @@ export const SelectTable: ComposedSelectTable = observer((props) => { if (readOnly) { return } - let outputOptions = records.map((item) => { - const validItem = { ...item } - delete validItem['__formily_key__'] - return validItem - }) - let outputValue = optionAsValue ? outputOptions : selectedRowKeys - if (mode === 'single') { - outputValue = outputValue[0] - outputOptions = outputOptions[0] - } + const { outputValue, outputOptions } = getOutputData( + selectedRowKeys, + records, + dataSource, + primaryKey, + valueType, + optionAsValue, + mode, + rowSelection?.checkStrictly + ) onChange?.(outputValue, outputOptions) } @@ -186,16 +202,6 @@ export const SelectTable: ComposedSelectTable = observer((props) => { // Antd rowSelection type const modeAsType: any = { multiple: 'checkbox', single: 'radio' }?.[mode] - useEffect(() => { - let inputValue = mode === 'single' ? [value] : isArr(value) ? value : [] - inputValue = optionAsValue - ? inputValue.map((record: any) => - isFn(rowKey) ? rowKey(record) : record?.[primaryKey] - ) - : inputValue - setSelected(inputValue) - }, [value, mode, primaryKey, rowKey]) - return (
{showSearch && !readPretty ? ( diff --git a/packages/antd/src/select-table/useFlatOptions.tsx b/packages/antd/src/select-table/useFlatOptions.tsx index 67ac2587bec..01a938faa57 100644 --- a/packages/antd/src/select-table/useFlatOptions.tsx +++ b/packages/antd/src/select-table/useFlatOptions.tsx @@ -1,7 +1,7 @@ const useFlatOptions = (tree: any[]) => { const flatData = (data) => { let list = [] - data.forEach((item) => { + data?.forEach((item) => { list = [...list, item] if (item?.children?.length) { list = [...list, ...flatData(item.children)] diff --git a/packages/antd/src/select-table/utils.ts b/packages/antd/src/select-table/utils.ts new file mode 100644 index 00000000000..de0f11cfcaa --- /dev/null +++ b/packages/antd/src/select-table/utils.ts @@ -0,0 +1,263 @@ +import { isArr, isFn } from '@formily/shared' +import { useFlatOptions } from './useFlatOptions' + +/** + * 获取树列表某个键值的集合 + * @param tree 树列表 + * @param primaryKey 键名称 + * @returns 键值数组集合 + */ +const getTreeKeys = (tree: any[], primaryKey: string) => + isArr(tree) + ? tree.reduce( + (prev, current) => [ + ...prev, + current[primaryKey], + ...getTreeKeys(current?.children, primaryKey), + ], + [] + ) + : [] + +/** + * 判断树列表中是否有任一 key 被选中 + * @param tree 树列表 + * @param selected 已选中的 keys + * @param primaryKey 键名 + * @returns + */ +const hasSelectedKey = (tree: any[], selected: any[], primaryKey: string) => { + const keys = getTreeKeys(tree, primaryKey) + const mergedKeys = [...keys, ...selected] + const validKeys = [...new Set(mergedKeys)] + return validKeys.length !== mergedKeys.length +} + +/** + * 判断列表项是否全部被选中 + * @param list 一阶列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 是否全部被选中 + */ +const isAllSelected = (list: any[], selected: any[], primaryKey: string) => { + const selectedList = list.filter((item) => + selected?.includes(item[primaryKey]) + ) + return selectedList.length === list.length +} + +/** + * 完善TableUI Keys(添加选中所有子元素的父元素,或移除未选中所有子元素的父元素) + * @param flatDataSource 完整数据平铺列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 完整的字段值集合 + */ +const completedKeys = ( + flatDataSource: any[] = [], + selected: any[], + primaryKey: string +) => { + let allSelectedKeys = [...selected] + flatDataSource.forEach((item) => { + if (item.children?.length) { + // 优先递归子元素 + allSelectedKeys = completedKeys( + item.children, + allSelectedKeys, + primaryKey + ) + if (isAllSelected(item.children, allSelectedKeys, primaryKey)) { + // 如果该元素的子元素全部选中,则也选中该项(即包含全选子元素的父元素) + allSelectedKeys = [...new Set([...allSelectedKeys, item[primaryKey]])] + } else { + // 如果该元素的子元素未全部选中,则移除该项 + allSelectedKeys = allSelectedKeys.filter( + (key) => key !== item[primaryKey] + ) + } + } + }) + return allSelectedKeys +} + +/** + * 获取数列表中被选中的有效路径 + * @param tree 数列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 有效的树路径 + */ +const getSelectedPath = (tree = [], selected, primaryKey) => { + const pathData = [] + + tree.forEach((item) => { + const validChildren = getSelectedPath(item.children, selected, primaryKey) + if (validChildren.length || selected?.includes(item[primaryKey])) { + pathData.push({ + ...item, + ...(validChildren.length ? { children: validChildren } : {}), + }) + } + }) + + return pathData +} + +/** + * 删除树列表的某个 key/value 键值对 + * @param tree + * @param key + * @returns + */ +const deleteTreeItem = (tree: any[], key: string) => + tree.map((item) => { + const validItem = { ...item } + delete validItem[key] + if (validItem.children?.length) { + validItem.children = deleteTreeItem(validItem.children, key) + } + return validItem + }) + +/** + * 根据 valueType 获取最终输出值 + * @param keys 当前选中的 key 集合(all完整类型) + * @param records 当前选中的 option 集合 + * @param dataSource 数据源集合 + * @param primaryKey 键名 + * @param originalValueType 值输出类型 + * @param originalOptionAsValue + * @param mode + * @param checkStrictly + * @returns 最终输出的 keys 和 options + */ +const getOutputData = ( + keys, + options, + dataSource, + primaryKey, + originalValueType, + originalOptionAsValue, + mode, + checkStrictly +) => { + const valueType = checkStrictly !== false ? 'all' : originalValueType // valueType 在 Strictly 为 false 时生效 + const optionAsValue = valueType === 'path' ? false : originalOptionAsValue // optionAsValue 在 path 模式不生效 + let outputValue = [] + let outputOptions = [] + + if (valueType === 'parent') { + // 移除所有选中值的子值 + let childrenKeys = [] + options.forEach((option) => { + childrenKeys = [ + ...childrenKeys, + ...getTreeKeys(option.children, primaryKey), + ] + }) + outputValue = keys.filter((key) => !childrenKeys.includes(key)) + outputOptions = options.filter((options) => + outputValue.includes(options[primaryKey]) + ) + } else if (valueType === 'child') { + outputValue = [...keys] + outputOptions = [...options] + outputOptions.forEach((option) => { + // 移除当前有子值被选中的父值 + if (hasSelectedKey(option.children, keys, primaryKey)) { + outputValue = outputValue.filter((key) => key !== option[primaryKey]) + outputOptions = outputOptions.filter( + (options) => options[primaryKey] !== option[primaryKey] + ) + } + }) + } else if (valueType === 'path') { + outputValue = getSelectedPath(dataSource, keys, primaryKey) + outputOptions = options + } else { + // valueType === 'all' + outputValue = [...keys] + outputOptions = [...options] + } + + outputOptions = deleteTreeItem(outputOptions, '__formily_key__') + outputValue = + optionAsValue && valueType !== 'path' ? outputOptions : outputValue + if (mode === 'single') { + outputValue = outputValue[0] + outputOptions = outputOptions[0] + } + + return { outputValue, outputOptions } +} + +/** + * 根据 valueType 获取 TableUI 显示值 + * @param keys 回填的数据(输出的)keys 集合 + * @param flatDataSource 平铺的数据源集合 + * @param primaryKey 键名称 + * @param originalValueType 值输出类型 + * @param originalOptionAsValue + * @param mode + * @param checkStrictly + * @param rowKey + * @returns [] TableUI keys 集合 + */ +const getUISelected = ( + value, + flatDataSource, + primaryKey, + originalValueType, + originalOptionAsValue, + mode, + checkStrictly, + rowKey +) => { + const valueType = checkStrictly !== false ? 'all' : originalValueType // valueType 在 Strictly 为 false 时生效 + const optionAsValue = valueType === 'path' ? false : originalOptionAsValue // optionAsValue 在 path 模式不生效 + + let keys = mode === 'single' ? [value] : isArr(value) ? value : [] + keys = + optionAsValue && valueType !== 'path' + ? keys.map((record: any) => + isFn(rowKey) ? rowKey(record) : record?.[primaryKey] + ) + : keys + + let newKeys = [] + if (valueType === 'parent') { + const options = flatDataSource.filter((item) => + keys.includes(item[primaryKey]) + ) + let childrenKeys = [] + options.forEach((option) => { + childrenKeys = [ + ...childrenKeys, + ...getTreeKeys(option.children, primaryKey), + ] + }) + newKeys = [...new Set([...keys, ...childrenKeys])] + } else if (valueType === 'child') { + newKeys = completedKeys(flatDataSource, keys, primaryKey) + } else if (valueType === 'path') { + const pathKeys = useFlatOptions(keys).map((item) => item[primaryKey]) + newKeys = completedKeys(flatDataSource, pathKeys, primaryKey) + } else { + // valueType === 'all' + newKeys = [...keys] + } + + return newKeys +} + +export { + hasSelectedKey, + getTreeKeys, + deleteTreeItem, + isAllSelected, + getUISelected, + getOutputData, + completedKeys, +} diff --git a/packages/next/docs/components/SelectTable.md b/packages/next/docs/components/SelectTable.md index 1f7d1b0ccf0..428bfef9744 100644 --- a/packages/next/docs/components/SelectTable.md +++ b/packages/next/docs/components/SelectTable.md @@ -609,7 +609,8 @@ export default () => ( | Property name | Type | Description | Default value | | ------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | | mode | `'multiple' \| 'single'` | Set mode of SelectTable | `'multiple'` | -| optionAsValue | boolean | use `option` as value | false | +| valueType | `'all' \| 'parent' \| 'child' \| 'path'` | value type, Only applies when checkStrictly is set to `false` | `'all'` | +| optionAsValue | boolean | use `option` as value, Only applies when valueType is not set to `'path'` | false | | showSearch | boolean | show `Search` component | false | | searchProps | object | `Search` component props | - | | primaryKey | `string \| (record) => string` | Row's unique key | `'key'` | diff --git a/packages/next/docs/components/SelectTable.zh-CN.md b/packages/next/docs/components/SelectTable.zh-CN.md index 3cbcd16c4ce..b8586f79117 100644 --- a/packages/next/docs/components/SelectTable.zh-CN.md +++ b/packages/next/docs/components/SelectTable.zh-CN.md @@ -605,7 +605,8 @@ export default () => ( | 属性名 | 类型 | 描述 | 默认值 | | ------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | ------------ | | mode | `'multiple' \| 'single'` | 设置 SelectTable 模式为单选或多选 | `'multiple'` | -| optionAsValue | boolean | 使用表格行数据作为值 | false | +| valueType | `'all' \| 'parent' \| 'child' \| 'path'` | 返回值类型,checkStrictly 设置为 `false` 时有效 | `'all'` | +| optionAsValue | boolean | 使用表格行数据作为值,valueType 值为 `'path'` 时无效 | false | | showSearch | boolean | 是否显示搜索组件 | false | | searchProps | object | Search 组件属性 | - | | primaryKey | `string \| (record) => string` | 表格行 key 的取值 | `'key'` | diff --git a/packages/next/src/select-table/index.tsx b/packages/next/src/select-table/index.tsx index 1362c698a8e..565dab4858c 100644 --- a/packages/next/src/select-table/index.tsx +++ b/packages/next/src/select-table/index.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState, useMemo } from 'react' +import React, { useState, useMemo } from 'react' import { observer, useFieldSchema, useField, Schema } from '@formily/react' import cls from 'classnames' import { isArr, isBool, isFn } from '@formily/shared' @@ -7,9 +7,10 @@ import { TableProps, ColumnProps } from '@alifd/next/types/table' import { SearchProps } from '@alifd/next/types/search' import { useFilterOptions } from './useFilterOptions' import { useFlatOptions } from './useFlatOptions' -import { useTitleAddon } from './useTitleAddon' import { useSize } from './useSize' -import { useCheckSlackly, getCheckedProps } from './useCheckSlackly' +import { useTitleAddon } from './useTitleAddon' +import { useCheckSlackly, getIndeterminate } from './useCheckSlackly' +import { getUISelected, getOutputData } from './utils' import { usePrefixCls } from '../__builtins__' type IFilterOption = boolean | ((option: any, keyword: string) => boolean) @@ -25,6 +26,7 @@ export interface ISelectTableProps mode?: 'multiple' | 'single' dataSource?: any[] optionAsValue?: boolean + valueType: 'all' | 'parent' | 'child' | 'path' showSearch?: boolean searchProps?: SearchProps primaryKey?: string | ((record: any) => string) @@ -83,6 +85,7 @@ export const SelectTable: ComposedSelectTable = observer((props) => { mode, dataSource: propsDataSource, optionAsValue, + valueType, showSearch, filterOption, filterSort, @@ -96,7 +99,6 @@ export const SelectTable: ComposedSelectTable = observer((props) => { ...otherTableProps } = props const prefixCls = usePrefixCls('formily-select-table', props) - const [selected, setSelected] = useState() const [searchValue, setSearchValue] = useState() const field = useField() as any const loading = isBool(props.loading) ? props.loading : field.loading @@ -109,12 +111,26 @@ export const SelectTable: ComposedSelectTable = observer((props) => { props?.size ) const primaryKey = isFn(rowKey) ? '__formily_key__' : rowKey + const columns = useColumns() + + // dataSource let dataSource = isArr(propsDataSource) ? propsDataSource : field.dataSource dataSource = isFn(rowKey) ? addPrimaryKey(dataSource, rowKey, primaryKey) : dataSource const flatDataSource = useFlatOptions(dataSource) - const columns = useColumns() + + // selected keys for Table UI + const selected = getUISelected( + value, + flatDataSource, + primaryKey, + valueType, + optionAsValue, + mode, + rowSelection?.checkStrictly, + rowKey + ) // Filter dataSource By Search const filteredDataSource = useFilterOptions( @@ -134,7 +150,7 @@ export const SelectTable: ComposedSelectTable = observer((props) => { // readPretty Value const readPrettyDataSource = useMemo( () => - orderedFilteredDataSource.filter((item) => + orderedFilteredDataSource?.filter((item) => selected?.includes(item?.[primaryKey]) ), [orderedFilteredDataSource, selected, primaryKey] @@ -150,16 +166,16 @@ export const SelectTable: ComposedSelectTable = observer((props) => { if (readOnly) { return } - let outputOptions = records.map((item) => { - const validItem = { ...item } - delete validItem['__formily_key__'] - return validItem - }) - let outputValue = optionAsValue ? outputOptions : selectedRowKeys - if (mode === 'single') { - outputValue = outputValue[0] - outputOptions = outputOptions[0] - } + const { outputValue, outputOptions } = getOutputData( + selectedRowKeys, + records, + dataSource, + primaryKey, + valueType, + optionAsValue, + mode, + rowSelection?.checkStrictly + ) onChange?.(outputValue, outputOptions) } @@ -187,19 +203,19 @@ export const SelectTable: ComposedSelectTable = observer((props) => { if (rowSelection?.checkStrictly !== false) { onInnerChange(selectedRowKeys, records) } else { + // fusion onSlacklyChange(selectedRowKeys) } } // Fusion TreeData SlacklyChange - const onSlacklyChange = (prevSelectedRowKeys: any[]) => { - const { selectedRowKeys, records } = useCheckSlackly( - prevSelectedRowKeys, + const onSlacklyChange = (currentSelected: any[]) => { + let { selectedRowKeys, records } = useCheckSlackly( + currentSelected, selected, primaryKey, flatDataSource ) - onInnerChange(selectedRowKeys, records) } @@ -214,16 +230,6 @@ export const SelectTable: ComposedSelectTable = observer((props) => { onInnerChange ) - useEffect(() => { - let inputValue = mode === 'single' ? [value] : isArr(value) ? value : [] - inputValue = optionAsValue - ? inputValue.map((record: any) => - isFn(rowKey) ? rowKey(record) : record?.[primaryKey] - ) - : inputValue - setSelected(inputValue) - }, [value, mode, primaryKey, rowKey]) - return (
{showSearch && !readPretty ? ( @@ -252,9 +258,7 @@ export const SelectTable: ComposedSelectTable = observer((props) => { ...rowSelection, getProps: (record, index) => ({ ...(rowSelection?.getProps?.(record, index) as any), - ...(rowSelection?.checkStrictly !== false - ? {} - : getCheckedProps(record, primaryKey, selected)), // 父子关联模式indeterminate值 + indeterminate: getIndeterminate(record, selected, primaryKey), // 父子关联模式indeterminate值 disabled, }), // fusion selectedRowKeys: selected, @@ -288,6 +292,7 @@ SelectTable.Column = TableColumn SelectTable.defaultProps = { showSearch: false, + valueType: 'all', primaryKey: 'key', mode: 'multiple', } diff --git a/packages/next/src/select-table/useCheckSlackly.tsx b/packages/next/src/select-table/useCheckSlackly.tsx index b74301accc4..195387826a5 100644 --- a/packages/next/src/select-table/useCheckSlackly.tsx +++ b/packages/next/src/select-table/useCheckSlackly.tsx @@ -1,91 +1,23 @@ -import { isArr } from '@formily/shared' +import { getTreeKeys, hasSelectedKey, completedKeys } from './utils' /** - * 获取该字段Checkbox的indeterminate属性 - * @param record 字段项 + * 判断该字段的 indeterminate 属性 + * @param record 当前字段 + * @param selected 已选中的字段值集合 * @param primaryKey 键名称 - * @param selected 当前选中的字段值集合 - * @returns indeterminate属性值 + * @returns indeterminate 属性值 */ -const getCheckedProps = (record: any, primaryKey: string, selected: any[]) => { - if (record.children?.length) { - const childrenDataSource = record.children - const selectedChildren = childrenDataSource.filter((item) => - selected?.includes(item[primaryKey]) - ) - return { - // checked受控,此处配置在rowSelection并不会生效,供getFinalTreeKeys使用 - checked: selectedChildren.length === childrenDataSource.length, - indeterminate: !!( - selectedChildren.length && - selectedChildren.length !== childrenDataSource.length - ), - } +const getIndeterminate = (record: any, selected: any[], primaryKey: string) => { + if (selected?.includes(record[primaryKey])) { + return undefined } - return {} -} - -/** - * 获取树列表某个键值的集合 - * @param tree 树列表 - * @param primaryKey 键名称 - * @returns 键值数组集合 - */ -const getTreeKeys = (tree: any[], primaryKey: string) => - isArr(tree) - ? tree.reduce( - (prev, current) => [ - ...prev, - current[primaryKey], - ...getTreeKeys(current?.children, primaryKey), - ], - [] - ) - : [] - -/** - * 获取最终选中值(添加选中所有子元素的父元素,或移除未选中所有子元素的父元素) - * @param tree 树列表 - * @param primaryKey 键名称 - * @param selectedRowKeys 当前选中的字段值集合 - * @returns 最终选中的字段值集合 - */ -const getFinalTreeKeys = ( - tree: any[] = [], - primaryKey: string, - selectedRowKeys: any[] -) => { - let finalSelectedRowKeys = [...selectedRowKeys] - - tree.forEach((item) => { - if (item.children?.length) { - // 优先递归子元素 - finalSelectedRowKeys = getFinalTreeKeys( - item.children, - primaryKey, - finalSelectedRowKeys - ) - if (getCheckedProps(item, primaryKey, finalSelectedRowKeys)?.checked) { - // 如果该元素的子元素全部选中,则也选中该项(即包含全选子元素的父元素) - finalSelectedRowKeys = [ - ...new Set([...finalSelectedRowKeys, item[primaryKey]]), - ] - } else { - // 如果该元素的子元素未全部选中,则移除该项 - finalSelectedRowKeys = finalSelectedRowKeys.filter( - (key) => key !== item[primaryKey] - ) - } - } - }) - - return finalSelectedRowKeys + return hasSelectedKey(record.children, selected, primaryKey) || undefined } interface ICheckSlackly { ( - selectedRowKeys: any[], - selected: any[], + currentSelected: any[], + allSelected: any[], primaryKey: string, flatDataSource: any[] ): { @@ -95,33 +27,34 @@ interface ICheckSlackly { } const useCheckSlackly: ICheckSlackly = ( - selectedRowKeys, - selected, + currentSelected, // onChange 返回的 keys + allSelected, // Table UI 展示的 keys primaryKey, flatDataSource ) => { - const isSelected = selectedRowKeys.length > selected.length - const currentKey = [...selectedRowKeys, ...selected].find( - (key) => !(selectedRowKeys.includes(key) && selected.includes(key)) + const isSelected = currentSelected.length > allSelected.length // 判断是选中还是取消 + const currentKey = [...currentSelected, ...allSelected].find( + (key) => !(currentSelected.includes(key) && allSelected.includes(key)) // 当前变化key不同时存在于两个selected ) const currentRecords = flatDataSource.find( (item) => item[primaryKey] === currentKey ) const currentTreeKeys = getTreeKeys([currentRecords], primaryKey) let newSelectedRowKeys = [] - if (isSelected) { - newSelectedRowKeys = [...new Set([...selected, ...currentTreeKeys])] + // 选中当前key及其子keys + newSelectedRowKeys = [...new Set([...allSelected, ...currentTreeKeys])] } else { - newSelectedRowKeys = selected.filter( + // 移除当前key及其子keys + newSelectedRowKeys = allSelected.filter( (key) => !currentTreeKeys.includes(key) ) } - // 添加选中所有子元素的父元素,或移除未选中所有子元素的父元素 - newSelectedRowKeys = getFinalTreeKeys( + + newSelectedRowKeys = completedKeys( flatDataSource, - primaryKey, - newSelectedRowKeys + newSelectedRowKeys, + primaryKey ) return { @@ -132,4 +65,4 @@ const useCheckSlackly: ICheckSlackly = ( } } -export { useCheckSlackly, getCheckedProps } +export { useCheckSlackly, getIndeterminate } diff --git a/packages/next/src/select-table/useFlatOptions.tsx b/packages/next/src/select-table/useFlatOptions.tsx index 67ac2587bec..01a938faa57 100644 --- a/packages/next/src/select-table/useFlatOptions.tsx +++ b/packages/next/src/select-table/useFlatOptions.tsx @@ -1,7 +1,7 @@ const useFlatOptions = (tree: any[]) => { const flatData = (data) => { let list = [] - data.forEach((item) => { + data?.forEach((item) => { list = [...list, item] if (item?.children?.length) { list = [...list, ...flatData(item.children)] diff --git a/packages/next/src/select-table/utils.ts b/packages/next/src/select-table/utils.ts new file mode 100644 index 00000000000..de0f11cfcaa --- /dev/null +++ b/packages/next/src/select-table/utils.ts @@ -0,0 +1,263 @@ +import { isArr, isFn } from '@formily/shared' +import { useFlatOptions } from './useFlatOptions' + +/** + * 获取树列表某个键值的集合 + * @param tree 树列表 + * @param primaryKey 键名称 + * @returns 键值数组集合 + */ +const getTreeKeys = (tree: any[], primaryKey: string) => + isArr(tree) + ? tree.reduce( + (prev, current) => [ + ...prev, + current[primaryKey], + ...getTreeKeys(current?.children, primaryKey), + ], + [] + ) + : [] + +/** + * 判断树列表中是否有任一 key 被选中 + * @param tree 树列表 + * @param selected 已选中的 keys + * @param primaryKey 键名 + * @returns + */ +const hasSelectedKey = (tree: any[], selected: any[], primaryKey: string) => { + const keys = getTreeKeys(tree, primaryKey) + const mergedKeys = [...keys, ...selected] + const validKeys = [...new Set(mergedKeys)] + return validKeys.length !== mergedKeys.length +} + +/** + * 判断列表项是否全部被选中 + * @param list 一阶列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 是否全部被选中 + */ +const isAllSelected = (list: any[], selected: any[], primaryKey: string) => { + const selectedList = list.filter((item) => + selected?.includes(item[primaryKey]) + ) + return selectedList.length === list.length +} + +/** + * 完善TableUI Keys(添加选中所有子元素的父元素,或移除未选中所有子元素的父元素) + * @param flatDataSource 完整数据平铺列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 完整的字段值集合 + */ +const completedKeys = ( + flatDataSource: any[] = [], + selected: any[], + primaryKey: string +) => { + let allSelectedKeys = [...selected] + flatDataSource.forEach((item) => { + if (item.children?.length) { + // 优先递归子元素 + allSelectedKeys = completedKeys( + item.children, + allSelectedKeys, + primaryKey + ) + if (isAllSelected(item.children, allSelectedKeys, primaryKey)) { + // 如果该元素的子元素全部选中,则也选中该项(即包含全选子元素的父元素) + allSelectedKeys = [...new Set([...allSelectedKeys, item[primaryKey]])] + } else { + // 如果该元素的子元素未全部选中,则移除该项 + allSelectedKeys = allSelectedKeys.filter( + (key) => key !== item[primaryKey] + ) + } + } + }) + return allSelectedKeys +} + +/** + * 获取数列表中被选中的有效路径 + * @param tree 数列表 + * @param selected 当前选中的字段值集合 + * @param primaryKey 键名称 + * @returns 有效的树路径 + */ +const getSelectedPath = (tree = [], selected, primaryKey) => { + const pathData = [] + + tree.forEach((item) => { + const validChildren = getSelectedPath(item.children, selected, primaryKey) + if (validChildren.length || selected?.includes(item[primaryKey])) { + pathData.push({ + ...item, + ...(validChildren.length ? { children: validChildren } : {}), + }) + } + }) + + return pathData +} + +/** + * 删除树列表的某个 key/value 键值对 + * @param tree + * @param key + * @returns + */ +const deleteTreeItem = (tree: any[], key: string) => + tree.map((item) => { + const validItem = { ...item } + delete validItem[key] + if (validItem.children?.length) { + validItem.children = deleteTreeItem(validItem.children, key) + } + return validItem + }) + +/** + * 根据 valueType 获取最终输出值 + * @param keys 当前选中的 key 集合(all完整类型) + * @param records 当前选中的 option 集合 + * @param dataSource 数据源集合 + * @param primaryKey 键名 + * @param originalValueType 值输出类型 + * @param originalOptionAsValue + * @param mode + * @param checkStrictly + * @returns 最终输出的 keys 和 options + */ +const getOutputData = ( + keys, + options, + dataSource, + primaryKey, + originalValueType, + originalOptionAsValue, + mode, + checkStrictly +) => { + const valueType = checkStrictly !== false ? 'all' : originalValueType // valueType 在 Strictly 为 false 时生效 + const optionAsValue = valueType === 'path' ? false : originalOptionAsValue // optionAsValue 在 path 模式不生效 + let outputValue = [] + let outputOptions = [] + + if (valueType === 'parent') { + // 移除所有选中值的子值 + let childrenKeys = [] + options.forEach((option) => { + childrenKeys = [ + ...childrenKeys, + ...getTreeKeys(option.children, primaryKey), + ] + }) + outputValue = keys.filter((key) => !childrenKeys.includes(key)) + outputOptions = options.filter((options) => + outputValue.includes(options[primaryKey]) + ) + } else if (valueType === 'child') { + outputValue = [...keys] + outputOptions = [...options] + outputOptions.forEach((option) => { + // 移除当前有子值被选中的父值 + if (hasSelectedKey(option.children, keys, primaryKey)) { + outputValue = outputValue.filter((key) => key !== option[primaryKey]) + outputOptions = outputOptions.filter( + (options) => options[primaryKey] !== option[primaryKey] + ) + } + }) + } else if (valueType === 'path') { + outputValue = getSelectedPath(dataSource, keys, primaryKey) + outputOptions = options + } else { + // valueType === 'all' + outputValue = [...keys] + outputOptions = [...options] + } + + outputOptions = deleteTreeItem(outputOptions, '__formily_key__') + outputValue = + optionAsValue && valueType !== 'path' ? outputOptions : outputValue + if (mode === 'single') { + outputValue = outputValue[0] + outputOptions = outputOptions[0] + } + + return { outputValue, outputOptions } +} + +/** + * 根据 valueType 获取 TableUI 显示值 + * @param keys 回填的数据(输出的)keys 集合 + * @param flatDataSource 平铺的数据源集合 + * @param primaryKey 键名称 + * @param originalValueType 值输出类型 + * @param originalOptionAsValue + * @param mode + * @param checkStrictly + * @param rowKey + * @returns [] TableUI keys 集合 + */ +const getUISelected = ( + value, + flatDataSource, + primaryKey, + originalValueType, + originalOptionAsValue, + mode, + checkStrictly, + rowKey +) => { + const valueType = checkStrictly !== false ? 'all' : originalValueType // valueType 在 Strictly 为 false 时生效 + const optionAsValue = valueType === 'path' ? false : originalOptionAsValue // optionAsValue 在 path 模式不生效 + + let keys = mode === 'single' ? [value] : isArr(value) ? value : [] + keys = + optionAsValue && valueType !== 'path' + ? keys.map((record: any) => + isFn(rowKey) ? rowKey(record) : record?.[primaryKey] + ) + : keys + + let newKeys = [] + if (valueType === 'parent') { + const options = flatDataSource.filter((item) => + keys.includes(item[primaryKey]) + ) + let childrenKeys = [] + options.forEach((option) => { + childrenKeys = [ + ...childrenKeys, + ...getTreeKeys(option.children, primaryKey), + ] + }) + newKeys = [...new Set([...keys, ...childrenKeys])] + } else if (valueType === 'child') { + newKeys = completedKeys(flatDataSource, keys, primaryKey) + } else if (valueType === 'path') { + const pathKeys = useFlatOptions(keys).map((item) => item[primaryKey]) + newKeys = completedKeys(flatDataSource, pathKeys, primaryKey) + } else { + // valueType === 'all' + newKeys = [...keys] + } + + return newKeys +} + +export { + hasSelectedKey, + getTreeKeys, + deleteTreeItem, + isAllSelected, + getUISelected, + getOutputData, + completedKeys, +}