feat: event filter support

This commit is contained in:
Jian Lu 2025-01-22 10:36:45 +08:00
parent 1d02eb1e8b
commit e973756d5a
31 changed files with 380 additions and 588 deletions

View File

@ -35,9 +35,15 @@ export function useFormEvents({ form }) {
}, },
}, },
], ],
// state: { states: {
// isSubmitting: 'isSubmitting', values: {
// }, name: 'values',
title: '表单值',
type: 'object',
properties: fieldsMap,
value: form.values,
},
},
actions: [ actions: [
{ {
name: 'reset', name: 'reset',

View File

@ -53,9 +53,10 @@ export class EventFlowPlugin extends Plugin {
} }
// 运行时注册事件 // 运行时注册事件
register(events: EventSetting[]) { register(events: EventSetting[]) {
events?.forEach((event) => { console.log('todo register', events);
this.on(event); // events?.forEach((event) => {
}); // this.on(event);
// });
} }
// 触发事件 // 触发事件
async emit({ name, eventName, uid, params }: { name: string; eventName: string; uid?: string; params?: any }) { async emit({ name, eventName, uid, params }: { name: string; eventName: string; uid?: string; params?: any }) {

View File

@ -14,6 +14,7 @@ import React, { useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps'; import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { Action } from './Action'; import { Action } from './Action';
import { ArrayBase } from '@formily/antd-v5';
interface LinkageRuleActionGroupProps { interface LinkageRuleActionGroupProps {
type: 'button' | 'field' | 'style'; type: 'button' | 'field' | 'style';

View File

@ -1,281 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { CopyOutlined } from '@ant-design/icons';
import { ArrayBase } from '@formily/antd-v5';
import { ArrayField } from '@formily/core';
import { ISchema, RecursionField, observer, useField, useFieldSchema } from '@formily/react';
import { toArr } from '@formily/shared';
import { Badge, Card, Collapse, CollapsePanelProps, CollapseProps, Empty, Input } from 'antd';
import { cloneDeep } from 'lodash';
import React, { Fragment, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useToken } from '../../../../style';
import { arrayCollapseItemStyle } from './LinkageHeader.style';
const LinkageRulesTitle = (props) => {
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex(props.index);
const { t } = useTranslation();
const value = array?.field?.value[index];
return <div> {index + 1}</div>;
// return (
// <Input.TextArea
// value={value.title}
// defaultValue={t('Linkage rule')}
// onChange={(ev) => {
// ev.stopPropagation();
// array.field.value.splice(index, 1, { ...value, title: ev.target.value });
// }}
// onBlur={(ev) => {
// ev.stopPropagation();
// array.field.value.splice(index, 1, { ...value, title: ev.target.value });
// }}
// autoSize
// style={{ width: '70%', border: 'none' }}
// onClick={(e) => {
// e.stopPropagation();
// }}
// />
// );
};
export interface IArrayCollapseProps extends CollapseProps {
defaultOpenPanelCount?: number;
}
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
};
const isAdditionComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('Addition') > -1;
};
const isIndexComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('Index') > -1;
};
const isRemoveComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('Remove') > -1;
};
const isMoveUpComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('MoveUp') > -1;
};
const isMoveDownComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('MoveDown') > -1;
};
const isCopyComponent = (schema: ISchema) => {
return schema['x-component']?.indexOf?.('Copy') > -1;
};
const isOperationComponent = (schema: ISchema) => {
return (
isAdditionComponent(schema) ||
isRemoveComponent(schema) ||
isMoveDownComponent(schema) ||
isMoveUpComponent(schema) ||
isCopyComponent(schema)
);
};
const range = (count: number) => Array.from({ length: count }).map((_, i) => i);
const takeDefaultActiveKeys = (dataSourceLength: number, defaultOpenPanelCount: number) => {
if (dataSourceLength < defaultOpenPanelCount) return range(dataSourceLength);
return range(defaultOpenPanelCount);
};
const insertActiveKeys = (activeKeys: number[], index: number) => {
if (activeKeys.length <= index) return activeKeys.concat(index);
return activeKeys.reduce((buf, key) => {
if (key < index) return buf.concat(key);
if (key === index) return buf.concat([key, key + 1]);
return buf.concat(key + 1);
}, []);
};
export const ArrayCollapse: ComposedArrayCollapse = observer(
(props: IArrayCollapseProps) => {
const field = useField<ArrayField>();
const dataSource = Array.isArray(field.value) ? field.value : [];
const [activeKeys, setActiveKeys] = useState<number[]>(
takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount),
);
const schema = useFieldSchema();
useEffect(() => {
if (!field.modified && dataSource.length) {
setActiveKeys(takeDefaultActiveKeys(dataSource.length, props.defaultOpenPanelCount));
}
}, [dataSource.length, field]);
if (!schema) throw new Error('can not found schema object');
const renderAddition = () => {
return schema.reduceProperties((addition, schema, key) => {
if (isAdditionComponent(schema)) {
return <RecursionField schema={schema} name={key} />;
}
return addition;
}, null);
};
const renderEmpty = () => {
if (dataSource.length) return;
return (
<Card className={props.className} style={arrayCollapseItemStyle}>
<Empty />
</Card>
);
};
const renderItems = () => {
if (!dataSource || dataSource.length === 0) {
return null;
}
return (
<Collapse
{...props}
activeKey={activeKeys}
onChange={(keys: string[]) => setActiveKeys(toArr(keys).map(Number))}
className={props.className}
style={arrayCollapseItemStyle}
>
{dataSource.map((item, index) => {
const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items;
const panelProps = field.query(`${field.address}.${index}`).get('componentProps');
const props: CollapsePanelProps = items['x-component-props'];
const header = () => {
const header = `${panelProps?.header || props.header || field.title}`;
const path = field.address.concat(index);
const errors = field.form.queryFeedbacks({
type: 'error',
address: `${path}.**`,
});
return (
<ArrayBase.Item index={index} record={() => field.value?.[index]}>
<RecursionField
schema={items}
name={index}
filterProperties={(schema) => {
if (!isIndexComponent(schema)) return false;
return true;
}}
onlyRenderProperties
/>
{errors.length ? (
<Badge size="small" className="errors-badge" count={errors.length}>
{header}
</Badge>
) : (
<LinkageRulesTitle item={item.initialValue || item} index={index} />
)}
</ArrayBase.Item>
);
};
const extra = (
<ArrayBase.Item index={index} record={item}>
<RecursionField
schema={items}
name={index}
filterProperties={(schema) => {
if (!isOperationComponent(schema)) return false;
return true;
}}
onlyRenderProperties
/>
{panelProps?.extra}
</ArrayBase.Item>
);
const content = (
<RecursionField
schema={items}
name={index}
filterProperties={(schema) => {
if (isIndexComponent(schema)) return false;
if (isOperationComponent(schema)) return false;
return true;
}}
/>
);
return (
<Collapse.Panel {...props} {...panelProps} forceRender key={index} header={header()} extra={extra}>
<ArrayBase.Item index={index} key={index} record={item}>
{content}
</ArrayBase.Item>
</Collapse.Panel>
);
})}
</Collapse>
);
};
return (
<ArrayBase
onAdd={(index) => {
setActiveKeys(insertActiveKeys(activeKeys, index));
}}
>
{/* {renderEmpty()} */}
{renderItems()}
{renderAddition()}
</ArrayBase>
);
},
{ displayName: 'ArrayCollapse' },
);
const CollapsePanel: React.FC<React.PropsWithChildren<CollapsePanelProps>> = ({ children }) => {
return <Fragment>{children}</Fragment>;
};
CollapsePanel.displayName = 'CollapsePanel';
ArrayCollapse.defaultProps = {
defaultOpenPanelCount: 5,
};
ArrayCollapse.displayName = 'ArrayCollapse';
ArrayCollapse.CollapsePanel = CollapsePanel;
ArrayBase.mixin(ArrayCollapse);
export default ArrayCollapse;
//@ts-ignore
ArrayCollapse.Copy = React.forwardRef((props: any, ref) => {
const { token } = useToken();
const self = useField();
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex(props.index);
if (!array) return null;
if (array.field?.pattern !== 'editable') return null;
return (
<CopyOutlined
{...props}
style={{
transition: 'all 0.25s ease-in-out',
color: token.colorText,
fontSize: token.fontSizeLG,
marginLeft: 6,
}}
ref={ref}
onClick={(e) => {
if (self?.disabled) return;
e.stopPropagation();
if (array.props?.disabled) return;
const value = cloneDeep(array?.field?.value[index]);
array.field.push(value);
if (props.onClick) {
props.onClick(e);
}
}}
/>
);
});
(ArrayCollapse as any).Copy.displayName = 'ArrayCollapse.Copy';

View File

@ -0,0 +1,104 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useDataSourceManager } from '../../../../data-source/data-source';
import { useEvent } from '../../../hooks/useEvent';
import { EventParam, EventSetting, EventDefinition } from '../../../types';
const useCurrentEventParams: (event?: EventSetting['event']) => {
[key: string]: EventParam;
} = (event) => {
const { definitions } = useEvent();
const definition = definitions?.find((d) => d.name === event?.definition && d.uid === event.uid);
const eventParams = definition?.events?.find((e) => e.name === event?.event)?.params;
return eventParams;
};
const useStateDefine = ({ definitions }: { definitions: EventDefinition[] }) => {
const stateDefine = definitions.filter((item) => item.uid && item.states);
const stateDefineOptions = {};
stateDefine.forEach((definition) => {
stateDefineOptions[definition.uid] = {
name: definition.uid,
title: `${definition.title}-${definition.uid}`,
type: 'object',
properties: definition.states,
};
});
return { stateDefineOptions };
};
export const useFilterOptions = (recordValues: EventSetting) => {
const selectedEvent: EventSetting['event'] = recordValues?.['event'];
const currentEventParamsDefine = useCurrentEventParams(selectedEvent);
const { definitions } = useEvent();
const { stateDefineOptions } = useStateDefine({ definitions });
const options: EventParam[] = [
{
name: '_eventParams',
title: '事件参数',
type: 'object',
properties: currentEventParamsDefine,
},
{
name: '_state',
title: '组件数据',
type: 'object',
properties: stateDefineOptions,
},
// {
// name: '_system',
// title: '系统参数',
// type: 'object',
// properties: {},
// },
];
const dm = useDataSourceManager();
const getOption = (opt: EventParam) => {
if (opt.type === 'object' && opt.properties) {
return {
name: opt.name,
title: opt.title,
children: Object.keys(opt.properties).map((key) =>
getOption({
...opt.properties[key],
name: key,
}),
),
};
}
if (opt.type === 'array' && opt.items) {
//TODO: 处理数组
return {
name: opt.name,
title: opt.title,
children: [],
};
}
const fieldInterface = dm?.collectionFieldInterfaceManager.getFieldInterface(opt?.interface || opt.type);
const { nested, children, operators } = fieldInterface?.filterable || {};
const res = {
name: opt.name,
type: opt.type,
// target: opt.target,
title: opt?.uiSchema?.title || opt.title || opt.name,
schema: opt?.uiSchema,
interface: opt.interface,
operators:
operators?.filter?.((operator) => {
return !operator?.visible || operator.visible(opt);
}) || [],
};
return res;
};
return options.map((opt) => getOption(opt));
};

