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:
parent
8668a18359
commit
a69074ead4
@ -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<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) => {
|
||||
const { collection, association } = props;
|
||||
const resource = useResource(props);
|
||||
@ -150,7 +207,9 @@ export const BlockProvider = (props) => {
|
||||
<MaybeCollectionProvider collection={collection}>
|
||||
<BlockAssociationContext.Provider value={association}>
|
||||
<BlockResourceContext.Provider value={resource}>
|
||||
<BlockRequestProvider {...props}>{props.children}</BlockRequestProvider>
|
||||
<BlockRequestProvider {...props}>
|
||||
<SharedFilterProvider {...props} />
|
||||
</BlockRequestProvider>
|
||||
</BlockResourceContext.Provider>
|
||||
</BlockAssociationContext.Provider>
|
||||
</MaybeCollectionProvider>
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
@ -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<any>({});
|
||||
|
||||
@ -28,7 +29,7 @@ const InternalTableBlockProvider = (props) => {
|
||||
rowKey,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<RenderChildrenWithAssociationFilter {...props} />
|
||||
</TableBlockContext.Provider>
|
||||
);
|
||||
};
|
||||
|
@ -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<any>({});
|
||||
@ -27,7 +27,7 @@ const InternalTableSelectorProvider = (props) => {
|
||||
rowKey,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
<RenderChildrenWithAssociationFilter {...props} />
|
||||
</TableSelectorContext.Provider>
|
||||
</RecordProvider>
|
||||
);
|
||||
@ -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) {
|
||||
|
@ -223,7 +223,7 @@ export const overridingSchema: ISchema = {
|
||||
'x-component': 'OverridingCollectionField',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
'currentCollection': '{{ currentCollection }}',
|
||||
currentCollection: '{{ currentCollection }}',
|
||||
},
|
||||
},
|
||||
view: {
|
||||
|
@ -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",
|
||||
|
@ -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": "结束日期字段",
|
||||
|
@ -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 (
|
||||
<div style={{ display: 'flex', ...style }} {...others}>
|
||||
@ -25,16 +27,21 @@ export const ActionBar = observer((props: any) => {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const hasActions = Object.keys(fieldSchema.properties ?? {}).length > 0;
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
style={
|
||||
!designable && !hasActions
|
||||
? undefined
|
||||
: {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
overflowX: 'auto',
|
||||
flexShrink: 0,
|
||||
...style,
|
||||
}}
|
||||
}
|
||||
}
|
||||
{...others}
|
||||
>
|
||||
<div
|
||||
|
@ -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} />;
|
||||
};
|
@ -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]}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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]);
|
||||
};
|
@ -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>
|
||||
);
|
||||
};
|
@ -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);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
@ -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 : (
|
||||
<BlockItem className={'noco-card-item'}>
|
||||
<Card style={{ marginBottom: 24 }} bordered={false} {...props}>
|
||||
<Card style={{ marginBottom: 24 }} bordered={false} {...restProps}>
|
||||
{props.children}
|
||||
</Card>
|
||||
</BlockItem>
|
||||
|
@ -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 = {
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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';
|
||||
|
@ -1,3 +1,2 @@
|
||||
export * from './Filter';
|
||||
export * from './useFilterActionProps';
|
||||
|
||||
|
@ -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<Field>();
|
||||
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 });
|
||||
|
@ -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 };
|
||||
|
@ -113,7 +113,7 @@ export class Designable {
|
||||
}
|
||||
const updateColumnSize = (parent: Schema) => {
|
||||
if (!parent) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
const len = Object.values(parent.properties).length;
|
||||
const schemas = [];
|
||||
|
@ -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) {
|
||||
|
@ -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")}}',
|
||||
|
@ -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';
|
||||
|
@ -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) => {
|
||||
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user