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

View File

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

View File

@ -1,25 +1,17 @@
import React, { useEffect, useState } from 'react';
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 { useAsyncEffect } from 'ahooks';
import { fuzzysearch } from '../../utils';
export const AutoComplete = (props) => {
export const AutoComplete = connect((props) => {
const fieldSchema = useFieldSchema();
const comdef = fieldSchema['default'];
const { fieldNames } = fieldSchema['x-component-props'];
const fieldFilter = fieldSchema['x-component-props']['params'];
const fieldNames = props.fieldNames || fieldSchema['x-component-props'].fieldNames;
const fieldFilter = props.params;
const [defultValue, setDefultValue] = useState([]);
const api = useAPIClient();
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 () => {
const defultOptions = await api.request({
url: fieldSchema['collectionName'] + ':list',
@ -33,55 +25,28 @@ export const AutoComplete = (props) => {
useEffect(() => {
changLable(defultValue);
}, [fieldNames.label]);
}, [fieldNames?.label]);
useEffect(() => {
setValue(comdef);
}, [comdef]);
const changLable = (defultOptions) => {
if (defultOptions) {
const item = defultOptions.map((item) => {
item['label'] = item[fieldNames.label];
item['value'] = item[fieldNames.value];
return item;
const items = [];
defultOptions.forEach((item) => {
if (!items.filter((option) => option[fieldNames.value] === item[fieldNames.value]).length) {
items.push(item);
}
});
setDefultValue(item);
setOptions(item);
}
};
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);
setDefultValue(items);
setOptions(items);
}
};
return (
<AntdAutoComplete
value={value}
defaultValue={comdef}
{...props}
options={options}
onSearch={onSearch}
onChange={onSearch}
filterOption={(inputValue, option) => fuzzysearch(inputValue, option[fieldNames.value].toString())}
allowClear
/>
);
};
});
AutoComplete.displayName = 'AutoComplete';
export default AutoComplete;

View File

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

View File

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

View File

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

View File

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

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
WHERE
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,
(
SELECT