feat: add support for filter action

This commit is contained in:
chenos 2020-12-08 21:19:39 +08:00
parent a44bab62fc
commit 0d3d30e0c2
9 changed files with 584 additions and 17 deletions

View File

@ -0,0 +1,59 @@
import React, { useRef, useState } from 'react';
import { Button, Popover } from 'antd';
import ViewFactory from '@/components/views';
export function Filter(props) {
console.log(props);
const drawerRef = useRef<any>();
const [visible, setVisible] = useState(false);
const { title, viewName, collection_name } = props.schema;
const { activeTab = {}, item = {}, associatedName, associatedKey } = props;
const { association } = activeTab;
const params = {};
if (association) {
params['resourceName'] = association;
params['associatedName'] = associatedName;
params['associatedKey'] = associatedKey;
} else {
params['resourceName'] = collection_name;
params['resourceKey'] = item.itemId;
}
return (
<>
<Popover
title="设置筛选"
trigger="click"
visible={visible}
placement={'bottomLeft'}
onVisibleChange={(visible) => {
setVisible(visible);
}}
className={'filters-popover'}
style={{
}}
overlayStyle={{
minWidth: 500
}}
content={(
<>
<div className={'popover-button-mask'} onClick={() => setVisible(false)}></div>
<ViewFactory
{...props}
viewName={'filter'}
{...params}
/>
</>
)}
>
<Button type={'primary'} onClick={() => {
setVisible(true);
}}>{title}</Button>
</Popover>
</>
)
}
export default Filter;

View File

