refactor: filter schema component (#213)

* refactor: filter schema component

* feat: improve filter schema component

* fix: cannot find module
This commit is contained in:
chenos 2022-03-01 18:06:06 +08:00 committed by GitHub
parent 5d974d7e32
commit bc27359637
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
31 changed files with 1016 additions and 713 deletions

View File

@ -40,7 +40,7 @@
"@types/react-dom": "^17.0.0", "@types/react-dom": "^17.0.0",
"@typescript-eslint/eslint-plugin": "^4.9.1", "@typescript-eslint/eslint-plugin": "^4.9.1",
"@typescript-eslint/parser": "^4.8.2", "@typescript-eslint/parser": "^4.8.2",
"antd": "^4.18.5", "antd": "^4.18.9",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"concurrently": "^7.0.0", "concurrently": "^7.0.0",
"cross-env": "^5.2.0", "cross-env": "^5.2.0",

View File

@ -24,7 +24,7 @@
"@formily/core": "^2.0.7", "@formily/core": "^2.0.7",
"@formily/react": "^2.0.7", "@formily/react": "^2.0.7",
"ahooks": "^3.0.5", "ahooks": "^3.0.5",
"antd": "^4.18.5", "antd": "^4.18.9",
"axios": "^0.24.0", "axios": "^0.24.0",
"classnames": "^2.3.1", "classnames": "^2.3.1",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",

View File

@ -1,8 +1,9 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { observer, useField } from '@formily/react'; import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
import { Button, Modal } from 'antd'; import { Button, Modal, Popover } from 'antd';
import classnames from 'classnames'; import classnames from 'classnames';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { useActionContext } from '../..';
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
import { SortableItem } from '../../common'; import { SortableItem } from '../../common';
import { useDesigner } from '../../hooks'; import { useDesigner } from '../../hooks';
@ -62,13 +63,23 @@ export const actionDesignerCss = css`
`; `;
export const Action: ComposedAction = observer((props: any) => { export const Action: ComposedAction = observer((props: any) => {
const { confirm, openMode, containerRefKey, component, useAction = useA, onClick, className, ...others } = props; const {
popover,
confirm,
openMode,
containerRefKey,
component,
useAction = useA,
onClick,
className,
...others
} = props;
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const Designer = useDesigner(); const Designer = useDesigner();
const field = useField(); const field = useField();
const { run } = useAction(); const { run } = useAction();
return ( const fieldSchema = useFieldSchema();
<ActionContext.Provider value={{ visible, setVisible, openMode, containerRefKey }}> const renderButton = () => (
<SortableItem <SortableItem
{...others} {...others}
onClick={(e: React.MouseEvent) => { onClick={(e: React.MouseEvent) => {
@ -94,11 +105,47 @@ export const Action: ComposedAction = observer((props: any) => {
<Designer /> <Designer />
{field.title} {field.title}
</SortableItem> </SortableItem>
{props.children} );
return (
<ActionContext.Provider value={{ button: renderButton(), visible, setVisible, openMode, containerRefKey }}>
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
{!popover && renderButton()}
{!popover && props.children}
</ActionContext.Provider> </ActionContext.Provider>
); );
}); });
Action.Popover = observer((props) => {
const { button, visible, setVisible } = useActionContext();
return (
<Popover
{...props}
destroyTooltipOnHide
visible={visible}
onVisibleChange={(visible) => {
setVisible(visible);
}}
content={props.children}
>
{button}
</Popover>
);
});
Action.Popover.Footer = observer((props) => {
return (
<div
className={css`
display: flex;
justify-content: flex-end;
width: 100%;
`}
>
{props.children}
</div>
);
});
Action.Designer = () => { Action.Designer = () => {
return ( return (
<GeneralSchemaDesigner> <GeneralSchemaDesigner>

View File

@ -3,6 +3,7 @@ import { createContext } from 'react';
export const ActionContext = createContext<ActionContextProps>({}); export const ActionContext = createContext<ActionContextProps>({});
export interface ActionContextProps { export interface ActionContextProps {
button?: any;
visible?: boolean; visible?: boolean;
setVisible?: (v: boolean) => void; setVisible?: (v: boolean) => void;
openMode?: 'drawer' | 'modal' | 'page'; openMode?: 'drawer' | 'modal' | 'page';

View File

@ -0,0 +1,53 @@
import { FormItem, Input } from '@formily/antd';
import { ISchema, observer, useForm } from '@formily/react';
import { Action, Form, SchemaComponent, SchemaComponentProvider, useActionContext } from '@nocobase/client';
import React from 'react';
const useCloseAction = () => {
const { setVisible } = useActionContext();
const form = useForm();
return {
async run() {
setVisible(false);
form.submit((values) => {
console.log(values);
});
},
};
};
const schema: ISchema = {
type: 'object',
properties: {
action1: {
'x-component': 'Action',
'x-component-props': {
type: 'primary',
popover: true,
openMode: 'popover',
},
type: 'void',
title: 'Open',
properties: {
popover: {
type: 'void',
'x-component': 'Action.Popover',
properties: {
hello: {
type: 'void',
'x-content': 'Hello',
},
},
},
},
},
},
};
export default observer(() => {
return (
<SchemaComponentProvider scope={{ useCloseAction }} components={{ Form, Action, Input, FormItem }}>
<SchemaComponent schema={schema} />
</SchemaComponentProvider>
);
});

View File

@ -36,3 +36,7 @@ group:
### 不同的打开方式 ### 不同的打开方式
<code src="./demos/demo3.tsx"/> <code src="./demos/demo3.tsx"/>
### Action + Action.Popover
<code src="./demos/demo4.tsx"/>

View File

@ -41,6 +41,7 @@ export const Cascader = connect(
}); });
// 兼容值为 object[] 的情况 // 兼容值为 object[] 的情况
const toValue = () => { const toValue = () => {
return ['11'];
return toArr(value).map((item) => { return toArr(value).map((item) => {
if (typeof item === 'object') { if (typeof item === 'object') {
return item[fieldNames.value]; return item[fieldNames.value];

View File

@ -0,0 +1,46 @@
import { createForm, onFieldValueChange } from '@formily/core';
import React, { useContext, useMemo } from 'react';
import { FormProvider, SchemaComponent } from '../../../schema-component';
import { useComponent } from '../../hooks';
import { FilterContext } from './context';
export const DynamicComponent = (props) => {
const { dynamicComponent } = useContext(FilterContext);
const component = useComponent(dynamicComponent);
const form = useMemo(
() =>
createForm({
values: {
value: props.value,
},
effects() {
onFieldValueChange('value', (field) => {
props?.onChange?.(field.value);
});
},
}),
[JSON.stringify(props.schema), JSON.stringify(props.value)],
);
const renderSchemaComponent = () => {
return (
<SchemaComponent
schema={{
name: 'value',
'x-component': 'Input',
...props.schema,
}}
/>
);
};
return (
<FormProvider form={form}>
{component
? React.createElement(component, {
value: props.value,
onChange: props?.onChange,
renderSchemaComponent,
})
: renderSchemaComponent()}
</FormProvider>
);
};

View File

@ -1,85 +0,0 @@
import { FilterOutlined } from '@ant-design/icons';
import { FormButtonGroup, Submit } from '@formily/antd';
import { createForm } from '@formily/core';
import { FieldContext, FormContext, observer, useFieldSchema } from '@formily/react';
import { uid } from '@formily/shared';
import { Button, Popover } from 'antd';
import flatten from 'flat';
import React, { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useCollection, useCollectionManager } from '../../../collection-manager';
import { SchemaComponent } from '../../core';
import { useDesignable } from '../../hooks';
import { useCompile } from '../../hooks/useCompile';
import { useFilterContext } from './hooks';
export const FilterAction = observer((props: any) => {
const { t } = useTranslation();
const compile = useCompile();
const { DesignableBar } = useDesignable();
const filedSchema = useFieldSchema();
const form = useMemo(() => createForm(), []);
const { visible, setVisible } = useFilterContext();
const obj = flatten(form.values.filter || {});
const count = Object.values(obj).filter((i) => (Array.isArray(i) ? i.length : i)).length;
const { fields } = useCollection();
const { getInterface } = useCollectionManager();
const properties = {};
fields?.forEach((field) => {
const { operators } = getInterface(field.interface);
properties[uid()] = {
type: 'void',
title: field?.uiSchema?.title ?? field.title,
'x-component': 'Filter.Column',
'x-component-props': {
operations: [...operators],
},
properties: {
[field.name ?? uid()]: { ...field?.uiSchema },
},
};
});
const schema = {
type: 'void',
properties: {
filter: {
type: 'array',
'x-component': 'Filter',
properties,
},
},
};
return (
<Popover
trigger={['click']}
placement={'bottomLeft'}
visible={visible}
onVisibleChange={setVisible}
content={
<div>
<FieldContext.Provider value={null}>
<FormContext.Provider value={form}>
<SchemaComponent schema={schema} name="filter" />
<FormButtonGroup align={'right'}>
<Submit
onSubmit={() => {
const { filter } = form.values;
console.log('Table.Filter====', form.values);
setVisible(false);
}}
>
{t('Submit')}
</Submit>
</FormButtonGroup>
</FormContext.Provider>
</FieldContext.Provider>
</div>
}
>
<Button icon={<FilterOutlined />}>
{count > 0 ? t('{{count}} filter items', { count }) : compile(filedSchema.title)}
<DesignableBar />
</Button>
</Popover>
);
});

View File

@ -1,61 +0,0 @@
import { createForm, onFieldValueChange } from '@formily/core';
import type { ISchema, Schema } from '@formily/react';
import { connect } from '@formily/react';
import deepmerge from 'deepmerge';
import React, { useMemo } from 'react';
import { SchemaComponent } from '../../core';
interface DynamicValueProps {
value?: any;
onChange?: any;
schema?: Schema;
operation?: any;
}
export const FilterDynamicValue = 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 <SchemaComponent schema={schema} />;
});

View File

@ -1,27 +1,33 @@
import { LoadingOutlined } from '@ant-design/icons'; import { ObjectField as ObjectFieldModel } from '@formily/core';
import { connect, mapProps, mapReadPretty } from '@formily/react'; import { observer, useField } from '@formily/react';
import React from 'react'; import React from 'react';
import { FilterAction } from './Filter.Action'; import { useRequest } from '../../../api-client';
import { FilterDynamicValue } from './Filter.DynamicValue'; import { FilterContext } from './context';
import { FilterGroup } from './FilterGroup'; import { FilterGroup } from './FilterGroup';
import './style.less'; import { SaveDefaultValue } from './SaveDefaultValue';
export const Filter: any = connect( const useDef = (options) => {
(props) => { const field = useField<ObjectFieldModel>();
return <FilterGroup bordered={false} {...props} />; return useRequest(() => Promise.resolve({ data: field.dataSource }), options);
},
mapProps((props, field) => {
return {
...props,
suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>,
}; };
}),
mapReadPretty((props) => { export const Filter: any = observer((props: any) => {
return null; const { useDataSource = useDef, dynamicComponent } = props;
}), const field = useField<ObjectFieldModel>();
useDataSource({
onSuccess(data) {
console.log('onSuccess', data?.data);
field.dataSource = data?.data || [];
},
});
return (
<div>
<FilterContext.Provider value={{ dynamicComponent, options: field.dataSource || [] }}>
<FilterGroup {...props} />
</FilterContext.Provider>
<pre>{JSON.stringify(field.value, null, 2)}</pre>
</div>
); );
});
Filter.DynamicValue = FilterDynamicValue; Filter.SaveDefaultValue = SaveDefaultValue;
Filter.Action = FilterAction;
export default Filter;

View File

@ -1,75 +1,88 @@
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined } from '@ant-design/icons';
import { useForm } from '@formily/react'; import { ObjectField as ObjectFieldModel } from '@formily/core';
import { isValid } from '@formily/shared'; import { ArrayField, connect, useField } from '@formily/react';
import { Select } from 'antd'; import { Select, Space } from 'antd';
import cls from 'classnames'; import React, { useContext } from 'react';
import React from 'react'; import { Trans, useTranslation } from 'react-i18next';
import { Trans } from 'react-i18next'; import { RemoveConditionContext } from './context';
import { FilterList } from './FilterList'; import { FilterItems } from './FilterItems';
const toValue = (value) => { export const FilterGroup = connect((props) => {
if (!value) { const field = useField<ObjectFieldModel>();
return { const remove = useContext(RemoveConditionContext);
logical: 'and', const { t } = useTranslation();
list: [{}], const keys = Object.keys(field.value || {});
}; const logic = keys.includes('$or') ? '$or' : '$and';
} const setLogic = (value) => {
if (value.and) { const obj = field.value || {};
return { field.value = {
logical: 'and', [value]: obj[logic] || [],
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 ( return (
<div className={cls('nb-filter-group', { bordered })}> <div
{onRemove && ( style={{
<a className={'nb-filter-group-close'} onClick={() => onRemove()}> position: 'relative',
<CloseCircleOutlined /> border: '1px dashed #dedede',
</a> padding: 14,
)} marginBottom: 8,
<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> {remove && (
<Select.Option value={'or'}>Any</Select.Option> <CloseCircleOutlined
style={{
position: 'absolute',
right: 10,
}}
onClick={() => remove()}
/>
)}
<div style={{ marginBottom: 8 }}>
<Trans>
{'Meet '}
<Select
value={logic}
onChange={(value) => {
setLogic(value);
}}
>
<Select.Option value={'$and'}>All</Select.Option>
<Select.Option value={'$or'}>Any</Select.Option>
</Select> </Select>
{' conditions in the group'}
</Trans> </Trans>
</div> </div>
<FilterList <div>
initialValue={value.list} <ArrayField name={logic} component={[FilterItems]} />
onChange={(list: any[]) => { </div>
const values = { <Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
[value.logical]: list.filter((item) => isValid(item) && Object.keys(item).length), <a
onClick={() => {
const value = field.value || {};
const items = value[logic] || [];
items.push({});
field.value = {
[logic]: items,
}; };
onChange?.(values);
}} }}
/> >
{t('Add condition')}
</a>
<a
onClick={() => {
const value = field.value || {};
const items = value[logic] || [];
items.push({
$and: [{}],
});
field.value = {
[logic]: items,
};
}}
>
{t('Add condition group')}
</a>
</Space>
</div> </div>
); );
} });

View File

@ -1,247 +1,54 @@
import { CloseCircleOutlined } from '@ant-design/icons'; import { CloseCircleOutlined } from '@ant-design/icons';
import { FormItem as FormilyFormItem, FormLayout, Space as AntdSpace } from '@formily/antd'; import { observer, useField } from '@formily/react';
import { createForm, onFieldReact, onFieldValueChange, onFormValuesChange } from '@formily/core'; import { Cascader, Select, Space } from 'antd';
import { Field } from '@formily/core/esm/models/Field'; import React, { useContext } from 'react';
import { Form } from '@formily/core/esm/models/Form'; import { RemoveConditionContext } from './context';
import { import { DynamicComponent } from './DynamicComponent';
FieldContext, import { useValues } from './useValues';
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 '../../core';
function useFilterColumns(): Map<SchemaKey, Schema> { export const FilterItem = observer((props: any) => {
const schema = useFieldSchema(); const field = useField<any>();
const columns = schema.reduceProperties((columns, current) => { const remove = useContext(RemoveConditionContext);
if (current['x-component'] === 'Filter.Column') { const { option, options, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
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 ( return (
onRemove && ( <div style={{ marginBottom: 8 }}>
<a onClick={() => onRemove()}> <Space>
<CloseCircleOutlined /> <Cascader
</a> fieldNames={{
)
);
};
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', label: 'title',
value: 'name', value: 'name',
options: 'options', children: 'children',
}}
style={{
width: 150,
}}
changeOnSelect={false}
key={field.address.toString()}
value={dataIndex}
options={options}
onChange={(value) => {
setDataIndex(value);
}}
/>
<Select
value={operator}
options={option?.operators}
onChange={(value) => {
setOperator(value);
}}
style={{
minWidth: 100,
}}
/>
{React.createElement(DynamicComponent, {
value,
schema: option?.schema,
onChange(value) {
setValue(value);
}, },
options: columnEnum, })}
}, <CloseCircleOutlined onClick={() => remove()} />
enum: columnEnum, </Space>
}, </div>
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,21 @@
import { ArrayField as ArrayFieldModel } from '@formily/core';
import { ObjectField, observer, useField } from '@formily/react';
import React from 'react';
import { RemoveConditionContext } from './context';
import { FilterGroup } from './FilterGroup';
import { FilterItem } from './FilterItem';
export const FilterItems = observer((props) => {
const field = useField<ArrayFieldModel>();
return (
<div>
{field?.value?.map((item, index) => {
return (
<RemoveConditionContext.Provider value={() => field.remove(index)}>
<ObjectField name={index} component={[item.$and || item.$or ? FilterGroup : FilterItem]} />
</RemoveConditionContext.Provider>
);
})}
</div>
);
});

View File

@ -1,83 +0,0 @@
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,19 @@
import { css } from '@emotion/css';
import { Button } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const SaveDefaultValue = (props) => {
const { t } = useTranslation();
return (
<Button
className={css`
border-color: rgb(241, 139, 98);
color: rgb(241, 139, 98);
`}
type={'dashed'}
>
{t('Save conditions')}
</Button>
);
};

View File

@ -1,8 +1,4 @@
import { createContext } from 'react'; import { createContext } from 'react';
export const FilterContext = createContext<FilterContextProps>({}); export const RemoveConditionContext = createContext(null);
export const FilterContext = createContext(null);
export interface FilterContextProps {
visible?: boolean;
setVisible?: (v: boolean) => void;
}

View File

@ -1,92 +0,0 @@
/**
* 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

@ -0,0 +1,101 @@
import { AntdSchemaComponentProvider, Filter, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client';
import React from 'react';
const schema: any = {
type: 'void',
properties: {
demo: {
name: 'filter',
type: 'object',
enum: [
{
name: 'name',
title: 'Name',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
type: 'string',
title: 'Name',
'x-component': 'Input',
},
},
{
name: 'age',
title: 'Age',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
type: 'string',
title: 'Age',
'x-component': 'InputNumber',
},
},
{
name: 'tags',
title: 'Tags',
schema: {
title: 'Tags',
},
children: [
{
name: 'slug',
title: 'Slug',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
title: 'Slug',
type: 'string',
'x-component': 'Input',
},
},
{
name: 'title',
title: 'Title',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
title: 'Title',
type: 'string',
'x-component': 'Input',
},
},
],
},
],
default: {
$or: [
{
name: {
$ne: null,
},
},
{
'tags.title': {
$eq: 'aaa',
},
},
],
},
'x-component': 'Filter',
'x-component-props': {},
},
},
};
export default () => {
return (
<SchemaComponentProvider>
<AntdSchemaComponentProvider>
<SchemaComponent components={{ Input, Filter }} schema={schema} />
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,180 @@
import { ISchema, useForm } from '@formily/react';
import {
AntdSchemaComponentProvider,
Filter,
Input,
SchemaComponent,
SchemaComponentProvider,
useActionContext,
useRequest
} from '@nocobase/client';
import React from 'react';
const dataSource = [
{
name: 'name',
title: 'Name',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
type: 'string',
title: 'Name',
'x-component': 'Input',
},
},
{
name: 'age',
title: 'Age',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
type: 'string',
title: 'Age',
'x-component': 'InputNumber',
},
},
{
name: 'tags',
title: 'Tags',
schema: {
title: 'Tags',
},
children: [
{
name: 'slug',
title: 'Slug',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
title: 'Slug',
type: 'string',
'x-component': 'Input',
},
},
{
name: 'title',
title: 'Title',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
title: 'Title',
type: 'string',
'x-component': 'Input',
},
},
],
},
];
const schema: ISchema = {
type: 'object',
properties: {
action1: {
'x-component': 'Action',
'x-component-props': {
type: 'primary',
popover: true,
openMode: 'popover',
},
type: 'void',
title: 'Open',
properties: {
popover: {
type: 'void',
'x-decorator': 'Form',
'x-decorator-props': {},
'x-component': 'Action.Popover',
'x-component-props': {
trigger: 'click',
placement: 'bottomLeft',
},
properties: {
filter: {
type: 'object',
default: {
$or: [
{
name: {
$ne: 'aa',
},
},
{
'tags.title': {
$eq: 'aaa',
},
},
],
},
'x-component': 'Filter',
'x-component-props': {
useDataSource(options) {
return useRequest(
() =>
Promise.resolve({
data: dataSource,
}),
options,
);
},
},
},
footer: {
type: 'void',
'x-component': 'Action.Popover.Footer',
properties: {
actions: {
type: 'void',
'x-component': 'ActionBar',
properties: {
saveDefault: {
type: 'void',
title: 'Submit',
'x-component': 'Filter.SaveDefaultValue',
'x-component-props': {},
},
submit: {
type: 'void',
title: 'Submit',
'x-component': 'Action',
'x-component-props': {
type: 'primary',
useAction() {
const form = useForm();
const ctx = useActionContext();
return {
async run() {
ctx.setVisible(false);
console.log('form.values', JSON.stringify(form.values, null, 2));
},
};
},
},
},
},
},
},
},
},
},
},
},
},
};
export default () => {
return (
<SchemaComponentProvider>
<AntdSchemaComponentProvider>
<SchemaComponent components={{ Input, Filter }} schema={schema} />
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
);
};

View File

@ -0,0 +1,170 @@
import { ISchema } from '@formily/react';
import {
AntdSchemaComponentProvider,
Filter,
Input,
SchemaComponent,
SchemaComponentProvider,
Select
} from '@nocobase/client';
import { Space } from 'antd';
import React, { useState } from 'react';
const options: any = [
{
name: 'name',
title: 'Name',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
type: 'string',
title: 'Name',
'x-component': 'Input',
},
},
{
name: 'age',
title: 'Age',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
type: 'string',
title: 'Age',
'x-component': 'InputNumber',
},
},
{
name: 'tags',
title: 'Tags',
schema: {
title: 'Tags',
},
children: [
{
name: 'slug',
title: 'Slug',
operators: [
{ label: 'in', value: '$in' },
{ label: 'not', value: '$not' },
],
schema: {
title: 'Slug',
type: 'string',
'x-component': 'Input',
},
},
{
name: 'title',
title: 'Title',
operators: [
{ label: 'eq', value: '$eq' },
{ label: 'ne', value: '$ne' },
],
schema: {
title: 'Title',
type: 'string',
'x-component': 'Input',
},
},
],
},
];
const defaultValue = {
$or: [
{
name: {
$ne: '{{node1.field1}}',
},
},
{
'tags.title': {
$eq: 'aaa',
},
},
],
};
const schema: ISchema = {
type: 'void',
properties: {
demo: {
name: 'filter',
type: 'object',
enum: options,
default: defaultValue,
'x-component': 'Filter',
'x-component-props': {
dynamicComponent: 'CustomDynamicComponent',
},
},
},
};
const ExpRE = /^\s*\{\{([\s\S]*)\}\}\s*$/;
const CustomDynamicComponent = (props) => {
const { value, onChange, renderSchemaComponent } = props;
let matched = null;
if (typeof value === 'string') {
matched = ExpRE.exec(value);
}
const [source, setSource] = useState(matched ? 'node1' : 'default');
const options = [
{
label: '字段1',
value: `{{${source}.field1}}`,
},
{
label: '字段2',
value: `{{${source}.field2}}`,
},
];
return (
<Space>
<Select
style={{ minWidth: 120 }}
value={source}
onChange={(value) => {
setSource(value);
onChange(null);
}}
options={[
{
label: '默认',
value: 'default',
},
{
label: '节点1',
value: 'node1',
},
]}
/>
{source === 'default' ? (
renderSchemaComponent()
) : (
<Select
style={{ minWidth: 120 }}
onChange={(value) => {
onChange(value);
}}
value={value}
options={options}
/>
)}
</Space>
);
};
export default () => {
return (
<SchemaComponentProvider>
<AntdSchemaComponentProvider>
<SchemaComponent components={{ Input, Filter, CustomDynamicComponent }} schema={schema} />
</AntdSchemaComponentProvider>
</SchemaComponentProvider>
);
};

View File

@ -1,9 +0,0 @@
import { useContext } from 'react';
import { FilterContext } from './context';
export const useFilterContext = () => {
const ctx = useContext(FilterContext);
return {
...ctx,
};
};

View File

@ -9,6 +9,14 @@ group:
## Examples ## Examples
### Filter usage ### Filter Action
<code src="./demos/demo1.tsx" /> <code src="./demos/demo3.tsx" />
### Filter Default Value
<code src="./demos/demo2.tsx" />
### Custom Dynamic Component
<code src="./demos/demo4.tsx" />

View File

@ -0,0 +1,65 @@
import { useField } from '@formily/react';
import { Input } from 'antd';
import flat from 'flat';
import { useContext } from 'react';
import { FilterContext } from './context';
export const useValues = () => {
const { options } = useContext(FilterContext);
const field = useField<any>();
const obj = flat(field.value || {});
const key = Object.keys(obj).shift() || '';
const [path, others = ''] = key.split('.$');
let [operator] = others.split('.');
const dataIndex = path.split('.');
let maxDepth = dataIndex.length;
if (operator) {
operator = '$' + operator;
++maxDepth;
}
const values = flat(field.value || {}, { maxDepth });
const value = Object.values<any>(values).shift();
const findOption = (dataIndex) => {
let items = options;
let option;
dataIndex.forEach((name, index) => {
const item = items.find((item) => item.name === name);
if (item) {
option = item;
}
items = item?.children || [];
});
return option;
};
const option = findOption(dataIndex);
console.log('option', option);
const operators = option?.operators;
return {
option,
dataIndex,
options,
operators,
operator,
value,
component: [Input, {}],
// 当 dataIndex 变化value 清空
setDataIndex(di: string[]) {
const option = findOption(di);
const op = option?.operators?.[0]?.value || '$eq';
field.value = flat.unflatten({
[`${di.join('.')}.${op}`]: null,
});
},
// 如果只是 Operator 变化value 要保留
setOperator(op: string) {
field.value = flat.unflatten({
[`${dataIndex.join('.')}.${op}`]: value,
});
},
setValue(v: any) {
field.value = flat.unflatten({
[`${dataIndex.join('.')}.${operator || '$eq'}`]: v,
});
},
};
};

View File

@ -1,5 +1,5 @@
import { createForm } from '@formily/core'; import { createForm } from '@formily/core';
import { FormProvider } from '@formily/react'; import { FormProvider, Schema } from '@formily/react';
import { uid } from '@formily/shared'; import { uid } from '@formily/shared';
import { useCookieState } from 'ahooks'; import { useCookieState } from 'ahooks';
import React, { useMemo, useState } from 'react'; import React, { useMemo, useState } from 'react';
@ -12,6 +12,26 @@ const randomString = (prefix: string = '') => {
return `${prefix}${uid()}`; return `${prefix}${uid()}`;
}; };
Schema.silent(true);
const Registry = {
silent: true,
compile(expression: string, scope = {}) {
console.log('expression', expression);
if (Registry.silent) {
try {
return new Function('$root', `with($root) { return (${expression}); }`)(scope);
} catch {
return `{{${expression}}}`;
}
} else {
return new Function('$root', `with($root) { return (${expression}); }`)(scope);
}
},
};
Schema.registerCompiler(Registry.compile);
export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (props) => { export const SchemaComponentProvider: React.FC<ISchemaComponentProvider> = (props) => {
const { designable, components, children } = props; const { designable, components, children } = props;
const [, setUid] = useState(uid()); const [, setUid] = useState(uid());

View File

@ -1,3 +1,4 @@
import flat from 'flat';
import React from 'react'; import React from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { SchemaInitializer } from '../SchemaInitializer'; import { SchemaInitializer } from '../SchemaInitializer';
@ -33,6 +34,28 @@ export const GridFormItemInitializers = (props: any) => {
}, },
}, },
}, },
{
type: 'item',
title: t('Add Filter'),
component: 'GeneralInitializer',
schema: {
name: 'filter',
type: 'object',
default: flat.unflatten({
$or: [
{ 'aa.$eq': 'b' },
{ 'bb.field.$eq': ['aabb', 'aaa'] },
{
'bb.field': {
$eq: ['aabb', 'aaa'],
},
},
],
}),
'x-component': 'Filter',
'x-component-props': {},
},
},
]} ]}
> >
{t('Configure fields')} {t('Configure fields')}

View File

@ -0,0 +1,42 @@
import { Switch } from 'antd';
import flat from 'flat';
import React from 'react';
import { SchemaInitializer } from '../../SchemaInitializer';
import { useCurrentSchema } from '../utils';
export const FilterActionInitializer = (props) => {
const { item, insert } = props;
const { exists, remove } = useCurrentSchema(item.schema['x-action'], 'x-action', item.find);
return (
<SchemaInitializer.Item
onClick={() => {
if (exists) {
return remove();
}
insert({
...item.schema,
name: 'filter',
type: 'object',
default: flat.unflatten({
$or: [
{ 'aa.$eq': 'b' },
{ 'bb.field.$eq': ['aabb', 'aaa'] },
{
'bb.field': {
$eq: ['aabb', 'aaa'],
},
},
],
}),
'x-component': 'Filter',
'x-component-props': {},
});
}}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
{item.title} <Switch style={{ marginLeft: 20 }} size={'small'} checked={exists} />
</div>
</SchemaInitializer.Item>
);
};

View File

@ -2,6 +2,7 @@ export * from './ActionInitializer';
export * from './AddNewActionInitializer'; export * from './AddNewActionInitializer';
export * from './CalendarBlockInitializer'; export * from './CalendarBlockInitializer';
export * from './CollectionFieldInitializer'; export * from './CollectionFieldInitializer';
export * from './FilterActionInitializer';
export * from './FormBlockInitializer'; export * from './FormBlockInitializer';
export * from './GeneralInitializer'; export * from './GeneralInitializer';
export * from './MarkdownBlockInitializer'; export * from './MarkdownBlockInitializer';

View File

@ -12,7 +12,7 @@ export const TableActionInitializers = {
{ {
type: 'item', type: 'item',
title: "{{t('Filter')}}", title: "{{t('Filter')}}",
component: 'ActionInitializer', component: 'FilterActionInitializer',
schema: { schema: {
title: '{{ t("Filter") }}', title: '{{ t("Filter") }}',
'x-action': 'filter', 'x-action': 'filter',

View File

@ -4125,10 +4125,10 @@ ansi-styles@^5.0.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
antd@^4.18.5: antd@^4.18.9:
version "4.18.5" version "4.18.9"
resolved "https://registry.npmjs.org/antd/-/antd-4.18.5.tgz#e5ffbe238fd6fdfcd1ed39ba96e4b1bd5f589757" resolved "https://registry.npmjs.org/antd/-/antd-4.18.9.tgz#a0d246786d5076ea7a53b67191cd39295fc2322a"
integrity sha512-5fN3C2lWAzonhOYYlNpzIw2OHl7vxFZ+4cJ7DK/XZrV+75OY61Y+OkanqMJwrFtDDamIez35OM7cAezGko9tew== integrity sha512-MbtFY2J8LvXUnxYH2QehdhP9qMEpHvOp7PmiTIHc7v6aSb+LILCibskRIMGNEKvvhBvsTdq0cnjanR9/IbqEAw==
dependencies: dependencies:
"@ant-design/colors" "^6.0.0" "@ant-design/colors" "^6.0.0"
"@ant-design/icons" "^4.7.0" "@ant-design/icons" "^4.7.0"
@ -4145,8 +4145,8 @@ antd@^4.18.5:
rc-collapse "~3.1.0" rc-collapse "~3.1.0"
rc-dialog "~8.6.0" rc-dialog "~8.6.0"
rc-drawer "~4.4.2" rc-drawer "~4.4.2"
rc-dropdown "~3.2.0" rc-dropdown "~3.2.5"
rc-field-form "~1.22.0-2" rc-field-form "~1.23.0"
rc-image "~5.2.5" rc-image "~5.2.5"
rc-input-number "~7.3.0" rc-input-number "~7.3.0"
rc-mentions "~1.6.1" rc-mentions "~1.6.1"
@ -4162,7 +4162,7 @@ antd@^4.18.5:
rc-slider "~9.7.4" rc-slider "~9.7.4"
rc-steps "~4.1.0" rc-steps "~4.1.0"
rc-switch "~3.2.0" rc-switch "~3.2.0"
rc-table "~7.22.2" rc-table "~7.23.0"
rc-tabs "~11.10.0" rc-tabs "~11.10.0"
rc-textarea "~0.3.0" rc-textarea "~0.3.0"
rc-tooltip "~5.1.1" rc-tooltip "~5.1.1"
@ -13360,7 +13360,7 @@ rc-drawer@~4.4.2:
classnames "^2.2.6" classnames "^2.2.6"
rc-util "^5.7.0" rc-util "^5.7.0"
rc-dropdown@^3.2.0, rc-dropdown@~3.2.0: rc-dropdown@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090" resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090"
integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw== integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw==
@ -13369,10 +13369,19 @@ rc-dropdown@^3.2.0, rc-dropdown@~3.2.0:
classnames "^2.2.6" classnames "^2.2.6"
rc-trigger "^5.0.4" rc-trigger "^5.0.4"
rc-field-form@~1.22.0-2: rc-dropdown@~3.2.5:
version "1.22.1" version "3.2.5"
resolved "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.22.1.tgz#0bd2f4e730ff2f071529d00bef28e062362890f5" resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.5.tgz#c211e571d29d15e7f725b5a75fc8c7f371fc3348"
integrity sha512-LweU7nBeqmC5r3HDUjRprcOXXobHXp/TGIxD7ppBq5FX6Iptt3ibdpRVg4RSyNulBNGHOuknHlRcguuIpvVMVg== integrity sha512-dVO2eulOSbEf+F4OyhCY5iGiMVhUYY/qeXxL7Ex2jDBt/xc89jU07mNoowV6aWxwVOc70pxEINff0oM2ogjluA==
dependencies:
"@babel/runtime" "^7.10.1"
classnames "^2.2.6"
rc-trigger "^5.0.4"
rc-field-form@~1.23.0:
version "1.23.0"
resolved "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.23.0.tgz#71e1430be5934118f1d00733e89896ffd81aa970"
integrity sha512-Zv5rrkbc/KJ5EDoUxX28Wy6Y8FOas03hffcYeSE821z7b7V6YtLrtQyJlv+0725xdCphqUoD/S+7b2KLl67Hew==
dependencies: dependencies:
"@babel/runtime" "^7.8.4" "@babel/runtime" "^7.8.4"
async-validator "^4.0.2" async-validator "^4.0.2"
@ -13576,10 +13585,10 @@ rc-switch@~3.2.0:
classnames "^2.2.1" classnames "^2.2.1"
rc-util "^5.0.1" rc-util "^5.0.1"
rc-table@~7.22.2: rc-table@~7.23.0:
version "7.22.2" version "7.23.0"
resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.22.2.tgz#218f3f53bc91660560a344c8290a91a841a60b0a" resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.23.0.tgz#e5f76998ecf3246147d45ed311417c08886e6507"
integrity sha512-Ng2gNkGi6ybl6dzneRn2H4Gp8XhIbRa5rXQ7ZhZcgWVmfVMok70UHGPXcf68tXW6O0/qckTf/eOVsoviSvK4sw== integrity sha512-Q1gneB2+lUa8EzCCfbrq+jO1qNSwQv1RUUXKB84W/Stdp4EvGOt2+QqGyfotMNM4JUw0fgGLwY+WjnhUhnLuQQ==
dependencies: dependencies:
"@babel/runtime" "^7.10.1" "@babel/runtime" "^7.10.1"
classnames "^2.2.5" classnames "^2.2.5"