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 <chenlinxh@gmail.com>
This commit is contained in:
anuoua 2022-12-26 11:35:35 +08:00 committed by GitHub
parent 8668a18359
commit a69074ead4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 815 additions and 46 deletions

View File

@ -1,12 +1,22 @@
import { css } from '@emotion/css';
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react'; import { RecursionField, useField, useFieldSchema } from '@formily/react';
import { useRequest } from 'ahooks'; import { useRequest } from 'ahooks';
import { Col, Row } from 'antd';
import template from 'lodash/template'; import template from 'lodash/template';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { Link } from 'react-router-dom'; 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 { CollectionProvider, useCollection, useCollectionManager } from '../collection-manager';
import { useRecordIndex } from '../record-provider'; import { useRecordIndex } from '../record-provider';
import { SharedFilterProvider } from './SharedFilterProvider';
export const BlockResourceContext = createContext(null); export const BlockResourceContext = createContext(null);
export const BlockAssociationContext = createContext(null); export const BlockAssociationContext = createContext(null);
@ -143,6 +153,53 @@ export const useBlockRequestContext = () => {
return useContext(BlockRequestContext); return useContext(BlockRequestContext);
}; };
export const RenderChildrenWithAssociationFilter: React.FC<any> = (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 (
<Component {...field.componentProps}>
<Row gutter={16} wrap={false}>
<Col
className={css`
width: 200px;
flex: 0 0 auto;
`}
>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => s['x-component'] === 'AssociationFilter'}
/>
</Col>
<Col
className={css`
flex: 1 1 auto;
min-width: 0;
`}
>
<RecursionField
schema={fieldSchema}
onlyRenderProperties
filterProperties={(s) => s['x-component'] !== 'AssociationFilter'}
/>
</Col>
</Row>
</Component>
);
}
return props.children;
};
export const BlockProvider = (props) => { export const BlockProvider = (props) => {
const { collection, association } = props; const { collection, association } = props;
const resource = useResource(props); const resource = useResource(props);
@ -150,7 +207,9 @@ export const BlockProvider = (props) => {
<MaybeCollectionProvider collection={collection}> <MaybeCollectionProvider collection={collection}>
<BlockAssociationContext.Provider value={association}> <BlockAssociationContext.Provider value={association}>
<BlockResourceContext.Provider value={resource}> <BlockResourceContext.Provider value={resource}>
<BlockRequestProvider {...props}>{props.children}</BlockRequestProvider> <BlockRequestProvider {...props}>
<SharedFilterProvider {...props} />
</BlockRequestProvider>
</BlockResourceContext.Provider> </BlockResourceContext.Provider>
</BlockAssociationContext.Provider> </BlockAssociationContext.Provider>
</MaybeCollectionProvider> </MaybeCollectionProvider>

View File

@ -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<string, SharedFilter>;
export type SharedFilterContextValue = {
sharedFilterStore: SharedFilter;
setSharedFilterStore: (filterStore: SharedFilterStore) => void;
getFilterParams: (filterStore?: SharedFilterStore) => any;
};
export const SharedFilterContext = createContext<SharedFilterContextValue>({
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<Record<string, SharedFilter>>({});
const setSharedFilterStore = (associationFilter: Record<string, SharedFilter>) => {
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 (
<SharedFilterContext.Provider
value={{
sharedFilterStore,
setSharedFilterStore,
getFilterParams,
}}
>
{props.children}
</SharedFilterContext.Provider>
);
};

View File

@ -3,8 +3,9 @@ import { FormContext, Schema, useField, useFieldSchema } from '@formily/react';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import React, { createContext, useContext, useEffect, useMemo } from 'react'; import React, { createContext, useContext, useEffect, useMemo } from 'react';
import { useCollectionManager } from '../collection-manager'; import { useCollectionManager } from '../collection-manager';
import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { useFixedSchema } from '../schema-component'; import { useFixedSchema } from '../schema-component';
import { BlockProvider, useBlockRequestContext } from './BlockProvider';
export const TableBlockContext = createContext<any>({}); export const TableBlockContext = createContext<any>({});
@ -28,7 +29,7 @@ const InternalTableBlockProvider = (props) => {
rowKey, rowKey,
}} }}
> >
{props.children} <RenderChildrenWithAssociationFilter {...props} />
</TableBlockContext.Provider> </TableBlockContext.Provider>
); );
}; };

View File