@ -3,6 +3,7 @@ import React from 'react';
import Create from './Create'; import Create from './Create';
import Update from './Update'; import Update from './Update';
import Destroy from './Destroy'; import Destroy from './Destroy';
import Filter from './Filter';
import { Space } from 'antd'; import { Space } from 'antd';
const ACTIONS = new Map<string, any>(); const ACTIONS = new Map<string, any>();
@ -14,6 +15,7 @@ export function registerAction(type: string, Action: any) {
registerAction('update', Update); registerAction('update', Update);
registerAction('create', Create); registerAction('create', Create);
registerAction('destroy', Destroy); registerAction('destroy', Destroy);
registerAction('filter', Filter);
export function getAction(type: string) { export function getAction(type: string) {
return ACTIONS.get(type); return ACTIONS.get(type);

View File

@ -0,0 +1,263 @@
import React, { useEffect, useState } from 'react';
import { Button, Select, Input, Space, Form, InputNumber, DatePicker } from 'antd';
import { ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
import useDynamicList from './useDynamicList';
import { connect } from '@formily/react-schema-renderer'
import { mapStyledProps } from '../shared'
import moment from 'moment';
export function FilterGroup(props: any) {
const { showDeleteButton = false, fields = [], onDelete, onChange, onAdd, dataSource = {} } = props;
const { list, getKey, push, remove, replace } = useDynamicList<any>(dataSource.list || [
{
type: 'item',
},
]);
return (
<div style={{marginBottom: 14, padding: 14, border: '1px dashed #dedede'}}>
<div style={{marginBottom: 14}}>
{' '}
<Select style={{width: 80}} onChange={(value) => {
onChange({...dataSource, andor: value});
}} defaultValue={'and'}>
<Select.Option value={'and'}></Select.Option>
<Select.Option value={'or'}></Select.Option>
</Select>
{' '}
</div>
<div>
{list.map((item, index) => {
// console.log(item);
const Component = item.type === 'group' ? FilterGroup : FilterItem;
return (
<div style={{marginBottom: 14}}>
{<Component
fields={fields}
dataSource={item}
showDeleteButton={list.length > 1}
onChange={(value) => {
replace(index, value);
const newList = [...list];
newList[index] = value;
onChange({...dataSource, list: newList});
// console.log(list, value, index);
}}
onDelete={() => {
remove(index);
const newList = [...list];
newList.splice(index, 1);
onChange({...dataSource, list: newList});
// console.log(list, index);
}}
/>}
</div>
);
})}
</div>
<div>
<Space>
<Button onClick={() => {
const data = {
type: 'item'
};
push(data);
const newList = [...list];
newList.push(data);
onChange({...dataSource, list: newList});
}}>
</Button>
<Button onClick={() => {
const data = {
type: 'group',
list: [
{
type: 'item',
},
],
};
push(data);
const newList = [...list];
newList.push(data);
onChange({...dataSource, list: newList});
}}>
</Button>
{showDeleteButton && <Button onClick={(e) => {
onDelete && onDelete(e);
}}>
</Button>}
</Space>
</div>
</div>
);
}
interface FieldOptions {
name: string;
title: string;
interface: string;
[key: string]: any;
}
interface FilterItemProps {
fields: FieldOptions[];
[key: string]: any;
}
const OP_MAP = {
string: [
{label: '等于', value: 'eq'},
{label: '不等于', value: 'neq'},
{label: '包含', value: 'cont'},
{label: '不包含', value: 'ncont'},
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
],
number: [
{label: '等于', value: 'eq'},
{label: '不等于', value: 'neq'},
{label: '大于', value: 'gt'},
{label: '大于等于', value: 'gte'},
{label: '小于', value: 'lt'},
{label: '小于等于', value: 'lte'},
{label: '介于', value: 'between'},
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
],
file: [
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
],
boolean: [
{label: '等于', value: 'eq'},
],
choices: [
{label: '等于', value: 'eq'},
{label: '不等于', value: 'neq'},
{label: '包含', value: 'cont'},
{label: '不包含', value: 'ncont'},
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
],
datetime: [
{label: '等于', value: 'eq'},
{label: '不等于', value: 'neq'},
{label: '大于', value: 'gt'},
{label: '大于等于', value: 'gte'},
{label: '小于', value: 'lt'},
{label: '小于等于', value: 'lte'},
{label: '介于', value: 'between'},
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
{label: '是今天', value: 'now'},
{label: '在今天之前', value: 'before_today'},
{label: '在今天之后', value: 'after_today'},
],
linkTo: [
{label: '包含', value: 'cont'},
{label: '不包含', value: 'ncont'},
{label: '非空', value: 'notnull'},
{label: '为空', value: 'null'},
],
};
const op = {
string: OP_MAP.string,
textarea: OP_MAP.string,
number: OP_MAP.number,
datetime: OP_MAP.datetime,
};
const StringInput = (props) => {
const { onChange, ...restProps } = props;
return (
<Input {...restProps} onChange={(e) => {
onChange(e.target.value);
}}/>
);
}
const controls = {
string: StringInput,
textarea: StringInput,
number: InputNumber,
// datetime: DatePicker,
datetime: (props) => {
const { value, onChange, ...restProps } = props;
const m = moment(value, 'YYYY-MM-DD HH:mm:ss');
return (
<DatePicker value={m.isValid() ? m : null} onChange={(value) => {
onChange(value ? value.format('YYYY-MM-DD HH:mm:ss') : null)
console.log(value.format('YYYY-MM-DD HH:mm:ss'));
}}/>
);
},
};
export function FilterItem(props: FilterItemProps) {
const { index, fields = [], showDeleteButton = false, onDelete, onChange, dataSource = {} } = props;
const [type, setType] = useState('string');
useEffect(() => {
const field = fields.find(field => field.name === dataSource.column);
if (field) {
setType(field.interface);
}
}, [
dataSource,
]);
const ValueControl = controls[type]||controls.string;
return (
<Input.Group compact>
<Select value={dataSource.column}
onChange={(value) => {
const field = fields.find(field => field.name === value);
if (field) {
setType(field.interface);
}
onChange({...dataSource, column: value});
}}
style={{ width: '30%' }} placeholder={'选择字段'}>
{fields.map(field => (
<Select.Option value={field.name}>{field.title}</Select.Option>
))}
</Select>
<Select value={dataSource.op} style={{ minWidth: 100 }}
onChange={(value) => {
onChange({...dataSource, op: value});
}}
>
{(op[type]||op.string).map(option => (
<Select.Option value={option.value}>{option.label}</Select.Option>
))}
</Select>
<ValueControl value={dataSource.value} onChange={(value) => {
onChange({...dataSource, value: value});
}} style={{ width: '30%' }}/>
{showDeleteButton && (
<Button onClick={(e) => {
onDelete && onDelete(e);
}}></Button>
)}
</Input.Group>
);
}
export const Filter = connect({
getProps: mapStyledProps,
})((props) => {
const dataSource = {
type: 'group',
list: [
{
type: 'item',
}
],
};
return <FilterGroup dataSource={dataSource} {...props}/>
});
export default Filter;

View File

@ -0,0 +1,160 @@
import { useCallback, useRef, useState } from 'react';
export default <T>(initialValue: T[]) => {
const counterRef = useRef(-1);
// key 存储器
const keyList = useRef<number[]>([]);
// 内部方法
const setKey = useCallback((index: number) => {
counterRef.current += 1;
keyList.current.splice(index, 0, counterRef.current);
}, []);
const [list, setList] = useState(() => {
(initialValue || []).forEach((_, index) => {
setKey(index);
});
return initialValue || [];
});
const resetList = (newList: T[] = []) => {
keyList.current = [];
counterRef.current = -1;
setList(() => {
(newList || []).forEach((_, index) => {
setKey(index);
});
return newList || [];
});
};
const insert = (index: number, obj: T) => {
setList((l) => {
const temp = [...l];
temp.splice(index, 0, obj);
setKey(index);
return temp;
});
};
const getAll = () => list;
const getKey = (index: number) => keyList.current[index];
const getIndex = (index: number) => keyList.current.findIndex((ele) => ele === index);
const merge = (index: number, obj: T[]) => {
setList((l) => {
const temp = [...l];
obj.forEach((_, i) => {
setKey(index + i);
});
temp.splice(index, 0, ...obj);
return temp;
});
};
const replace = (index: number, obj: T) => {
setList((l) => {
const temp = [...l];
temp[index] = obj;
return temp;
});
};
const remove = (index: number) => {
setList((l) => {
const temp = [...l];
temp.splice(index, 1);
// remove keys if necessary
try {
keyList.current.splice(index, 1);
} catch (e) {
console.error(e);
}
return temp;
});
};
const move = (oldIndex: number, newIndex: number) => {
if (oldIndex === newIndex) {
return;
}
setList((l) => {
const newList = [...l];
const temp = newList.filter((_: {}, index: number) => index !== oldIndex);
temp.splice(newIndex, 0, newList[oldIndex]);
// move keys if necessary
try {
const keyTemp = keyList.current.filter((_: {}, index: number) => index !== oldIndex);
keyTemp.splice(newIndex, 0, keyList.current[oldIndex]);
keyList.current = keyTemp;
} catch (e) {
console.error(e);
}
return temp;
});
};
const push = (obj: T) => {
setList((l) => {
setKey(l.length);
return l.concat([obj]);
});
};
const pop = () => {
// remove keys if necessary
try {
keyList.current = keyList.current.slice(0, keyList.current.length - 1);
} catch (e) {
console.error(e);
}
setList((l) => l.slice(0, l.length - 1));
};
const unshift = (obj: T) => {
setList((l) => {
setKey(0);
return [obj].concat(l);
});
};
const sortForm = (result: unknown[]) =>
result
.map((item, index) => ({ key: index, item })) // add index into obj
.sort((a, b) => getIndex(a.key) - getIndex(b.key)) // sort based on the index of table
.filter((item) => !!item.item) // remove undefined(s)
.map((item) => item.item); // retrive the data
const shift = () => {
// remove keys if necessary
try {
keyList.current = keyList.current.slice(1, keyList.current.length);
} catch (e) {
console.error(e);
}
setList((l) => l.slice(1, l.length));
};
return {
list,
insert,
merge,
replace,
remove,
getAll,
getKey,
getIndex,
move,
push,
pop,
unshift,
shift,
sortForm,
resetList,
};
};

View File

@ -13,6 +13,7 @@ import { Radio } from './radio'
import { Range } from './range' import { Range } from './range'
import { Rating } from './rating' import { Rating } from './rating'
import { Upload } from './upload' import { Upload } from './upload'
import { Filter } from './filter'
export const setup = () => { export const setup = () => {
registerFormFields({ registerFormFields({
@ -37,6 +38,7 @@ export const setup = () => {
radio: Radio.Group, radio: Radio.Group,
range: Range, range: Range,
rating: Rating, rating: Rating,
upload: Upload upload: Upload,
filter: Filter,
}) })
} }

View File

@ -0,0 +1,54 @@
import React from 'react';
import { Tooltip, Card } from 'antd';
import {
SchemaForm,
SchemaMarkupField as Field,
createFormActions,
createAsyncFormActions,
Submit,
Reset,
FormButtonGroup,
registerFormFields,
FormValidator,
setValidationLanguage,
} from '@formily/antd';
import { QuestionCircleOutlined } from '@ant-design/icons';
export function FilterForm(props: any) {
const actions = createAsyncFormActions();
const { title, fields: properties ={} } = props.schema||{};
return (
<SchemaForm
colon={true}
layout={'vertical'}
initialValues={{}}
actions={actions}
schema={{
type: 'object',
properties,
}}
expressionScope={{
text(...args: any[]) {
return React.createElement('span', {}, ...args)
},
tooltip(title: string, offset = 3) {
return (
<Tooltip title={title}>
<QuestionCircleOutlined
style={{ margin: '0 3px', cursor: 'default', marginLeft: offset }}
/>
</Tooltip>
);
},
}}
>
<FormButtonGroup>
<Reset></Reset>
<Submit onClick={async () => {
const { values = {} } = await actions.submit();
console.log(values);
}}></Submit>
</FormButtonGroup>
</SchemaForm>
);
}

View File

@ -19,3 +19,4 @@ setValidationLanguage('zh-CN');
export { Form } from './Form'; export { Form } from './Form';
export { DrawerForm } from './DrawerForm'; export { DrawerForm } from './DrawerForm';
export { FilterForm } from './FilterForm';

View File

@ -5,7 +5,7 @@ import { useRequest } from 'umi';
import { Spin } from '@nocobase/client'; import { Spin } from '@nocobase/client';
import { SimpleTable } from './SimpleTable'; import { SimpleTable } from './SimpleTable';
import { Table } from './Table'; import { Table } from './Table';
import { Form, DrawerForm } from './Form/index'; import { Form, DrawerForm, FilterForm } from './Form/index';
import { Details } from './Details'; import { Details } from './Details';
import './style.less'; import './style.less';
import { Login } from './Form/Login'; import { Login } from './Form/Login';
@ -21,6 +21,7 @@ export function getViewTemplate(template: string) {
return TEMPLATES.get(template); return TEMPLATES.get(template);
} }
registerView('FilterForm', FilterForm)
registerView('DrawerForm', DrawerForm); registerView('DrawerForm', DrawerForm);
registerView('PermissionForm', DrawerForm); registerView('PermissionForm', DrawerForm);
registerView('Form', Form); registerView('Form', Form);

View File

@ -21,10 +21,11 @@ const transforms = {
const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode); const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode);
const schema = {}; const schema = {};
for (const field of fields) { for (const field of fields) {
if (!get(field.component, 'showInForm')) { if (!field.get('component.showInForm')) {
continue; continue;
} }
const type = get(field.component, 'type', 'string'); const interfaceType = field.get('interface');
const type = field.get('component.type') || 'string';
const prop: any = { const prop: any = {
type, type,
title: field.title||field.name, title: field.title||field.name,
@ -40,7 +41,7 @@ const transforms = {
if (defaultValue) { if (defaultValue) {
prop.default = defaultValue; prop.default = defaultValue;
} }
if (['radio', 'select', 'checkboxes'].includes(type)) { if (['radio', 'select', 'checkboxes'].includes(interfaceType)) {
prop.enum = get(field.options, 'dataSource', []); prop.enum = get(field.options, 'dataSource', []);
} }
schema[field.name] = { schema[field.name] = {
@ -62,12 +63,23 @@ const transforms = {
} }
return arr; return arr;
}, },
filter: async (fields: Model[], ctx?: any) => {
const properties = {
filter: {
type: 'filter',
'x-component-props': {
fields,
},
}
}
return properties;
},
}; };
export default async (ctx, next) => { export default async (ctx, next) => {
const { resourceName, resourceKey } = ctx.action.params; const { resourceName, resourceKey } = ctx.action.params;
const [View, Field, Action] = ctx.db.getModels(['views', 'fields', 'actions']) as ModelCtor<Model>[]; const [View, Collection, Field, Action] = ctx.db.getModels(['views', 'collections', 'fields', 'actions']) as ModelCtor<Model>[];
const view = await View.findOne(View.parseApiJson({ let view = await View.findOne(View.parseApiJson({
filter: { filter: {
collection_name: resourceName, collection_name: resourceName,
name: resourceKey, name: resourceKey,
@ -76,8 +88,15 @@ export default async (ctx, next) => {
// appends: ['actions', 'fields'], // appends: ['actions', 'fields'],
// }, // },
})); }));
// console.log('getView', ctx.action.params, mode); if (!view) {
const collection = await view.getCollection(); // 如果不存在 view新建一个
view = new View({type: resourceKey, template: 'FilterForm'});
}
const collection = await Collection.findOne({
where: {
name: resourceName,
},
});
const fields = await collection.getFields({ const fields = await collection.getFields({
where: { where: {
developerMode: ctx.state.developerMode, developerMode: ctx.state.developerMode,
@ -94,9 +113,8 @@ export default async (ctx, next) => {
['sort', 'asc'], ['sort', 'asc'],
] ]
}); });
const actionNames = view.options.actionNames||[]; const actionNames = view.get('actionNames') || [];
console.log(view.options); if (view.get('type') === 'table') {
if (view.type === 'table') {
const defaultTabs = await collection.getTabs({ const defaultTabs = await collection.getTabs({
where: { where: {
default: true, default: true,
@ -104,14 +122,21 @@ export default async (ctx, next) => {
}); });
view.setDataValue('defaultTabName', get(defaultTabs, [0, 'name'])); view.setDataValue('defaultTabName', get(defaultTabs, [0, 'name']));
} }
if (view.options.updateViewName) { if (view.get('updateViewName')) {
view.setDataValue('rowViewName', view.options.updateViewName); view.setDataValue('rowViewName', view.get('updateViewName'));
} }
view.setDataValue('viewCollectionName', view.collection_name); view.setDataValue('viewCollectionName', view.collection_name);
let title = collection.get('title');
const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode);
if (mode === 'update') {
title = `编辑${title}`;
} else {
title = `创建${title}`;
}
ctx.body = { ctx.body = {
...view.toJSON(), ...view.get(),
...(view.options||{}), title,
ofs: fields, original: fields,
fields: await (transforms[view.type]||transforms.table)(fields, ctx), fields: await (transforms[view.type]||transforms.table)(fields, ctx),
actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({ actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({
...action.toJSON(), ...action.toJSON(),