add Filter component into schema componens (#176)

* feat: add next Filter component into schema component

* fix: bugfix

Co-authored-by: chenos <chenlinxh@gmail.com>
This commit is contained in:
SemmyWong 2022-01-29 23:07:05 +08:00 committed by GitHub
parent 2929c77775
commit 8e1543269f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 616 additions and 2 deletions

View File

@ -0,0 +1,8 @@
import React from 'react';
import * as components from '.';
import { SchemaComponentOptions } from '../components/SchemaComponentOptions';
export const AntdSchemaComponentProvider = (props) => {
const { children } = props;
return <SchemaComponentOptions components={{ ...(components as any) }}>{children}</SchemaComponentOptions>;
};

View File

@ -0,0 +1,84 @@
import { LoadingOutlined } from '@ant-design/icons';
import { createForm, onFieldValueChange } from '@formily/core';
import { connect, FieldContext, FormContext, ISchema, mapProps, mapReadPretty, Schema } from '@formily/react';
import deepmerge from 'deepmerge';
import React, { useMemo } from 'react';
import { SchemaComponent } from '../../components';
import { FilterGroup } from './FilterGroup';
import './style.less';
export const Filter: any = connect(
(props) => <FilterGroup bordered={false} {...props} />,
mapProps((props, field) => {
return {
...props,
suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>,
};
}),
mapReadPretty((props) => {
return null;
}),
);
interface DynamicValueProps {
value?: any;
onChange?: any;
schema?: Schema;
operation?: any;
}
Filter.DynamicValue = connect((props: DynamicValueProps) => {
const { onChange, value, operation } = props;
const fieldName = Object.keys(props?.schema?.properties || {}).shift() ?? 'value';
const fieldSchema = Object.values(props?.schema?.properties || {}).shift();
const form = useMemo(
() =>
createForm({
initialValues: {
[fieldName]: value,
},
effects(form) {
onFieldValueChange(fieldName, (field) => {
onChange(field.value);
});
},
}),
[],
);
const extra: ISchema = deepmerge(
{
required: false,
'x-read-pretty': false,
name: fieldName,
default: value,
'x-decorator': 'FormilyFormItem',
'x-decorator-props': {
asterisk: true,
feedbackLayout: 'none',
},
'x-component-props': {
style: {
minWidth: '150px',
},
},
},
operation?.schema || {},
);
const schema = {
type: 'object',
properties: {
[fieldName]: deepmerge(fieldSchema, extra, {
arrayMerge: (target, source) => source,
}) as any,
},
};
return (
<FieldContext.Provider value={null}>
<FormContext.Provider value={form}>
<SchemaComponent schema={schema} />
</FormContext.Provider>
</FieldContext.Provider>
);
});
export default Filter;

View File

@ -0,0 +1,75 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { useForm } from '@formily/react';
import { isValid } from '@formily/shared';
import { Select } from 'antd';
import cls from 'classnames';
import React from 'react';
import { Trans } from 'react-i18next';
import { FilterList } from './FilterList';
const toValue = (value) => {
if (!value) {
return {
logical: 'and',
list: [{}],
};
}
if (value.and) {
return {
logical: 'and',
list: value.and,
};
}
if (value.or) {
return {
logical: 'and',
list: value.or,
};
}
return {
logical: 'and',
list: [{}],
};
};
export function FilterGroup(props) {
const { bordered = true, onRemove, onChange } = props;
const value = toValue(props.value);
const form = useForm();
console.log('list', form.values, value);
return (
<div className={cls('nb-filter-group', { bordered })}>
{onRemove && (
<a className={'nb-filter-group-close'} onClick={() => onRemove()}>
<CloseCircleOutlined />
</a>
)}
<div style={{ marginBottom: 14 }}>
<Trans>
<Select
style={{ width: 80 }}
onChange={(logical) => {
onChange?.({
[logical]: value.list,
});
}}
defaultValue={value.logical}
>
<Select.Option value={'and'}>All</Select.Option>
<Select.Option value={'or'}>Any</Select.Option>
</Select>
</Trans>
</div>
<FilterList
initialValue={value.list}
onChange={(list: any[]) => {
const values = {
[value.logical]: list.filter((item) => isValid(item) && Object.keys(item).length),
};
onChange?.(values);
}}
/>
</div>
);
}

View File

@ -0,0 +1,247 @@
import { CloseCircleOutlined } from '@ant-design/icons';
import { FormItem as FormilyFormItem, FormLayout, Space as AntdSpace } from '@formily/antd';
import { createForm, onFieldReact, onFieldValueChange, onFormValuesChange } from '@formily/core';
import { Field } from '@formily/core/esm/models/Field';
import { Form } from '@formily/core/esm/models/Form';
import {
FieldContext,
FormContext,
ISchema,
Schema,
SchemaKey,
SchemaOptionsContext,
useFieldSchema,
} from '@formily/react';
import { isValid, uid } from '@formily/shared';
import { get } from 'lodash';
import React, { useContext, useMemo } from 'react';
import { SchemaComponent } from '../../components';
function useFilterColumns(): Map<SchemaKey, Schema> {
const schema = useFieldSchema();
const columns = schema.reduceProperties((columns, current) => {
if (current['x-component'] === 'Filter.Column') {
const fieldName = Object.keys(current.properties).shift();
columns.set(fieldName, current);
return columns;
}
return columns;
}, new Map<SchemaKey, Schema>());
return columns;
}
export const FilterItem = (props) => {
const { value, initialValues = {}, onRemove, onChange } = props;
const options = useContext(SchemaOptionsContext);
const columns = useFilterColumns();
const toValues = (value) => {
if (!value) {
return {};
}
if (Object.keys(value).length === 0) {
return {};
}
const fieldName = Object.keys(value).shift();
const nested = value[fieldName];
const column = columns.get(fieldName).toJSON();
const operations = column?.['x-component-props']?.['operations'] || [];
if (!nested) {
return {
column,
operations,
};
}
if (Object.keys(nested).length === 0) {
return {
column,
operations,
};
}
const operationValue = Object.keys(nested).shift();
console.log('toValues', { operationValue });
const operation = operations.find((operation) => operation.value === operationValue);
console.log('toValues', { operation });
if (!operation) {
return {
operations,
column,
};
}
if (operation.noValue) {
return {
column,
operations,
operation,
};
}
return {
column,
operation,
operations,
value: nested[operationValue],
};
};
const values = toValues(value);
console.log('toValues', values, value);
const Remove = (props) => {
return (
onRemove && (
<a onClick={() => onRemove()}>
<CloseCircleOutlined />
</a>
)
);
};
const form = useMemo(
() =>
createForm({
initialValues: values,
effects: (form) => {
onFieldValueChange('column', (field: Field, form: Form) => {
const column = (field.value || {}) as ISchema;
const operations = column?.['x-component-props']?.['operations'] || [];
field.query('operation').take((f: Field) => {
f.setDataSource(operations);
f.value = get(operations, [0]);
});
field.query('value').take((f: Field) => {
f.value = undefined;
f.componentProps.schema = column;
});
});
onFieldReact('operation', (field: Field) => {
console.log('operation', field.value);
const operation = field.value || {};
field.query('value').take((f: Field) => {
f.visible = !operation.noValue;
if (operation.noValue) {
f.value = undefined;
}
f.componentProps.operation = operation;
});
});
onFormValuesChange((form) => {
const { column, operation, value } = form?.values || {};
if (!operation?.value) {
return;
}
const fieldName = Object.keys(column.properties).shift();
if (operation?.noValue) {
onChange({
[fieldName]: {
[operation.value]: true,
},
});
} else {
onChange(
isValid(value)
? {
[fieldName]: {
[operation.value]: value,
},
}
: {},
);
}
console.log('form.values', form.values);
});
},
}),
[],
);
const columnEnum: any = [...columns.values()].map((column) => column.toJSON());
const schema: ISchema = {
type: 'void',
properties: {
space: {
type: 'void',
'x-component': 'AntdSpace',
properties: {
column: {
type: 'object',
name: 'column',
'x-decorator': 'FormilyFormItem',
'x-decorator-props': {
asterisk: true,
feedbackLayout: 'none',
},
'x-component': 'Select',
'x-component-props': {
objectValue: true,
style: {
width: 100,
},
fieldNames: {
label: 'title',
value: 'name',
options: 'options',
},
options: columnEnum,
},
enum: columnEnum,
},
operation: {
type: 'object',
name: 'operation',
'x-decorator': 'FormilyFormItem',
'x-decorator-props': {
asterisk: true,
feedbackLayout: 'none',
},
'x-component': 'Select',
'x-component-props': {
objectValue: true,
style: {
width: 100,
},
fieldNames: {
label: 'label',
value: 'value',
options: 'options',
},
options: values.operations,
},
enum: values.operations,
},
value: {
type: 'object',
name: 'value',
'x-decorator': 'FormilyFormItem',
'x-decorator-props': {
asterisk: true,
feedbackLayout: 'none',
},
'x-component': 'Filter.DynamicValue',
'x-component-props': {
schema: values.column,
operation: values.operation,
},
},
[uid()]: {
type: 'void',
'x-component': 'Remove',
},
},
},
},
};
return (
<FieldContext.Provider value={null}>
<FormContext.Provider value={form}>
<FormLayout layout={'inline'}>
<SchemaComponent schema={schema} components={{ AntdSpace, FormilyFormItem, Remove }}></SchemaComponent>
</FormLayout>
</FormContext.Provider>
</FieldContext.Provider>
);
};
export default FilterItem;

View File

@ -0,0 +1,83 @@
import { onFormReset } from '@formily/core';
import { useForm } from '@formily/react';
import { uid } from '@formily/shared';
import { useMap } from 'ahooks';
import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { FilterGroup } from './FilterGroup';
import { FilterItem } from './FilterItem';
export function FilterList(props) {
const { initialValue = [] } = props;
const form = useForm();
const { t } = useTranslation();
const [map, { set, setAll, remove, reset }] = useMap<string, any>(
initialValue.map((item, index) => {
return [`index-${index}`, item];
}),
);
useEffect(() => {
const id = uid();
form.addEffects(id, () => {
onFormReset((form) => {
setAll([]);
setTimeout(() => {
reset();
}, 0);
});
return () => {
form.removeEffects(id);
};
});
}, []);
useEffect(() => {
props.onChange?.([...map.values()]);
}, [map]);
return (
<div className={'nb-filter-list'}>
<div>
{[...map.entries()].map(([index, item]) => {
if (item.and || item.or) {
return (
<FilterGroup
key={index}
value={item}
onChange={(value: any) => set(index, value)}
onRemove={() => remove(index)}
/>
);
}
return (
<FilterItem
key={index}
value={item}
onChange={(value: any) => set(index, value)}
onRemove={() => remove(index)}
/>
);
})}
</div>
<div style={{ marginTop: 16 }}>
<a
onClick={() => {
set(uid(), {});
}}
>
{t('Add filter')}
</a>
<a
style={{ marginLeft: 16 }}
onClick={() => {
set(uid(), {
and: [{}],
});
}}
>
{t('Add filter group')}
</a>
</div>
</div>
);
}

View File

@ -0,0 +1,92 @@
/**
* title: Filter
*/
import { observer, useForm } from '@formily/react';
import { AntdSchemaComponentProvider, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema = {
type: 'void',
properties: {
demo: {
name: 'filter',
type: 'array',
'x-component': 'Filter',
'x-component-props': {
onChange: (value) => {
console.log('=====', JSON.stringify(value, null, 2));
},
},
default: {
and: [
{
field1: {
eq: 'aa',
},
},
{
field1: {
eq: 500,
},
},
],
},
properties: {
column1: {
type: 'void',
title: '字段1',
'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '等于', value: 'eq' },
{ label: '不等于', value: 'ne' },
],
},
properties: {
field1: {
type: 'string',
'x-component': 'Input',
},
},
},
column2: {
type: 'void',
title: '字段2',
'x-component': 'Filter.Column',
'x-component-props': {
operations: [
{ label: '大于', value: 'gt' },
{ label: '小于', value: 'lt' },
{ label: '非空', value: 'notNull', noValue: true },
],
},
properties: {
field2: {
type: 'number',
'x-component': 'InputNumber',
},
},
},
},
},
output: {
type: 'string',
'x-component': 'Output',
},
},
};
const Output = observer(() => {
const form = useForm();
return <pre>{JSON.stringify(form.values, null, 2)}</pre>;
});
export default () => {
return (
<SchemaComponentProvider>
<AntdSchemaComponentProvider>
<SchemaComponent components={{ Output }} schema={schema} />
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
);
};

View File

@ -6,3 +6,9 @@ group:
---
# Filter
## Examples
### Filter usage
<code src="./demos/demo1.tsx" />

View File

@ -0,0 +1 @@
export * from './Filter';

View File

@ -0,0 +1,18 @@
.nb-filter-group {
position: relative;
margin-bottom: 14px;
min-width: 400px;
&-close {
position: absolute;
right: 10px;
}
&.bordered {
border: 1px dashed rgb(222, 222, 222);
padding: 14px;
}
}
.nb-filter-list {
.ant-form-inline {
margin-bottom: 8px;
}
}

View File

@ -9,6 +9,7 @@ export * from './checkbox';
export * from './color-select';
export * from './date-picker';
export * from './dnd-context';
export * from './filter';
export * from './form';
export * from './form-item';
export * from './grid';
@ -29,4 +30,3 @@ export * from './time-picker';
export * from './tree-select';
export * from './upload';
export * from './void-table';

View File

@ -1,6 +1,6 @@
export * from './antd';
export * from './antd/AntdSchemaComponentProvider';
export * from './components';
export * from './context';
export * from './hooks';
export * from './types';