View File

@ -0,0 +1,47 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { useCompile } from '@nocobase/client';
import { useMemo } from 'react';
import { useEvent } from '../../../hooks/useEvent';
import { EventParam } from '../../../types';
export const useVariableOptions = () => {
const { definitions } = useEvent();
const compile = useCompile();
const opts = useMemo(() => {
const options = [];
const state2Variables = (state: EventParam) => {
const children = [];
if (state.properties) {
Object.keys(state.properties).forEach((key) => {
children.push(state2Variables(state.properties[key]));
});
}
return {
label: compile(state?.uiSchema?.title || state.title),
value: state.name,
children: children,
};
};
options.push({
label: '组件数据',
value: '$_state',
children: definitions
.filter((def) => def.uid && def.states)
.map((def) => ({
label: `${def?.title}-${def.uid}`,
value: `${def.uid}`,
children: Object.keys(def.states).map((key) => state2Variables({ name: `${key}`, ...def.states[key] })),
})),
});
return options;
}, [definitions, compile]);
return opts;
};

View File

@ -21,23 +21,44 @@ import { DynamicComponentProps } from '../../../schema-component/antd/filter/Dyn
import { FilterContext } from '../../../schema-component/antd/filter/context'; import { FilterContext } from '../../../schema-component/antd/filter/context';
import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput'; import { VariableInput, getShouldChange } from '../../../schema-settings/VariableInput/VariableInput';
import { Actions } from './Actions'; import { Actions } from './Actions';
import { EnableLinkage } from './components/EnableLinkage';
import { ArrayCollapse } from './components/LinkageHeader';
import { FormProvider, createSchemaField } from '@formily/react'; import { FormProvider, createSchemaField } from '@formily/react';
import { Filter } from '../Filter2'; import { ArrayCollapse } from '../components/LinkageHeader';
import { Filter } from '../Filter';
import { ArrayBase } from '@formily/antd-v5';
import { useFilterOptions } from './hooks/useFilterOptions';
import { EventDefinition, EventSetting } from '../../types';
import { useVariableOptions } from './hooks/useVariableOptions';
import { uniqBy } from 'lodash';
export interface Props { export interface Props {
dynamicComponent: any; dynamicComponent: any;
definitions: EventDefinition[];
} }
export const ActionsSetting = withDynamicSchemaProps( export const ActionsSetting = withDynamicSchemaProps(
observer((props: Props) => { observer((props: Props) => {
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
const { options, defaultValues, collectionName, form, variables, localVariables, record, dynamicComponent } = const array = ArrayBase.useArray();
useProps(props); // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema const recordValues = ArrayBase.useRecord();
const index = ArrayBase.useIndex();
const {
definitions,
options,
defaultValues,
collectionName,
form,
variables,
localVariables,
record,
dynamicComponent,
} = props;
const { getAllCollectionsInheritChain } = useCollectionManager_deprecated(); const { getAllCollectionsInheritChain } = useCollectionManager_deprecated();
const parentRecordData = useCollectionParentRecordData(); const parentRecordData = useCollectionParentRecordData();
const filterOptions = useFilterOptions(recordValues);
const variableOptions = useVariableOptions();
console.log('variableOptions', variableOptions);
const components = useMemo(() => ({ ArrayCollapse, Filter }), []); const components = useMemo(() => ({ ArrayCollapse, Filter }), []);
const schema = useMemo( const schema = useMemo(
() => ({ () => ({
@ -50,6 +71,10 @@ export const ActionsSetting = withDynamicSchemaProps(
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
accordion: true, accordion: true,
titleRender: (item: any, index: number) => {
return `动作 ${index + 1}`;
},
showEmpty: false,
}, },
items: { items: {
type: 'object', type: 'object',
@ -77,7 +102,7 @@ export const ActionsSetting = withDynamicSchemaProps(
'x-component': 'Filter', 'x-component': 'Filter',
'x-use-component-props': () => { 'x-use-component-props': () => {
return { return {
options, options: filterOptions,
className: css` className: css`
position: relative; position: relative;
width: 100%; width: 100%;
@ -100,6 +125,10 @@ export const ActionsSetting = withDynamicSchemaProps(
localVariables, localVariables,
getAllCollectionsInheritChain, getAllCollectionsInheritChain,
})} })}
returnScope={(scope) => {
// console.log('scope', [...scope, ...variableOptions]);
return uniqBy([...scope, ...variableOptions], 'key');
}}
/> />
); );
}, },
@ -161,6 +190,7 @@ export const ActionsSetting = withDynamicSchemaProps(
props, props,
record, record,
variables, variables,
filterOptions,
], ],
); );
const value = useMemo( const value = useMemo(
@ -168,22 +198,22 @@ export const ActionsSetting = withDynamicSchemaProps(
[dynamicComponent, fieldSchema, options], [dynamicComponent, fieldSchema, options],
); );
// return <SchemaComponent components={components} schema={schema} />; return <SchemaComponent components={components} schema={schema} />;
return ( // return (
// 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确 // // 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
// <FormProvider form={form}> // // <FormProvider form={form}>
<SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}> // <SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}>
<RecordProvider record={record} parent={parentRecordData}> // <RecordProvider record={record} parent={parentRecordData}>
<FilterContext.Provider value={value}> // <FilterContext.Provider value={value}>
<CollectionProvider name={collectionName}> // <CollectionProvider name={collectionName}>
<SchemaComponent components={components} schema={schema} /> // <SchemaComponent components={components} schema={schema} />
</CollectionProvider> // </CollectionProvider>
</FilterContext.Provider> // </FilterContext.Provider>
</RecordProvider> // </RecordProvider>
</SubFormProvider> // </SubFormProvider>
// </FormProvider> // // </FormProvider>
); // );
}), }),
{ displayName: 'ActionsSetting' }, { displayName: 'ActionsSetting' },
); );

