feat: actions 2.0 (#7105)

* refactor: streamline AddNewActionModel and remove unused imports in TableModel

* feat: add LinkPopupActionModel and update PopupActionModel usage

* feat: add PopupRecordActionModel and update index export

* feat: remove LinkPopupActionModel

* refactor: update ViewActionModel to extend RecordActionModel and remove LinkPopupActionModel export

* fix: correct import path for ReactiveField in FormFieldModel

* fix: correct import path for ReactiveField in FormFieldModel

* refactor: update ActionModel to improve button rendering and props handling

* refactor: simplify AddNewActionModel by removing unused imports and streamlining flow registration

* refactor: streamline DeleteActionModel by removing unnecessary confirmation steps

* feat: enhance LinkActionModel with new navigation and parameter handling features

* feat: add Emotion CSS support to LinkActionModel

* refactor: update BulkDeleteActionModel to improve flow registration and event handling

* refactor: update AddNewActionModel and BulkDeleteActionModel to use title and icon props

* refactor: update RefreshActionModel to use title and icon props, and improve flow registration

* refactor: rename LinkActionModel to LinkRecordActionModel

* feat: add LinkGlobalActionModel and register flow for link handling

* feat: implement openLinkAction and integrate with LinkGlobalActionModel and LinkRecordActionModel

* feat: add CustomRequestRecordActionModel and update index export

* feat: add CustomRequestGlobalActionModel and update index export

* feat: add BulkEditActionModel and update index export
This commit is contained in:
Zeke Zhang 2025-06-20 09:54:03 +08:00 committed by GitHub
parent 1e28346b0e
commit 78d4b27690
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 570 additions and 147 deletions

View File

@ -0,0 +1,94 @@
/**
* 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 { Variable } from '../../schema-component/antd/variable/Variable';
export const openLinkAction = {
title: '编辑链接',
uiSchema: {
url: {
title: 'URL',
'x-decorator': 'FormItem',
'x-component': Variable.TextArea,
description: 'Do not concatenate search params in the URL',
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `Search parameters`,
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
'x-component-props': {
style: {
flexWrap: 'nowrap',
maxWidth: '100%',
},
className: css`
& > .ant-space-item:first-child,
& > .ant-space-item:last-child {
flex-shrink: 0;
}
`,
},
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: `{{t("Name")}}`,
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': Variable.TextArea,
'x-component-props': {
placeholder: `{{t("Value")}}`,
useTypedConstant: true,
changeOnSelect: true,
},
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: 'Add parameter',
'x-component': 'ArrayItems.Addition',
},
},
},
openInNewWindow: {
type: 'boolean',
'x-content': 'Open in new window',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
},
handler(ctx, params) {
ctx.globals.modal.confirm({
title: `TODO`,
content: JSON.stringify(params, null, 2),
});
},
};

View File

@ -8,63 +8,25 @@
*/
import { ButtonProps } from 'antd';
import React from 'react';
import { FlowPage } from '../../FlowPage';
import { GlobalActionModel } from '../base/ActionModel';
import { openModeAction } from '../../actions/openModeAction';
export class AddNewActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
type: 'primary',
children: 'Add new',
title: 'Add new',
icon: 'PlusOutlined',
};
}
AddNewActionModel.registerFlow({
sort: 200,
title: '事件',
key: 'event1',
title: '点击事件',
key: 'handleClick',
on: {
eventName: 'click',
},
steps: {
step1: {
title: '弹窗配置',
uiSchema: {
width: {
type: 'number',
title: '宽度',
'x-decorator': 'FormItem',
'x-component': 'NumberPicker',
'x-component-props': {
placeholder: '请输入宽度',
},
},
},
defaultParams: {
width: 800,
},
handler(ctx, params) {
// eslint-disable-next-line prefer-const
let currentDrawer: any;
function DrawerContent() {
return (
<div>
<FlowPage
parentId={ctx.model.uid}
sharedContext={{ parentBlockModel: ctx.shared.currentBlockModel, currentDrawer }}
/>
</div>
);
}
currentDrawer = ctx.globals.drawer.open({
// title: '命令式 Drawer',
header: null,
width: params.width,
content: <DrawerContent />,
});
},
},
open: openModeAction,
},
});

View File

