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:
parent
2929c77775
commit
8e1543269f
@ -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>;
|
||||
};
|
84
packages/client/src/schema-component/antd/filter/Filter.tsx
Normal file
84
packages/client/src/schema-component/antd/filter/Filter.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
247
packages/client/src/schema-component/antd/filter/FilterItem.tsx
Normal file
247
packages/client/src/schema-component/antd/filter/FilterItem.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -6,3 +6,9 @@ group:
|
||||
---
|
||||
|
||||
# Filter
|
||||
|
||||
## Examples
|
||||
|
||||
### Filter usage
|
||||
|
||||
<code src="./demos/demo1.tsx" />
|
||||
|
@ -0,0 +1 @@
|
||||
export * from './Filter';
|
18
packages/client/src/schema-component/antd/filter/style.less
Normal file
18
packages/client/src/schema-component/antd/filter/style.less
Normal 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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
export * from './antd';
|
||||
export * from './antd/AntdSchemaComponentProvider';
|
||||
export * from './components';
|
||||
export * from './context';
|
||||
export * from './hooks';
|
||||
export * from './types';
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user