View File

@ -7,19 +7,19 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import React from 'react'; import React, { useMemo } from 'react';
import { useControllableValue } from 'ahooks'; import { useControllableValue } from 'ahooks';
import { Card, Space, TreeSelect } from 'antd'; import { Card, Space, TreeSelect } from 'antd';
import { EventDefinition } from '../../types'; import { EventDefinition, EventSetting } from '../types';
export default function EventSelect(props: { export default function EventSelect(props: {
definitions: EventDefinition[]; definitions?: EventDefinition[];
value: any; value?: EventSetting['event'];
onChange: (v: any) => void; onChange?: (v: EventSetting['event']) => void;
className?: string; className?: string;
}) { }) {
const { definitions, className } = props; const { definitions, className } = props;
const [state, setState] = useControllableValue<String>(props, { const [state, setState] = useControllableValue<EventSetting['event']>(props, {
defaultValue: undefined, defaultValue: undefined,
}); });
@ -29,21 +29,32 @@ export default function EventSelect(props: {
selectable: false, selectable: false,
children: children:
module?.events?.map((event) => ({ module?.events?.map((event) => ({
value: module.name + '.' + event.name, value: `${module.name}.${event.name}${module.uid ? `.${module.uid}` : ''}`,
title: event.title, title: event.title,
})) || [], })) || [],
})); }));
treeData = treeData?.filter((item) => item.children.length > 0); treeData = treeData?.filter((item) => item.children.length > 0);
const selectedEvent = useMemo(() => {
if (!state) return undefined;
return `${state.definition}.${state.event}${state.uid ? `.${state.uid}` : ''}`;
}, [state]);
return ( return (
<TreeSelect <TreeSelect
value={state} value={selectedEvent}
dropdownStyle={{ maxHeight: 400, overflow: 'auto' }} dropdownStyle={{ maxHeight: 400, overflow: 'auto' }}
placeholder="Please select" placeholder="Please select"
allowClear allowClear
treeDefaultExpandAll treeDefaultExpandAll
onChange={(value) => { onChange={(value) => {
setState(value); console.log('value', value);
const [definition, event, uid] = (value as any).split('.');
setState({
definition,
event,
uid,
});
}} }}
treeData={treeData} treeData={treeData}
className={className} className={className}

View File

@ -1,44 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ArrayBase } from '@formily/antd-v5';
import { Switch } from 'antd';
import React from 'react';
import { useTranslation } from 'react-i18next';
export const EnableLinkage = React.forwardRef((props: any, ref) => {
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex(props.index);
const { t } = useTranslation();
return (
<Switch
{...props}
checkedChildren={t('On')}
unCheckedChildren={t('Off')}
checked={!array?.field?.value[index].disabled}
size={'small'}
style={{
transition: 'all 0.25s ease-in-out',
color: 'rgba(0, 0, 0, 0.8)',
fontSize: 16,
marginLeft: 6,
marginBottom: 3,
}}
onChange={(checked, e) => {
e.stopPropagation();
array.field.value.splice(index, 1, { ...array?.field?.value[index], disabled: !checked });
}}
onClick={(checked, e) => {
e.stopPropagation();
}}
/>
);
});
EnableLinkage.displayName = 'EnableLinkage';

View File

@ -1,139 +0,0 @@
/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { css } from '@emotion/css';
import { observer, useFieldSchema } from '@formily/react';
import React, { useMemo } from 'react';
import { withDynamicSchemaProps } from '../../../hoc/withDynamicSchemaProps';
import { InputNumber, SchemaComponent, useProps } from '../../../schema-component';
import { ArrayCollapse } from './components/LinkageHeader';
import { FormProvider, createSchemaField } from '@formily/react';
import { ActionsSetting } from '../ActionsSetting';
import EventSelect from './EventSelect';
export interface Props {
dynamicComponent: any;
}
export const EventsSetting = withDynamicSchemaProps(
observer((props: Props) => {
const components = useMemo(() => ({ ArrayCollapse, ActionsSetting, EventSelect }), []);
const { definitions } = useProps(props);
const schema = useMemo(
() => ({
type: 'object',
properties: {
events: {
type: 'array',
// default: defaultValues,
'x-component': 'ArrayCollapse',
'x-decorator': 'FormItem',
'x-component-props': {
accordion: true,
},
items: {
type: 'object',
'x-component': 'ArrayCollapse.CollapsePanel',
'x-component-props': {
// extra: <EnableLinkage />,
},
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '4px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
eventTitle: {
'x-component': 'h4',
'x-content': '{{ t("触发事件") }}',
},
event: {
'x-component': EventSelect,
'x-component-props': {
definitions,
className: css`
margin-bottom: 12px;
`,
},
},
actionTitle: {
'x-component': 'h4',
'x-content': '{{ t("执行动作") }}',
},
actions: {
type: 'void',
'x-component': ActionsSetting,
'x-component-props': {
...props,
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
// moveUp: {
// type: 'void',
// 'x-component': 'ArrayCollapse.MoveUp',
// },
// moveDown: {
// type: 'void',
// 'x-component': 'ArrayCollapse.MoveDown',
// },
// copy: {
// type: 'void',
// 'x-component': 'ArrayCollapse.Copy',
// },
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add events") }}',
'x-component': 'ArrayCollapse.Addition',
'x-reactions': {
dependencies: ['rules'],
fulfill: {
state: {
// disabled: '{{$deps[0].length >= 3}}',
},
},
},
},
},
},
},
}),
[],
);
return <SchemaComponent components={components} schema={schema} />;
// return (
// // 这里使用 SubFormProvider 包裹,是为了让子表格的联动规则中 “当前对象” 的配置显示正确
// // <FormProvider form={form}>
// <SubFormProvider value={{ value: null, collection: { name: collectionName } as any }}>
// <RecordProvider record={record} parent={parentRecordData}>
// <FilterContext.Provider value={value}>
// <CollectionProvider name={collectionName}>
// <SchemaComponent components={components} schema={schema} />
// </CollectionProvider>
// </FilterContext.Provider>
// </RecordProvider>
// </SubFormProvider>
// // </FormProvider>
// );
}),
{ displayName: 'Evets' },
);

View File

@ -18,6 +18,7 @@ import { FilterAction } from './FilterAction';
import { FilterGroup } from './FilterGroup'; import { FilterGroup } from './FilterGroup';
import { SaveDefaultValue } from './SaveDefaultValue'; import { SaveDefaultValue } from './SaveDefaultValue';
import { FilterContext, FilterContextProps } from './context'; import { FilterContext, FilterContextProps } from './context';
import { ArrayBase } from '@formily/antd-v5';
const useDef = (options: UseRequestOptions) => { const useDef = (options: UseRequestOptions) => {
const field = useField<ObjectFieldModel>(); const field = useField<ObjectFieldModel>();
@ -35,9 +36,8 @@ export interface FilterProps extends Omit<FilterContextProps, 'field' | 'fieldSc
export const Filter: any = withDynamicSchemaProps( export const Filter: any = withDynamicSchemaProps(
observer((props: any) => { observer((props: any) => {
const { useDataSource = useDef } = props; const { useDataSource = useDef } = props;
console.log('Filter111', props);
// 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema // 新版 UISchema1.0 之后)中已经废弃了 useProps这里之所以继续保留是为了兼容旧版的 UISchema
const { options, dynamicComponent, className, collectionName } = useProps(props); const { options, dynamicComponent, className, collectionName } = props;
const field = useField<ObjectFieldModel>(); const field = useField<ObjectFieldModel>();
const fieldSchema: any = useFieldSchema(); const fieldSchema: any = useFieldSchema();
@ -46,6 +46,10 @@ export const Filter: any = withDynamicSchemaProps(
field.dataSource = data?.data || []; field.dataSource = data?.data || [];
}, },
}); });
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex();
console.log('array', array);
console.log('index', index);
useEffect(() => { useEffect(() => {
if (fieldSchema.defaultValue) { if (fieldSchema.defaultValue) {
@ -64,6 +68,7 @@ export const Filter: any = withDynamicSchemaProps(
collectionName, collectionName,
}} }}
> >
{index}
<FilterGroup {...props} bordered={false} /> <FilterGroup {...props} bordered={false} />
</FilterContext.Provider> </FilterContext.Provider>
</div> </div>

View File

@ -43,7 +43,7 @@ export const SaveDefaultValue = (props) => {
}); });
dn.refresh(); dn.refresh();
filterSchema.default = defaultValue; filterSchema.default = defaultValue;
console.log('filterSchema', defaultValue); // console.log('filterSchema', defaultValue);
}} }}
> >
{t('Save conditions')} {t('Save conditions')}

View File

@ -16,38 +16,13 @@ import { Badge, Card, Collapse, CollapsePanelProps, CollapseProps, Empty, Input
import { cloneDeep } from 'lodash'; import { cloneDeep } from 'lodash';
import React, { Fragment, useEffect, useState } from 'react'; import React, { Fragment, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useToken } from '../../../../style'; import { useToken } from '../../../style';
import { arrayCollapseItemStyle } from './LinkageHeader.style'; import { arrayCollapseItemStyle } from './LinkageHeader.style';
const LinkageRulesTitle = (props) => {
const array = ArrayBase.useArray();
const index = ArrayBase.useIndex(props.index);
const { t } = useTranslation();
const value = array?.field?.value[index];
return <div>{`事件 ${index + 1}`}</div>;
// return (
// <Input.TextArea
// value={value.title}
// defaultValue={t('Linkage rule')}
// onChange={(ev) => {
// ev.stopPropagation();
// array.field.value.splice(index, 1, { ...value, title: ev.target.value });
// }}
// onBlur={(ev) => {
// ev.stopPropagation();
// array.field.value.splice(index, 1, { ...value, title: ev.target.value });
// }}
// autoSize
// style={{ width: '70%', border: 'none' }}
// onClick={(e) => {
// e.stopPropagation();
// }}
// />
// );
};
export interface IArrayCollapseProps extends CollapseProps { export interface IArrayCollapseProps extends CollapseProps {
defaultOpenPanelCount?: number; defaultOpenPanelCount?: number;
titleRender?: (item: any, index: number) => React.ReactNode;
showEmpty?: boolean;
} }
type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> & { type ComposedArrayCollapse = React.FC<React.PropsWithChildren<IArrayCollapseProps>> & {
CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>; CollapsePanel?: React.FC<React.PropsWithChildren<CollapsePanelProps>>;
@ -103,6 +78,7 @@ const insertActiveKeys = (activeKeys: number[], index: number) => {
export const ArrayCollapse: ComposedArrayCollapse = observer( export const ArrayCollapse: ComposedArrayCollapse = observer(
(props: IArrayCollapseProps) => { (props: IArrayCollapseProps) => {
const { titleRender, showEmpty = true } = props;
const field = useField<ArrayField>(); const field = useField<ArrayField>();
const dataSource = Array.isArray(field.value) ? field.value : []; const dataSource = Array.isArray(field.value) ? field.value : [];
const [activeKeys, setActiveKeys] = useState<number[]>( const [activeKeys, setActiveKeys] = useState<number[]>(
@ -124,6 +100,7 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
return addition; return addition;
}, null); }, null);
}; };
const renderEmpty = () => { const renderEmpty = () => {
if (dataSource.length) return; if (dataSource.length) return;
return ( return (
@ -144,8 +121,7 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
onChange={(keys: string[]) => setActiveKeys(toArr(keys).map(Number))} onChange={(keys: string[]) => setActiveKeys(toArr(keys).map(Number))}
className={props.className} className={props.className}
style={arrayCollapseItemStyle} style={arrayCollapseItemStyle}
> items={dataSource.map((item, index) => {
{dataSource.map((item, index) => {
const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items; const items = Array.isArray(schema.items) ? schema.items[index] || schema.items[0] : schema.items;
const panelProps = field.query(`${field.address}.${index}`).get('componentProps'); const panelProps = field.query(`${field.address}.${index}`).get('componentProps');
@ -172,13 +148,12 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
<Badge size="small" className="errors-badge" count={errors.length}> <Badge size="small" className="errors-badge" count={errors.length}>
{header} {header}
</Badge> </Badge>
) : ( ) : titleRender ? (
<LinkageRulesTitle item={item.initialValue || item} index={index} /> titleRender(item, index)
)} ) : null}
</ArrayBase.Item> </ArrayBase.Item>
); );
}; };
const extra = ( const extra = (
<ArrayBase.Item index={index} record={item}> <ArrayBase.Item index={index} record={item}>
<RecursionField <RecursionField
@ -195,25 +170,28 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
); );
const content = ( const content = (
<RecursionField <ArrayBase.Item index={index} key={index} record={item}>
schema={items} <RecursionField
name={index} schema={items}
filterProperties={(schema) => { name={index}
if (isIndexComponent(schema)) return false; filterProperties={(schema) => {
if (isOperationComponent(schema)) return false; if (isIndexComponent(schema)) return false;
return true; if (isOperationComponent(schema)) return false;
}} return true;
/> }}
); />
return ( </ArrayBase.Item>
<Collapse.Panel {...props} {...panelProps} forceRender key={index} header={header()} extra={extra}>
<ArrayBase.Item index={index} key={index} record={item}>
{content}
</ArrayBase.Item>
</Collapse.Panel>
); );
return {
forceRender: true,
key: index,
label: header(),
extra: extra,
children: content,
};
})} })}
</Collapse> />
); );
}; };
return ( return (
@ -222,7 +200,7 @@ export const ArrayCollapse: ComposedArrayCollapse = observer(
setActiveKeys(insertActiveKeys(activeKeys, index)); setActiveKeys(insertActiveKeys(activeKeys, index));
}} }}
> >
{renderEmpty()} {showEmpty && renderEmpty()}
{renderItems()} {renderItems()}
{renderAddition()} {renderAddition()}
</ArrayBase> </ArrayBase>