@ -3,7 +3,7 @@ import { Schema, useField, useFieldSchema } from '@formily/react';
import React, { createContext, useContext, useEffect } from 'react'; import React, { createContext, useContext, useEffect } from 'react';
import { useCollectionManager } from '../collection-manager'; import { useCollectionManager } from '../collection-manager';
import { RecordProvider, useRecord } from '../record-provider'; import { RecordProvider, useRecord } from '../record-provider';
import { BlockProvider, useBlockRequestContext } from './BlockProvider'; import { BlockProvider, RenderChildrenWithAssociationFilter, useBlockRequestContext } from './BlockProvider';
import { useFormBlockContext } from './FormBlockProvider'; import { useFormBlockContext } from './FormBlockProvider';
export const TableSelectorContext = createContext<any>({}); export const TableSelectorContext = createContext<any>({});
@ -27,7 +27,7 @@ const InternalTableSelectorProvider = (props) => {
rowKey, rowKey,
}} }}
> >
{props.children} <RenderChildrenWithAssociationFilter {...props} />
</TableSelectorContext.Provider> </TableSelectorContext.Provider>
</RecordProvider> </RecordProvider>
); );
@ -59,7 +59,6 @@ export const TableSelectorProvider = (props) => {
// const value = ctx.form.query(collectionFieldSchema?.name).value(); // const value = ctx.form.query(collectionFieldSchema?.name).value();
const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']); const collectionField = getCollectionJoinField(collectionFieldSchema?.['x-collection-field']);
console.log('TableSelectorProvider', collectionFieldSchema, collectionField, record);
const params = { ...props.params }; const params = { ...props.params };
const appends = useAssociationNames(props.collection); const appends = useAssociationNames(props.collection);
if (props.dragSort) { if (props.dragSort) {

View File

@ -223,7 +223,7 @@ export const overridingSchema: ISchema = {
'x-component': 'OverridingCollectionField', 'x-component': 'OverridingCollectionField',
'x-component-props': { 'x-component-props': {
type: 'primary', type: 'primary',
'currentCollection': '{{ currentCollection }}', currentCollection: '{{ currentCollection }}',
}, },
}, },
view: { view: {

View File

@ -133,6 +133,7 @@ export default {
"Override": "Override", "Override": "Override",
"Override field": "Override field", "Override field": "Override field",
"Configure fields of {{title}}": "Configure fields of {{title}}", "Configure fields of {{title}}": "Configure fields of {{title}}",
"Association fields filter": "Association fields filter",
"PK & FK fields": "PK & FK fields", "PK & FK fields": "PK & FK fields",
"Association fields": "Association fields", "Association fields": "Association fields",
"Parent collection fields": "Parent collection fields", "Parent collection fields": "Parent collection fields",
@ -273,6 +274,7 @@ export default {
"Allow uploading multiple files": "Allow uploading multiple files", "Allow uploading multiple files": "Allow uploading multiple files",
"Configure calendar": "Configure calendar", "Configure calendar": "Configure calendar",
"Title field": "Title field", "Title field": "Title field",
"Custom title": "Custom title",
"Start date field": "Start date field", "Start date field": "Start date field",
"End date field": "End date field", "End date field": "End date field",
"Navigate": "Navigate", "Navigate": "Navigate",

View File

@ -138,6 +138,7 @@ export default {
"Override": "重写", "Override": "重写",
"Override field": "重写字段", "Override field": "重写字段",
"Configure fields of {{title}}": "「{{title}}」的字段配置", "Configure fields of {{title}}": "「{{title}}」的字段配置",
"Association fields filter": "关系筛选",
"PK & FK fields": "主外键字段", "PK & FK fields": "主外键字段",
"Association fields": "关系字段", "Association fields": "关系字段",
"System fields": "系统字段", "System fields": "系统字段",
@ -326,6 +327,7 @@ export default {
"Configure calendar": "配置日历", "Configure calendar": "配置日历",
"Title field": "标题字段", "Title field": "标题字段",
"Custom title": "自定义标题",
"Show lunar": "展示农历", "Show lunar": "展示农历",
"Start date field": "开始日期字段", "Start date field": "开始日期字段",
"End date field": "结束日期字段", "End date field": "结束日期字段",

View File

@ -4,11 +4,13 @@ import { Space } from 'antd';
import React from 'react'; import React from 'react';
import { useSchemaInitializer } from '../../../schema-initializer'; import { useSchemaInitializer } from '../../../schema-initializer';
import { DndContext } from '../../common'; import { DndContext } from '../../common';
import { useDesignable } from '../../hooks';
export const ActionBar = observer((props: any) => { export const ActionBar = observer((props: any) => {
const { layout = 'tow-columns', style, ...others } = props; const { layout = 'tow-columns', style, ...others } = props;
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { render } = useSchemaInitializer(fieldSchema['x-initializer']); const { render } = useSchemaInitializer(fieldSchema['x-initializer']);
const { designable } = useDesignable();
if (layout === 'one-column') { if (layout === 'one-column') {
return ( return (
<div style={{ display: 'flex', ...style }} {...others}> <div style={{ display: 'flex', ...style }} {...others}>
@ -25,16 +27,21 @@ export const ActionBar = observer((props: any) => {
</div> </div>
); );
} }
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
return ( return (
<div <div
style={{ style={
!designable && !hasActions
? undefined
: {
display: 'flex', display: 'flex',
justifyContent: 'space-between', justifyContent: 'space-between',
alignItems: 'center', alignItems: 'center',
overflowX: 'auto', overflowX: 'auto',
flexShrink: 0, flexShrink: 0,
...style, ...style,
}} }
}
{...others} {...others}
> >
<div <div

View File

@ -0,0 +1,35 @@
import { Schema, useFieldSchema } from '@formily/react';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { useAPIClient } from '../../../api-client';
import { createDesignable, SchemaComponentContext, useDesignable } from '../..';
import { ActionInitializer } from '../../../schema-initializer/items/ActionInitializer';
export const ActionBarAssociationFilterAction = (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 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 <ActionInitializer {...newProps} schema={schema} />;
};

View File

@ -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 (
<SchemaInitializer.Button
className={css`
margin-top: 16px;
`}
icon={'SettingOutlined'}
title={t('Configure fields')}
items={[associatedFieldGroup, dividerItem, deleteItem]}
/>
);
};

View File

@ -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 (
<GeneralSchemaDesigner {...props} disableInitializer={true}>
<SchemaSettings.ModalItem
title={t('Custom title')}
schema={
{
type: 'object',
title: t('Custom title'),
properties: {
title: {
default: fieldSchema?.title,
description: `${t('Original title: ')}${collectionField?.uiSchema?.title || fieldSchema?.title}`,
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {},
},
},
} as ISchema
}
onSubmit={({ title }) => {
if (title) {
// field.title = title;
fieldSchema.title = title;
dn.emit('patch', {
schema: {
'x-uid': fieldSchema['x-uid'],
title: fieldSchema.title,
},
});
}
dn.refresh();
}}
/>
<SchemaSettings.SelectItem
key="title-field"
title={t('Title field')}
options={options}
value={fieldSchema['x-component-props']?.fieldNames?.label}
onChange={onTitleFieldChange}
/>
<SchemaSettings.Remove
breakRemoveOn={{
'x-component': 'Grid',
}}
/>
</GeneralSchemaDesigner>
);
};

View File

@ -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<React.Key[]>([]);
const [selectedKeys, setSelectedKeys] = useState<React.Key[]>([]);
const [autoExpandParent, setAutoExpandParent] = useState<boolean>(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<any>) => {
run({
...params?.[0],
filter: {
[`${labelKey}.$includes`]: e.target.value,
},
});
};
const title = fieldSchema.title ?? collectionField.uiSchema?.title;
return (
<SortableItem
className={cls(
'nb-block-item',
props.className,
css`
position: relative;
&:hover {
> .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;
}
}
}
`,
)}
>
<Designer />
<Collapse
defaultActiveKey={[collectionField.uiSchemaUid]}
ghost
expandIcon={searchVisible ? () => null : undefined}
>
<Panel
className={css`
& .ant-collapse-content-box {
padding: 0 8px !important;
max-height: 200px;
overflow: auto;
}
& .ant-collapse-header {
padding: 10px !important;
background: #fafafa;
}
`}
header={
<Row
className={css`
align-items: center;
width: 100%;
min-width: 0;
height: 22px;
flex-wrap: nowrap;
${searchVisible ? 'border-bottom: 1px solid #dcdcdc;' : ''}
`}
gutter={5}
>
<Col
title={compile(title)}
className={css`
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`}
>
{searchVisible ? (
<Input
bordered={false}
autoFocus
placeholder="Search..."
className={css`
outline: none;
background: #fafafa;
width: 100%;
border: none;
height: 20px;
padding: 4px;
&::placeholder {
color: #dcdcdc;
}
`}
onClick={handleSearchClick}
onChange={handleSearchInput}
/>
) : (
compile(title)
)}
</Col>
<Col
className={css`
flex: 0 0 auto;
`}
>
{searchVisible ? (
<CloseOutlined
className={css`
color: #aeaeae !important;
font-size: 11px;
`}
onClick={handleSearchToggle}
/>
) : (
<SearchOutlined
className={css`
color: #aeaeae !important;
`}
onClick={handleSearchToggle}
/>
)}
</Col>
</Row>
}
key={collectionField.uiSchemaUid}
>
<Tree
style={{ padding: '16px 0' }}
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
treeData={treeData}
onSelect={onSelect}
fieldNames={fieldNames}
titleRender={(node) => compile(node[labelKey])}
selectedKeys={selectedKeys}
blockNode
/>
</Panel>
</Collapse>
</SortableItem>
);
};

View File

@ -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 (
<SortableItem
className={cls(
'nb-block-item',
props.className,
css`
position: relative;
&:hover {
> .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;
}
}
}
`,
)}
>
<Designer />
{props.children}
{render()}
</SortableItem>
);
};
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]);
};

View File

@ -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 (
<SchemaInitializer.Item onClick={handleClick}>
<div>{props.title}</div>
</SchemaInitializer.Item>
);
};

View File

@ -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 (
<SchemaInitializer.SwitchItem
checked={exists}
title={item.title}
onClick={() => {
if (exists) {
return remove();
}
const s = merge(schema || {}, item.schema || {});
item?.schemaInitialize?.(s);
insert(s);
}}
/>
);
};

View File

@ -5,12 +5,13 @@ import { useSchemaTemplate } from '../../../schema-templates';
import { BlockItem } from '../block-item'; import { BlockItem } from '../block-item';
export const CardItem: React.FC = (props) => { export const CardItem: React.FC = (props) => {
const { children, ...restProps } = props;
const template = useSchemaTemplate(); const template = useSchemaTemplate();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const templateKey = fieldSchema['x-template-key']; const templateKey = fieldSchema['x-template-key'];
return templateKey && !template ? null : ( return templateKey && !template ? null : (
<BlockItem className={'noco-card-item'}> <BlockItem className={'noco-card-item'}>
<Card style={{ marginBottom: 24 }} bordered={false} {...props}> <Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
{props.children} {props.children}
</Card> </Card>
</BlockItem> </BlockItem>

View File

@ -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'; import React from 'react';
const schema: any = { const schema: any = {

View File

@ -1,12 +1,12 @@
import { ISchema, useForm } from '@formily/react'; import { ISchema, useForm } from '@formily/react';
import { import {
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
Filter, SharedFilter,
Input, Input,
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
useActionContext, useActionContext,
useRequest useRequest,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';

View File

@ -1,11 +1,11 @@
import { ISchema } from '@formily/react'; import { ISchema } from '@formily/react';
import { import {
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
Filter, SharedFilter,
Input, Input,
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
Select Select,
} from '@nocobase/client'; } from '@nocobase/client';
import { Space } from 'antd'; import { Space } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';

View File

@ -2,13 +2,13 @@ import {
AntdSchemaComponentProvider, AntdSchemaComponentProvider,
CollectionManagerProvider, CollectionManagerProvider,
CollectionProvider, CollectionProvider,
Filter, SharedFilter,
Input, Input,
SchemaComponent, SchemaComponent,
SchemaComponentProvider, SchemaComponentProvider,
useCollection, useCollection,
useCollectionManager, useCollectionManager,
useFilterOptions useFilterOptions,
} from '@nocobase/client'; } from '@nocobase/client';
import { Select } from 'antd'; import { Select } from 'antd';
import React, { useState } from 'react'; import React, { useState } from 'react';

View File

@ -1,3 +1,2 @@
export * from './Filter'; export * from './Filter';
export * from './useFilterActionProps'; export * from './useFilterActionProps';

View File

@ -1,8 +1,10 @@
import { Field } from '@formily/core'; import { Field } from '@formily/core';
import { useField, useFieldSchema } from '@formily/react'; import { useField, useFieldSchema } from '@formily/react';
import flat from 'flat'; import flat from 'flat';
import { useContext } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useBlockRequestContext } from '../../../block-provider'; import { useBlockRequestContext } from '../../../block-provider';
import { concatFilter, SharedFilterContext } from '../../../block-provider/SharedFilterProvider';
import { useCollection, useCollectionManager } from '../../../collection-manager'; import { useCollection, useCollectionManager } from '../../../collection-manager';
export const useFilterOptions = (collectionName: string) => { export const useFilterOptions = (collectionName: string) => {
@ -69,7 +71,10 @@ export const useFilterFieldOptions = (fields) => {
}; };
const isEmpty = (obj) => { 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) => { export const removeNullCondition = (filter) => {
@ -77,26 +82,13 @@ export const removeNullCondition = (filter) => {
const values = {}; const values = {};
for (const key in items) { for (const key in items) {
const value = items[key]; const value = items[key];
if (value !== null && !isEmpty(value)) { if (value != null && !isEmpty(value)) {
values[key] = value; values[key] = value;
} }
} }
return flat.unflatten(values); 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 = () => { export const useFilterActionProps = () => {
const { name } = useCollection(); const { name } = useCollection();
const options = useFilterOptions(name); const options = useFilterOptions(name);
@ -107,6 +99,7 @@ export const useFilterActionProps = () => {
export const useFilterFieldProps = ({ options, service, params }) => { export const useFilterFieldProps = ({ options, service, params }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const field = useField<Field>(); const field = useField<Field>();
const { sharedFilterStore, setSharedFilterStore, getFilterParams } = useContext(SharedFilterContext);
return { return {
options, options,
onSubmit(values) { onSubmit(values) {
@ -114,7 +107,17 @@ export const useFilterFieldProps = ({ options, service, params }) => {
const defaultFilter = removeNullCondition(params.filter); const defaultFilter = removeNullCondition(params.filter);
// filter parameter for the filter action // filter parameter for the filter action
const filter = removeNullCondition(values?.filter); 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; const items = filter?.$and || filter?.$or;
if (items?.length) { if (items?.length) {
field.title = t('{{count}} filter items', { count: items?.length || 0 }); field.title = t('{{count}} filter items', { count: items?.length || 0 });

View File

@ -19,7 +19,7 @@ export const SortableProvider = (props) => {
}; };
export const Sortable = (props: any) => { export const Sortable = (props: any) => {
const { component, style, children, ...others } = props; const { component, style, children, openMode, ...others } = props;
const { droppable } = useContext(SortableContext); const { droppable } = useContext(SortableContext);
const { isOver, setNodeRef } = droppable; const { isOver, setNodeRef } = droppable;
const droppableStyle = { ...style }; const droppableStyle = { ...style };

View File

@ -113,7 +113,7 @@ export class Designable {
} }
const updateColumnSize = (parent: Schema) => { const updateColumnSize = (parent: Schema) => {
if (!parent) { if (!parent) {
return; return [];
} }
const len = Object.values(parent.properties).length; const len = Object.values(parent.properties).length;
const schemas = []; const schemas = [];

View File

@ -2,6 +2,7 @@ import { useFieldSchema } from '@formily/react';
import { isPlainObj } from '@formily/shared'; import { isPlainObj } from '@formily/shared';
import React, { createContext, useContext } from 'react'; import React, { createContext, useContext } from 'react';
import { SchemaComponentOptions } from '../schema-component'; import { SchemaComponentOptions } from '../schema-component';
import get from 'lodash/get';
import * as globals from './buttons'; import * as globals from './buttons';
import * as initializerComponents from './components'; import * as initializerComponents from './components';
import * as items from './items'; import * as items from './items';
@ -25,7 +26,7 @@ export const useSchemaInitializer = (name: string, props = {}) => {
return { exists: false, render: (props?: any) => render(null) }; 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'] }; const initializerProps = { ...props, ...fieldSchema?.['x-initializer-props'] };
if (!initializer) { if (!initializer) {

View File

@ -1,3 +1,5 @@
import { ISchema, Schema } from '@formily/react';
// 表格操作配置 // 表格操作配置
export const TableActionInitializers = { export const TableActionInitializers = {
title: "{{t('Configure actions')}}", title: "{{t('Configure actions')}}",
@ -52,6 +54,23 @@ export const TableActionInitializers = {
{ {
type: 'divider', 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', type: 'subMenu',
title: '{{t("Customize")}}', title: '{{t("Customize")}}',

View File

@ -19,3 +19,5 @@ export * from './TableActionInitializers';
export * from './TableColumnInitializers'; export * from './TableColumnInitializers';
export * from './TableSelectorInitializers'; export * from './TableSelectorInitializers';
export * from './TabPaneInitializers'; export * from './TabPaneInitializers';
// association filter
export * from '../../schema-component/antd/association-filter/AssociationFilter';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import { merge } from '@formily/shared'; import { merge } from '@formily/shared';
import { SchemaInitializer } from ".."; import { SchemaInitializer } from '..';
import { useCurrentSchema } from '../utils'; import { useCurrentSchema } from '../utils';
export const InitializerWithSwitch = (props) => { export const InitializerWithSwitch = (props) => {

View File

@ -37,3 +37,8 @@ export * from './TableSelectorInitializer';
export * from './UpdateActionInitializer'; export * from './UpdateActionInitializer';
export * from './UpdateSubmitActionInitializer'; export * from './UpdateSubmitActionInitializer';
export * from './ViewActionInitializer'; 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';