feat(custom-request): support variable in success message of custom request action (#6694)

* feat: support variable  in success message of custom request action

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug

* fix: bug
This commit is contained in:
Katherine 2025-04-24 17:59:01 +08:00 committed by GitHub
parent d1d8f4bbe7
commit a9fb7ea56e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 274 additions and 98 deletions

View File

@ -227,8 +227,8 @@ export function useCollectValuesToSubmit() {
]);
}
function interpolateVariables(str: string, scope: Record<string, any>): string {
return str.replace(/\{\{\s*([a-zA-Z0-9_$-.]+?)\s*\}\}/g, (_, key) => {
export function interpolateVariables(str: string, scope: Record<string, any>): string {
return str.replace(/\{\{\s*([a-zA-Z0-9_$.-]+?)\s*\}\}/g, (_, key) => {
return scope[key] !== undefined ? String(scope[key]) : '';
});
}

View File

@ -29,10 +29,9 @@ export const useAfterSuccessOptions = () => {
}, [fieldsOptions, userFieldOptions]);
const { settings: popupRecordSettings, shouldDisplayPopupRecord } = usePopupVariable();
const { currentRoleSettings } = useCurrentRoleVariable();
const record = useCollectionRecordData();
return useMemo(() => {
return [
(record || form) && {
form && {
value: '$record',
label: t('Response record', { ns: 'client' }),
children: [...fields],

View File

@ -31,3 +31,4 @@ export { default as useParseDataScopeFilter } from './hooks/useParseDataScopeFil
export * from './isPatternDisabled';
export { SchemaSettingsPlugin } from './SchemaSettingsPlugin';
export * from './VariableInput';
export { replaceVariables } from './LinkageRules/bindLinkageRulesToFiled';

View File

@ -18,6 +18,10 @@ import {
useNavigateNoUpdate,
useBlockRequestContext,
useContextVariable,
useLocalVariables,
useVariables,
replaceVariables,
interpolateVariables,
} from '@nocobase/client';
import { isURL } from '@nocobase/utils/client';
import { App } from 'antd';
@ -38,12 +42,21 @@ export const useCustomizeRequestActionProps = () => {
const { modal, message } = App.useApp();
const dataSourceKey = useDataSourceKey();
const { ctx } = useContextVariable();
const localVariables = useLocalVariables();
const variables = useVariables();
return {
async onClick(e?, callBack?) {
const selectedRecord = field.data?.selectedRowData ? field.data?.selectedRowData : ctx;
const selectedRecord = field?.data?.selectedRowData ? field?.data?.selectedRowData : ctx;
const { skipValidator, onSuccess } = actionSchema?.['x-action-settings'] ?? {};
const { manualClose, redirecting, redirectTo, successMessage, actionAfterSuccess } = onSuccess || {};
const {
manualClose,
redirecting,
redirectTo,
successMessage: rawSuccessMessage,
actionAfterSuccess,
} = onSuccess || {};
let successMessage = rawSuccessMessage;
const xAction = actionSchema?.['x-action'];
if (skipValidator !== true && xAction === 'customize:form:request') {
await form.submit();
@ -71,6 +84,19 @@ export const useCustomizeRequestActionProps = () => {
},
responseType: fieldSchema['x-response-type'] === 'stream' ? 'blob' : 'json',
});
try {
const { exp, scope: expScope } = await replaceVariables(successMessage, {
variables,
localVariables: [
...localVariables,
{ name: '$nResponse', ctx: new Proxy({ ...res?.data, ...res?.data?.data }, {}) },
],
});
successMessage = interpolateVariables(exp, expScope);
} catch (error) {
console.log(error);
}
if (res.headers['content-disposition']) {
const contentDisposition = res.headers['content-disposition'];
const utf8Match = contentDisposition.match(/filename\*=utf-8''([^;]+)/i);

View File

@ -1,92 +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 { useFieldSchema } from '@formily/react';
import {
AfterSuccess,
ButtonEditor,
RefreshDataBlockRequest,
RemoveButton,
SchemaSettings,
SchemaSettingsLinkageRules,
SecondConFirm,
useCollection,
useCollectionRecord,
useSchemaToolbar,
SchemaSettingAccessControl,
useDataBlockProps,
useCollectionManager_deprecated,
} from '@nocobase/client';
import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
export const customizeCustomRequestActionSettings = new SchemaSettings({
name: 'actionSettings:customRequest',
items: [
{
name: 'editButton',
Component: ButtonEditor,
useComponentProps() {
const fieldSchema = useFieldSchema();
return {
isLink: fieldSchema['x-action'] === 'customize:table:request',
};
},
},
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{
name: 'secondConFirm',
Component: SecondConFirm,
},
{
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
},
{
name: 'request settings',
Component: CustomRequestSettingsItem,
},
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{
name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest,
useComponentProps() {
return {
isPopupAction: false,
};
},
useVisible() {
const collection = useCollection();
return !!collection;
},
},
{
name: 'delete',
sort: 100,
Component: RemoveButton as any,
useComponentProps() {
const { removeButtonProps } = useSchemaToolbar();
return removeButtonProps;
},
},
],
});

View File

@ -0,0 +1,242 @@
/**
* 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 {
ButtonEditor,
RefreshDataBlockRequest,
RemoveButton,
SchemaSettings,
SchemaSettingsLinkageRules,
SecondConFirm,
useCollection,
useSchemaToolbar,
SchemaSettingAccessControl,
useDesignable,
useGlobalVariable,
usePlugin,
SchemaSettingsModalItem,
useAfterSuccessOptions,
BlocksSelector,
} from '@nocobase/client';
import React from 'react';
import { ISchema, useFieldSchema } from '@formily/react';
import { useTranslation } from 'react-i18next';
import { CustomRequestSettingsItem } from './components/CustomRequestActionDesigner';
const useVariableOptions = () => {
const scopes = useAfterSuccessOptions();
const { t } = useTranslation();
return [
{
value: '$nResponse',
label: t('Response', { ns: 'client' }),
children: null,
},
...scopes.filter((v: any) => ['currentUser', 'currentTime', '$nRole'].includes(v.value)),
].filter(Boolean);
};
const useLinkVariableOptions = () => {
const scopes = useAfterSuccessOptions();
const environmentVariables = useGlobalVariable('$env');
return [...scopes.filter((v: any) => v.value !== '$record'), environmentVariables].filter(Boolean);
};
const useLinkVariableProps = () => {
const scope = useLinkVariableOptions();
return {
scope,
useTypedConstant: true,
};
};
export function AfterSuccess() {
const { dn } = useDesignable();
const { t } = useTranslation();
const fieldSchema = useFieldSchema();
const { onSuccess } = fieldSchema?.['x-action-settings'] || {};
const templatePlugin: any = usePlugin('@nocobase/plugin-block-template');
const isInBlockTemplateConfigPage = templatePlugin?.isInBlockTemplateConfigPage?.();
return (
<SchemaSettingsModalItem
dialogRootClassName="dialog-after-successful-submission"
width={700}
title={t('After successful submission')}
initialValues={
onSuccess
? {
actionAfterSuccess: onSuccess?.redirecting ? 'redirect' : 'previous',
...onSuccess,
}
: {
manualClose: false,
redirecting: false,
successMessage: '{{t("Saved successfully")}}',
actionAfterSuccess: 'previous',
}
}
schema={
{
type: 'object',
title: t('After successful submission'),
properties: {
successMessage: {
title: t('Popup message'),
'x-decorator': 'FormItem',
'x-component': 'Variable.RawTextArea',
'x-component-props': {
scope: useVariableOptions,
style: { minWidth: '220px' },
},
},
manualClose: {
title: t('Message popup close method'),
enum: [
{ label: t('Automatic close'), value: false },
{ label: t('Manually close'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: {
title: t('Then'),
'x-hidden': true,
enum: [
{ label: t('Stay on current page'), value: false },
{ label: t('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: t('Action after successful submission'),
enum: [
{ label: t('Stay on the current popup or page'), value: 'stay' },
{ label: t('Return to the previous popup or page'), value: 'previous' },
{ label: t('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: t('Link'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useLinkVariableProps,
},
blocksToRefresh: {
type: 'array',
title: t('Refresh data blocks'),
'x-decorator': 'FormItem',
'x-use-decorator-props': () => {
return {
tooltip: t('After successful submission, the selected data blocks will be automatically refreshed.'),
};
},
'x-component': BlocksSelector,
'x-hidden': isInBlockTemplateConfigPage, // 模板配置页面暂不支持该配置
},
},
} as ISchema
}
onSubmit={(onSuccess) => {
fieldSchema['x-action-settings']['onSuccess'] = onSuccess;
dn.emit('patch', {
schema: {
['x-uid']: fieldSchema['x-uid'],
'x-action-settings': fieldSchema['x-action-settings'],
},
});
}}
/>
);
}
export const customizeCustomRequestActionSettings = new SchemaSettings({
name: 'actionSettings:customRequest',
items: [
{
name: 'editButton',
Component: ButtonEditor,
useComponentProps() {
const fieldSchema = useFieldSchema();
return {
isLink: fieldSchema['x-action'] === 'customize:table:request',
};
},
},
{
name: 'linkageRules',
Component: SchemaSettingsLinkageRules,
useComponentProps() {
const { linkageRulesProps } = useSchemaToolbar();
return {
...linkageRulesProps,
};
},
},
{
name: 'secondConFirm',
Component: SecondConFirm,
},
{
name: 'afterSuccessfulSubmission',
Component: AfterSuccess,
},
{
name: 'request settings',
Component: CustomRequestSettingsItem,
},
{
...SchemaSettingAccessControl,
useVisible() {
return true;
},
},
{
name: 'refreshDataBlockRequest',
Component: RefreshDataBlockRequest,
useComponentProps() {
return {
isPopupAction: false,
};
},
useVisible() {
const collection = useCollection();
return !!collection;
},
},
{
name: 'delete',
sort: 100,
Component: RemoveButton as any,
useComponentProps() {
const { removeButtonProps } = useSchemaToolbar();
return removeButtonProps;
},
},
],
});