View File

@ -7,6 +7,6 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
export const arrayCollapseItemStyle = { export const useFilterOptions = () => {
marginBottom: 10, return { filterOptions: [] };
}; };

View File

@ -21,12 +21,17 @@ import {
useFormBlockType, useFormBlockType,
useRecord, useRecord,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React, { useMemo } from 'react';
import { ISchema, useField } from '@formily/react'; import { ISchema, useField } from '@formily/react';
import { SchemaSettingsKey, useEvent } from '..'; import { SchemaSettingsKey, useEvent } from '..';
import { useFieldSchema } from '@formily/react'; import { useFieldSchema } from '@formily/react';
import { useLinkageCollectionFieldOptions } from './ActionsSetting/action-hooks'; import { useLinkageCollectionFieldOptions } from './ActionsSetting/action-hooks';
import { EventsSetting } from './EventsSetting'; import { ActionsSetting } from './ActionsSetting';
import EventSelect from './EventSelect';
import { ArrayCollapse } from './components/LinkageHeader';
import { css } from '@emotion/css';
import { useFilterOptions } from './hooks/useFilterOptions';
import { FormItem, FormLayout } from '@formily/antd-v5';
// packages/core/client/src/schema-settings/SchemaSettings.tsx // packages/core/client/src/schema-settings/SchemaSettings.tsx
export const EventSettingItem = (props) => { export const EventSettingItem = (props) => {
@ -38,7 +43,6 @@ export const EventSettingItem = (props) => {
const { definitions, register } = useEvent(); const { definitions, register } = useEvent();
const { dn } = useDesignable(); const { dn } = useDesignable();
const fieldSchema = useFieldSchema(); const fieldSchema = useFieldSchema();
console.log('definitions', definitions);
const { readPretty, Component, afterSubmit } = props; const { readPretty, Component, afterSubmit } = props;
const collectionName = 't_aierml1wni1'; const collectionName = 't_aierml1wni1';
const options = useLinkageCollectionFilterOptions(collectionName); const options = useLinkageCollectionFilterOptions(collectionName);
@ -52,7 +56,7 @@ export const EventSettingItem = (props) => {
return ( return (
<SchemaSettingsModalItem <SchemaSettingsModalItem
title={'Event Setting'} title={'Event Setting'}
components={{ EventsSetting }} components={{ ArrayCollapse, ActionsSetting, EventSelect, FormLayout }}
initialValues={{}} initialValues={{}}
scope={{}} scope={{}}
width={1000} width={1000}
@ -62,38 +66,100 @@ export const EventSettingItem = (props) => {
title: '事件配置', title: '事件配置',
properties: { properties: {
events: { events: {
'x-component': EventsSetting, type: 'array',
'x-use-component-props': () => { // default: defaultValues,
return { 'x-component': 'ArrayCollapse',
definitions, 'x-decorator': 'FormItem',
options, 'x-component-props': {
defaultValues: undefined, accordion: true,
linkageOptions, titleRender: (item: any, index: number) => {
category: 'default', return `事件 ${index + 1}`;
elementType: 'field', },
collectionName, },
form, items: {
variables, type: 'object',
localVariables, 'x-component': 'ArrayCollapse.CollapsePanel',
record, 'x-component-props': {
formBlockType, // extra: <EnableLinkage />,
}; },
properties: {
layout: {
type: 'void',
'x-component': 'FormLayout',
'x-component-props': {
labelStyle: {
marginTop: '4px',
},
labelCol: 8,
wrapperCol: 16,
},
properties: {
eventTitle: {
'x-component': 'h4',
'x-content': '{{ t("触发事件") }}',
},
event: {
'x-component': EventSelect,
'x-component-props': {
definitions,
className: css`
margin-bottom: 12px;
`,
},
},
actionTitle: {
'x-component': 'h4',
'x-content': '{{ t("执行动作") }}',
},
actions: {
type: 'void',
'x-component': ActionsSetting,
'x-use-component-props': () => {
return {
definitions,
options,
linkageOptions,
category: 'default',
elementType: 'field',
collectionName,
form,
variables,
localVariables,
record,
formBlockType,
};
},
},
},
},
remove: {
type: 'void',
'x-component': 'ArrayCollapse.Remove',
},
},
},
properties: {
add: {
type: 'void',
title: '{{ t("Add events") }}',
'x-component': 'ArrayCollapse.Addition',
},
}, },
}, },
}, },
} as ISchema } as ISchema
} }
onSubmit={({ events }) => { onSubmit={({ events }) => {
console.log('onSubmit', events); console.log('todo onSubmit', JSON.parse(JSON.stringify(events)));
fieldSchema[SchemaSettingsKey] = events; // fieldSchema[SchemaSettingsKey] = events;
dn.emit('patch', { // dn.emit('patch', {
schema: { // schema: {
'x-uid': fieldSchema['x-uid'], // 'x-uid': fieldSchema['x-uid'],
[SchemaSettingsKey]: events, // [SchemaSettingsKey]: events,
}, // },
}); // });
register(events); // register(events);
dn.refresh(); // dn.refresh();
}} }}
/> />
); );

View File

@ -20,6 +20,8 @@ export interface EventParam {
[key: string]: EventParam; [key: string]: EventParam;
}; };
items?: EventParam; items?: EventParam;
interface?: string;
uiSchema?: any;
} }
/** 事件动作 */ /** 事件动作 */
@ -42,6 +44,7 @@ export interface EventEvent {
params?: { params?: {
[key: string]: EventParam; [key: string]: EventParam;
}; };
value?: any;
} }
/** 事件定义 */ /** 事件定义 */
@ -60,7 +63,11 @@ export interface EventDefinition {
/** 事件设置 */ /** 事件设置 */
export interface EventSetting { export interface EventSetting {
event: string; event: {
definition: string;
event: string;
uid?: string;
};
/** 标识同一类型组件的不同实例 */ /** 标识同一类型组件的不同实例 */
uid?: string; uid?: string;
condition: string; condition: string;