From a69074ead4c333f0f3d56fae3e55a0c40e568471 Mon Sep 17 00:00:00 2001 From: anuoua Date: Mon, 26 Dec 2022 11:35:35 +0800 Subject: [PATCH] feat: association filter (#1274) * feat: association filter * feat: association filter update * feat: association filter remove unused designer * feat: feat: association filter lowercase fix * feat: feat: association filter lowercase fix * feat: feat: association filter configure field fix * feat: association field remove AssociationFieldsFilter.Designer * feat: association field fixed layout * feat: associate filter multiple to simple * feat: association field "title" to "id" * feat: associaion filter interface limit * feat: association filter move to association-filter folder * feat: association filter change style * fix: card item error * fix: add RenderChildrenWithAssociationFilter * feat: association-filter fix style * feat: associate filter fix filter params * feat: assocition filter layout fix * feat: association filter change schema * feat: association filter rename * feat: association filter rename * feat: association filter break layout fix * feat: association filter fix table layout * feat: association filter fix ActionBar style * feat: association filter fix ActionBar style * feat: association filter bug fix * feat: association filter change valueKey * feat: association filter remove collectionFieldKey * feat: improve code * feat: association filter style fix * feat: association filter custom title * feat: association filter max height overscroll * feat: association filter add linkTo & i18n * feat: association filter ellipsis; * feat: association fields add linkTo Co-authored-by: chenos --- .../src/block-provider/BlockProvider.tsx | 65 ++++- .../block-provider/SharedFilterProvider.tsx | 64 ++++ .../src/block-provider/TableBlockProvider.tsx | 5 +- .../block-provider/TableSelectorProvider.tsx | 5 +- .../Configuration/schemas/collectionFields.ts | 2 +- packages/core/client/src/locale/en_US.ts | 2 + packages/core/client/src/locale/zh_CN.ts | 2 + .../antd/action/ActionBar.tsx | 23 +- .../ActionBarAssociationFilterAction.tsx | 35 +++ .../AssociationFilter.Initializer.tsx | 61 ++++ .../AssociationFilter.Item.Designer.tsx | 92 ++++++ .../AssociationFilter.Item.tsx | 276 ++++++++++++++++++ .../association-filter/AssociationFilter.tsx | 86 ++++++ .../AssociationFilterDesignerDelete.tsx | 25 ++ .../AssociationFilterDesignerDisplayField.tsx | 24 ++ .../antd/card-item/CardItem.tsx | 3 +- .../antd/filter/demos/demo2.tsx | 8 +- .../antd/filter/demos/demo3.tsx | 4 +- .../antd/filter/demos/demo4.tsx | 4 +- .../antd/filter/demos/demo6.tsx | 4 +- .../src/schema-component/antd/filter/index.ts | 1 - .../antd/filter/useFilterActionProps.ts | 35 ++- .../common/sortable-item/SortableItem.tsx | 2 +- .../schema-component/hooks/useDesignable.tsx | 2 +- .../SchemaInitializerProvider.tsx | 3 +- .../buttons/TableActionInitializers.tsx | 19 ++ .../src/schema-initializer/buttons/index.ts | 2 + .../items/InitializerWithSwitch.tsx | 2 +- .../src/schema-initializer/items/index.tsx | 5 + 29 files changed, 815 insertions(+), 46 deletions(-) create mode 100644 packages/core/client/src/block-provider/SharedFilterProvider.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/ActionBarAssociationFilterAction.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx create mode 100644 packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx diff --git a/packages/core/client/src/block-provider/BlockProvider.tsx b/packages/core/client/src/block-provider/BlockProvider.tsx index 1eba60655..4b6bf0c09 100644 --- a/packages/core/client/src/block-provider/BlockProvider.tsx +++ b/packages/core/client/src/block-provider/BlockProvider.tsx @@ -1,12 +1,22 @@ +import { css } from '@emotion/css'; import { Field } from '@formily/core'; -import { useField, useFieldSchema } from '@formily/react'; +import { RecursionField, useField, useFieldSchema } from '@formily/react'; import { useRequest } from 'ahooks'; +import { Col, Row } from 'antd'; import template from 'lodash/template'; import React, { createContext, useContext } from 'react'; import { Link } from 'react-router-dom'; -import { ACLCollectionProvider, TableFieldResource, useAPIClient, useRecord, WithoutTableFieldResource } from '../'; +import { + ACLCollectionProvider, + TableFieldResource, + useAPIClient, + useDesignable, + useRecord, + WithoutTableFieldResource, +} from '../'; import { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager'; import { useRecordIndex } from '../record-provider'; +import { SharedFilterProvider } from './SharedFilterProvider'; export const BlockResourceContext = createContext(null); export const BlockAssociationContext = createContext(null); @@ -143,6 +153,53 @@ export const useBlockRequestContext = () => { return useContext(BlockRequestContext); }; +export const RenderChildrenWithAssociationFilter: React.FC = (props) => { + const fieldSchema = useFieldSchema(); + const { findComponent } = useDesignable(); + const field = useField(); + const Component = findComponent(field.component?.[0]) || React.Fragment; + const associationFilterSchema = fieldSchema.reduceProperties((buf, s) => { + if (s['x-component'] === 'AssociationFilter') { + return s; + } + return buf; + }, null); + + if (associationFilterSchema) { + return ( + + + + s['x-component'] === 'AssociationFilter'} + /> + + + s['x-component'] !== 'AssociationFilter'} + /> + + + + ); + } + return props.children; +}; + export const BlockProvider = (props) => { const { collection, association } = props; const resource = useResource(props); @@ -150,7 +207,9 @@ export const BlockProvider = (props) => { - {props.children} + + + diff --git a/packages/core/client/src/block-provider/SharedFilterProvider.tsx b/packages/core/client/src/block-provider/SharedFilterProvider.tsx new file mode 100644 index 000000000..a65485348 --- /dev/null +++ b/packages/core/client/src/block-provider/SharedFilterProvider.tsx @@ -0,0 +1,64 @@ +import React, { createContext, FC, useState } from 'react'; + +export enum SHARED_FILTER_CONDITION { + AND = '$and', + OR = '$or', +} + +export type SharedFilter = { + [K in SHARED_FILTER_CONDITION]?: any; +}; + +export type SharedFilterStore = Record; + +export type SharedFilterContextValue = { + sharedFilterStore: SharedFilter; + setSharedFilterStore: (filterStore: SharedFilterStore) => void; + getFilterParams: (filterStore?: SharedFilterStore) => any; +}; + +export const SharedFilterContext = createContext({ + sharedFilterStore: {}, + setSharedFilterStore: undefined!, + getFilterParams: undefined!, +}); + +export const concatFilter = (f1: SharedFilter, f2: SharedFilter): SharedFilter => { + const newAnd = [f1.$and, f2.$and].filter((i) => i); + const newOr = [f1.$or, f2.$or].filter((i) => i); + const newFilter: SharedFilter = {}; + newAnd.length && (newFilter.$and = newAnd); + newOr.length && (newFilter.$or = newOr); + return newFilter; +}; + +export const SharedFilterProvider: FC<{ params?: any }> = (props) => { + const [sharedFilterStore, setSharedFilterStoreUnwrap] = useState>({}); + + const setSharedFilterStore = (associationFilter: Record) => { + setSharedFilterStoreUnwrap(associationFilter); + }; + + const getFilterParams = (filterStore?: SharedFilterStore) => { + const newAssociationFilterList = Object.entries(filterStore ?? sharedFilterStore).map(([key, filter]) => filter); + const newAssociationFilter = newAssociationFilterList.length + ? { + $and: newAssociationFilterList, + } + : {}; + + return newAssociationFilter; + }; + + return ( + + {props.children} + + ); +}; diff --git a/packages/core/client/src/block-provider/TableBlockProvider.tsx b/packages/core/client/src/block-provider/TableBlockProvider.tsx index 40c54b407..d14c2306b 100644 --- a/packages/core/client/src/block-provider/TableBlockProvider.tsx +++ b/packages/core/client/src/block-provider/TableBlockProvider.tsx @@ -3,8 +3,9 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react'; import uniq from 'lodash/uniq'; import React, { createContext, useContext, useEffect, useMemo } from 'react'; import { useCollectionManager } from '../collection-manager'; +import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { useFixedSchema } from '../schema-component'; -import { BlockProvider, useBlockRequestContext } from './BlockProvider'; + export const TableBlockContext = createContext({}); @@ -28,7 +29,7 @@ const InternalTableBlockProvider = (props) => { rowKey, }} > - {props.children} + ); }; diff --git a/packages/core/client/src/block-provider/TableSelectorProvider.tsx b/packages/core/client/src/block-provider/TableSelectorProvider.tsx index 4e8ef738a..e3b6f07ef 100644 --- a/packages/core/client/src/block-provider/TableSelectorProvider.tsx +++ b/packages/core/client/src/block-provider/TableSelectorProvider.tsx @@ -3,7 +3,7 @@ import { Schema, useField, useFieldSchema } from '@formily/react'; import React, { createContext, useContext, useEffect } from 'react'; import { useCollectionManager } from '../collection-manager'; import { RecordProvider, useRecord } from '../record-provider'; -import { BlockProvider, useBlockRequestContext } from './BlockProvider'; +import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider'; import { useFormBlockContext } from './FormBlockProvider'; export const TableSelectorContext = createContext({}); @@ -27,7 +27,7 @@ const InternalTableSelectorProvider = (props) => { rowKey, }} > - {props.children} + ); @@ -59,7 +59,6 @@ export const TableSelectorProvider = (props) => { // const value = ctx.form.query(collectionFieldSchema?.name).value(); const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); - console.log('TableSelectorProvider', collectionFieldSchema, collectionField, record); const params = { ...props.params }; const appends = useAssociationNames(props.collection); if (props.dragSort) { diff --git a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts index 6cf617ede..fb1daa7a2 100644 --- a/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts +++ b/packages/core/client/src/collection-manager/Configuration/schemas/collectionFields.ts @@ -223,7 +223,7 @@ export const overridingSchema: ISchema = { 'x-component': 'OverridingCollectionField', 'x-component-props': { type: 'primary', - 'currentCollection': '{{ currentCollection }}', + currentCollection: '{{ currentCollection }}', }, }, view: { diff --git a/packages/core/client/src/locale/en_US.ts b/packages/core/client/src/locale/en_US.ts index 6d8bbd8cf..7913d294a 100644 --- a/packages/core/client/src/locale/en_US.ts +++ b/packages/core/client/src/locale/en_US.ts @@ -133,6 +133,7 @@ export default { "Override": "Override", "Override field": "Override field", "Configure fields of {{title}}": "Configure fields of {{title}}", + "Association fields filter": "Association fields filter", "PK & FK fields": "PK & FK fields", "Association fields": "Association fields", "Parent collection fields": "Parent collection fields", @@ -273,6 +274,7 @@ export default { "Allow uploading multiple files": "Allow uploading multiple files", "Configure calendar": "Configure calendar", "Title field": "Title field", + "Custom title": "Custom title", "Start date field": "Start date field", "End date field": "End date field", "Navigate": "Navigate", diff --git a/packages/core/client/src/locale/zh_CN.ts b/packages/core/client/src/locale/zh_CN.ts index 4a4c9f675..7c6671bb7 100644 --- a/packages/core/client/src/locale/zh_CN.ts +++ b/packages/core/client/src/locale/zh_CN.ts @@ -138,6 +138,7 @@ export default { "Override": "重写", "Override field": "重写字段", "Configure fields of {{title}}": "「{{title}}」的字段配置", + "Association fields filter": "关系筛选", "PK & FK fields": "主外键字段", "Association fields": "关系字段", "System fields": "系统字段", @@ -326,6 +327,7 @@ export default { "Configure calendar": "配置日历", "Title field": "标题字段", + "Custom title": "自定义标题", "Show lunar": "展示农历", "Start date field": "开始日期字段", "End date field": "结束日期字段", diff --git a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx index 745bc93f2..5f4e0d42a 100644 --- a/packages/core/client/src/schema-component/antd/action/ActionBar.tsx +++ b/packages/core/client/src/schema-component/antd/action/ActionBar.tsx @@ -4,11 +4,13 @@ import { Space } from 'antd'; import React from 'react'; import { useSchemaInitializer } from '../../../schema-initializer'; import { DndContext } from '../../common'; +import { useDesignable } from '../../hooks'; export const ActionBar = observer((props: any) => { const { layout = 'tow-columns', style, ...others } = props; const fieldSchema = useFieldSchema(); const { render } = useSchemaInitializer(fieldSchema['x-initializer']); + const { designable } = useDesignable(); if (layout === 'one-column') { return (
@@ -25,16 +27,21 @@ export const ActionBar = observer((props: any) => {
); } + const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0; return (
{ + const { refresh } = useContext(SchemaComponentContext); + const fieldSchema = useFieldSchema(); + const api = useAPIClient(); + const { t } = useTranslation(); + const dn = createDesignable({ t, api, refresh, current: fieldSchema }); + dn.loadAPIClientEvents(); + + const handleInsert = (s: Schema) => { + dn.insertBeforeBegin(s); + }; + + const schema = { + type: 'void', + 'x-action': 'associateFilter', + 'x-initializer': 'AssociationFilter.Initializer', + 'x-component': 'AssociationFilter', + properties: {}, + }; + + const newProps = { + ...props, + insert: handleInsert, + wrap: (s) => s, + }; + + return ; +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx new file mode 100644 index 000000000..306c26ea3 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Initializer.tsx @@ -0,0 +1,61 @@ +import { css } from '@emotion/css'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollection } from '../../../collection-manager'; +import { SchemaInitializer, SchemaInitializerItemOptions } from '../../../schema-initializer'; + +export const AssociationFilterInitializer = () => { + const { t } = useTranslation(); + const { fields } = useCollection(); + + const associatedFields = fields.filter((field) => + ['o2o', 'oho', 'obo', 'm2o', 'createdBy', 'updatedBy', 'o2m', 'm2m', 'linkTo'].includes(field.interface), + ); + + const items: SchemaInitializerItemOptions[] = associatedFields.map((field) => ({ + type: 'item', + key: field.key, + title: field.uiSchema.title, + component: 'AssociationFilterDesignerDisplayField', + schema: { + name: field.name, + title: field.uiSchema.title, + type: 'void', + 'x-designer': 'AssociationFilter.Item.Designer', + 'x-component': 'AssociationFilter.Item', + 'x-component-props': { + fieldNames: { + label: field.targetKey || 'id', + }, + }, + properties: {}, + }, + })); + + const associatedFieldGroup: SchemaInitializerItemOptions = { + type: 'itemGroup', + title: t('Association fields'), + children: items, + }; + + const dividerItem: SchemaInitializerItemOptions = { + type: 'divider', + }; + + const deleteItem: SchemaInitializerItemOptions = { + type: 'item', + title: t('Delete'), + component: 'AssociationFilterDesignerDelete', + }; + + return ( + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx new file mode 100644 index 000000000..f923a9aef --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.Designer.tsx @@ -0,0 +1,92 @@ +import { ISchema, useFieldSchema } from '@formily/react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useCollectionManager } from '../../../collection-manager'; +import { GeneralSchemaDesigner, SchemaSettings } from '../../../schema-settings'; +import { useCompile, useDesignable } from '../../hooks'; +import { AssociationFilter } from './AssociationFilter'; + +export const AssociationFilterItemDesigner = (props) => { + const fieldSchema = useFieldSchema(); + const { t } = useTranslation(); + const collectionField = AssociationFilter.useAssociationField(); + + const { getCollectionFields } = useCollectionManager(); + const compile = useCompile(); + const { dn } = useDesignable(); + + const targetFields = getCollectionFields(collectionField.target) ?? []; + + const options = targetFields + .filter( + (field) => field?.interface && ['id', 'input', 'phone', 'email', 'integer', 'number'].includes(field?.interface), + ) + .map((field) => ({ + value: field?.name, + label: compile(field?.uiSchema?.title) || field?.name, + })); + + const onTitleFieldChange = (label) => { + const schema = { + ['x-uid']: fieldSchema['x-uid'], + }; + const fieldNames = { + label, + }; + fieldSchema['x-component-props'] = fieldSchema['x-component-props'] || {}; + fieldSchema['x-component-props']['fieldNames'] = fieldNames; + schema['x-component-props'] = fieldSchema['x-component-props']; + dn.emit('patch', { + schema, + }); + dn.refresh(); + }; + + return ( + + { + if (title) { + // field.title = title; + fieldSchema.title = title; + dn.emit('patch', { + schema: { + 'x-uid': fieldSchema['x-uid'], + title: fieldSchema.title, + }, + }); + } + dn.refresh(); + }} + /> + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx new file mode 100644 index 000000000..57a9cb518 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.Item.tsx @@ -0,0 +1,276 @@ +import { CloseOutlined, SearchOutlined } from '@ant-design/icons'; +import { css } from '@emotion/css'; +import { useFieldSchema } from '@formily/react'; +import { Col, Collapse, Input, Row, Tree } from 'antd'; +import cls from 'classnames'; +import React, { ChangeEvent, MouseEvent, useContext, useState } from 'react'; +import { useRequest } from '../../../api-client'; +import { useBlockRequestContext } from '../../../block-provider'; +import { SharedFilterContext } from '../../../block-provider/SharedFilterProvider'; +import { SortableItem } from '../../common'; +import { useCompile, useDesigner } from '../../hooks'; +import { AssociationFilter } from './AssociationFilter'; + +const { Panel } = Collapse; + +export const AssociationFilterItem = (props) => { + const collectionField = AssociationFilter.useAssociationField(); + + if (!collectionField) { + return null; + } + + const fieldSchema = useFieldSchema(); + const Designer = useDesigner(); + const compile = useCompile(); + const { service } = useBlockRequestContext(); + const { setSharedFilterStore, sharedFilterStore, getFilterParams } = useContext(SharedFilterContext); + const [searchVisible, setSearchVisible] = useState(false); + + const collectionFieldName = collectionField.name; + + const valueKey = collectionField?.targetKey || 'id'; + const labelKey = fieldSchema['x-component-props']?.fieldNames?.label || valueKey; + + const fieldNames = { + title: labelKey || valueKey, + key: valueKey, + }; + + const { data, params, loading, run } = useRequest( + { + resource: collectionField.target, + action: 'list', + params: { + fields: [labelKey, valueKey], + }, + }, + { + refreshDeps: [labelKey, valueKey], + debounceWait: 300, + }, + ); + + const treeData = data?.data || []; + + const [expandedKeys, setExpandedKeys] = useState([]); + const [selectedKeys, setSelectedKeys] = useState([]); + const [autoExpandParent, setAutoExpandParent] = useState(true); + + const onExpand = (expandedKeysValue: React.Key[]) => { + setExpandedKeys(expandedKeysValue); + setAutoExpandParent(false); + }; + + const onSelect = (selectedKeysValue: React.Key[]) => { + setSelectedKeys(selectedKeysValue); + + const orList = selectedKeysValue.map((item) => ({ + [collectionFieldName]: { + [valueKey]: { + $eq: item, + }, + }, + })); + + const newFilter = + orList.length > 0 + ? { + $or: orList, + } + : {}; + + const newAssociationFilterStore = { + ...sharedFilterStore, + [collectionFieldName]: newFilter, + }; + + setSharedFilterStore(newAssociationFilterStore); + + const paramFilter = getFilterParams(newAssociationFilterStore); + + service.run({ ...service.params?.[0], page: 1, filter: paramFilter }); + }; + + const handleSearchToggle = (e: MouseEvent) => { + const filter = params?.[0]?.filter; + if (searchVisible || filter) { + run({ + ...params?.[0], + filter: undefined, + }); + } + setSearchVisible(!searchVisible); + e.stopPropagation(); + }; + + const handleSearchClick = (e: MouseEvent) => { + e.stopPropagation(); + }; + + const handleSearchInput = (e: ChangeEvent) => { + run({ + ...params?.[0], + filter: { + [`${labelKey}.$includes`]: e.target.value, + }, + }); + }; + + const title = fieldSchema.title ?? collectionField.uiSchema?.title; + + return ( + .general-schema-designer { + display: block; + } + } + &.nb-form-item:hover { + > .general-schema-designer { + background: rgba(241, 139, 98, 0.06) !important; + border: 0 !important; + top: -5px !important; + bottom: -5px !important; + left: -5px !important; + right: -5px !important; + } + } + > .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + border: 2px solid rgba(241, 139, 98, 0.3); + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: #f18b62; + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + } + } + } + `, + )} + > + + null : undefined} + > + + + {searchVisible ? ( + + ) : ( + compile(title) + )} + + + {searchVisible ? ( + + ) : ( + + )} + + + } + key={collectionField.uiSchemaUid} + > + compile(node[labelKey])} + selectedKeys={selectedKeys} + blockNode + /> + + + + ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx new file mode 100644 index 000000000..b3b26e4a1 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilter.tsx @@ -0,0 +1,86 @@ +import { css } from '@emotion/css'; +import { useFieldSchema } from '@formily/react'; +import cls from 'classnames'; +import React from 'react'; +import { useCollection } from '../../../collection-manager'; +import { useSchemaInitializer } from '../../../schema-initializer'; +import { SortableItem } from '../../common'; +import { useDesigner } from '../../hooks'; +import { AssociationFilterInitializer } from './AssociationFilter.Initializer'; +import { AssociationFilterItem } from './AssociationFilter.Item'; +import { AssociationFilterItemDesigner } from './AssociationFilter.Item.Designer'; + +export const AssociationFilter = (props) => { + const Designer = useDesigner(); + const filedSchema = useFieldSchema(); + + const { exists, render } = useSchemaInitializer(filedSchema['x-initializer']); + + return ( + .general-schema-designer { + display: block; + } + } + &.nb-form-item:hover { + > .general-schema-designer { + background: rgba(241, 139, 98, 0.06) !important; + border: 0 !important; + top: -5px !important; + bottom: -5px !important; + left: -5px !important; + right: -5px !important; + } + } + > .general-schema-designer { + position: absolute; + z-index: 999; + top: 0; + bottom: 0; + left: 0; + right: 0; + display: none; + border: 2px solid rgba(241, 139, 98, 0.3); + pointer-events: none; + > .general-schema-designer-icons { + position: absolute; + right: 2px; + top: 2px; + line-height: 16px; + pointer-events: all; + .ant-space-item { + background-color: #f18b62; + color: #fff; + line-height: 16px; + width: 16px; + padding-left: 1px; + } + } + } + `, + )} + > + + {props.children} + {render()} + + ); +}; + +AssociationFilter.Initializer = AssociationFilterInitializer; +AssociationFilter.Item = AssociationFilterItem as typeof AssociationFilterItem & { + Designer: typeof AssociationFilterItemDesigner; +}; +AssociationFilter.Item.Designer = AssociationFilterItemDesigner; + +AssociationFilter.useAssociationField = () => { + const fieldSchema = useFieldSchema(); + const { getField } = useCollection(); + return React.useMemo(() => getField(fieldSchema.name as any), [fieldSchema.name]); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx new file mode 100644 index 000000000..db011d333 --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDelete.tsx @@ -0,0 +1,25 @@ +import React, { useContext } from 'react'; +import { useFieldSchema } from '@formily/react'; +import { SchemaInitializer } from '../../../schema-initializer/SchemaInitializer'; +import { createDesignable, SchemaComponentContext } from '../..'; +import { useAPIClient } from '../../../api-client'; +import { useTranslation } from 'react-i18next'; + +export const AssociationFilterDesignerDelete = (props) => { + const { refresh } = useContext(SchemaComponentContext); + const fieldSchema = useFieldSchema(); + const api = useAPIClient(); + const { t } = useTranslation(); + const dn = createDesignable({ t, api, refresh, current: fieldSchema }); + dn.loadAPIClientEvents(); + + const handleClick = () => { + dn.remove(fieldSchema); + }; + + return ( + +
{props.title}
+
+ ); +}; diff --git a/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx new file mode 100644 index 000000000..bd326556c --- /dev/null +++ b/packages/core/client/src/schema-component/antd/association-filter/AssociationFilterDesignerDisplayField.tsx @@ -0,0 +1,24 @@ +import { merge } from '@formily/shared'; +import React from 'react'; + +import { SchemaInitializer } from '../../../schema-initializer'; +import { useCurrentSchema } from '../../../schema-initializer/utils'; + +export const AssociationFilterDesignerDisplayField = (props) => { + const { schema, item, insert } = props; + const { exists, remove } = useCurrentSchema(schema.name, 'name', item.find, item.remove); + return ( + { + if (exists) { + return remove(); + } + const s = merge(schema || {}, item.schema || {}); + item?.schemaInitialize?.(s); + insert(s); + }} + /> + ); +}; diff --git a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx index f948e6c2d..93bb2f054 100644 --- a/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx +++ b/packages/core/client/src/schema-component/antd/card-item/CardItem.tsx @@ -5,12 +5,13 @@ import { useSchemaTemplate } from '../../../schema-templates'; import { BlockItem } from '../block-item'; export const CardItem: React.FC = (props) => { + const { children, ...restProps } = props; const template = useSchemaTemplate(); const fieldSchema = useFieldSchema(); const templateKey = fieldSchema['x-template-key']; return templateKey && !template ? null : ( - + {props.children} diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo2.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo2.tsx index d937adff6..39baaac19 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo2.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo2.tsx @@ -1,4 +1,10 @@ -import { AntdSchemaComponentProvider, Filter, Input, SchemaComponent, SchemaComponentProvider } from '@nocobase/client'; +import { + AntdSchemaComponentProvider, + SharedFilter, + Input, + SchemaComponent, + SchemaComponentProvider, +} from '@nocobase/client'; import React from 'react'; const schema: any = { diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo3.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo3.tsx index 20ae24622..e48b3ee7d 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo3.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo3.tsx @@ -1,12 +1,12 @@ import { ISchema, useForm } from '@formily/react'; import { AntdSchemaComponentProvider, - Filter, + SharedFilter, Input, SchemaComponent, SchemaComponentProvider, useActionContext, - useRequest + useRequest, } from '@nocobase/client'; import React from 'react'; diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo4.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo4.tsx index 80f634617..539092527 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo4.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo4.tsx @@ -1,11 +1,11 @@ import { ISchema } from '@formily/react'; import { AntdSchemaComponentProvider, - Filter, + SharedFilter, Input, SchemaComponent, SchemaComponentProvider, - Select + Select, } from '@nocobase/client'; import { Space } from 'antd'; import React, { useState } from 'react'; diff --git a/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx b/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx index a228d70d7..7380e9f73 100644 --- a/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx +++ b/packages/core/client/src/schema-component/antd/filter/demos/demo6.tsx @@ -2,13 +2,13 @@ import { AntdSchemaComponentProvider, CollectionManagerProvider, CollectionProvider, - Filter, + SharedFilter, Input, SchemaComponent, SchemaComponentProvider, useCollection, useCollectionManager, - useFilterOptions + useFilterOptions, } from '@nocobase/client'; import { Select } from 'antd'; import React, { useState } from 'react'; diff --git a/packages/core/client/src/schema-component/antd/filter/index.ts b/packages/core/client/src/schema-component/antd/filter/index.ts index 4ab735746..cfc45fd76 100644 --- a/packages/core/client/src/schema-component/antd/filter/index.ts +++ b/packages/core/client/src/schema-component/antd/filter/index.ts @@ -1,3 +1,2 @@ export * from './Filter'; export * from './useFilterActionProps'; - diff --git a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts index 3b819979a..e8d718b63 100644 --- a/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts +++ b/packages/core/client/src/schema-component/antd/filter/useFilterActionProps.ts @@ -1,8 +1,10 @@ import { Field } from '@formily/core'; import { useField, useFieldSchema } from '@formily/react'; import flat from 'flat'; +import { useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { useBlockRequestContext } from '../../../block-provider'; +import { concatFilter, SharedFilterContext } from '../../../block-provider/SharedFilterProvider'; import { useCollection, useCollectionManager } from '../../../collection-manager'; export const useFilterOptions = (collectionName: string) => { @@ -69,7 +71,10 @@ export const useFilterFieldOptions = (fields) => { }; const isEmpty = (obj) => { - return obj && Object.keys(obj).length === 0 && Object.getPrototypeOf(obj) === Object.prototype; + return ( + (Array.isArray(obj) && obj.length === 0) || + (obj && Object.keys(obj).length === 0 && Object.getPrototypeOf(obj) === Object.prototype) + ); }; export const removeNullCondition = (filter) => { @@ -77,26 +82,13 @@ export const removeNullCondition = (filter) => { const values = {}; for (const key in items) { const value = items[key]; - if (value !== null && !isEmpty(value)) { + if (value != null && !isEmpty(value)) { values[key] = value; } } return flat.unflatten(values); }; -export const mergeFilter = (filter1, filter2) => { - if (filter1 && filter2) { - return { $and: [filter1, filter2] }; - } - if (!filter1 && filter2) { - return filter2; - } - if (filter1 && !filter2) { - return filter1; - } - return {}; -}; - export const useFilterActionProps = () => { const { name } = useCollection(); const options = useFilterOptions(name); @@ -107,6 +99,7 @@ export const useFilterActionProps = () => { export const useFilterFieldProps = ({ options, service, params }) => { const { t } = useTranslation(); const field = useField(); + const { sharedFilterStore, setSharedFilterStore, getFilterParams } = useContext(SharedFilterContext); return { options, onSubmit(values) { @@ -114,7 +107,17 @@ export const useFilterFieldProps = ({ options, service, params }) => { const defaultFilter = removeNullCondition(params.filter); // filter parameter for the filter action const filter = removeNullCondition(values?.filter); - service.run({ ...service.params?.[0], page: 1, filter: mergeFilter(defaultFilter, filter) }); + + const newSharedFilterStore = { + ...sharedFilterStore, + ActionBar: concatFilter(defaultFilter, filter), + }; + + setSharedFilterStore(newSharedFilterStore); + + const paramFilter = getFilterParams(newSharedFilterStore); + + service.run({ ...service.params?.[0], page: 1, filter: paramFilter }); const items = filter?.$and || filter?.$or; if (items?.length) { field.title = t('{{count}} filter items', { count: items?.length || 0 }); diff --git a/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx b/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx index ddaeb462f..ddbbc5d8f 100644 --- a/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx +++ b/packages/core/client/src/schema-component/common/sortable-item/SortableItem.tsx @@ -19,7 +19,7 @@ export const SortableProvider = (props) => { }; export const Sortable = (props: any) => { - const { component, style, children, ...others } = props; + const { component, style, children, openMode, ...others } = props; const { droppable } = useContext(SortableContext); const { isOver, setNodeRef } = droppable; const droppableStyle = { ...style }; diff --git a/packages/core/client/src/schema-component/hooks/useDesignable.tsx b/packages/core/client/src/schema-component/hooks/useDesignable.tsx index a51c98540..fd29cf98d 100644 --- a/packages/core/client/src/schema-component/hooks/useDesignable.tsx +++ b/packages/core/client/src/schema-component/hooks/useDesignable.tsx @@ -113,7 +113,7 @@ export class Designable { } const updateColumnSize = (parent: Schema) => { if (!parent) { - return; + return []; } const len = Object.values(parent.properties).length; const schemas = []; diff --git a/packages/core/client/src/schema-initializer/SchemaInitializerProvider.tsx b/packages/core/client/src/schema-initializer/SchemaInitializerProvider.tsx index caec9ee78..42bf02437 100644 --- a/packages/core/client/src/schema-initializer/SchemaInitializerProvider.tsx +++ b/packages/core/client/src/schema-initializer/SchemaInitializerProvider.tsx @@ -2,6 +2,7 @@ import { useFieldSchema } from '@formily/react'; import { isPlainObj } from '@formily/shared'; import React, { createContext, useContext } from 'react'; import { SchemaComponentOptions } from '../schema-component'; +import get from 'lodash/get'; import * as globals from './buttons'; import * as initializerComponents from './components'; import * as items from './items'; @@ -25,7 +26,7 @@ export const useSchemaInitializer = (name: string, props = {}) => { return { exists: false, render: (props?: any) => render(null) }; } - const initializer = initializers?.[name || fieldSchema?.['x-initializer']]; + const initializer = get(initializers, name || fieldSchema?.['x-initializer']); const initializerProps = { ...props, ...fieldSchema?.['x-initializer-props'] }; if (!initializer) { diff --git a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx index 95689d6e5..4d500bc7d 100644 --- a/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx +++ b/packages/core/client/src/schema-initializer/buttons/TableActionInitializers.tsx @@ -1,3 +1,5 @@ +import { ISchema, Schema } from '@formily/react'; + // 表格操作配置 export const TableActionInitializers = { title: "{{t('Configure actions')}}", @@ -52,6 +54,23 @@ export const TableActionInitializers = { { type: 'divider', }, + { + type: 'item', + title: "{{t('Association fields filter')}}", + component: 'ActionBarAssociationFilterAction', + schema: { + 'x-align': 'left', + }, + find: (schema: Schema) => { + const resultSchema = Object.entries(schema.parent.properties).find( + ([, value]) => value['x-component'] === 'AssociationFilter', + )?.[1]; + return resultSchema; + }, + }, + { + type: 'divider', + }, { type: 'subMenu', title: '{{t("Customize")}}', diff --git a/packages/core/client/src/schema-initializer/buttons/index.ts b/packages/core/client/src/schema-initializer/buttons/index.ts index d8cfc76d2..7a62f77cd 100644 --- a/packages/core/client/src/schema-initializer/buttons/index.ts +++ b/packages/core/client/src/schema-initializer/buttons/index.ts @@ -19,3 +19,5 @@ export * from './TableActionInitializers'; export * from './TableColumnInitializers'; export * from './TableSelectorInitializers'; export * from './TabPaneInitializers'; +// association filter +export * from '../../schema-component/antd/association-filter/AssociationFilter'; diff --git a/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx b/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx index 03895a1e1..ae1067ecd 100644 --- a/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx +++ b/packages/core/client/src/schema-initializer/items/InitializerWithSwitch.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { merge } from '@formily/shared'; -import { SchemaInitializer } from ".."; +import { SchemaInitializer } from '..'; import { useCurrentSchema } from '../utils'; export const InitializerWithSwitch = (props) => { diff --git a/packages/core/client/src/schema-initializer/items/index.tsx b/packages/core/client/src/schema-initializer/items/index.tsx index 42eb9fe50..3c8843a15 100644 --- a/packages/core/client/src/schema-initializer/items/index.tsx +++ b/packages/core/client/src/schema-initializer/items/index.tsx @@ -37,3 +37,8 @@ export * from './TableSelectorInitializer'; export * from './UpdateActionInitializer'; export * from './UpdateSubmitActionInitializer'; export * from './ViewActionInitializer'; +// association filter +export * from '../../schema-component/antd/association-filter/AssociationFilter'; +export * from '../../schema-component/antd/association-filter/ActionBarAssociationFilterAction'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDisplayField'; +export * from '../../schema-component/antd/association-filter/AssociationFilterDesignerDelete';