mirror of
https://gitee.com/nocobase/nocobase.git
synced 2024-11-29 18:58:26 +08:00
refactor: filter schema component (#213)
* refactor: filter schema component * feat: improve filter schema component * fix: cannot find module
This commit is contained in:
parent
5d974d7e32
commit
bc27359637
@ -40,7 +40,7 @@
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^4.9.1",
|
||||
"@typescript-eslint/parser": "^4.8.2",
|
||||
"antd": "^4.18.5",
|
||||
"antd": "^4.18.9",
|
||||
"classnames": "^2.3.1",
|
||||
"concurrently": "^7.0.0",
|
||||
"cross-env": "^5.2.0",
|
||||
|
@ -24,7 +24,7 @@
|
||||
"@formily/core": "^2.0.7",
|
||||
"@formily/react": "^2.0.7",
|
||||
"ahooks": "^3.0.5",
|
||||
"antd": "^4.18.5",
|
||||
"antd": "^4.18.9",
|
||||
"axios": "^0.24.0",
|
||||
"classnames": "^2.3.1",
|
||||
"file-saver": "^2.0.5",
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { observer, useField } from '@formily/react';
|
||||
import { Button, Modal } from 'antd';
|
||||
import { observer, RecursionField, useField, useFieldSchema } from '@formily/react';
|
||||
import { Button, Modal, Popover } from 'antd';
|
||||
import classnames from 'classnames';
|
||||
import React, { useState } from 'react';
|
||||
import { useActionContext } from '../..';
|
||||
import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings';
|
||||
import { SortableItem } from '../../common';
|
||||
import { useDesigner } from '../../hooks';
|
||||
@ -62,43 +63,89 @@ export const actionDesignerCss = css`
|
||||
`;
|
||||
|
||||
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 Designer = useDesigner();
|
||||
const field = useField();
|
||||
const { run } = useAction();
|
||||
const fieldSchema = useFieldSchema();
|
||||
const renderButton = () => (
|
||||
<SortableItem
|
||||
{...others}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const onOk = () => {
|
||||
onClick?.(e);
|
||||
setVisible(true);
|
||||
run();
|
||||
};
|
||||
if (confirm) {
|
||||
Modal.confirm({
|
||||
...confirm,
|
||||
onOk,
|
||||
});
|
||||
} else {
|
||||
onOk();
|
||||
}
|
||||
}}
|
||||
component={component || Button}
|
||||
className={classnames(className, actionDesignerCss)}
|
||||
>
|
||||
<Designer />
|
||||
{field.title}
|
||||
</SortableItem>
|
||||
);
|
||||
return (
|
||||
<ActionContext.Provider value={{ visible, setVisible, openMode, containerRefKey }}>
|
||||
<SortableItem
|
||||
{...others}
|
||||
onClick={(e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const onOk = () => {
|
||||
onClick?.(e);
|
||||
setVisible(true);
|
||||
run();
|
||||
};
|
||||
if (confirm) {
|
||||
Modal.confirm({
|
||||
...confirm,
|
||||
onOk,
|
||||
});
|
||||
} else {
|
||||
onOk();
|
||||
}
|
||||
}}
|
||||
component={component || Button}
|
||||
className={classnames(className, actionDesignerCss)}
|
||||
>
|
||||
<Designer />
|
||||
{field.title}
|
||||
</SortableItem>
|
||||
{props.children}
|
||||
<ActionContext.Provider value={{ button: renderButton(), visible, setVisible, openMode, containerRefKey }}>
|
||||
{popover && <RecursionField basePath={field.address} onlyRenderProperties schema={fieldSchema} />}
|
||||
{!popover && renderButton()}
|
||||
{!popover && props.children}
|
||||
</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 = () => {
|
||||
return (
|
||||
<GeneralSchemaDesigner>
|
||||
|
@ -3,6 +3,7 @@ import { createContext } from 'react';
|
||||
export const ActionContext = createContext<ActionContextProps>({});
|
||||
|
||||
export interface ActionContextProps {
|
||||
button?: any;
|
||||
visible?: boolean;
|
||||
setVisible?: (v: boolean) => void;
|
||||
openMode?: 'drawer' | 'modal' | 'page';
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -36,3 +36,7 @@ group:
|
||||
### 不同的打开方式
|
||||
|
||||
<code src="./demos/demo3.tsx"/>
|
||||
|
||||
### Action + Action.Popover
|
||||
|
||||
<code src="./demos/demo4.tsx"/>
|
||||
|
@ -41,6 +41,7 @@ export const Cascader = connect(
|
||||
});
|
||||
// 兼容值为 object[] 的情况
|
||||
const toValue = () => {
|
||||
return ['11'];
|
||||
return toArr(value).map((item) => {
|
||||
if (typeof item === 'object') {
|
||||
return item[fieldNames.value];
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
});
|
@ -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} />;
|
||||
});
|
@ -1,27 +1,33 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons';
|
||||
import { connect, mapProps, mapReadPretty } from '@formily/react';
|
||||
import { ObjectField as ObjectFieldModel } from '@formily/core';
|
||||
import { observer, useField } from '@formily/react';
|
||||
import React from 'react';
|
||||
import { FilterAction } from './Filter.Action';
|
||||
import { FilterDynamicValue } from './Filter.DynamicValue';
|
||||
import { useRequest } from '../../../api-client';
|
||||
import { FilterContext } from './context';
|
||||
import { FilterGroup } from './FilterGroup';
|
||||
import './style.less';
|
||||
import { SaveDefaultValue } from './SaveDefaultValue';
|
||||
|
||||
export const Filter: any = connect(
|
||||
(props) => {
|
||||
return <FilterGroup bordered={false} {...props} />;
|
||||
},
|
||||
mapProps((props, field) => {
|
||||
return {
|
||||
...props,
|
||||
suffix: <span>{field?.['loading'] || field?.['validating'] ? <LoadingOutlined /> : props.suffix}</span>,
|
||||
};
|
||||
}),
|
||||
mapReadPretty((props) => {
|
||||
return null;
|
||||
}),
|
||||
);
|
||||
const useDef = (options) => {
|
||||
const field = useField<ObjectFieldModel>();
|
||||
return useRequest(() => Promise.resolve({ data: field.dataSource }), options);
|
||||
};
|
||||
|
||||
Filter.DynamicValue = FilterDynamicValue;
|
||||
Filter.Action = FilterAction;
|
||||
export const Filter: any = observer((props: any) => {
|
||||
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>
|
||||
);
|
||||
});
|
||||
|
||||
export default Filter;
|
||||
Filter.SaveDefaultValue = SaveDefaultValue;
|
||||
|
@ -1,75 +1,88 @@
|
||||
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';
|
||||
import { ObjectField as ObjectFieldModel } from '@formily/core';
|
||||
import { ArrayField, connect, useField } from '@formily/react';
|
||||
import { Select, Space } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { FilterItems } from './FilterItems';
|
||||
|
||||
const toValue = (value) => {
|
||||
if (!value) {
|
||||
return {
|
||||
logical: 'and',
|
||||
list: [{}],
|
||||
export const FilterGroup = connect((props) => {
|
||||
const field = useField<ObjectFieldModel>();
|
||||
const remove = useContext(RemoveConditionContext);
|
||||
const { t } = useTranslation();
|
||||
const keys = Object.keys(field.value || {});
|
||||
const logic = keys.includes('$or') ? '$or' : '$and';
|
||||
const setLogic = (value) => {
|
||||
const obj = field.value || {};
|
||||
field.value = {
|
||||
[value]: obj[logic] || [],
|
||||
};
|
||||
}
|
||||
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={{
|
||||
position: 'relative',
|
||||
border: '1px dashed #dedede',
|
||||
padding: 14,
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
{remove && (
|
||||
<CloseCircleOutlined
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 10,
|
||||
}}
|
||||
onClick={() => remove()}
|
||||
/>
|
||||
)}
|
||||
<div style={{ marginBottom: 14 }}>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Trans>
|
||||
{'Meet '}
|
||||
<Select
|
||||
style={{ width: 80 }}
|
||||
onChange={(logical) => {
|
||||
onChange?.({
|
||||
[logical]: value.list,
|
||||
});
|
||||
value={logic}
|
||||
onChange={(value) => {
|
||||
setLogic(value);
|
||||
}}
|
||||
defaultValue={value.logical}
|
||||
>
|
||||
<Select.Option value={'and'}>All</Select.Option>
|
||||
<Select.Option value={'or'}>Any</Select.Option>
|
||||
<Select.Option value={'$and'}>All</Select.Option>
|
||||
<Select.Option value={'$or'}>Any</Select.Option>
|
||||
</Select>
|
||||
{' conditions in the group'}
|
||||
</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>
|
||||
<ArrayField name={logic} component={[FilterItems]} />
|
||||
</div>
|
||||
<Space size={16} style={{ marginTop: 8, marginBottom: 8 }}>
|
||||
<a
|
||||
onClick={() => {
|
||||
const value = field.value || {};
|
||||
const items = value[logic] || [];
|
||||
items.push({});
|
||||
field.value = {
|
||||
[logic]: items,
|
||||
};
|
||||
}}
|
||||
>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1,247 +1,54 @@
|
||||
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 '../../core';
|
||||
|
||||
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',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
import { observer, useField } from '@formily/react';
|
||||
import { Cascader, Select, Space } from 'antd';
|
||||
import React, { useContext } from 'react';
|
||||
import { RemoveConditionContext } from './context';
|
||||
import { DynamicComponent } from './DynamicComponent';
|
||||
import { useValues } from './useValues';
|
||||
|
||||
export const FilterItem = observer((props: any) => {
|
||||
const field = useField<any>();
|
||||
const remove = useContext(RemoveConditionContext);
|
||||
const { option, options, dataIndex, operator, setDataIndex, setOperator, value, setValue } = useValues();
|
||||
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>
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Space>
|
||||
<Cascader
|
||||
fieldNames={{
|
||||
label: 'title',
|
||||
value: 'name',
|
||||
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);
|
||||
},
|
||||
})}
|
||||
<CloseCircleOutlined onClick={() => remove()} />
|
||||
</Space>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FilterItem;
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
};
|
@ -1,8 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export const FilterContext = createContext<FilterContextProps>({});
|
||||
|
||||
export interface FilterContextProps {
|
||||
visible?: boolean;
|
||||
setVisible?: (v: boolean) => void;
|
||||
}
|
||||
export const RemoveConditionContext = createContext(null);
|
||||
export const FilterContext = createContext(null);
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
101
packages/client/src/schema-component/antd/filter/demos/demo2.tsx
Normal file
101
packages/client/src/schema-component/antd/filter/demos/demo2.tsx
Normal 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>
|
||||
);
|
||||
};
|
180
packages/client/src/schema-component/antd/filter/demos/demo3.tsx
Normal file
180
packages/client/src/schema-component/antd/filter/demos/demo3.tsx
Normal 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>
|
||||
);
|
||||
};
|
170
packages/client/src/schema-component/antd/filter/demos/demo4.tsx
Normal file
170
packages/client/src/schema-component/antd/filter/demos/demo4.tsx
Normal 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>
|
||||
);
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { useContext } from 'react';
|
||||
import { FilterContext } from './context';
|
||||
|
||||
export const useFilterContext = () => {
|
||||
const ctx = useContext(FilterContext);
|
||||
return {
|
||||
...ctx,
|
||||
};
|
||||
};
|
@ -9,6 +9,14 @@ group:
|
||||
|
||||
## 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" />
|
||||
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { createForm } from '@formily/core';
|
||||
import { FormProvider } from '@formily/react';
|
||||
import { FormProvider, Schema } from '@formily/react';
|
||||
import { uid } from '@formily/shared';
|
||||
import { useCookieState } from 'ahooks';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
@ -12,6 +12,26 @@ const randomString = (prefix: string = '') => {
|
||||
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) => {
|
||||
const { designable, components, children } = props;
|
||||
const [, setUid] = useState(uid());
|
||||
|
@ -1,3 +1,4 @@
|
||||
import flat from 'flat';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
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')}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -2,6 +2,7 @@ export * from './ActionInitializer';
|
||||
export * from './AddNewActionInitializer';
|
||||
export * from './CalendarBlockInitializer';
|
||||
export * from './CollectionFieldInitializer';
|
||||
export * from './FilterActionInitializer';
|
||||
export * from './FormBlockInitializer';
|
||||
export * from './GeneralInitializer';
|
||||
export * from './MarkdownBlockInitializer';
|
||||
|
@ -12,7 +12,7 @@ export const TableActionInitializers = {
|
||||
{
|
||||
type: 'item',
|
||||
title: "{{t('Filter')}}",
|
||||
component: 'ActionInitializer',
|
||||
component: 'FilterActionInitializer',
|
||||
schema: {
|
||||
title: '{{ t("Filter") }}',
|
||||
'x-action': 'filter',
|
||||
|
41
yarn.lock
41
yarn.lock
@ -4125,10 +4125,10 @@ ansi-styles@^5.0.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b"
|
||||
integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==
|
||||
|
||||
antd@^4.18.5:
|
||||
version "4.18.5"
|
||||
resolved "https://registry.npmjs.org/antd/-/antd-4.18.5.tgz#e5ffbe238fd6fdfcd1ed39ba96e4b1bd5f589757"
|
||||
integrity sha512-5fN3C2lWAzonhOYYlNpzIw2OHl7vxFZ+4cJ7DK/XZrV+75OY61Y+OkanqMJwrFtDDamIez35OM7cAezGko9tew==
|
||||
antd@^4.18.9:
|
||||
version "4.18.9"
|
||||
resolved "https://registry.npmjs.org/antd/-/antd-4.18.9.tgz#a0d246786d5076ea7a53b67191cd39295fc2322a"
|
||||
integrity sha512-MbtFY2J8LvXUnxYH2QehdhP9qMEpHvOp7PmiTIHc7v6aSb+LILCibskRIMGNEKvvhBvsTdq0cnjanR9/IbqEAw==
|
||||
dependencies:
|
||||
"@ant-design/colors" "^6.0.0"
|
||||
"@ant-design/icons" "^4.7.0"
|
||||
@ -4145,8 +4145,8 @@ antd@^4.18.5:
|
||||
rc-collapse "~3.1.0"
|
||||
rc-dialog "~8.6.0"
|
||||
rc-drawer "~4.4.2"
|
||||
rc-dropdown "~3.2.0"
|
||||
rc-field-form "~1.22.0-2"
|
||||
rc-dropdown "~3.2.5"
|
||||
rc-field-form "~1.23.0"
|
||||
rc-image "~5.2.5"
|
||||
rc-input-number "~7.3.0"
|
||||
rc-mentions "~1.6.1"
|
||||
@ -4162,7 +4162,7 @@ antd@^4.18.5:
|
||||
rc-slider "~9.7.4"
|
||||
rc-steps "~4.1.0"
|
||||
rc-switch "~3.2.0"
|
||||
rc-table "~7.22.2"
|
||||
rc-table "~7.23.0"
|
||||
rc-tabs "~11.10.0"
|
||||
rc-textarea "~0.3.0"
|
||||
rc-tooltip "~5.1.1"
|
||||
@ -13360,7 +13360,7 @@ rc-drawer@~4.4.2:
|
||||
classnames "^2.2.6"
|
||||
rc-util "^5.7.0"
|
||||
|
||||
rc-dropdown@^3.2.0, rc-dropdown@~3.2.0:
|
||||
rc-dropdown@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.0.tgz#da6c2ada403842baee3a9e909a0b1a91ba3e1090"
|
||||
integrity sha512-j1HSw+/QqlhxyTEF6BArVZnTmezw2LnSmRk6I9W7BCqNCKaRwleRmMMs1PHbuaG8dKHVqP6e21RQ7vPBLVnnNw==
|
||||
@ -13369,10 +13369,19 @@ rc-dropdown@^3.2.0, rc-dropdown@~3.2.0:
|
||||
classnames "^2.2.6"
|
||||
rc-trigger "^5.0.4"
|
||||
|
||||
rc-field-form@~1.22.0-2:
|
||||
version "1.22.1"
|
||||
resolved "https://registry.npmjs.org/rc-field-form/-/rc-field-form-1.22.1.tgz#0bd2f4e730ff2f071529d00bef28e062362890f5"
|
||||
integrity sha512-LweU7nBeqmC5r3HDUjRprcOXXobHXp/TGIxD7ppBq5FX6Iptt3ibdpRVg4RSyNulBNGHOuknHlRcguuIpvVMVg==
|
||||
rc-dropdown@~3.2.5:
|
||||
version "3.2.5"
|
||||
resolved "https://registry.npmjs.org/rc-dropdown/-/rc-dropdown-3.2.5.tgz#c211e571d29d15e7f725b5a75fc8c7f371fc3348"
|
||||
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:
|
||||
"@babel/runtime" "^7.8.4"
|
||||
async-validator "^4.0.2"
|
||||
@ -13576,10 +13585,10 @@ rc-switch@~3.2.0:
|
||||
classnames "^2.2.1"
|
||||
rc-util "^5.0.1"
|
||||
|
||||
rc-table@~7.22.2:
|
||||
version "7.22.2"
|
||||
resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.22.2.tgz#218f3f53bc91660560a344c8290a91a841a60b0a"
|
||||
integrity sha512-Ng2gNkGi6ybl6dzneRn2H4Gp8XhIbRa5rXQ7ZhZcgWVmfVMok70UHGPXcf68tXW6O0/qckTf/eOVsoviSvK4sw==
|
||||
rc-table@~7.23.0:
|
||||
version "7.23.0"
|
||||
resolved "https://registry.npmjs.org/rc-table/-/rc-table-7.23.0.tgz#e5f76998ecf3246147d45ed311417c08886e6507"
|
||||
integrity sha512-Q1gneB2+lUa8EzCCfbrq+jO1qNSwQv1RUUXKB84W/Stdp4EvGOt2+QqGyfotMNM4JUw0fgGLwY+WjnhUhnLuQQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.10.1"
|
||||
classnames "^2.2.5"
|
||||
|
Loading…
Reference in New Issue
Block a user