diff --git a/packages/app/src/components/actions/Filter.tsx b/packages/app/src/components/actions/Filter.tsx new file mode 100644 index 0000000000..2706842c27 --- /dev/null +++ b/packages/app/src/components/actions/Filter.tsx @@ -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(); + 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 ( + <> + { + setVisible(visible); + }} + className={'filters-popover'} + style={{ + }} + overlayStyle={{ + minWidth: 500 + }} + content={( + <> +
setVisible(false)}>
+ + + )} + > + +
+ + ) +} + +export default Filter; diff --git a/packages/app/src/components/actions/index.tsx b/packages/app/src/components/actions/index.tsx index 542282c426..f059b8192a 100644 --- a/packages/app/src/components/actions/index.tsx +++ b/packages/app/src/components/actions/index.tsx @@ -3,6 +3,7 @@ import React from 'react'; import Create from './Create'; import Update from './Update'; import Destroy from './Destroy'; +import Filter from './Filter'; import { Space } from 'antd'; const ACTIONS = new Map(); @@ -14,6 +15,7 @@ export function registerAction(type: string, Action: any) { registerAction('update', Update); registerAction('create', Create); registerAction('destroy', Destroy); +registerAction('filter', Filter); export function getAction(type: string) { return ACTIONS.get(type); diff --git a/packages/app/src/components/form.fields/filter/index.tsx b/packages/app/src/components/form.fields/filter/index.tsx new file mode 100644 index 0000000000..8714583e53 --- /dev/null +++ b/packages/app/src/components/form.fields/filter/index.tsx @@ -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(dataSource.list || [ + { + type: 'item', + }, + ]); + return ( +
+
+ 满足组内 + {' '} + + {' '} + 条件 +
+
+ {list.map((item, index) => { + // console.log(item); + const Component = item.type === 'group' ? FilterGroup : FilterItem; + return ( +
+ { 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); + }} + />} +
+ ); + })} +
+
+ + + + {showDeleteButton && } + +
+
+ ); +} + +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 ( + { + 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 ( + { + 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 ( + + + + { + onChange({...dataSource, value: value}); + }} style={{ width: '30%' }}/> + {showDeleteButton && ( + + )} + + ); +} + +export const Filter = connect({ + getProps: mapStyledProps, +})((props) => { + const dataSource = { + type: 'group', + list: [ + { + type: 'item', + } + ], + }; + return +}); + +export default Filter; diff --git a/packages/app/src/components/form.fields/filter/useDynamicList.ts b/packages/app/src/components/form.fields/filter/useDynamicList.ts new file mode 100644 index 0000000000..31b7b7a9e0 --- /dev/null +++ b/packages/app/src/components/form.fields/filter/useDynamicList.ts @@ -0,0 +1,160 @@ +import { useCallback, useRef, useState } from 'react'; + +export default (initialValue: T[]) => { + const counterRef = useRef(-1); + // key 存储器 + const keyList = useRef([]); + + // 内部方法 + 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, + }; +}; diff --git a/packages/app/src/components/form.fields/registry.ts b/packages/app/src/components/form.fields/registry.ts index 740d007e83..2badc2b5dd 100644 --- a/packages/app/src/components/form.fields/registry.ts +++ b/packages/app/src/components/form.fields/registry.ts @@ -13,6 +13,7 @@ import { Radio } from './radio' import { Range } from './range' import { Rating } from './rating' import { Upload } from './upload' +import { Filter } from './filter' export const setup = () => { registerFormFields({ @@ -37,6 +38,7 @@ export const setup = () => { radio: Radio.Group, range: Range, rating: Rating, - upload: Upload + upload: Upload, + filter: Filter, }) } diff --git a/packages/app/src/components/views/Form/FilterForm.tsx b/packages/app/src/components/views/Form/FilterForm.tsx new file mode 100644 index 0000000000..d43168b293 --- /dev/null +++ b/packages/app/src/components/views/Form/FilterForm.tsx @@ -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 ( + + + + ); + }, + }} + > + + 取消 + { + const { values = {} } = await actions.submit(); + console.log(values); + }}>确定 + + + ); +} diff --git a/packages/app/src/components/views/Form/index.tsx b/packages/app/src/components/views/Form/index.tsx index 8bb51f3a13..603c566783 100644 --- a/packages/app/src/components/views/Form/index.tsx +++ b/packages/app/src/components/views/Form/index.tsx @@ -19,3 +19,4 @@ setValidationLanguage('zh-CN'); export { Form } from './Form'; export { DrawerForm } from './DrawerForm'; +export { FilterForm } from './FilterForm'; diff --git a/packages/app/src/components/views/index.tsx b/packages/app/src/components/views/index.tsx index b71a247db8..7b3b461f55 100644 --- a/packages/app/src/components/views/index.tsx +++ b/packages/app/src/components/views/index.tsx @@ -5,7 +5,7 @@ import { useRequest } from 'umi'; import { Spin } from '@nocobase/client'; import { SimpleTable } from './SimpleTable'; import { Table } from './Table'; -import { Form, DrawerForm } from './Form/index'; +import { Form, DrawerForm, FilterForm } from './Form/index'; import { Details } from './Details'; import './style.less'; import { Login } from './Form/Login'; @@ -21,6 +21,7 @@ export function getViewTemplate(template: string) { return TEMPLATES.get(template); } +registerView('FilterForm', FilterForm) registerView('DrawerForm', DrawerForm); registerView('PermissionForm', DrawerForm); registerView('Form', Form); diff --git a/packages/plugin-pages/src/actions/getView.ts b/packages/plugin-pages/src/actions/getView.ts index 06c8a8f3de..240a0576aa 100644 --- a/packages/plugin-pages/src/actions/getView.ts +++ b/packages/plugin-pages/src/actions/getView.ts @@ -21,10 +21,11 @@ const transforms = { const mode = get(ctx.action.params, ['values', 'mode'], ctx.action.params.mode); const schema = {}; for (const field of fields) { - if (!get(field.component, 'showInForm')) { + if (!field.get('component.showInForm')) { continue; } - const type = get(field.component, 'type', 'string'); + const interfaceType = field.get('interface'); + const type = field.get('component.type') || 'string'; const prop: any = { type, title: field.title||field.name, @@ -40,7 +41,7 @@ const transforms = { if (defaultValue) { prop.default = defaultValue; } - if (['radio', 'select', 'checkboxes'].includes(type)) { + if (['radio', 'select', 'checkboxes'].includes(interfaceType)) { prop.enum = get(field.options, 'dataSource', []); } schema[field.name] = { @@ -62,12 +63,23 @@ const transforms = { } return arr; }, + filter: async (fields: Model[], ctx?: any) => { + const properties = { + filter: { + type: 'filter', + 'x-component-props': { + fields, + }, + } + } + return properties; + }, }; export default async (ctx, next) => { const { resourceName, resourceKey } = ctx.action.params; - const [View, Field, Action] = ctx.db.getModels(['views', 'fields', 'actions']) as ModelCtor[]; - const view = await View.findOne(View.parseApiJson({ + const [View, Collection, Field, Action] = ctx.db.getModels(['views', 'collections', 'fields', 'actions']) as ModelCtor[]; + let view = await View.findOne(View.parseApiJson({ filter: { collection_name: resourceName, name: resourceKey, @@ -76,8 +88,15 @@ export default async (ctx, next) => { // appends: ['actions', 'fields'], // }, })); - // console.log('getView', ctx.action.params, mode); - const collection = await view.getCollection(); + if (!view) { + // 如果不存在 view,新建一个 + view = new View({type: resourceKey, template: 'FilterForm'}); + } + const collection = await Collection.findOne({ + where: { + name: resourceName, + }, + }); const fields = await collection.getFields({ where: { developerMode: ctx.state.developerMode, @@ -94,9 +113,8 @@ export default async (ctx, next) => { ['sort', 'asc'], ] }); - const actionNames = view.options.actionNames||[]; - console.log(view.options); - if (view.type === 'table') { + const actionNames = view.get('actionNames') || []; + if (view.get('type') === 'table') { const defaultTabs = await collection.getTabs({ where: { default: true, @@ -104,14 +122,21 @@ export default async (ctx, next) => { }); view.setDataValue('defaultTabName', get(defaultTabs, [0, 'name'])); } - if (view.options.updateViewName) { - view.setDataValue('rowViewName', view.options.updateViewName); + if (view.get('updateViewName')) { + view.setDataValue('rowViewName', view.get('updateViewName')); } 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 = { - ...view.toJSON(), - ...(view.options||{}), - ofs: fields, + ...view.get(), + title, + original: fields, fields: await (transforms[view.type]||transforms.table)(fields, ctx), actions: actions.filter(action => actionNames.includes(action.name)).map(action => ({ ...action.toJSON(),