refactor: optimize filter component in filter form to match filterable settings (#6110)

* fix: display checkbox field as select in filter form

* fix: bug

* chore: filterCollectionffield

* refactor: code improve

* fix: bug

* fix: bug

* fix: isTruly and isFalsy

* fix: bug

* fix: bug

* fix: bug

* fix: bug

---------

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
Katherine 2025-01-24 23:00:35 +08:00 committed by GitHub
parent 7c0d424173
commit a00dcb78cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 216 additions and 32 deletions

View File

@ -10,10 +10,11 @@
import { useFieldSchema } from '@formily/react';
import React from 'react';
import { withDynamicSchemaProps } from '../hoc/withDynamicSchemaProps';
import { DatePickerProvider, ActionBarProvider } from '../schema-component';
import { DatePickerProvider, ActionBarProvider, SchemaComponentOptions } from '../schema-component';
import { DefaultValueProvider } from '../schema-settings';
import { CollectOperators } from './CollectOperators';
import { FormBlockProvider } from './FormBlockProvider';
import { FilterCollectionField } from '../modules/blocks/filter-blocks/FilterCollectionField';
export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const filedSchema = useFieldSchema();
@ -21,22 +22,24 @@ export const FilterFormBlockProvider = withDynamicSchemaProps((props) => {
const deprecatedOperators = filedSchema['x-filter-operators'] || {};
return (
<CollectOperators defaultOperators={deprecatedOperators}>
<DatePickerProvider value={{ utc: false }}>
<ActionBarProvider
forceProps={{
style: {
overflowX: 'auto',
maxWidth: '100%',
float: 'right',
},
}}
>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
</DefaultValueProvider>
</ActionBarProvider>
</DatePickerProvider>
</CollectOperators>
<SchemaComponentOptions components={{ CollectionField: FilterCollectionField }}>
<CollectOperators defaultOperators={deprecatedOperators}>
<DatePickerProvider value={{ utc: false }}>
<ActionBarProvider
forceProps={{
style: {
overflowX: 'auto',
maxWidth: '100%',
float: 'right',
},
}}
>
<DefaultValueProvider isAllowToSetDefaultValue={() => false}>
<FormBlockProvider name="filter-form" {...props}></FormBlockProvider>
</DefaultValueProvider>
</ActionBarProvider>
</DatePickerProvider>
</CollectOperators>
</SchemaComponentOptions>
);
});

View File

@ -164,8 +164,49 @@ export const time = [
];
export const boolean = [
{ label: '{{t("Yes")}}', value: '$isTruly', selected: true, noValue: true },
{ label: '{{t("No")}}', value: '$isFalsy', noValue: true },
{
label: '{{t("Yes")}}',
value: '$isTruly',
selected: true,
noValue: true,
schema: {
'x-component': 'Select',
'x-component-props': {
multiple: false,
options: [
{
label: '{{t("Yes")}}',
value: true,
},
{
label: '{{t("No")}}',
value: false,
},
],
},
},
},
{
label: '{{t("No")}}',
value: '$isFalsy',
noValue: true,
schema: {
'x-component': 'Select',
'x-component-props': {
multiple: false,
options: [
{
label: '{{t("Yes")}}',
value: true,
},
{
label: '{{t("No")}}',
value: false,
},
],
},
},
},
];
export const tableoid = [

View File

@ -0,0 +1,119 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Field } from '@formily/core';
import { connect, Schema, useField, useFieldSchema } from '@formily/react';
import { untracked } from '@formily/reactive';
import { merge } from '@formily/shared';
import { concat } from 'lodash';
import React, { useEffect } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { useDynamicComponentProps } from '../../../hoc/withDynamicSchemaProps';
import { ErrorFallback, useCompile, useComponent } from '../../../schema-component';
import { useIsAllowToSetDefaultValue } from '../../../schema-settings/hooks/useIsAllowToSetDefaultValue';
import { useCollectionManager_deprecated } from '../../../';
import {
CollectionFieldProvider,
useCollectionField,
} from '../../../data-source/collection-field/CollectionFieldProvider';
type Props = {
component: any;
children?: React.ReactNode;
};
const setFieldProps = (field: Field, key: string, value: any) => {
untracked(() => {
if (field[key] === undefined) {
field[key] = value;
}
});
};
const setRequired = (field: Field, fieldSchema: Schema, uiSchema: Schema) => {
if (typeof fieldSchema['required'] === 'undefined') {
field.required = !!uiSchema['required'];
}
};
/**
* TODO: 初步适配
* @internal
*/
export const FilterCollectionFieldInternalField: React.FC = (props: Props) => {
const compile = useCompile();
const field = useField<Field>();
const fieldSchema = useFieldSchema();
const { getInterface } = useCollectionManager_deprecated();
const { uiSchema: uiSchemaOrigin, defaultValue, interface: collectionInterface } = useCollectionField();
const { isAllowToSetDefaultValue } = useIsAllowToSetDefaultValue();
const targetInterface = getInterface(collectionInterface);
const operator = targetInterface?.filterable?.operators?.find(
(v, index) => v.value === fieldSchema['x-filter-operator'] || index === 0,
);
const Component = useComponent(
operator?.schema?.['x-component'] ||
fieldSchema['x-component-props']?.['component'] ||
uiSchemaOrigin?.['x-component'] ||
'Input',
);
const ctx = useFormBlockContext();
const dynamicProps = useDynamicComponentProps(uiSchemaOrigin?.['x-use-component-props'], props);
// TODO: 初步适配
useEffect(() => {
if (!uiSchemaOrigin) {
return;
}
const uiSchema = compile(uiSchemaOrigin);
setFieldProps(field, 'content', uiSchema['x-content']);
setFieldProps(field, 'title', uiSchema.title);
setFieldProps(field, 'description', uiSchema.description);
if (ctx?.form) {
const defaultVal = isAllowToSetDefaultValue() ? fieldSchema.default || defaultValue : undefined;
defaultVal !== null && defaultVal !== undefined && setFieldProps(field, 'initialValue', defaultVal);
}
if (!field.validator && (uiSchema['x-validator'] || fieldSchema['x-validator'])) {
const concatSchema = concat([], uiSchema['x-validator'] || [], fieldSchema['x-validator'] || []);
field.validator = concatSchema;
}
if (fieldSchema['x-disabled'] === true) {
field.disabled = true;
}
if (fieldSchema['x-read-pretty'] === true) {
field.readPretty = true;
}
setRequired(field, fieldSchema, uiSchema);
// @ts-ignore
field.dataSource = uiSchema.enum;
const originalProps =
compile({ ...(operator?.schema?.['x-component-props'] || {}), ...(uiSchema['x-component-props'] || {}) }) || {};
field.componentProps = merge(originalProps, field.componentProps || {}, dynamicProps || {});
}, [uiSchemaOrigin]);
if (!uiSchemaOrigin) return null;
return <Component {...props} {...dynamicProps} />;
};
export const FilterCollectionField = connect((props) => {
const fieldSchema = useFieldSchema();
const field = useField<Field>();
return (
<ErrorBoundary FallbackComponent={ErrorFallback.Modal} onError={(err) => console.log(err)}>
<CollectionFieldProvider name={fieldSchema.name}>
<FilterCollectionFieldInternalField {...props} />
</CollectionFieldProvider>
</ErrorBoundary>
);
});
FilterCollectionField.displayName = 'FilterCollectionField';

View File

@ -421,7 +421,13 @@ export const filterSelectComponentFieldSettings = new SchemaSettings({
return isSelectFieldMode && !isFieldReadPretty;
},
},
getAllowMultiple({ title: 'Allow multiple selection' }),
{
...getAllowMultiple({ title: 'Allow multiple selection' }),
useVisible() {
const field = useField();
return field.componentProps.multiple !== false;
},
},
{
...titleField,
useVisible: useIsAssociationField,

View File

@ -17,6 +17,7 @@ import React from 'react';
import { ReadPretty } from './ReadPretty';
import { FieldNames, defaultFieldNames, getCurrentOptions } from './utils';
import { BaseOptionType, DefaultOptionType } from 'antd/es/select';
import { useCompile } from '../../';
export type SelectProps<
ValueType = any,
@ -120,6 +121,7 @@ const filterOption = (input, option) => (option?.label ?? '').toLowerCase().incl
const InternalSelect = connect(
(props: SelectProps) => {
const { objectValue, loading, value, rawOptions, defaultValue, ...others } = props;
const compile = useCompile();
let mode: any = props.multiple ? 'multiple' : props.mode;
if (mode && !['multiple', 'tags'].includes(mode)) {
mode = undefined;
@ -172,7 +174,7 @@ const InternalSelect = connect(
</Tag>
);
}}
{...others}
{...compile(others)}
onChange={(changed) => {
props.onChange?.(changed === undefined ? null : changed);
}}

View File

@ -10,7 +10,26 @@
import { Op } from 'sequelize';
export default {
$isFalsy() {
$isFalsy(value) {
if (value === true || value === 'true') {
return {
[Op.or]: {
[Op.is]: null,
[Op.eq]: false,
},
};
}
return {
[Op.eq]: true,
};
},
$isTruly(value) {
if (value === true || value === 'true') {
return {
[Op.eq]: true,
};
}
return {
[Op.or]: {
[Op.is]: null,
@ -18,10 +37,4 @@ export default {
},
};
},
$isTruly() {
return {
[Op.eq]: true,
};
},
} as Record<string, any>;

View File

@ -12515,7 +12515,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneBelongsTo',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@ -12562,7 +12562,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.oneToOneHasOne',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',
@ -12656,7 +12656,7 @@ export const oneFilterFormBlockWithAllAssociationFieldsV1333Beta: PageConfig = {
'x-use-decorator-props': 'useFormItemProps',
'x-collection-field': 'general.manyToOne',
'x-component-props': {
multiple: false,
multiple: true,
fieldNames: {
label: 'id',
value: 'id',