fix: 优化自定义筛选组件和逻辑 (#486)

<!-- Note -->
<!-- This is a template for submitting a new feature.
Use the bug fix template if you're submitting a bug fix pull request by adding `template=bug_fix.md` to your pull request URL. -->

# Description
<!-- Describe the new feature or modification to an existing feature clearly and consciously. -->

# Motivation
<!-- Explain the reason for adding or modifying this feature. -->

# Key changes
<!-- Provide a technically detailed description of the key changes made. -->
- Frontend
- Backend

# Test plan
## Suggestions
<!-- Provide any suggestions or recommendations for improvements in the testing plan. -->

## Underlying risk
<!-- Identify any potential risks or issues that may arise from the new feature or modification. -->

# Showcase
<!-- Including any screenshots of the new feature or modification. -->

Co-authored-by: sealday <sealday@gmail.com>
Co-authored-by: bai.jingfeng <bai.jingfeng@foxmail.com>
Reviewed-on: daoyoucloud/tachycode#486
Co-authored-by: wjh <wwwjh0710@163.com>
Co-committed-by: wjh <wwwjh0710@163.com>
This commit is contained in:
wjh 2024-03-25 17:24:21 +08:00 committed by baijingfeng
parent 610fee7035
commit aa2542b706
9 changed files with 132 additions and 190 deletions

View File

@ -9,89 +9,45 @@ import {
useFilterBlock, useFilterBlock,
} from '@nocobase/client'; } from '@nocobase/client';
import flat from 'flat'; import flat from 'flat';
import { hasDuplicateKeys } from '../utils';
export const removeNullCondition = (filter, fieldSchema?) => { export const removeNullCondition = (filter, fieldSchema?) => {
const filterSchema = fieldSchema ? fieldSchema['x-filter-rules'] : ''; const filterSchema = fieldSchema ? fieldSchema['x-filter-rules'] : '';
const filterSchemaItem = flat(filterSchema || ''); const filterSchemaItem = flat(filterSchema || '');
const filterItem = {};
const items = flat(filter || {}); const items = flat(filter || {});
const values = {}; const values = {};
let isFilterCustom = false; const isCustomFilter = filterSchema?.$and?.length || filterSchema?.$or?.length;
if (filterSchema && (filterSchema.$and?.length || filterSchema.$or?.length)) { const isFilterCustom = isCustomFilter ? hasDuplicateKeys(items, filterSchemaItem) : false;
for (const key in items) {
for (const filterItems in filterSchemaItem) {
if (key.includes(filterItems)) {
isFilterCustom = true;
break;
}
}
}
}
if (!isFilterCustom) { if (!isFilterCustom) {
if (Object.keys(items).filter((item) => item.includes('custom')).length) { if (isCustomFilter) {
if (filterSchema && (filterSchema.$and?.length || filterSchema.$or?.length)) {
for (const filterKey in filterSchemaItem) { for (const filterKey in filterSchemaItem) {
const match = filterSchemaItem[filterKey]?.slice(11, -2); const match = filterSchemaItem[filterKey]?.slice(11, -2);
const collection = match?.split('.')[0]; const collection = match?.split('.')[0];
if (Object.keys(items).filter((item) => item.includes(collection)).length) { const filterItems = Object.keys(items).filter((item) => item.includes(collection))[0];
for (const key in items) { if (filterItems) {
if (key.includes('custom')) { filterSchemaItem[filterKey] = items[filterItems];
if (key.includes(collection)) {
if (key.includes(match)) {
if (Object.keys(items[key]).length) {
filterSchemaItem[filterKey] = items[key];
filterItem[match] = items[key];
} }
} }
if (key.includes(collection)) delete items[key]; for (const item in items) {
else { if (!item.includes('custom')) {
const value = items[key]; values[item] = items[item];
if (value != null && !isEmpty(value)) {
values[key] = value;
}
}
}
} else {
values[key] = items[key];
}
}
} else if (Object.keys(items).filter((item) => !item.includes('custom')).length) {
const filterItem = Object.keys(items).filter((item) => !item.includes('custom'));
filterItem.forEach((key) => {
values[key] = items[key];
});
} }
} }
for (const item in filterSchemaItem) { for (const item in filterSchemaItem) {
for (const key in filterItem) {
if (filterSchemaItem[item].includes(key)) {
filterSchemaItem[item] = filterItem[key];
}
}
if (filterSchemaItem[item].includes('$nFilter')) { if (filterSchemaItem[item].includes('$nFilter')) {
delete filterSchemaItem[item]; delete filterSchemaItem[item];
} }
} }
const flatValue = flat.unflatten(values); const flatValue = flat.unflatten(values);
const flatFieldSchema = flat.unflatten(filterSchemaItem); const flatFieldSchema = flat.unflatten(filterSchemaItem);
flatValue['$and'] = flatValue['$and']?.filter(Boolean);
return { return {
$and: [flatValue, flatFieldSchema], $and: [flatValue, flatFieldSchema],
}; };
} else { } else {
for (const key in items) { for (const key in items) {
if (!key.includes('custom')) {
const value = items[key]; const value = items[key];
if (value != null && !isEmpty(value)) { if (!key.includes('custom') && value != null && !isEmpty(value)) {
values[key] = value;
}
}
}
return flat.unflatten(values);
}
} else {
for (const key in items) {
const value = items[key];
if (value != null && !isEmpty(value)) {
values[key] = value; values[key] = value;
} }
} }
@ -123,13 +79,11 @@ export const useFilterBlockActionProps = () => {
getDataBlocks().map(async (block) => { getDataBlocks().map(async (block) => {
const target = targets.find((target) => target.uid === block.uid); const target = targets.find((target) => target.uid === block.uid);
if (!target) return; if (!target) return;
const param = block.service.params?.[0] || {}; const param = block.service.params?.[0] || {};
for (const key in form.values) { for (const key in form.values) {
if ( if (
(typeof form.values[key] === 'object' && (typeof form.values[key] === 'object' &&
!form.values[key]?.length && (JSON.stringify(form.values[key]) === '{}' || JSON.stringify(form.values[key]) === '[]')) ||
!Object.keys(form.values[key]).length) ||
!form.values[key] !form.values[key]
) { ) {
delete form.values[key]; delete form.values[key];

View File

@ -5,6 +5,7 @@ import { useCollectionManager, useRequest } from '@nocobase/client';
import _ from 'lodash'; import _ from 'lodash';
const AssociationCascader = connect((props) => { const AssociationCascader = connect((props) => {
const { fieldNames } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const collection = fieldSchema['collectionName']; const collection = fieldSchema['collectionName'];
const associationField = props.associationField; const associationField = props.associationField;
@ -36,8 +37,8 @@ const AssociationCascader = connect((props) => {
return acc; return acc;
}, {}) || {}; }, {}) || {};
const options = Object.entries(dict).map(([key, values]) => ({ const options = Object.entries(dict).map(([key, values]) => ({
name: key, [fieldNames.label]: key,
id: key, [fieldNames.value]: key,
children: values.map((value) => ({ name: value, id: value })), children: values.map((value) => ({ name: value, id: value })),
})); }));
return options; return options;

View File

@ -1,25 +1,17 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { AutoComplete as AntdAutoComplete } from 'antd'; import { AutoComplete as AntdAutoComplete } from 'antd';
import { useFieldSchema, useForm } from '@formily/react'; import { connect, useFieldSchema, useForm } from '@formily/react';
import { useAPIClient, useDesigner, useRequest } from '@nocobase/client'; import { useAPIClient, useDesigner, useRequest } from '@nocobase/client';
import { useAsyncEffect } from 'ahooks'; import { useAsyncEffect } from 'ahooks';
import { fuzzysearch } from '../../utils';
export const AutoComplete = (props) => { export const AutoComplete = connect((props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const comdef = fieldSchema['default']; const fieldNames = props.fieldNames || fieldSchema['x-component-props'].fieldNames;
const { fieldNames } = fieldSchema['x-component-props']; const fieldFilter = props.params;
const fieldFilter = fieldSchema['x-component-props']['params'];
const [defultValue, setDefultValue] = useState([]); const [defultValue, setDefultValue] = useState([]);
const api = useAPIClient(); const api = useAPIClient();
const [options, setOptions] = useState([]); const [options, setOptions] = useState([]);
const form = useForm();
const autoValue = form.values['custom'] ? form.values['custom'][fieldSchema['collectionName']] : '';
const [value, setValue] = useState(autoValue);
if (!autoValue && value) {
setValue('');
setOptions(defultValue);
}
useAsyncEffect(async () => { useAsyncEffect(async () => {
const defultOptions = await api.request({ const defultOptions = await api.request({
url: fieldSchema['collectionName'] + ':list', url: fieldSchema['collectionName'] + ':list',
@ -33,55 +25,28 @@ export const AutoComplete = (props) => {
useEffect(() => { useEffect(() => {
changLable(defultValue); changLable(defultValue);
}, [fieldNames.label]); }, [fieldNames?.label]);
useEffect(() => {
setValue(comdef);
}, [comdef]);
const changLable = (defultOptions) => { const changLable = (defultOptions) => {
if (defultOptions) { if (defultOptions) {
const item = defultOptions.map((item) => { const items = [];
item['label'] = item[fieldNames.label]; defultOptions.forEach((item) => {
item['value'] = item[fieldNames.value]; if (!items.filter((option) => option[fieldNames.value] === item[fieldNames.value]).length) {
return item; items.push(item);
}
}); });
setDefultValue(item); setDefultValue(items);
setOptions(item); setOptions(items);
}
};
if (!form.values['custom']) {
form.values['custom'] = {};
}
const onSearch = (data) => {
if (data) {
const searchValue = defultValue.filter((item) => item.label.includes(data));
if (searchValue.length) {
setOptions(searchValue);
} else {
setOptions([]);
}
const valueLabel = options.filter((item) => item.value === data)[0];
if (valueLabel) {
form.values.custom[fieldSchema['collectionName']] = valueLabel.label;
setValue(valueLabel.label);
} else {
form.values.custom[fieldSchema['collectionName']] = data;
setValue(data);
}
} else {
form.values.custom[fieldSchema['collectionName']] = {};
setValue(data);
setOptions(defultValue);
} }
}; };
return ( return (
<AntdAutoComplete <AntdAutoComplete
value={value} {...props}
defaultValue={comdef}
options={options} options={options}
onSearch={onSearch} filterOption={(inputValue, option) => fuzzysearch(inputValue, option[fieldNames.value].toString())}
onChange={onSearch}
allowClear allowClear
/> />
); );
}; });
AutoComplete.displayName = 'AutoComplete';
export default AutoComplete;

View File

@ -1,5 +1,5 @@
import { CloseCircleFilled, CloseOutlined, LoadingOutlined } from '@ant-design/icons'; import { CloseCircleFilled, CloseOutlined, LoadingOutlined } from '@ant-design/icons';
import { connect, mapProps, mapReadPretty, useFieldSchema } from '@formily/react'; import { connect, mapProps, mapReadPretty, useFieldSchema, useForm } from '@formily/react';
import { isValid, toArr } from '@formily/shared'; import { isValid, toArr } from '@formily/shared';
import { isPlainObject } from '@nocobase/utils/client'; import { isPlainObject } from '@nocobase/utils/client';
import type { SelectProps } from 'antd'; import type { SelectProps } from 'antd';
@ -24,11 +24,10 @@ const ObjectSelect = (props: Props) => {
const { value, options, onChange, fieldNames, mode, loading, rawOptions, defaultValue, ...others } = props; const { value, options, onChange, fieldNames, mode, loading, rawOptions, defaultValue, ...others } = props;
const [defoptions, setDefOptions] = useState(); const [defoptions, setDefOptions] = useState();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const comDefult = fieldSchema.default;
const collectionName = fieldSchema['collectionName']; const collectionName = fieldSchema['collectionName'];
const filterField = fieldSchema['x-component-props']['params']; const filterField = others?.['params'];
const fieldName = fieldSchema['x-component-props'].fieldNames;
const api = useAPIClient(); const api = useAPIClient();
const form = useForm();
useAsyncEffect(async () => { useAsyncEffect(async () => {
if (collectionName) { if (collectionName) {
const defValue = await api.request({ const defValue = await api.request({
@ -39,16 +38,13 @@ const ObjectSelect = (props: Props) => {
}, },
}); });
const changOptions = defValue?.data?.data.map((value) => { const changOptions = defValue?.data?.data.map((value) => {
value['label'] = value[fieldName.label]; value['label'] = value[fieldNames.label];
value['value'] = value[fieldNames.label]; value['value'] = value[fieldNames.value];
return value; return value;
}); });
setDefOptions(changOptions); setDefOptions(changOptions);
} }
}, [filterField?.filter, collectionName]); }, [filterField?.filter, collectionName]);
useEffect(() => {
onChange?.(comDefult);
}, [fieldSchema.default]);
const toValue = (v: any) => { const toValue = (v: any) => {
if (isEmptyObject(v)) { if (isEmptyObject(v)) {
return; return;
@ -58,15 +54,12 @@ const ObjectSelect = (props: Props) => {
.map((val) => { .map((val) => {
return isPlainObject(val) ? val[fieldNames.value] : val; return isPlainObject(val) ? val[fieldNames.value] : val;
}); });
const currentOptions = getCurrentOptions(values, options, fieldNames)?.map((val) => { const filterOption = defoptions ? defoptions : options;
if (collectionName) { const currentOptions = getCurrentOptions(values, filterOption, fieldNames)?.map((val) => {
return val[fieldName.label];
} else {
return { return {
label: val[fieldNames.label], label: val[fieldNames.label],
value: val[fieldNames.value], value: val[fieldNames.value],
}; };
}
}); });
if (['tags', 'multiple'].includes(mode) || props.multiple) { if (['tags', 'multiple'].includes(mode) || props.multiple) {
return currentOptions; return currentOptions;
@ -83,7 +76,7 @@ const ObjectSelect = (props: Props) => {
role="button" role="button"
data-testid={`select-object-${mode || 'single'}`} data-testid={`select-object-${mode || 'single'}`}
value={toValue(value)} value={toValue(value)}
defaultValue={toValue(defaultValue) || comDefult} defaultValue={toValue(defaultValue)}
allowClear={{ allowClear={{
clearIcon: <CloseCircleFilled role="button" aria-label="icon-close-select" />, clearIcon: <CloseCircleFilled role="button" aria-label="icon-close-select" />,
}} }}
@ -106,17 +99,17 @@ const ObjectSelect = (props: Props) => {
onChange={(changed) => { onChange={(changed) => {
const current = getCurrentOptions( const current = getCurrentOptions(
toArr(changed).map((v) => v.value), toArr(changed).map((v) => v.value),
rawOptions || options, defoptions || rawOptions || options,
fieldNames, fieldNames,
); );
if (collectionName) {
onChange?.(changed['label']);
} else {
if (['tags', 'multiple'].includes(mode as string) || props.multiple) { if (['tags', 'multiple'].includes(mode as string) || props.multiple) {
onChange?.(current); onChange?.(current);
} else { } else {
onChange?.(current.shift() || null); onChange?.(current.shift() || null);
} }
if (collectionName) {
const name = fieldSchema['name'].toString().split('.')[1];
form.values['custom'][name] = changed?.['value'];
} }
}} }}
mode={mode} mode={mode}

View File

@ -41,7 +41,6 @@ import {
import _ from 'lodash'; import _ from 'lodash';
import { SchemaSettingsRemove } from '../../schema-settings/SchemaSettingsRemove'; import { SchemaSettingsRemove } from '../../schema-settings/SchemaSettingsRemove';
import { tval, useTranslation } from '../../locale'; import { tval, useTranslation } from '../../locale';
import { ContractsController } from 'packages/plugins/@hera/plugin-rental/dist/server/actions';
const FieldComponentProps: React.FC = observer( const FieldComponentProps: React.FC = observer(
(props) => { (props) => {
@ -296,7 +295,7 @@ export const FilterCustomItemInitializer: React.FC<{
...(defaultSchema['x-component-props'] || {}), ...(defaultSchema['x-component-props'] || {}),
fieldNames: { fieldNames: {
label: 'name', label: 'name',
value: 'id', value: 'name',
}, },
associationField, associationField,
}, },

View File

@ -43,13 +43,7 @@ export const SchemaSettingsRemove: FC<SchemaSettingsRemoveProps> = (props) => {
} }
await dn.remove(null, options); await dn.remove(null, options);
await confirm?.onOk?.(); await confirm?.onOk?.();
if (fieldSchema['collectionName']) {
delete form.values['custom'][fieldSchema['collectionName']];
} else {
if (form.values['custom']?.[fieldName]) {
delete form.values['custom'][fieldName]; delete form.values['custom'][fieldName];
}
}
for (const key in form.fields) { for (const key in form.fields) {
if (key.includes(name) && form.fields[key].title === title) { if (key.includes(name) && form.fields[key].title === title) {
delete form.fields[key]; delete form.fields[key];

View File

@ -176,19 +176,15 @@ export const EditDefaultValue = () => {
}, },
}, },
}} }}
onSubmit={(defaultValue) => { onSubmit={({ default: { value } }) => {
let value; field.setValue(value);
if (defaultValue.default && !defaultValue.custom) {
value = defaultValue.default.value?.value ? defaultValue.default.value.value : defaultValue.default.value;
} else if (defaultValue.custom) {
value = defaultValue.custom[collectionName];
}
field.setInitialValue(value);
fieldSchema['default'] = value; fieldSchema['default'] = value;
fieldSchema['x-component-props']['defaultValue'] = value;
dn.emit('patch', { dn.emit('patch', {
schema: { schema: {
'x-uid': fieldSchema['x-uid'], 'x-uid': fieldSchema['x-uid'],
default: value, ['x-component-props']: fieldSchema['x-component-props'],
default: fieldSchema.default,
}, },
}); });
dn.refresh(); dn.refresh();
@ -343,6 +339,7 @@ export const EditTitleField = () => {
...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'], ...collectionField?.uiSchema?.['x-component-props']?.['fieldNames'],
...field.componentProps.fieldNames, ...field.componentProps.fieldNames,
label, label,
value: label,
}; };
fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {};
fieldSchema['x-component-props']['fieldNames'] = fieldNames; fieldSchema['x-component-props']['fieldNames'] = fieldNames;
@ -507,14 +504,11 @@ export const SchemaSettingComponent = () => {
onChange={(mode) => { onChange={(mode) => {
field.component = mode; field.component = mode;
fieldSchema['x-component'] = mode; fieldSchema['x-component'] = mode;
fieldSchema.default = '';
const schema = {
['x-uid']: fieldSchema['x-uid'],
['x-component']: mode,
default: '',
};
void dn.emit('patch', { void dn.emit('patch', {
schema, schema: {
['x-uid']: fieldSchema['x-uid'],
['x-component']: fieldSchema['x-component'],
},
}); });
dn.refresh(); dn.refresh();
}} }}
@ -524,8 +518,8 @@ export const SchemaSettingComponent = () => {
export const SchemaSettingCollection = () => { export const SchemaSettingCollection = () => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const field = useField();
const collections = useCollectionManager(); const collections = useCollectionManager();
const field = useField<Field>();
const options = collections?.dataSource['options']?.collections.map((value) => { const options = collections?.dataSource['options']?.collections.map((value) => {
return { return {
label: value.title, label: value.title,
@ -540,16 +534,25 @@ export const SchemaSettingCollection = () => {
options={options} options={options}
value={fieldSchema['collectionName']} value={fieldSchema['collectionName']}
onChange={(name) => { onChange={(name) => {
field.setValue('');
fieldSchema['collectionName'] = name; fieldSchema['collectionName'] = name;
fieldSchema['name'] = 'custom.' + name;
fieldSchema.default = ''; fieldSchema.default = '';
const schema = { fieldSchema['x-component-props'] = {
['x-uid']: fieldSchema['x-uid'], ...fieldSchema['x-component-props'],
collectionName: name, fieldNames: {
name: 'custom.' + name, label: 'name',
default: '', value: 'name',
},
defaultValue: '',
}; };
void dn.emit('patch', { schema }); void dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
collectionName: fieldSchema['collectionName'],
['x-component-props']: fieldSchema['x-component-props'],
default: '',
},
});
dn.refresh(); dn.refresh();
}} }}
/> />

View File

@ -0,0 +1,33 @@
export function fuzzysearch(needle: string, haystack: string): boolean {
const hlen = haystack.length;
const nlen = needle.length;
if (nlen > hlen) {
return false;
}
if (nlen === hlen) {
return needle === haystack;
}
outer: for (let i = 0, j = 0; i < nlen; i++) {
const nch = needle.charCodeAt(i);
while (j < hlen) {
if (haystack.charCodeAt(j++) === nch) {
continue outer;
}
}
return false;
}
return true;
}
// 数组去重
const getArrayOfNoDuplicateValue = (arr: Array<string | number>): Array<string | number> => {
return [...new Set(arr)];
};
// 判断两个对象是否包含有相同的key
export function hasDuplicateKeys(A: Object, B: Object): boolean {
const keysA = getArrayOfNoDuplicateValue(Object.keys(A));
const keysB = getArrayOfNoDuplicateValue(Object.keys(B));
const keysA_B = getArrayOfNoDuplicateValue([...keysA, ...keysB]);
return keysA_B.length < keysA.length + keysB.length;
}

View File

@ -235,7 +235,7 @@ SELECT
records r records r
WHERE WHERE
r.contract_id = COALESCE(main.id, c.id) r.contract_id = COALESCE(main.id, c.id)
AND s.end_date + INTERVAL '1 day' >= r.date AND s.end_date >= r.date
) AS records, ) AS records,
( (
SELECT SELECT