diff --git a/packages/core/client/src/locale/zh-CN.json b/packages/core/client/src/locale/zh-CN.json index e6f32bbc2..8ec14eaf4 100644 --- a/packages/core/client/src/locale/zh-CN.json +++ b/packages/core/client/src/locale/zh-CN.json @@ -949,5 +949,6 @@ "AssociationCascader":"关系型级联选择", "Edit Collection":"修改数据", "Field Component":"字段组件", - "Custom filter":"自定义筛选" + "Custom filter":"自定义筛选", + "Parent Association field":"父级关联字段" } diff --git a/packages/core/client/src/schema-component/antd/association-cascader/AssociationCascader.tsx b/packages/core/client/src/schema-component/antd/association-cascader/AssociationCascader.tsx new file mode 100644 index 000000000..13695bbd4 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-cascader/AssociationCascader.tsx @@ -0,0 +1,136 @@ +import React, { useMemo } from 'react'; +import { useCollectionManager, useRequest } from '@tachybase/client'; +import { connect, mapProps } from '@tachybase/schema'; + +import { Cascader } from 'antd'; +import _ from 'lodash'; + +export const AssociationCascader = connect( + (props) => { + const { fieldNames, collection, associationField } = props; + const cm = useCollectionManager(); + const titleField = cm.getCollection(collection).titleField; + const joinTitleField = cm.getCollection(collection + '.' + associationField).titleField; + + const params = { + appends: [associationField], + fields: [titleField], + pageSize: 99999, + filter: props?.params?.filter ? { ...props?.params?.filter } : {}, + }; + + const { data } = useRequest({ + resource: collection, + action: 'list', + params, + }); + + const options = useMemo(() => { + if (props.chartCascader) { + const dataOptions = {}; + const options = []; + data?.data.forEach((item) => { + if (item?.[associationField]) { + const field = []; + if (Array.isArray(item[associationField]) && item[associationField].length) { + field.push(...item[associationField]); + } else { + field.push({ ...item[associationField] }); + } + field.forEach((fieldItem) => { + if (!Object.keys(fieldItem).length) return; + if (dataOptions[fieldItem[joinTitleField]]) { + dataOptions[fieldItem[joinTitleField]].push({ + label: item[titleField], + value: item[titleField], + }); + } else { + dataOptions[fieldItem[joinTitleField]] = [ + { + label: item[titleField], + value: item[titleField], + }, + ]; + } + }); + } + }); + for (const key in dataOptions) { + options.push({ + label: key, + value: key, + children: [...dataOptions[key]], + }); + } + return options; + } else { + const dict: { [key: string]: string[] } = + data?.data.reduce((acc, item) => { + if (item[associationField]) { + const key = item[associationField][joinTitleField]; + acc[key] ??= []; + acc[key].push(item[titleField]); + } + return acc; + }, {}) || {}; + const options = Object.entries(dict).map(([key, values]) => ({ + [fieldNames.label]: key, + [fieldNames.value]: key, + children: values.map((value) => ({ [fieldNames.label]: value, [fieldNames.value]: value })), + })); + return options; + } + }, [associationField, joinTitleField, titleField, data?.data]); + + return ( + + ); + }, + mapProps((props) => { + return { ...props }; + }), +); + +export const SingleValueCascader = (props) => { + const { value, options, onChange, fieldNames, chartCascader, titleField, joinTitleField } = props; + const fieldLabel = fieldNames ? fieldNames.value : titleField; + const joinFieldLabel = fieldNames ? fieldNames.value : 'value'; + const arrayValue = value && options ? [] : undefined; + if (arrayValue) { + for (const option of options) { + if (option[fieldLabel] === value) { + arrayValue.push(option[fieldLabel]); + break; + } + for (const subOption of option.children) { + if (subOption[joinFieldLabel] === value) { + arrayValue.push(option[joinFieldLabel]); + arrayValue.push(subOption[joinFieldLabel]); + break; + } + } + } + } + const newProps = { + ...props, + value: arrayValue, + onChange: (v) => { + if (v) { + onChange(v[v.length - 1]); + } else { + onChange(v); + } + }, + }; + return ; +}; + +SingleValueCascader.displayName = 'SingleValueCascader'; +AssociationCascader.displayName = 'AssociationCascader'; +export default AssociationCascader; diff --git a/packages/core/client/src/schema-component/antd/association-cascader/index.ts b/packages/core/client/src/schema-component/antd/association-cascader/index.ts new file mode 100644 index 000000000..9a5834683 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-cascader/index.ts @@ -0,0 +1 @@ +export * from './AssociationCascader'; diff --git a/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx b/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx index 5c3f6eadf..216cd8d16 100644 --- a/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx +++ b/packages/core/client/src/schema-component/antd/auto-complete/AutoComplete.tsx @@ -1,11 +1,68 @@ -import { connect, mapProps, mapReadPretty } from '@tachybase/schema'; +import React, { useState } from 'react'; +import { connect, mapProps, mapReadPretty, useFieldSchema } from '@tachybase/schema'; +import { fuzzysearch } from '@tachybase/utils/client'; +import { useAsyncEffect } from 'ahooks'; import { AutoComplete as AntdAutoComplete } from 'antd'; +import { useAPIClient } from '../../../api-client'; import { ReadPretty } from '../input'; export const AutoComplete = connect( - AntdAutoComplete, + (props) => { + const { fieldNames, params: fieldFilter } = props; + const fieldSchema = useFieldSchema(); + const targetKey = fieldNames?.value; + const api = useAPIClient(); + const [defultOptions, setDefultOptions] = useState([]); + const [options, setOptions] = useState([]); + + const getNoDuplicateTextOptions = (rawOptions) => { + const targetOptions = rawOptions.filter((option, index, self) => { + return self.findIndex((item) => item[targetKey] === option[targetKey]) === index; + }); + return targetOptions; + }; + + useAsyncEffect(async () => { + const { + data: { data: rawOptions }, + } = await api.request({ + url: fieldSchema['collectionName'] + ':list', + params: { + pageSize: 99999, + filter: fieldFilter ? { ...fieldFilter.filter } : {}, + }, + }); + const targetOptions = getNoDuplicateTextOptions(rawOptions); + setDefultOptions(targetOptions); + setOptions(targetOptions); + }, [fieldFilter?.filter, fieldSchema['collectionName']]); + + const onSearch = (value) => { + // 在筛选项开头跟踪用户的原始输入值 + if (value) { + setOptions([ + { + [targetKey]: value, + }, + ...defultOptions, + ]); + } else { + setOptions(defultOptions); + } + }; + + return ( + fuzzysearch(inputValue || [], option[targetKey]?.toString() || '')} + allowClear + onSearch={onSearch} + /> + ); + }, mapProps({ dataSource: 'options', }), diff --git a/packages/core/client/src/schema-component/antd/index.ts b/packages/core/client/src/schema-component/antd/index.ts index 3df3b5471..6967aefdf 100644 --- a/packages/core/client/src/schema-component/antd/index.ts +++ b/packages/core/client/src/schema-component/antd/index.ts @@ -6,6 +6,7 @@ export * from './action'; export * from './appends-tree-select'; export * from './association-field'; export * from './association-select'; +export * from './association-cascader'; export * from './auto-complete'; export * from './block-item'; export * from './card-item'; diff --git a/packages/core/client/src/schema-initializer/items/CustomFilterFormItemInitializer.tsx b/packages/core/client/src/schema-initializer/items/CustomFilterFormItemInitializer.tsx index 7bbfc7db4..38e0e0b8e 100644 --- a/packages/core/client/src/schema-initializer/items/CustomFilterFormItemInitializer.tsx +++ b/packages/core/client/src/schema-initializer/items/CustomFilterFormItemInitializer.tsx @@ -226,7 +226,7 @@ export const FilterCustomItemInitializer: React.FC<{ }, associationField: { type: 'string', - title: t('Association field'), + title: t('Parent Association field'), 'x-decorator': 'FormItem', 'x-component': 'Select', 'x-visible': false, diff --git a/packages/core/utils/src/common.ts b/packages/core/utils/src/common.ts index 650c6077a..716599e48 100644 --- a/packages/core/utils/src/common.ts +++ b/packages/core/utils/src/common.ts @@ -47,10 +47,6 @@ export const nextTick = (fn: () => void) => { }; export function fuzzysearch(needle: string, haystack: string): boolean { - if (!needle || !haystack) { - return false; - } - const hlen = haystack.length; const nlen = needle.length; diff --git a/packages/plugins/@hera/plugin-core/src/client/index.tsx b/packages/plugins/@hera/plugin-core/src/client/index.tsx index 47fd0a317..329c5f2cd 100644 --- a/packages/plugins/@hera/plugin-core/src/client/index.tsx +++ b/packages/plugins/@hera/plugin-core/src/client/index.tsx @@ -39,8 +39,6 @@ import { import { TstzrangeFieldInterface } from './interfaces/TstzrangeFieldInterface'; import { Locale, tval } from './locale'; import { AdminLayout, DetailsPage, PageLayout } from './pages'; -import { AutoComplete } from './schema-components'; -import AssociationCascader from './schema-components/association-cascader/AssociationCascader'; import { CreateSubmitActionInitializer, SettingBlockInitializer } from './schema-initializer'; import { useCreateActionProps } from './schema-initializer/actions/hooks/useCreateActionProps'; import { EditTitle, IsTablePageSize, PageModeSetting, usePaginationVisible } from './schema-settings'; @@ -122,9 +120,7 @@ export class PluginCoreClient extends Plugin { this.app.addComponents({ AdminLayout, - AssociationCascader, AssociatedField, - AutoComplete, CalcResult, CodeMirror, CreateSubmitActionInitializer, diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-components/association-cascader/AssociationCascader.tsx b/packages/plugins/@hera/plugin-core/src/client/schema-components/association-cascader/AssociationCascader.tsx deleted file mode 100644 index 8de3a0699..000000000 --- a/packages/plugins/@hera/plugin-core/src/client/schema-components/association-cascader/AssociationCascader.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import React, { useMemo } from 'react'; -import { useCollectionManager, useRequest } from '@tachybase/client'; -import { connect } from '@tachybase/schema'; - -import { Cascader } from 'antd'; -import _ from 'lodash'; - -const AssociationCascader = connect((props) => { - const { fieldNames, collection, associationField } = props; - const cm = useCollectionManager(); - const titleField = cm.getCollection(collection).titleField; - const joinTitleField = cm.getCollection(collection + '.' + associationField).titleField; - - const params = { - appends: [associationField], - fields: [titleField], - pageSize: 99999, - filter: props?.params?.filter ? { ...props?.params?.filter } : {}, - }; - - const { data } = useRequest({ - resource: collection, - action: 'list', - params, - }); - - const options = useMemo(() => { - if (props.chartCascader) { - const dataOptions = {}; - const options = []; - data?.data.forEach((item) => { - if (item?.[associationField]) { - const field = []; - if (Array.isArray(item[associationField]) && item[associationField].length) { - field.push(...item[associationField]); - } else { - field.push({ ...item[associationField] }); - } - field.forEach((fieldItem) => { - if (!Object.keys(fieldItem).length) return; - if (dataOptions[fieldItem[joinTitleField]]) { - dataOptions[fieldItem[joinTitleField]].push({ - label: item[titleField], - value: item[titleField], - }); - } else { - dataOptions[fieldItem[joinTitleField]] = [ - { - label: item[titleField], - value: item[titleField], - }, - ]; - } - }); - } - }); - for (const key in dataOptions) { - options.push({ - label: key, - value: key, - children: [...dataOptions[key]], - }); - } - return options; - } else { - const dict: { [key: string]: string[] } = - data?.data.reduce((acc, item) => { - if (item[associationField]) { - const key = item[associationField][joinTitleField]; - acc[key] ??= []; - acc[key].push(item[titleField]); - } - return acc; - }, {}) || {}; - const options = Object.entries(dict).map(([key, values]) => ({ - [fieldNames.label]: key, - [fieldNames.value]: key, - children: values.map((value) => ({ [fieldNames.label]: value, [fieldNames.value]: value })), - })); - return options; - } - }, [associationField, joinTitleField, titleField, data?.data]); - - return ( - - ); -}); - -const SingleValueCascader = (props) => { - const { value, options, onChange, fieldNames, chartCascader, titleField, joinTitleField } = props; - const fieldLabel = fieldNames ? fieldNames.value : titleField; - const joinFieldLabel = fieldNames ? fieldNames.value : 'value'; - const arrayValue = value && options ? [] : undefined; - if (arrayValue) { - for (const option of options) { - if (option[fieldLabel] === value) { - arrayValue.push(option[fieldLabel]); - break; - } - for (const subOption of option.children) { - if (subOption[joinFieldLabel] === value) { - arrayValue.push(option[joinFieldLabel]); - arrayValue.push(subOption[joinFieldLabel]); - break; - } - } - } - } - const newProps = { - ...props, - value: arrayValue, - onChange: (v) => { - if (v) { - onChange(v[v.length - 1]); - } else { - onChange(v); - } - }, - }; - return ; -}; - -SingleValueCascader.displayName = 'SingleValueCascader'; -AssociationCascader.displayName = 'AssociationCascader'; -export default AssociationCascader; diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-components/auto-complete/AutoComplete.tsx b/packages/plugins/@hera/plugin-core/src/client/schema-components/auto-complete/AutoComplete.tsx deleted file mode 100644 index bf4409972..000000000 --- a/packages/plugins/@hera/plugin-core/src/client/schema-components/auto-complete/AutoComplete.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import React, { useState } from 'react'; -import { useAPIClient } from '@tachybase/client'; -import { connect, useFieldSchema } from '@tachybase/schema'; - -import { useAsyncEffect } from 'ahooks'; -import { AutoComplete as AntdAutoComplete } from 'antd'; - -import { fuzzysearch } from '../../utils'; - -export const AutoComplete = connect((props) => { - const { fieldNames, params: fieldFilter } = props; - const fieldSchema = useFieldSchema(); - const targetKey = fieldNames?.value; - const api = useAPIClient(); - const [defultOptions, setDefultOptions] = useState([]); - const [options, setOptions] = useState([]); - - const getNoDuplicateTextOptions = (rawOptions) => { - const targetOptions = rawOptions.filter((option, index, self) => { - return self.findIndex((item) => item[targetKey] === option[targetKey]) === index; - }); - return targetOptions; - }; - - useAsyncEffect(async () => { - const { - data: { data: rawOptions }, - } = await api.request({ - url: fieldSchema['collectionName'] + ':list', - params: { - pageSize: 99999, - filter: fieldFilter ? { ...fieldFilter.filter } : {}, - }, - }); - const targetOptions = getNoDuplicateTextOptions(rawOptions); - setDefultOptions(targetOptions); - setOptions(targetOptions); - }, [fieldFilter?.filter, fieldSchema['collectionName']]); - - const onSearch = (value) => { - // 在筛选项开头跟踪用户的原始输入值 - if (value) { - setOptions([ - { - [targetKey]: value, - }, - ...defultOptions, - ]); - } else { - setOptions(defultOptions); - } - }; - - return ( - fuzzysearch(inputValue, option[targetKey]?.toString())} - allowClear - onSearch={onSearch} - /> - ); -}); -AutoComplete.displayName = 'AutoComplete'; -export default AutoComplete; diff --git a/packages/plugins/@hera/plugin-core/src/client/schema-components/index.ts b/packages/plugins/@hera/plugin-core/src/client/schema-components/index.ts deleted file mode 100644 index e609965ef..000000000 --- a/packages/plugins/@hera/plugin-core/src/client/schema-components/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './auto-complete/AutoComplete';