@ -10,20 +10,25 @@
import { MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
export class BulkDeleteActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
children: 'Delete',
title: 'Delete',
icon: 'DeleteOutlined',
};
}
BulkDeleteActionModel.registerFlow({
key: 'event1',
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
step1: {
secondaryConfirmationAction,
delete: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for deletion.');
@ -38,5 +43,6 @@ BulkDeleteActionModel.registerFlow({
ctx.globals.message.success('Selected records deleted successfully.');
},
},
refreshOnCompleteAction,
},
});

View File

@ -0,0 +1,63 @@
/**
* 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 { MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { openModeAction } from '../../actions/openModeAction';
export class BulkEditActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Bulk edit',
icon: 'EditOutlined',
};
}
BulkEditActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
openModeAction,
bulkEdit: {
title: '更新的数据',
uiSchema: {
updateMode: {
'x-component': 'Radio.Group',
'x-component-props': {
options: [
{ label: '更新选中行', value: 'selected' },
{ label: '更新所有行', value: 'all' },
],
},
},
},
defaultParams(ctx) {
return {
updateMode: 'selected',
};
},
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for bulk edit.');
return;
}
const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource;
if (resource.getSelectedRows().length === 0) {
ctx.globals.message.warning('No records selected for bulk edit.');
return;
}
await resource.destroySelectedRows();
ctx.globals.message.success('Successfully.');
},
},
},
});

View File

@ -15,9 +15,8 @@ import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { GlobalActionModel } from '../base/ActionModel';
export class CustomRequestActionModel extends GlobalActionModel {
export class CustomRequestGlobalActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Custom request',
};
}
@ -35,7 +34,7 @@ const useVariableProps = () => {
};
};
CustomRequestActionModel.registerFlow({
CustomRequestGlobalActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {

View File

@ -0,0 +1,304 @@
/**
* 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 type { ButtonProps } from 'antd/es/button';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
import { BlocksSelector } from '../../../schema-component/antd/action/Action.Designer';
import { useAfterSuccessOptions } from '../../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { RecordActionModel } from '../base/ActionModel';
export class CustomRequestRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Custom request',
};
}
const fieldNames = {
value: 'value',
label: 'label',
};
const useVariableProps = () => {
const environmentVariables = useGlobalVariable('$env');
const scope = useAfterSuccessOptions();
return {
scope: [environmentVariables, ...scope].filter(Boolean),
fieldNames,
};
};
CustomRequestRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
secondaryConfirmation: secondaryConfirmationAction,
request: {
title: '请求设置',
uiSchema: {
method: {
type: 'string',
required: true,
title: 'HTTP method',
'x-decorator-props': {
tooltip:
'When the HTTP method is Post, Put or Patch, and this custom request inside the form, the request body will be automatically filled in with the form data',
},
'x-decorator': 'FormItem',
'x-component': 'Select',
'x-component-props': {
showSearch: false,
allowClear: false,
className: 'auto-width',
},
enum: [
{ label: 'GET', value: 'GET' },
{ label: 'POST', value: 'POST' },
{ label: 'PUT', value: 'PUT' },
{ label: 'PATCH', value: 'PATCH' },
{ label: 'DELETE', value: 'DELETE' },
],
default: 'POST',
},
url: {
type: 'string',
required: true,
title: 'URL',
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useVariableProps,
'x-component-props': {
placeholder: 'https://www.nocobase.com',
},
},
headers: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: 'Headers',
description: '"Content-Type" only support "application/json", and no need to specify',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useVariableProps,
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: 'Add request header',
'x-component': 'ArrayItems.Addition',
},
},
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: 'Parameters',
items: {
type: 'object',
properties: {
space: {
type: 'void',
'x-component': 'Space',
properties: {
name: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
},
},
value: {
type: 'string',
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useVariableProps,
},
remove: {
type: 'void',
'x-decorator': 'FormItem',
'x-component': 'ArrayItems.Remove',
},
},
},
},
},
properties: {
add: {
type: 'void',
title: 'Add parameter',
'x-component': 'ArrayItems.Addition',
},
},
},
data: {
type: 'string',
title: 'Body',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Variable.JSON',
'x-component-props': {
scope: '{{useCustomRequestVariableOptions}}',
fieldNames: {
value: 'name',
label: 'title',
},
changeOnSelect: true,
autoSize: {
minRows: 10,
},
placeholder: 'Input request data',
},
description: 'Only support standard JSON data',
},
timeout: {
type: 'number',
title: 'Timeout config',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'InputNumber',
'x-component-props': {
addonAfter: 'ms',
min: 1,
step: 1000,
defaultValue: 5000,
},
},
responseType: {
type: 'string',
title: 'Response type',
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Select',
default: 'json',
enum: [
{ value: 'json', label: 'JSON' },
{ value: 'stream', label: 'Stream' },
],
},
},
async handler(ctx, params) {
ctx.globals.modal({
title: 'TODO: Custom request action handler',
});
},
},
afterSuccess: {
title: '提交成功后',
uiSchema: {
successMessage: {
title: 'Popup message',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
manualClose: {
title: 'Message popup close method',
enum: [
{ label: 'Automatic close', value: false },
{ label: 'Manually close', value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: {
title: 'Then',
'x-hidden': true,
enum: [
{ label: 'Stay on current page', value: false },
{ label: 'Redirect to', value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
'x-reactions': {
target: 'redirectTo',
fulfill: {
state: {
visible: '{{!!$self.value}}',
},
},
},
},
actionAfterSuccess: {
title: 'Action after successful submission',
enum: [
{ label: 'Stay on the current popup or page', value: 'stay' },
{ label: 'Return to the previous popup or page', value: 'previous' },
{ label: 'Redirect to', value: 'redirect' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
'x-reactions': {
target: 'redirectTo',
fulfill: {
state: {
visible: "{{$self.value==='redirect'}}",
},
},
},
},
redirectTo: {
title: 'Link',
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
// eslint-disable-next-line react-hooks/rules-of-hooks
'x-use-component-props': () => useVariableProps(),
},
blocksToRefresh: {
type: 'array',
title: 'Refresh data blocks',
'x-decorator': 'FormItem',
'x-use-decorator-props': () => {
return {
tooltip: 'After successful submission, the selected data blocks will be automatically refreshed.',
};
},
'x-component': BlocksSelector,
// 'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
},
},
handler(ctx, params) {},
},
refresh: refreshOnCompleteAction,
},
});

View File

@ -7,54 +7,28 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd';
import type { ButtonProps } from 'antd/es/button';
import { RecordActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { MultiRecordResource } from '@nocobase/flow-engine';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
export class DeleteActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
children: 'Delete',
type: 'link',
title: 'Delete',
};
}
DeleteActionModel.registerFlow({
key: 'event1',
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
confirm: {
uiSchema: {
title: {
type: 'string',
title: 'Confirm title',
'x-decorator': 'FormItem',
'x-component': 'Input',
},
content: {
type: 'string',
title: 'Confirm content',
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
title: 'Confirm Deletion',
content: 'Are you sure you want to delete this record?',
},
async handler(ctx, params) {
const confirmed = await ctx.globals.modal.confirm({
title: params.title,
content: params.content,
});
if (!confirmed) {
ctx.globals.message.info('Deletion cancelled.');
return ctx.exit();
}
},
},
step1: {
secondaryConfirmation: secondaryConfirmationAction,
delete: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for deletion.');
@ -69,5 +43,6 @@ DeleteActionModel.registerFlow({
ctx.globals.message.success('Record deleted successfully.');
},
},
refresh: refreshOnCompleteAction,
},
});

View File

@ -0,0 +1,29 @@
/**
* 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 type { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { openLinkAction } from '../../actions/openLinkAction';
export class LinkGlobalActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Link',
};
}
LinkGlobalActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
navigate: openLinkAction,
},
});

View File

@ -9,22 +9,22 @@
import type { ButtonProps } from 'antd';
import { RecordActionModel } from '../base/ActionModel';
import { openLinkAction } from '../../actions/openLinkAction';
export class LinkActionModel extends RecordActionModel {
export class LinkRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
children: 'Link',
};
}
LinkActionModel.registerFlow({
key: 'event1',
LinkRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
step1: {
handler(ctx, params) {},
},
navigate: openLinkAction,
},
});

View File

@ -11,14 +11,13 @@ import type { ButtonProps } from 'antd/es/button';
import { openModeAction } from '../../actions/openModeAction';
import { RecordActionModel } from '../base/ActionModel';
export class PopupActionModel extends RecordActionModel {
export class PopupRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Popup',
};
}
PopupActionModel.registerFlow({
PopupRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
on: {

View File

@ -9,20 +9,24 @@
import { ButtonProps } from 'antd';
import { GlobalActionModel } from '../base/ActionModel';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
export class RefreshActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
children: 'Refresh',
title: 'Refresh',
icon: 'ReloadOutlined',
};
}
RefreshActionModel.registerFlow({
key: 'event1',
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
step1: {
secondaryConfirmationAction,
refresh: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for refresh.');

View File

@ -7,50 +7,24 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { ButtonProps } from 'antd';
import React from 'react';
import { FlowPage } from '../../FlowPage';
import type { ButtonProps } from 'antd/es/button';
import { RecordActionModel } from '../base/ActionModel';
import { openModeAction } from '../../actions/openModeAction';
export class ViewActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
children: 'View',
type: 'link',
title: 'View',
};
}
ViewActionModel.registerFlow({
key: 'event1',
key: 'handleClick',
title: '点击事件',
on: {
eventName: 'click',
},
steps: {
step1: {
handler(ctx, params) {
// eslint-disable-next-line prefer-const
let currentDrawer: any;
function DrawerContent() {
return (
<div>
<FlowPage
parentId={ctx.model.uid}
sharedContext={{
currentDrawer,
parentRecord: ctx.extra.currentRecord,
parentBlockModel: ctx.shared.currentBlockModel,
}}
/>
</div>
);
}
currentDrawer = ctx.globals.drawer.open({
// title: '命令式 Drawer',
width: 800,
content: <DrawerContent />,
});
},
},
open: openModeAction,
},
});

View File

@ -9,13 +9,16 @@
export * from './AddNewActionModel';
export * from './BulkDeleteActionModel';
export * from './CustomRequestActionModel';
export * from './BulkEditActionModel';
export * from './CustomRequestRecordActionModel';
export * from './CustomRequestGlobalActionModel';
export * from './DeleteActionModel';
export * from './DuplicateActionModel';
export * from './EditActionModel';
export * from './FilterActionModel';
export * from './LinkActionModel';
export * from './PopupActionModel';
export * from './LinkRecordActionModel';
export * from './LinkGlobalActionModel';
export * from './PopupRecordActionModel';
export * from './RefreshActionModel';
export * from './UpdateRecordActionModel';
export * from './ViewActionModel';

View File

@ -11,44 +11,56 @@ import { FlowModel } from '@nocobase/flow-engine';
import { Button } from 'antd';
import type { ButtonProps } from 'antd/es/button';
import React from 'react';
import IconPicker from '../../../schema-component/antd/icon-picker/IconPicker';
import { Icon } from '../../../icon/Icon';
export class ActionModel extends FlowModel {
declare props: ButtonProps;
defaultProps: ButtonProps = {
type: 'default',
children: 'Action',
title: 'Action',
};
render() {
return <Button {...this.defaultProps} {...this.props} />;
const props = { ...this.defaultProps, ...this.props };
const icon = <Icon type={props.icon as any} />;
return (
<Button {...props} icon={icon}>
{props.children || props.title}
</Button>
);
}
}
ActionModel.registerFlow({
key: 'default',
title: '通用配置',
auto: true,
title: '基础',
sort: 100,
steps: {
step1: {
buttonProps: {
title: '编辑按钮',
uiSchema: {
children: {
type: 'string',
title: '标题',
title: {
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: '请输入标题',
},
title: 'Button title',
},
icon: {
'x-decorator': 'FormItem',
'x-component': IconPicker,
title: 'Button icon',
},
},
defaultParams(ctx) {
return {
type: 'default',
...ctx.model.defaultProps,
title: ctx.model.defaultProps.title,
icon: ctx.model.defaultProps.icon,
};
},
handler(ctx, params) {
ctx.model.setProps('children', params.children);
ctx.model.setProps(params);
ctx.model.setProps('onClick', (event) => {
ctx.model.dispatchEvent('click', {
...ctx.extra,

View File

@ -11,7 +11,6 @@ import { SettingOutlined } from '@ant-design/icons';
import { css } from '@emotion/css';
import {
AddActionButton,
AddActionModel,
AddFieldButton,
Collection,
FlowModelRenderer,