Merge branch '2.0-i18n' into 2.0

This commit is contained in:
gchust 2025-06-28 22:36:34 +08:00
commit 4b79a4ad25
75 changed files with 1152 additions and 747 deletions

View File

@ -28,7 +28,7 @@ SubModel1.registerFlow({
},
},
handler(ctx, params) {
ctx.model.setProps('children', params.title);
ctx.model.setProps('children', ctx.globals.flowEngine.translate(params.title));
},
},
},

View File

@ -18,7 +18,7 @@ HelloFlowModel.registerFlow('defaultFlow', {
uiSchema: {
name: {
type: 'string',
title: 'Name',
title: "{{t('Name')}}",
'x-component': Input,
},
},

View File

@ -1,3 +1,13 @@
/**
* 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 { tval } from '@nocobase/utils/client';
import { useGlobalVariable } from '../../application/hooks/useGlobalVariable';
import { BlocksSelector } from '../../schema-component/antd/action/Action.Designer';
import { useAfterSuccessOptions } from '../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions';
@ -16,30 +26,30 @@ const useVariableProps = () => {
};
export const afterSuccessAction = {
title: '提交成功后',
title: tval('After successful submission'),
uiSchema: {
successMessage: {
title: 'Popup message',
title: tval('Popup message'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
manualClose: {
title: 'Message popup close method',
title: tval('Message popup close method'),
enum: [
{ label: 'Automatic close', value: false },
{ label: 'Manually close', value: true },
{ label: tval('Automatic close'), value: false },
{ label: tval('Manually close'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: {
title: 'Then',
title: tval('Then'),
'x-hidden': true,
enum: [
{ label: 'Stay on current page', value: false },
{ label: 'Redirect to', value: true },
{ label: tval('Stay on current page'), value: false },
{ label: tval('Redirect to'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -54,11 +64,11 @@ export const afterSuccessAction = {
},
},
actionAfterSuccess: {
title: 'Action after successful submission',
title: tval('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' },
{ label: tval('Stay on the current popup or page'), value: 'stay' },
{ label: tval('Return to the previous popup or page'), value: 'previous' },
{ label: tval('Redirect to'), value: 'redirect' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -73,7 +83,7 @@ export const afterSuccessAction = {
},
},
redirectTo: {
title: 'Link',
title: tval('Link'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -81,11 +91,11 @@ export const afterSuccessAction = {
},
blocksToRefresh: {
type: 'array',
title: 'Refresh data blocks',
title: tval('Refresh data blocks'),
'x-decorator': 'FormItem',
'x-use-decorator-props': () => {
return {
tooltip: 'After successful submission, the selected data blocks will be automatically refreshed.',
tooltip: tval('After successful submission, the selected data blocks will be automatically refreshed.'),
};
},
'x-component': BlocksSelector,

View File

@ -8,42 +8,43 @@
*/
import { defineAction } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
export const confirm = defineAction({
name: 'confirm',
title: '二次确认',
title: tval('Secondary confirmation'),
uiSchema: {
enable: {
type: 'boolean',
title: 'Enable secondary confirmation',
title: tval('Enable secondary confirmation'),
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
title: {
type: 'string',
title: 'Title',
default: 'Delete record',
title: tval('Title'),
default: tval('Delete record'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
content: {
type: 'string',
title: 'Content',
default: 'Are you sure you want to delete it?',
title: tval('Content'),
default: tval('Are you sure you want to delete it?'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
},
defaultParams: {
enable: true,
title: 'Delete record',
content: 'Are you sure you want to delete it?',
title: tval('Delete record'),
content: tval('Are you sure you want to delete it?'),
},
async handler(ctx, params) {
if (params.enable) {
const confirmed = await ctx.globals.modal.confirm({
title: params.title,
content: params.content,
title: ctx.model.translate(params.title),
content: ctx.model.translate(params.content),
});
if (!confirmed) {

View File

@ -9,11 +9,12 @@
import { defineAction, MultiRecordResource, useStepSettingContext } from '@nocobase/flow-engine';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FilterGroup } from '../components/FilterGroup';
export const dataScope = defineAction({
name: 'dataScope',
title: '数据范围',
title: tval('Data scope'),
uiSchema: {
filter: {
type: 'object',

View File

@ -8,22 +8,23 @@
*/
import { css } from '@emotion/css';
import { tval } from '@nocobase/utils/client';
import { Variable } from '../../schema-component/antd/variable/Variable';
export const openLinkAction = {
title: '编辑链接',
title: tval('Edit link'),
uiSchema: {
url: {
title: 'URL',
title: tval('URL'),
'x-decorator': 'FormItem',
'x-component': Variable.TextArea,
description: 'Do not concatenate search params in the URL',
description: tval('Do not concatenate search params in the URL'),
},
params: {
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: `Search parameters`,
title: tval('Search parameters'),
items: {
type: 'object',
properties: {
@ -48,7 +49,7 @@ export const openLinkAction = {
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: `{{t("Name")}}`,
placeholder: tval('Name'),
},
},
value: {
@ -56,7 +57,7 @@ export const openLinkAction = {
'x-decorator': 'FormItem',
'x-component': Variable.TextArea,
'x-component-props': {
placeholder: `{{t("Value")}}`,
placeholder: tval('Value'),
useTypedConstant: true,
changeOnSelect: true,
},
@ -73,14 +74,14 @@ export const openLinkAction = {
properties: {
add: {
type: 'void',
title: 'Add parameter',
title: tval('Add parameter'),
'x-component': 'ArrayItems.Addition',
},
},
},
openInNewWindow: {
type: 'boolean',
'x-content': 'Open in new window',
'x-content': tval('Open in new window'),
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},

View File

@ -1,26 +1,36 @@
/**
* 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 React from 'react';
import { tval } from '@nocobase/utils/client';
import { FlowPage } from '../FlowPage';
export const openModeAction = {
title: '打开方式',
title: tval('Open mode'),
uiSchema: {
mode: {
type: 'string',
title: '打开方式',
title: tval('Open mode'),
enum: [
{ label: 'Drawer', value: 'drawer' },
{ label: 'Modal', value: 'modal' },
{ label: tval('Drawer'), value: 'drawer' },
{ label: tval('Modal'), value: 'modal' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
size: {
type: 'string',
title: '弹窗尺寸',
title: tval('Popup size'),
enum: [
{ label: '小', value: 'small' },
{ label: '中', value: 'medium' },
{ label: '大', value: 'large' },
{ label: tval('Small'), value: 'small' },
{ label: tval('Medium'), value: 'medium' },
{ label: tval('Large'), value: 'large' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -57,7 +67,7 @@ export const openModeAction = {
};
currentDrawer = ctx.globals[params.mode].open({
title: '命令式 Drawer',
title: tval('Imperative Drawer'),
width: sizeToWidthMap[params.size],
content: <DrawerContent />,
});

View File

@ -8,31 +8,32 @@
*/
import { defineAction } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import React from 'react';
import { FlowPage } from '../FlowPage';
export const openView = defineAction({
name: 'openView',
title: '打开方式配置',
title: tval('Open mode configuration'),
uiSchema: {
mode: {
type: 'string',
title: '打开方式',
title: tval('Open mode'),
enum: [
{ label: 'Drawer', value: 'drawer' },
{ label: 'Dialog', value: 'dialog' },
{ label: 'Page', value: 'page' },
{ label: tval('Drawer'), value: 'drawer' },
{ label: tval('Dialog'), value: 'dialog' },
{ label: tval('Page'), value: 'page' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
},
size: {
type: 'string',
title: '弹窗尺寸',
title: tval('Popup size'),
enum: [
{ label: '小', value: 'small' },
{ label: '中', value: 'medium' },
{ label: '大', value: 'large' },
{ label: tval('Small'), value: 'small' },
{ label: tval('Medium'), value: 'medium' },
{ label: tval('Large'), value: 'large' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',

View File

@ -1,9 +1,20 @@
/**
* 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 { tval } from '@nocobase/utils/client';
export const refreshOnCompleteAction = {
title: '执行后刷新数据',
title: tval('Refresh data after execution'),
uiSchema: {
enable: {
type: 'boolean',
title: 'Enable refresh',
title: tval('Enable refresh'),
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
@ -16,7 +27,7 @@ export const refreshOnCompleteAction = {
async handler(ctx, params) {
if (params.enable) {
await ctx.extra.currentResource.refresh();
ctx.globals.message.success('Data refreshed successfully.');
ctx.globals.message.success(ctx.model.translate('Data refreshed successfully'));
}
},
};

View File

@ -1,23 +1,34 @@
/**
* 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 { tval } from '@nocobase/utils/client';
export const secondaryConfirmationAction = {
title: '二次确认',
title: tval('Secondary confirmation'),
uiSchema: {
enable: {
type: 'boolean',
title: 'Enable secondary confirmation',
title: tval('Enable secondary confirmation'),
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
title: {
type: 'string',
title: 'Title',
default: 'Delete record',
title: tval('Title'),
default: tval('Delete record'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
content: {
type: 'string',
title: 'Content',
default: 'Are you sure you want to delete it?',
title: tval('Content'),
default: tval('Are you sure you want to delete it?'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
},
@ -25,14 +36,14 @@ export const secondaryConfirmationAction = {
defaultParams(ctx) {
return {
enable: true,
title: 'Delete record',
content: 'Are you sure you want to delete it?',
title: tval('Delete record'),
content: tval('Are you sure you want to delete it?'),
};
},
async handler(ctx, params) {
if (params.enable) {
const confirmed = await ctx.globals.modal.confirm({
title: params.title,
title: ctx.globals.flowEngine.translate(params.title),
content: params.content,
});

View File

@ -9,11 +9,11 @@
import { css } from '@emotion/css';
import { defineAction } from '@nocobase/flow-engine';
import { getPickerFormat } from '@nocobase/utils/client';
import { getPickerFormat, tval } from '@nocobase/utils/client';
import { ExpiresRadio, DateFormatCom } from '../components';
export const dateTimeFormat = defineAction({
title: 'Date display format',
title: tval('Date display format'),
name: 'dateDisplayFormat',
uiSchema: {
picker: {
@ -80,7 +80,7 @@ export const dateTimeFormat = defineAction({
value: 'DD/MM/YYYY',
},
{
label: 'custom',
label: tval('Custom'),
value: 'custom',
},
],
@ -146,7 +146,7 @@ export const dateTimeFormat = defineAction({
value: 'HH:mm:ss',
},
{
label: 'custom',
label: tval('Custom'),
value: 'custom',
},
],

View File

@ -10,6 +10,7 @@
import { defineAction, useStepSettingContext } from '@nocobase/flow-engine';
import { Select } from 'antd';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { useCompile } from '../../schema-component';
import { getUniqueKeyFromCollection } from '../../collection-manager/interfaces/utils';
import { isTitleField } from '../../data-source';
@ -37,7 +38,7 @@ const SelectOptions = (props) => {
export const titleField = defineAction({
name: 'titleField',
title: 'Title field',
title: tval('Title field'),
uiSchema: {
label: {
'x-component': SelectOptions,

View File

@ -8,23 +8,24 @@
*/
import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { GlobalActionModel } from '../base/ActionModel';
export class AddNewActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
type: 'primary',
title: 'Add new',
title: tval('Add new'),
icon: 'PlusOutlined',
};
}
AddNewActionModel.define({
title: 'Add new',
title: tval('Add new'),
});
AddNewActionModel.registerFlow({
sort: 200,
title: '点击事件',
title: tval('Click event'),
key: 'handleClick',
on: {
eventName: 'click',

View File

@ -9,24 +9,23 @@
import { MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { tval } from '@nocobase/utils/client';
import { GlobalActionModel } from '../base/ActionModel';
export class BulkDeleteActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Delete',
title: tval('Delete'),
icon: 'DeleteOutlined',
};
}
BulkDeleteActionModel.define({
title: 'Delete',
title: tval('Delete'),
});
BulkDeleteActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
@ -36,17 +35,18 @@ BulkDeleteActionModel.registerFlow({
},
delete: {
async handler(ctx, params) {
const t = ctx.model.translate;
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for deletion.');
ctx.globals.message.error(t('No resource selected for deletion'));
return;
}
const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource;
if (resource.getSelectedRows().length === 0) {
ctx.globals.message.warning('No records selected for deletion.');
ctx.globals.message.warning(t('No records selected for deletion'));
return;
}
await resource.destroySelectedRows();
ctx.globals.message.success('Selected records deleted successfully.');
ctx.globals.message.success(t('Selected records deleted successfully'));
},
},
},

View File

@ -9,38 +9,39 @@
import { MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { openModeAction } from '../../actions/openModeAction';
import { GlobalActionModel } from '../base/ActionModel';
export class BulkEditActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Bulk edit',
title: tval('Bulk edit'),
icon: 'EditOutlined',
};
}
BulkEditActionModel.define({
title: 'Bulk edit',
title: tval('Bulk edit'),
hide: true,
});
BulkEditActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
openModeAction,
bulkEdit: {
title: '更新的数据',
title: tval('Data will be updated'),
uiSchema: {
updateMode: {
'x-component': 'Radio.Group',
'x-component-props': {
options: [
{ label: '更新选中行', value: 'selected' },
{ label: '更新所有行', value: 'all' },
{ label: tval('Update selected data?'), value: 'selected' },
{ label: tval('Update all data?'), value: 'all' },
],
},
},
@ -51,17 +52,18 @@ BulkEditActionModel.registerFlow({
};
},
async handler(ctx, params) {
const t = ctx.model.translate;
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for bulk edit.');
ctx.globals.message.error(t('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.');
ctx.globals.message.warning(t('No records selected for bulk edit'));
return;
}
await resource.destroySelectedRows();
ctx.globals.message.success('Successfully.');
//TODO: await resource.updateSelectedRows(params);
ctx.globals.message.success(t('updateSelectedRows not implemented!'));
},
},
},

View File

@ -8,6 +8,7 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
import { BlocksSelector } from '../../../schema-component/antd/action/Action.Designer';
import { useAfterSuccessOptions } from '../../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions';
@ -17,12 +18,12 @@ import { GlobalActionModel } from '../base/ActionModel';
export class CustomRequestGlobalActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Custom request',
title: tval('Custom request'),
};
}
CustomRequestGlobalActionModel.define({
title: 'Custom request',
title: tval('Custom request'),
hide: true,
});
@ -41,22 +42,23 @@ const useVariableProps = () => {
CustomRequestGlobalActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
secondaryConfirmation: secondaryConfirmationAction,
request: {
title: '请求设置',
title: tval('Request settings'),
uiSchema: {
method: {
type: 'string',
required: true,
title: 'HTTP method',
title: tval('HTTP method'),
'x-decorator-props': {
tooltip:
tooltip: tval(
'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',
@ -77,20 +79,20 @@ CustomRequestGlobalActionModel.registerFlow({
url: {
type: 'string',
required: true,
title: 'URL',
title: tval('URL'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useVariableProps,
'x-component-props': {
placeholder: 'https://www.nocobase.com',
placeholder: tval('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',
title: tval('Headers'),
description: tval('"Content-Type" only support "application/json", and no need to specify'),
items: {
type: 'object',
properties: {
@ -103,7 +105,7 @@ CustomRequestGlobalActionModel.registerFlow({
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
placeholder: tval('Name'),
},
},
value: {
@ -124,7 +126,7 @@ CustomRequestGlobalActionModel.registerFlow({
properties: {
add: {
type: 'void',
title: 'Add request header',
title: tval('Add request header'),
'x-component': 'ArrayItems.Addition',
},
},
@ -133,7 +135,7 @@ CustomRequestGlobalActionModel.registerFlow({
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: 'Parameters',
title: tval('Parameters'),
items: {
type: 'object',
properties: {
@ -146,7 +148,7 @@ CustomRequestGlobalActionModel.registerFlow({
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
placeholder: tval('Name'),
},
},
value: {
@ -167,14 +169,14 @@ CustomRequestGlobalActionModel.registerFlow({
properties: {
add: {
type: 'void',
title: 'Add parameter',
title: tval('Add parameter'),
'x-component': 'ArrayItems.Addition',
},
},
},
data: {
type: 'string',
title: 'Body',
title: tval('Body'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Variable.JSON',
@ -188,13 +190,13 @@ CustomRequestGlobalActionModel.registerFlow({
autoSize: {
minRows: 10,
},
placeholder: 'Input request data',
placeholder: tval('Input request data'),
},
description: 'Only support standard JSON data',
description: tval('Only support standard JSON data'),
},
timeout: {
type: 'number',
title: 'Timeout config',
title: tval('Timeout config'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'InputNumber',
@ -207,7 +209,7 @@ CustomRequestGlobalActionModel.registerFlow({
},
responseType: {
type: 'string',
title: 'Response type',
title: tval('Response type'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Select',
@ -220,35 +222,35 @@ CustomRequestGlobalActionModel.registerFlow({
},
async handler(ctx, params) {
ctx.globals.modal({
title: 'TODO: Custom request action handler',
title: tval('TODO: Custom request action handler'),
});
},
},
afterSuccess: {
title: '提交成功后',
title: tval('After successful submission'),
uiSchema: {
successMessage: {
title: 'Popup message',
title: tval('Popup message'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
manualClose: {
title: 'Message popup close method',
title: tval('Message popup close method'),
enum: [
{ label: 'Automatic close', value: false },
{ label: 'Manually close', value: true },
{ label: tval('Automatic close'), value: false },
{ label: tval('Manually close'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: {
title: 'Then',
title: tval('Then'),
'x-hidden': true,
enum: [
{ label: 'Stay on current page', value: false },
{ label: 'Redirect to', value: true },
{ label: tval('Stay on current page'), value: false },
{ label: tval('Redirect to'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -263,11 +265,11 @@ CustomRequestGlobalActionModel.registerFlow({
},
},
actionAfterSuccess: {
title: 'Action after successful submission',
title: tval('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' },
{ label: tval('Stay on the current popup or page'), value: 'stay' },
{ label: tval('Return to the previous popup or page'), value: 'previous' },
{ label: tval('Redirect to'), value: 'redirect' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -282,7 +284,7 @@ CustomRequestGlobalActionModel.registerFlow({
},
},
redirectTo: {
title: 'Link',
title: tval('Link'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -290,11 +292,11 @@ CustomRequestGlobalActionModel.registerFlow({
},
blocksToRefresh: {
type: 'array',
title: 'Refresh data blocks',
title: tval('Refresh data blocks'),
'x-decorator': 'FormItem',
'x-use-decorator-props': () => {
return {
tooltip: 'After successful submission, the selected data blocks will be automatically refreshed.',
tooltip: tval('After successful submission, the selected data blocks will be automatically refreshed.'),
};
},
'x-component': BlocksSelector,

View File

@ -8,6 +8,7 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { useGlobalVariable } from '../../../application/hooks/useGlobalVariable';
import { BlocksSelector } from '../../../schema-component/antd/action/Action.Designer';
import { useAfterSuccessOptions } from '../../../schema-component/antd/action/hooks/useGetAfterSuccessVariablesOptions';
@ -18,12 +19,12 @@ import { RecordActionModel } from '../base/ActionModel';
export class CustomRequestRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Custom request',
title: tval('Custom request'),
};
}
CustomRequestRecordActionModel.define({
title: 'Custom request',
title: tval('Custom request'),
hide: true,
});
@ -42,22 +43,23 @@ const useVariableProps = () => {
CustomRequestRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
secondaryConfirmation: secondaryConfirmationAction,
request: {
title: '请求设置',
title: tval('Request settings'),
uiSchema: {
method: {
type: 'string',
required: true,
title: 'HTTP method',
title: tval('HTTP method'),
'x-decorator-props': {
tooltip:
tooltip: tval(
'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',
@ -78,20 +80,20 @@ CustomRequestRecordActionModel.registerFlow({
url: {
type: 'string',
required: true,
title: 'URL',
title: tval('URL'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
'x-use-component-props': useVariableProps,
'x-component-props': {
placeholder: 'https://www.nocobase.com',
placeholder: tval('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',
title: tval('Headers'),
description: tval('"Content-Type" only support "application/json", and no need to specify'),
items: {
type: 'object',
properties: {
@ -104,7 +106,7 @@ CustomRequestRecordActionModel.registerFlow({
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
placeholder: tval('Name'),
},
},
value: {
@ -125,7 +127,7 @@ CustomRequestRecordActionModel.registerFlow({
properties: {
add: {
type: 'void',
title: 'Add request header',
title: tval('Add request header'),
'x-component': 'ArrayItems.Addition',
},
},
@ -134,7 +136,7 @@ CustomRequestRecordActionModel.registerFlow({
type: 'array',
'x-component': 'ArrayItems',
'x-decorator': 'FormItem',
title: 'Parameters',
title: tval('Parameters'),
items: {
type: 'object',
properties: {
@ -147,7 +149,7 @@ CustomRequestRecordActionModel.registerFlow({
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Name',
placeholder: tval('Name'),
},
},
value: {
@ -168,14 +170,14 @@ CustomRequestRecordActionModel.registerFlow({
properties: {
add: {
type: 'void',
title: 'Add parameter',
title: tval('Add parameter'),
'x-component': 'ArrayItems.Addition',
},
},
},
data: {
type: 'string',
title: 'Body',
title: tval('Body'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Variable.JSON',
@ -183,19 +185,19 @@ CustomRequestRecordActionModel.registerFlow({
scope: '{{useCustomRequestVariableOptions}}',
fieldNames: {
value: 'name',
label: 'title',
label: tval('title'),
},
changeOnSelect: true,
autoSize: {
minRows: 10,
},
placeholder: 'Input request data',
placeholder: tval('Input request data'),
},
description: 'Only support standard JSON data',
description: tval('Only support standard JSON data'),
},
timeout: {
type: 'number',
title: 'Timeout config',
title: tval('Timeout config'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'InputNumber',
@ -208,7 +210,7 @@ CustomRequestRecordActionModel.registerFlow({
},
responseType: {
type: 'string',
title: 'Response type',
title: tval('Response type'),
'x-decorator': 'FormItem',
'x-decorator-props': {},
'x-component': 'Select',
@ -221,35 +223,35 @@ CustomRequestRecordActionModel.registerFlow({
},
async handler(ctx, params) {
ctx.globals.modal({
title: 'TODO: Custom request action handler',
title: tval('TODO: Custom request action handler'),
});
},
},
afterSuccess: {
title: '提交成功后',
title: tval('After successful submission'),
uiSchema: {
successMessage: {
title: 'Popup message',
title: tval('Popup message'),
'x-decorator': 'FormItem',
'x-component': 'Input.TextArea',
'x-component-props': {},
},
manualClose: {
title: 'Message popup close method',
title: tval('Message popup close method'),
enum: [
{ label: 'Automatic close', value: false },
{ label: 'Manually close', value: true },
{ label: tval('Automatic close'), value: false },
{ label: tval('Manually close'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
'x-component-props': {},
},
redirecting: {
title: 'Then',
title: tval('Then'),
'x-hidden': true,
enum: [
{ label: 'Stay on current page', value: false },
{ label: 'Redirect to', value: true },
{ label: tval('Stay on current page'), value: false },
{ label: tval('Redirect to'), value: true },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -264,11 +266,11 @@ CustomRequestRecordActionModel.registerFlow({
},
},
actionAfterSuccess: {
title: 'Action after successful submission',
title: tval('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' },
{ label: tval('Stay on the current popup or page'), value: 'stay' },
{ label: tval('Return to the previous popup or page'), value: 'previous' },
{ label: tval('Redirect to'), value: 'redirect' },
],
'x-decorator': 'FormItem',
'x-component': 'Radio.Group',
@ -283,7 +285,7 @@ CustomRequestRecordActionModel.registerFlow({
},
},
redirectTo: {
title: 'Link',
title: tval('Link'),
'x-decorator': 'FormItem',
'x-component': 'Variable.TextArea',
// eslint-disable-next-line react-hooks/rules-of-hooks
@ -291,11 +293,11 @@ CustomRequestRecordActionModel.registerFlow({
},
blocksToRefresh: {
type: 'array',
title: 'Refresh data blocks',
title: tval('Refresh data blocks'),
'x-decorator': 'FormItem',
'x-use-decorator-props': () => {
return {
tooltip: 'After successful submission, the selected data blocks will be automatically refreshed.',
tooltip: tval('After successful submission, the selected data blocks will be automatically refreshed.'),
};
},
'x-component': BlocksSelector,

View File

@ -9,22 +9,23 @@
import { MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { RecordActionModel } from '../base/ActionModel';
export class DeleteActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Delete',
title: tval('Delete'),
};
}
DeleteActionModel.define({
title: 'Delete',
title: tval('Delete'),
});
DeleteActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
@ -34,17 +35,18 @@ DeleteActionModel.registerFlow({
},
delete: {
async handler(ctx, params) {
const t = ctx.model.translate;
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for deletion.');
ctx.globals.message.error(t('No resource selected for deletion'));
return;
}
if (!ctx.shared.currentRecord) {
ctx.globals.message.error('No resource or record selected for deletion.');
ctx.globals.message.error(t('No resource or record selected for deletion'));
return;
}
const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource;
await resource.destroy(ctx.shared.currentRecord);
ctx.globals.message.success('Record deleted successfully.');
ctx.globals.message.success(t('Record deleted successfully'));
},
},
},

View File

@ -8,31 +8,32 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { openModeAction } from '../../actions/openModeAction';
import { RecordActionModel } from '../base/ActionModel';
export class DuplicateActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Duplicate',
title: tval('Duplicate'),
};
}
DuplicateActionModel.define({
title: 'Duplicate',
title: tval('Duplicate'),
hide: true,
});
DuplicateActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
open: openModeAction,
duplicateMode: {
title: '复制方式',
title: tval('Duplicate mode'),
uiSchema: {
// TODO
duplicateMode: {
@ -40,15 +41,15 @@ DuplicateActionModel.registerFlow({
'x-component': 'Select',
enum: [
{
label: '快速复制',
label: tval('Quick duplicate'),
value: 'quickDuplicate',
},
{
label: '表单复制',
label: tval('Form duplicate'),
value: 'formDuplicate',
},
],
title: '复制方式',
title: tval('Duplicate mode'),
},
},
defaultParams(ctx) {

View File

@ -8,23 +8,24 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { openModeAction } from '../../actions/openModeAction';
import { RecordActionModel } from '../base/ActionModel';
export class EditActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Edit',
title: tval('Edit'),
};
}
EditActionModel.define({
title: 'Edit',
title: tval('Edit'),
});
EditActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -10,6 +10,7 @@
import { MultiRecordResource, useFlowModel, useStepSettingContext } from '@nocobase/flow-engine';
import { Button, ButtonProps, Popover, Select, Space } from 'antd';
import React, { FC } from 'react';
import { tval } from '@nocobase/utils/client';
import { FilterGroup } from '../../components/FilterGroup';
import { GlobalActionModel } from '../base/ActionModel';
import { DataBlockModel } from '../base/BlockModel';
@ -19,14 +20,15 @@ const FilterContent: FC<{ value: any }> = (props) => {
const currentBlockModel = modelInstance.ctx.shared.currentBlockModel as DataBlockModel;
const fields = currentBlockModel.collection.getFields();
const ignoreFieldsNames = modelInstance.props.ignoreFieldsNames || [];
const t = modelInstance.translate;
return (
<>
<FilterGroup value={props.value} fields={fields} ignoreFieldsNames={ignoreFieldsNames} ctx={modelInstance.ctx} />
<Space style={{ width: '100%', display: 'flex', justifyContent: 'flex-end' }}>
<Button onClick={() => modelInstance.dispatchEvent('reset')}>Reset</Button>
<Button onClick={() => modelInstance.dispatchEvent('reset')}>{t('Reset')}</Button>
<Button type="primary" onClick={() => modelInstance.dispatchEvent('submit')}>
Submit
{t('Submit')}
</Button>
</Space>
</>
@ -42,7 +44,7 @@ export class FilterActionModel extends GlobalActionModel {
defaultProps: any = {
type: 'default',
children: 'Filter',
title: tval('Filter'),
icon: 'FilterOutlined',
filterValue: { $and: [] },
ignoreFieldsNames: [],
@ -63,16 +65,16 @@ export class FilterActionModel extends GlobalActionModel {
}
FilterActionModel.define({
title: 'Filter',
title: tval('Filter'),
});
FilterActionModel.registerFlow({
key: 'filterSettings',
title: '筛选配置',
title: tval('Filter configuration'),
auto: true,
steps: {
ignoreFieldsNames: {
title: '可筛选字段',
title: tval('Filterable fields'),
uiSchema: {
ignoreFieldsNames: {
type: 'array',
@ -90,7 +92,7 @@ FilterActionModel.registerFlow({
},
'x-component-props': {
mode: 'multiple',
placeholder: '请选择不可筛选的字段',
placeholder: tval('Please select non-filterable fields'),
},
},
},
@ -104,7 +106,7 @@ FilterActionModel.registerFlow({
},
},
defaultValue: {
title: '默认筛选条件',
title: tval('Default filter conditions'),
uiSchema: {
filter: {
type: 'object',
@ -141,7 +143,7 @@ FilterActionModel.registerFlow({
FilterActionModel.registerFlow({
key: 'handleSubmit',
title: '提交',
title: tval('Submit'),
on: {
eventName: 'submit',
},
@ -162,7 +164,7 @@ FilterActionModel.registerFlow({
FilterActionModel.registerFlow({
key: 'handleReset',
title: '重置',
title: tval('Reset'),
on: {
eventName: 'reset',
},
@ -183,7 +185,7 @@ FilterActionModel.registerFlow({
FilterActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -8,22 +8,23 @@
*/
import type { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { GlobalActionModel } from '../base/ActionModel';
import { openLinkAction } from '../../actions/openLinkAction';
export class LinkGlobalActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Link',
title: tval('Link'),
};
}
LinkGlobalActionModel.define({
title: 'Link',
title: tval('Link'),
});
LinkGlobalActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -8,24 +8,25 @@
*/
import type { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { openLinkAction } from '../../actions/openLinkAction';
import { RecordActionModel } from '../base/ActionModel';
export class LinkRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
children: 'Link',
children: tval('Link'),
};
}
LinkRecordActionModel.define({
title: 'Link',
title: tval('Link'),
hide: true,
});
LinkRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -8,22 +8,23 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { openModeAction } from '../../actions/openModeAction';
import { RecordActionModel } from '../base/ActionModel';
export class PopupRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
title: 'Popup',
title: tval('Popup'),
};
}
PopupRecordActionModel.define({
title: 'Popup',
title: tval('Popup'),
});
PopupRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -8,31 +8,33 @@
*/
import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { GlobalActionModel } from '../base/ActionModel';
export class RefreshActionModel extends GlobalActionModel {
defaultProps: ButtonProps = {
title: 'Refresh',
title: tval('Refresh'),
icon: 'ReloadOutlined',
};
}
RefreshActionModel.define({
title: 'Refresh',
title: tval('Refresh'),
});
RefreshActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
refresh: {
async handler(ctx, params) {
const t = ctx.model.translate;
const currentResource = ctx.shared?.currentBlockModel?.resource;
if (!currentResource) {
ctx.globals.message.error('No resource selected for refresh.');
ctx.globals.message.error(t('No resource selected for refresh'));
return;
}
currentResource.loading = true;

View File

@ -12,29 +12,30 @@ import { afterSuccessAction } from '../../actions/afterSuccessAction';
import { refreshOnCompleteAction } from '../../actions/refreshOnCompleteAction';
import { secondaryConfirmationAction } from '../../actions/secondaryConfirmationAction';
import { RecordActionModel } from '../base/ActionModel';
import { tval } from '@nocobase/utils/client';
export class UpdateRecordActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'Update record',
title: tval('Update record'),
};
}
UpdateRecordActionModel.define({
title: 'Update record',
title: tval('Update record action'),
hide: true,
});
UpdateRecordActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},
steps: {
secondaryConfirmation: secondaryConfirmationAction,
update: {
title: '字段赋值',
title: tval('Assign field values'),
handler: async (ctx, params) => {},
},
afterSuccess: afterSuccessAction,

View File

@ -8,22 +8,23 @@
*/
import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { RecordActionModel } from '../base/ActionModel';
export class ViewActionModel extends RecordActionModel {
defaultProps: ButtonProps = {
type: 'link',
title: 'View',
title: tval('View'),
};
}
ViewActionModel.define({
title: 'View',
title: tval('View'),
});
ViewActionModel.registerFlow({
key: 'handleClick',
title: '点击事件',
title: tval('Click event'),
on: {
eventName: 'click',
},

View File

@ -9,6 +9,7 @@
import { FlowModel } from '@nocobase/flow-engine';
import { Button } from 'antd';
import { tval } from '@nocobase/utils/client';
import type { ButtonProps } from 'antd/es/button';
import React from 'react';
import { Icon } from '../../../icon/Icon';
@ -19,7 +20,7 @@ export class ActionModel extends FlowModel {
defaultProps: ButtonProps = {
type: 'default',
title: 'Action',
title: tval('Action'),
};
render() {
@ -37,21 +38,21 @@ export class ActionModel extends FlowModel {
ActionModel.registerFlow({
key: 'default',
title: '通用配置',
title: tval('General configuration'),
auto: true,
steps: {
buttonProps: {
title: '编辑按钮',
title: tval('Edit button'),
uiSchema: {
title: {
'x-decorator': 'FormItem',
'x-component': 'Input',
title: 'Button title',
title: tval('Button title'),
},
icon: {
'x-decorator': 'FormItem',
'x-component': IconPicker,
title: 'Button icon',
title: tval('Button icon'),
},
},
defaultParams(ctx) {
@ -61,7 +62,7 @@ ActionModel.registerFlow({
};
},
handler(ctx, params) {
ctx.model.setProps('title', params.title);
ctx.model.setProps('title', ctx.globals.flowEngine.translate(params.title));
ctx.model.setProps('icon', params.icon);
ctx.model.setProps('onClick', (event) => {
ctx.model.dispatchEvent('click', {
@ -78,7 +79,7 @@ export class GlobalActionModel extends ActionModel {}
export class RecordActionModel extends ActionModel {
defaultProps: ButtonProps = {
type: 'link',
children: 'Action',
children: tval('Action'),
};
render() {
@ -95,21 +96,21 @@ export class RecordActionModel extends ActionModel {
RecordActionModel.registerFlow({
key: 'default',
title: '通用配置',
title: tval('General configuration'),
auto: true,
steps: {
buttonProps: {
title: '编辑按钮',
title: tval('Edit button'),
uiSchema: {
title: {
'x-decorator': 'FormItem',
'x-component': 'Input',
title: 'Button title',
title: tval('Button title'),
},
icon: {
'x-decorator': 'FormItem',
'x-component': IconPicker,
title: 'Button icon',
title: tval('Button icon'),
},
},
defaultParams(ctx) {
@ -126,7 +127,7 @@ RecordActionModel.registerFlow({
if (!currentBlockModel) {
throw new Error('Current block model is not set in shared context');
}
ctx.model.setProps('title', params.title);
ctx.model.setProps('title', ctx.globals.flowEngine.translate(params.title));
ctx.model.setProps('icon', params.icon);
ctx.model.setProps('onClick', (event) => {
ctx.model.dispatchEvent('click', {

View File

@ -21,6 +21,7 @@ import {
import { Alert, Space } from 'antd';
import _ from 'lodash';
import React, { useState } from 'react';
import { tval } from '@nocobase/utils/client';
import { Grid } from '../../components/Grid';
import JsonEditor from '../../components/JsonEditor';
import { BlockModel } from './BlockModel';
@ -97,6 +98,7 @@ export class GridModel extends FlowModel<GridModelStructure> {
}
render() {
const t = this.translate;
return (
<div style={{ padding: 16 }}>
<Space direction={'vertical'} style={{ width: '100%' }} size={16}>
@ -117,14 +119,14 @@ export class GridModel extends FlowModel<GridModelStructure> {
/>
<Space>
<AddBlockButton model={this} subModelKey="items" subModelBaseClass={this.subModelBaseClass}>
<FlowSettingsButton icon={<PlusOutlined />}>{'Add block'}</FlowSettingsButton>
<FlowSettingsButton icon={<PlusOutlined />}>{t('Add block')}</FlowSettingsButton>
</AddBlockButton>
<FlowSettingsButton
onClick={() => {
this.openStepSettingsDialog('defaultFlow', 'grid');
}}
>
Configure rows
{t('Configure rows')}
</FlowSettingsButton>
</Space>
</Space>
@ -145,23 +147,24 @@ GridModel.registerFlow({
grid: {
uiSchema: {
rows: {
title: 'Rows',
title: tval('Rows'),
'x-decorator': 'FormItem',
'x-component': JsonEditor,
'x-component-props': {
autoSize: { minRows: 10, maxRows: 20 },
description: 'Configure the rows and columns of the grid.',
description: tval('Configure the rows and columns of the grid.'),
},
},
sizes: {
title: 'Sizes',
title: tval('Sizes'),
'x-decorator': 'FormItem',
'x-component': JsonEditor,
'x-component-props': {
rows: 5,
},
description:
description: tval(
'Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.',
),
},
},
async handler(ctx, params) {

View File

@ -14,6 +14,7 @@ import { FlowModel, FlowModelRenderer, FlowSettingsButton } from '@nocobase/flow
import { Tabs } from 'antd';
import _ from 'lodash';
import React from 'react';
import { tval } from '@nocobase/utils/client';
type PageModelStructure = {
subModels: {
@ -63,7 +64,7 @@ export class PageModel extends FlowModel<PageModelStructure> {
});
}}
>
Add tab
{this.flowEngine.translate('Add tab')}
</FlowSettingsButton>
}
/>
@ -82,24 +83,24 @@ export class PageModel extends FlowModel<PageModelStructure> {
PageModel.registerFlow({
key: 'default',
title: '基础配置',
title: tval('Basic configuration'),
auto: true,
steps: {
settings: {
title: '配置页面',
title: tval('Configure page'),
uiSchema: {
title: {
type: 'string',
title: 'Page Title',
title: tval('Page Title'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter page title',
placeholder: tval('Enter page title'),
},
},
enableTabs: {
type: 'boolean',
title: 'Enable tabs',
title: tval('Enable tabs'),
'x-decorator': 'FormItem',
'x-component': 'Switch',
},
@ -111,7 +112,7 @@ PageModel.registerFlow({
};
},
async handler(ctx, params) {
ctx.model.setProps('title', params.title);
ctx.model.setProps('title', ctx.globals.flowEngine.translate(params.title));
ctx.model.setProps('enableTabs', params.enableTabs);
if (ctx.shared.currentDrawer) {

View File

@ -12,6 +12,7 @@ import { Card, Modal } from 'antd';
import moment from 'moment';
import React from 'react';
import { Calendar, momentLocalizer } from 'react-big-calendar';
import { tval } from '@nocobase/utils/client';
import 'react-big-calendar/lib/css/react-big-calendar.css';
import { DataBlockModel } from '../../base/BlockModel';
@ -47,7 +48,7 @@ export class CalendarBlockModel extends DataBlockModel {
}
CalendarBlockModel.define({
title: 'Calendar',
title: tval('Calendar'),
group: 'Content',
hide: true,
defaultOptions: {
@ -64,13 +65,20 @@ CalendarBlockModel.registerFlow({
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
const t = ctx.model.translate;
Modal.info({
title: 'Event Selected',
title: t('Event selected'),
content: (
<div>
<p>Title: {ctx.extra.event.nickname}</p>
<p>Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>
{t('Title')}: {ctx.extra.event.nickname}
</p>
<p>
{t('Start')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
<p>
{t('End')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
</div>
),
});
@ -88,13 +96,20 @@ CalendarBlockModel.registerFlow({
step1: {
handler(ctx, params) {
console.log('ctx.extra.event', ctx.extra.event);
const t = ctx.model.translate;
Modal.info({
title: 'Double Click',
title: t('Double click'),
content: (
<div>
<p>Title: {ctx.extra.event.nickname}</p>
<p>Start: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>End: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}</p>
<p>
{t('Title')}: {ctx.extra.event.nickname}
</p>
<p>
{t('Start')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
<p>
{t('End')}: {moment(ctx.extra.event.createdAt).format('YYYY-MM-DD HH:mm:ss')}
</p>
</div>
),
});
@ -113,20 +128,20 @@ CalendarBlockModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'Data Source Key',
title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter data source key',
placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
title: 'Collection Name',
title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter collection name',
placeholder: tval('Enter collection name'),
},
},
},
@ -149,29 +164,29 @@ CalendarBlockModel.registerFlow({
uiSchema: {
titleAccessor: {
type: 'string',
title: 'Title accessor',
title: tval('Title accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter title accessor',
placeholder: tval('Enter title accessor'),
},
},
startAccessor: {
type: 'string',
title: 'Start accessor',
title: tval('Start accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter start accessor',
placeholder: tval('Enter start accessor'),
},
},
endAccessor: {
type: 'string',
title: 'End accessor',
title: tval('End accessor'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter end accessor',
placeholder: tval('Enter end accessor'),
},
},
},

View File

@ -8,6 +8,7 @@
*/
import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { FormModel } from './FormModel';
@ -16,14 +17,14 @@ export class FormActionModel extends ActionModel {}
export class FormSubmitActionModel extends FormActionModel {
defaultProps: ButtonProps = {
children: 'Submit',
title: tval('Submit'),
type: 'primary',
htmlType: 'submit',
};
}
FormSubmitActionModel.define({
title: 'Submit',
title: tval('Submit'),
});
FormSubmitActionModel.registerFlow({
@ -35,7 +36,7 @@ FormSubmitActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) {
ctx.globals.message.error('No resource selected for submission.');
ctx.globals.message.error(ctx.model.flowEngine.translate('No resource selected for submission.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel as FormModel;

View File

@ -13,6 +13,7 @@ import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, FlowModelRenderer, SingleRecordResource } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel';
import { EditableFieldModel } from '../../fields/EditableField/EditableFieldModel';
@ -80,20 +81,20 @@ FormModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'Data Source Key',
title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter data source key',
placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
title: 'Collection Name',
title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter collection name',
placeholder: tval('Enter collection name'),
},
},
},
@ -126,7 +127,7 @@ FormModel.registerFlow({
});
FormModel.define({
title: 'Form',
title: tval('Form'),
group: 'Content',
defaultOptions: {
use: 'FormModel',

View File

@ -116,7 +116,7 @@ export class QuickEditForm extends FlowModel {
resolve(this.form.values); // 在 close 之后 resolve
}}
>
Submit
{this.ctx.globals.flowEngine.translate('Submit')}
</Submit>
</FormButtonGroup>
</FormProvider>

View File

@ -12,6 +12,7 @@ import { observer } from '@formily/reactive-react';
import { AddActionButton, FlowModel, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine';
import { Skeleton, Space } from 'antd';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { SupportedFieldInterfaces } from '../../base/FieldModel';
@ -63,7 +64,7 @@ export class TableActionsColumnModel extends FlowModel {
},
]}
>
<Space>{this.props.title || 'Actions'}</Space>
<Space>{this.props.title || this.flowEngine.translate('Actions')}</Space>
</FlowsFloatContextMenu>
),
render: this.render(),

View File

@ -11,6 +11,7 @@ import { QuestionCircleOutlined } from '@ant-design/icons';
import { FlowsFloatContextMenu } from '@nocobase/flow-engine';
import { TableColumnProps, Tooltip } from 'antd';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
import { ReadPrettyFieldModel } from '../../fields/ReadPrettyField/ReadPrettyFieldModel';
@ -65,7 +66,7 @@ export class TableColumnModel extends FieldModel {
}
TableColumnModel.define({
title: 'Table Column',
title: tval('Table column'),
icon: 'TableColumn',
defaultOptions: {
use: 'TableColumnModel',
@ -103,13 +104,13 @@ TableColumnModel.registerFlow({
},
},
editColumTitle: {
title: 'Column title',
title: tval('Column title'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': {
placeholder: 'Column title',
placeholder: tval('Column title'),
},
},
},
@ -119,17 +120,18 @@ TableColumnModel.registerFlow({
};
},
handler(ctx, params) {
ctx.model.setProps('title', params.title || ctx.model.collectionField?.title);
const title = ctx.globals.flowEngine.translate(params.title || ctx.model.collectionField?.title);
ctx.model.setProps('title', title);
},
},
editTooltip: {
title: 'Edit tooltip',
title: tval('Edit tooltip'),
uiSchema: {
tooltip: {
'x-component': 'Input.TextArea',
'x-decorator': 'FormItem',
'x-component-props': {
placeholder: 'Edit tooltip',
placeholder: tval('Edit tooltip'),
},
},
},
@ -138,7 +140,7 @@ TableColumnModel.registerFlow({
},
},
editColumnWidth: {
title: 'Column width',
title: tval('Column width'),
uiSchema: {
width: {
'x-component': 'NumberPicker',
@ -153,7 +155,7 @@ TableColumnModel.registerFlow({
},
},
enableEditable: {
title: 'Editable',
title: tval('Editable'),
uiSchema: {
editable: {
'x-component': 'Switch',

View File

@ -22,6 +22,7 @@ import { Card, Space, Spin, Table } from 'antd';
import classNames from 'classnames';
import _ from 'lodash';
import React, { useRef } from 'react';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { QuickEditForm } from '../form/QuickEditForm';
@ -101,7 +102,7 @@ export class TableModel extends DataBlockModel<TableModelStructure> {
appendItems={[
{
key: 'actions',
label: 'Actions column',
label: tval('Actions column'),
createModelOptions: {
use: 'TableActionsColumnModel',
},
@ -263,20 +264,20 @@ TableModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'Data Source Key',
title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter data source key',
placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
title: 'Collection Name',
title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter collection name',
placeholder: tval('Enter collection name'),
},
},
},
@ -299,7 +300,7 @@ TableModel.registerFlow({
},
},
editPageSize: {
title: 'Edit page size',
title: tval('Edit page size'),
uiSchema: {
pageSize: {
'x-component': 'Select',
@ -328,14 +329,14 @@ TableModel.registerFlow({
},
dataScope: {
use: 'dataScope',
title: '设置数据范围',
title: tval('Set data scope'),
},
},
});
TableModel.define({
title: 'Table',
group: 'Content',
title: tval('Table'),
group: tval('Content'),
defaultOptions: {
use: 'TableModel',
subModels: {

View File

@ -21,6 +21,7 @@ import {
import { Button, Card, Pagination, Skeleton, Space } from 'antd';
import _ from 'lodash';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { ColumnDefinition, TabulatorFull as Tabulator } from 'tabulator-tables';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
@ -41,7 +42,7 @@ export class TabulatorColumnModel extends FlowModel {
getColumnProps(): ColumnDefinition {
return {
title: 'abcd',
title: tval('abcd'),
width: 100,
headerSort: false,
editable: true,
@ -126,7 +127,7 @@ export class TabulatorTableActionsColumnModel extends TabulatorColumnModel {
containerStyle={{ display: 'block', padding: '11px 8px', margin: '-11px -8px' }}
>
<Space>
{this.props.title || 'Actions'}
{this.props.title || tval('Actions')}
<AddActionButton model={this} subModelBaseClass="RecordActionModel" subModelKey="actions">
<SettingOutlined />
</AddActionButton>
@ -194,7 +195,7 @@ export class TabulatorModel extends DataBlockModel<S> {
appendItems={[
{
key: 'actions',
label: 'Actions column',
label: tval('Actions column'),
createModelOptions: {
use: 'TabulatorTableActionsColumnModel',
},
@ -260,7 +261,7 @@ export class TabulatorModel extends DataBlockModel<S> {
<FlowModelRenderer model={action} showFlowSettings sharedContext={{ currentBlockModel: this }} />
))}
<AddActionButton model={this} subModelBaseClass="GlobalActionModel" subModelKey="actions">
<Button icon={<SettingOutlined />}>Configure actions</Button>
<Button icon={<SettingOutlined />}>{this.translate('Configure actions')}</Button>
</AddActionButton>
</Space>
<div ref={this.tabulatorRef} />
@ -292,20 +293,20 @@ TabulatorModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'Data Source Key',
title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter data source key',
placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
title: 'Collection Name',
title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter collection name',
placeholder: tval('Enter collection name'),
},
},
},
@ -342,8 +343,8 @@ TabulatorModel.registerFlow({
});
TabulatorModel.define({
title: 'Tabulator',
group: 'Content',
title: tval('Tabulator'),
group: tval('Content'),
requiresDataSource: true,
hide: true,
defaultOptions: {

View File

@ -11,6 +11,7 @@ import { Select } from 'antd';
import React from 'react';
import { FlowModelRenderer, useFlowEngine, useFlowModel, reactive } from '@nocobase/flow-engine';
import { useCompile } from '../../../../../schema-component';
import { tval } from '@nocobase/utils/client';
import { AssociationFieldEditableFieldModel } from './AssociationFieldEditableFieldModel';
function toValue(record: any | any[], fieldNames, multiple = false) {
@ -41,7 +42,7 @@ function LabelByField(props) {
const currentModel: any = useFlowModel();
const flowEngine = useFlowEngine();
if (modelCache.has(cacheKey)) {
return option[fieldNames.label] ? <FlowModelRenderer model={modelCache.get(cacheKey)} /> : 'N/A';
return option[fieldNames.label] ? <FlowModelRenderer model={modelCache.get(cacheKey)} /> : tval('N/A');
}
const collectionManager = currentModel.collectionField.collection.collectionManager;
const target = currentModel.collectionField?.options?.target;
@ -76,7 +77,7 @@ function LabelByField(props) {
return (
<span key={option[fieldNames.value]}>
{option[fieldNames.label] ? <FlowModelRenderer model={model} uid={option[fieldNames.value]} /> : 'N/A'}
{option[fieldNames.label] ? <FlowModelRenderer model={model} uid={option[fieldNames.value]} /> : tval('N/A')}
</span>
);
}
@ -258,7 +259,7 @@ AssociationSelectEditableFieldModel.registerFlow({
paginationState.page++;
}
} catch (error) {
console.error('滚动分页请求失败:', error);
console.error('Scroll pagination request failed:', error);
} finally {
paginationState.loading = false;
}
@ -316,13 +317,13 @@ AssociationSelectEditableFieldModel.registerFlow({
AssociationSelectEditableFieldModel.registerFlow({
key: 'fieldNames',
title: 'Specific properties',
title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
fieldNames: {
use: 'titleField',
title: 'Title field',
title: tval('Title field'),
},
},
});

View File

@ -9,6 +9,7 @@
import { connect, mapProps } from '@formily/react';
import { ColorPicker as AntdColorPicker } from 'antd';
import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from './EditableFieldModel';
const ColorPicker = connect(
@ -23,7 +24,7 @@ const ColorPicker = connect(
},
presets: [
{
label: 'Recommended',
label: tval('Recommended'),
colors: [
'#8BBB11',
'#52C41A',

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { DatePicker } from '@formily/antd-v5';
import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from '../EditableFieldModel';
export class DateTimeFieldModel extends EditableFieldModel {
@ -35,11 +36,11 @@ DateTimeFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
title: 'Specific properties',
title: tval('Specific properties'),
steps: {
dateFormat: {
use: 'dateDisplayFormat',
title: 'Date display format',
title: tval('Date display format'),
},
},
});

View File

@ -12,6 +12,7 @@ import type { FieldPatternTypes, FieldValidator } from '@formily/core';
import { Field, Form } from '@formily/core';
import { FieldContext } from '@formily/react';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
import { ReactiveField } from '../../../formily/ReactiveField';
import { FormModel } from '../..';
@ -107,7 +108,7 @@ export class EditableFieldModel extends FieldModel<Structure> {
EditableFieldModel.registerFlow({
key: 'init',
auto: true,
title: 'Basic',
title: tval('Basic'),
sort: 150,
steps: {
createField: {
@ -125,13 +126,13 @@ EditableFieldModel.registerFlow({
},
},
editTitle: {
title: 'Edit Title',
title: tval('Edit Title'),
uiSchema: {
title: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': {
placeholder: 'Enter field title',
placeholder: tval('Enter field title'),
},
},
},
@ -145,7 +146,7 @@ EditableFieldModel.registerFlow({
},
},
initialValue: {
title: 'Set default value',
title: tval('Set default value'),
uiSchema: {
defaultValue: {
'x-component': 'Input',
@ -158,14 +159,14 @@ EditableFieldModel.registerFlow({
},
},
required: {
title: 'Required',
title: tval('Required'),
uiSchema: {
required: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
checkedChildren: 'Yes',
unCheckedChildren: 'No',
checkedChildren: tval('Yes'),
unCheckedChildren: tval('No'),
},
},
},
@ -174,14 +175,14 @@ EditableFieldModel.registerFlow({
},
},
displayLabel: {
title: 'Display label',
title: tval('Display label'),
uiSchema: {
displayLabel: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
checkedChildren: 'Yes',
unCheckedChildren: 'No',
checkedChildren: tval('Yes'),
unCheckedChildren: tval('No'),
},
},
},
@ -193,7 +194,7 @@ EditableFieldModel.registerFlow({
},
},
editDescription: {
title: 'Edit description',
title: tval('Edit description'),
uiSchema: {
description: {
'x-component': 'Input.TextArea',
@ -205,7 +206,7 @@ EditableFieldModel.registerFlow({
},
},
editTooltip: {
title: 'Edit tooltip',
title: tval('Edit tooltip'),
uiSchema: {
tooltip: {
'x-component': 'Input.TextArea',
@ -217,7 +218,7 @@ EditableFieldModel.registerFlow({
},
},
pattern: {
title: 'Pattern',
title: tval('Pattern'),
uiSchema: {
pattern: {
'x-component': 'Select',
@ -225,19 +226,19 @@ EditableFieldModel.registerFlow({
enum: [
{
value: 'editable',
label: 'Editable',
label: tval('Editable'),
},
{
value: 'disabled',
label: 'Disabled',
label: tval('Disabled'),
},
{
value: 'readOnly',
label: 'ReadOnly',
label: tval('ReadOnly'),
},
{
value: 'readPretty',
label: 'ReadPretty',
label: tval('ReadPretty'),
},
],
},

View File

@ -29,11 +29,11 @@ NanoIDEditableFieldModel.registerFlow({
const { size, customAlphabet } = ctx.model.collectionField.options || { size: 21 };
function isValidNanoid(value) {
if (value?.length !== size) {
return 'Field value size is' + ` ${size || 21}`;
return ctx.globals.flowEngine.translate('Field value size is') + ` ${size || 21}`;
}
for (let i = 0; i < value.length; i++) {
if (customAlphabet?.indexOf(value[i]) === -1) {
return `Field value do not meet the requirements`;
return ctx.globals.flowEngine.translate('Field value do not meet the requirements');
}
}
}

View File

@ -8,6 +8,7 @@
*/
import { Password } from '@formily/antd-v5';
import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from './EditableFieldModel';
export class PasswordEditableFieldModel extends EditableFieldModel {
@ -21,14 +22,17 @@ PasswordEditableFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
title: 'Group3',
title: tval('Password Options'),
steps: {
placeholder: {
title: 'Placeholder',
title: tval('Placeholder'),
uiSchema: {
checkStrength: {
placeholder: {
'x-component': 'Input',
'x-decorator': 'FormItem',
'x-component-props': {
placeholder: tval('Enter placeholder text'),
},
},
},
handler(ctx, params) {
@ -36,14 +40,14 @@ PasswordEditableFieldModel.registerFlow({
},
},
checkStrength: {
title: 'Check strength',
title: tval('Check strength'),
uiSchema: {
checkStrength: {
'x-component': 'Switch',
'x-decorator': 'FormItem',
'x-component-props': {
checkedChildren: 'Yes',
unCheckedChildren: 'No',
checkedChildren: tval('Yes'),
unCheckedChildren: tval('No'),
},
},
},

View File

@ -11,8 +11,7 @@ import React from 'react';
import { AssociationReadPrettyFieldModel } from './AssociationReadPrettyFieldModel';
import { FlowEngineProvider, reactive } from '@nocobase/flow-engine';
import { getUniqueKeyFromCollection } from '../../../../../collection-manager/interfaces/utils';
import { useCompile } from '../../../../../schema-component';
import { isTitleField } from '../../../../../data-source';
import { tval } from '@nocobase/utils/client';
export class AssociationSelectReadPrettyFieldModel extends AssociationReadPrettyFieldModel {
public static readonly supportedFieldInterfaces = [
@ -71,7 +70,7 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
<React.Fragment key={idx}>
{idx > 0 && <span style={{ color: 'rgb(170, 170, 170)' }}>,</span>}
<FlowEngineProvider engine={this.flowEngine}>
{v?.[fieldNames.label] ? mol.render() : 'N/A'}
{v?.[fieldNames.label] ? mol.render() : this.flowEngine.translate('N/A')}
</FlowEngineProvider>
</React.Fragment>
);
@ -85,13 +84,13 @@ export class AssociationSelectReadPrettyFieldModel extends AssociationReadPretty
AssociationSelectReadPrettyFieldModel.registerFlow({
key: 'fieldNames',
title: 'Specific properties',
title: tval('Specific properties'),
auto: true,
sort: 200,
steps: {
fieldNames: {
use: 'titleField',
title: 'Title field',
title: tval('Title field'),
handler(ctx, params) {
const { target } = ctx.model.collectionField.options;
const collectionManager = ctx.model.collectionField.collection.collectionManager;

View File

@ -10,6 +10,7 @@
import React from 'react';
import dayjs from 'dayjs';
import { reactive } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { ReadPrettyFieldModel } from './ReadPrettyFieldModel';
export class DateTimeReadPrettyFieldModel extends ReadPrettyFieldModel {
@ -54,11 +55,11 @@ DateTimeReadPrettyFieldModel.registerFlow({
key: 'key3',
auto: true,
sort: 1000,
title: 'Specific properties',
title: tval('Specific properties'),
steps: {
dateFormat: {
use: 'dateDisplayFormat',
title: 'Date display format',
title: tval('Date display format'),
defaultParams: (ctx) => {
const { showTime, dateFormat, timeFormat, picker } = ctx.model.props;
return {

View File

@ -28,7 +28,7 @@ export class JsonReadPrettyFieldModel extends ReadPrettyFieldModel {
try {
content = JSON.stringify(value, null, space ?? 2);
} catch (error) {
content = '[Invalid JSON]';
content = this.flowEngine.translate('Invalid JSON format');
}
}

View File

@ -9,6 +9,7 @@
import React from 'react';
import { reactive } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel';
export class ReadPrettyFieldModel extends FieldModel {
@ -29,7 +30,7 @@ export class ReadPrettyFieldModel extends FieldModel {
ReadPrettyFieldModel.registerFlow({
key: 'ReadPrettyFieldDefault',
auto: true,
title: 'Basic',
title: tval('Basic'),
sort: 100,
steps: {
step1: {

View File

@ -13,6 +13,7 @@ import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, Collection, FlowModelRenderer } from '@nocobase/flow-engine';
import { Card } from 'antd';
import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FilterBlockModel } from '../../base/BlockModel';
import { FilterFormFieldModel } from './FilterFormFieldModel';
@ -69,7 +70,7 @@ export class FilterFormModel extends FilterBlockModel {
FilterFormModel.define({
hide: true,
title: 'Form',
title: tval('Form'),
});
FilterFormModel.registerFlow({
@ -82,20 +83,20 @@ FilterFormModel.registerFlow({
uiSchema: {
dataSourceKey: {
type: 'string',
title: 'Data Source Key',
title: tval('Data Source Key'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter data source key',
placeholder: tval('Enter data source key'),
},
},
collectionName: {
type: 'string',
title: 'Collection Name',
title: tval('Collection Name'),
'x-decorator': 'FormItem',
'x-component': 'Input',
'x-component-props': {
placeholder: 'Enter collection name',
placeholder: tval('Enter collection name'),
},
},
},

View File

@ -9,12 +9,13 @@
import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel';
import { FilterFormActionModel } from './FilterFormActionModel';
export class FilterFormResetActionModel extends FilterFormActionModel {
defaultProps: ButtonProps = {
children: 'Reset',
children: tval('Reset'),
};
}
@ -27,7 +28,7 @@ FilterFormResetActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.form) {
ctx.globals.message.error('No form available for reset.');
ctx.globals.message.error(ctx.globals.flowEngine.translate('No form available for reset.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;

View File

@ -9,13 +9,14 @@
import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps, ButtonType } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel';
import { FilterFormActionModel } from './FilterFormActionModel';
export class FilterFormSubmitActionModel extends FilterFormActionModel {
defaultProps: ButtonProps = {
children: 'Filter',
title: tval('Filter'),
type: 'primary',
};
}
@ -29,7 +30,7 @@ FilterFormSubmitActionModel.registerFlow({
step1: {
async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.form) {
ctx.globals.message.error('No form available for submission.');
ctx.globals.message.error(ctx.globals.flowEngine.translate('No form available for submission.'));
return;
}
const currentBlockModel = ctx.shared.currentBlockModel;

View File

@ -9,6 +9,7 @@
import { Card } from 'antd';
import React, { createRef } from 'react';
import { tval } from '@nocobase/utils/client';
import { BlockModel } from '../../base/BlockModel';
function waitForRefCallback<T extends HTMLElement>(ref: React.RefObject<T>, cb: (el: T) => void, timeout = 3000) {
@ -34,8 +35,8 @@ export class HtmlBlockModel extends BlockModel {
}
HtmlBlockModel.define({
title: 'HTML',
group: 'Content',
title: tval('HTML'),
group: tval('Content'),
hide: true,
defaultOptions: {
use: 'HtmlBlockModel',
@ -58,7 +59,7 @@ HtmlBlockModel.registerFlow({
uiSchema: {
html: {
type: 'string',
title: 'HTML 内容',
title: tval('HTML content'),
'x-component': 'Input.TextArea',
'x-component-props': {
autoSize: true,

View File

@ -895,5 +895,87 @@
"Refresh data blocks": "Refresh data blocks",
"Select data blocks to refresh": "Select data blocks to refresh",
"After successful submission, the selected data blocks will be automatically refreshed.": "After successful submission, the selected data blocks will be automatically refreshed.",
"Reset link expiration": "Reset link expiration"
"Reset link expiration": "Reset link expiration",
"Imperative Drawer": "Imperative Drawer",
"Click event": "Click event",
"Form duplicate": "Form duplicate",
"Filter configuration": "Filter configuration",
"Default filter conditions": "Default filter conditions",
"No resource selected for deletion": "No resource selected for deletion",
"No records selected for deletion": "No records selected for deletion",
"Selected records deleted successfully": "Selected records deleted successfully",
"No resource selected for bulk edit": "No resource selected for bulk edit",
"No records selected for bulk edit": "No records selected for bulk edit",
"Successfully": "Successfully",
"No resource selected for refresh": "No resource selected for refresh",
"No resource or record selected for deletion": "No resource or record selected for deletion",
"Record deleted successfully": "Record deleted successfully",
"Please select non-filterable fields": "Please select non-filterable fields",
"Update record action": "Update record",
"Basic configuration": "Basic configuration",
"Configure page": "Configure page",
"Page Title": "Page Title",
"Enter page title": "Enter page title",
"Enable tabs": "Enable tabs",
"HTML content": "HTML content",
"Action": "Action",
"General configuration": "General configuration",
"Open mode configuration": "Open mode configuration",
"Medium": "Medium",
"Refresh data after execution": "Refresh data after execution",
"Enable refresh": "Enable refresh",
"Data refreshed successfully": "Data refreshed successfully",
"Secondary confirmation": "Secondary confirmation",
"Content": "Content",
"Edit link": "Edit link",
"URL": "URL",
"Do not concatenate search params in the URL": "Do not concatenate search params in the URL",
"Search parameters": "Search parameters",
"Add parameter": "Add parameter",
"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": "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",
"\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" only support \"application/json\", and no need to specify",
"Add request header": "Add request header",
"Input request data": "Input request data",
"Only support standard JSON data": "Only support standard JSON data",
"Timeout config": "Timeout config",
"Response type": "Response type",
"Action after successful submission": "Action after successful submission",
"Stay on the current popup or page": "Stay on the current popup or page",
"Return to the previous popup or page": "Return to the previous popup or page",
"Actions column": "Actions column",
"Data Source Key": "Data Source Key",
"Enter data source key": "Enter data source key",
"Collection Name": "Collection Name",
"Enter collection name": "Enter collection name",
"Edit page size": "Edit page size",
"Set data scope": "Set data scope",
"Edit Title": "Edit Title",
"Enter field title": "Enter field title",
"Display label": "Display label",
"ReadOnly": "ReadOnly",
"ReadPretty": "ReadPretty",
"Modal": "Modal",
"Rows": "Rows",
"Configure rows": "Configure rows",
"Sizes": "Sizes",
"Configure the rows and columns of the grid.": "Configure the rows and columns of the grid.",
"Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.": "Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.",
"Event selected": "Event selected",
"Double click": "Double click",
"Start": "Start",
"End": "End",
"Title accessor": "Title accessor",
"Enter title accessor": "Enter title accessor",
"Start accessor": "Start accessor",
"Enter start accessor": "Enter start accessor",
"End accessor": "End accessor",
"Enter end accessor": "Enter end accessor",
"Recommended": "Recommended",
"Password Options": "Password Options",
"Placeholder": "Placeholder",
"Enter placeholder text": "Enter placeholder text",
"Check strength": "Check strength",
"N/A": "N/A",
"No form available for reset.": "No form available for reset.",
"No form available for submission.": "No form available for submission."
}

View File

@ -1139,5 +1139,76 @@
"Calendar Month":"日历月",
"Calendar Year":"日历年",
"Scan to input":"扫码录入",
"Disable manual input":"禁止手动输入"
"Disable manual input":"禁止手动输入",
"Imperative Drawer": "命令式抽屉",
"Click event": "点击事件",
"Form duplicate": "表单复制",
"Filter configuration": "筛选配置",
"Default filter conditions": "默认筛选条件",
"No resource selected for deletion": "未选择要删除的资源",
"No records selected for deletion": "未选择要删除的记录",
"Selected records deleted successfully": "选中的记录删除成功",
"No resource selected for bulk edit": "未选择要批量编辑的资源",
"No records selected for bulk edit": "未选择要批量编辑的记录",
"Successfully": "成功",
"No resource selected for refresh": "未选择要刷新的资源",
"No resource or record selected for deletion": "未选择要删除的资源或记录",
"Record deleted successfully": "记录删除成功",
"Please select non-filterable fields": "请选择不可筛选的字段",
"Update record action": "更新记录",
"Basic configuration": "基础配置",
"Configure page": "配置页面",
"Page Title": "页面标题",
"Enter page title": "输入页面标题",
"Enable tabs": "启用标签页",
"HTML content": "HTML内容",
"General configuration": "通用配置",
"Open mode configuration": "打开方式配置",
"Medium": "中",
"Refresh data after execution": "执行后刷新数据",
"Enable refresh": "启用刷新",
"Data refreshed successfully": "数据刷新成功",
"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": "当 HTTP 方法为 Post、Put 或 Patch且此自定义请求在表单内时请求体将自动填充表单数据",
"\"Content-Type\" only support \"application/json\", and no need to specify": "\"Content-Type\" 仅支持 \"application/json\",无需指定",
"Add request header": "添加请求头",
"Input request data": "输入请求数据",
"Only support standard JSON data": "仅支持标准 JSON 数据",
"Timeout config": "超时配置",
"Response type": "响应类型",
"Actions column": "操作列",
"Data Source Key": "数据源键",
"Enter data source key": "输入数据源键",
"Collection Name": "数据表名称",
"Enter collection name": "输入数据表名称",
"Edit page size": "编辑页面大小",
"Set data scope": "设置数据范围",
"Edit Title": "编辑标题",
"Enter field title": "输入字段标题",
"Display label": "显示标签",
"ReadOnly": "只读",
"ReadPretty": "美观只读",
"Modal": "对话框",
"Rows": "行",
"Configure rows": "配置行",
"Sizes": "尺寸",
"Configure the rows and columns of the grid.": "配置网格的行和列。",
"Configure the sizes of each row. The value is an array of numbers representing the width of each column in the row.": "配置每行的尺寸。该值是一个数字数组,表示行中每列的宽度。",
"Event selected": "已选择事件",
"Double click": "双击",
"Start": "开始",
"End": "结束",
"Title accessor": "标题访问器",
"Enter title accessor": "输入标题访问器",
"Start accessor": "开始访问器",
"Enter start accessor": "输入开始访问器",
"End accessor": "结束访问器",
"Enter end accessor": "输入结束访问器",
"Recommended": "推荐",
"Password Options": "密码选项",
"Placeholder": "占位符",
"Enter placeholder text": "输入占位符文本",
"Check strength": "检查强度",
"N/A": "不适用",
"No form available for reset.": "没有可用的表单进行重置。",
"No form available for submission.": "没有可用的表单进行提交。"
}

View File

@ -1,149 +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 { describe, it, expect, beforeEach } from 'vitest';
import { TranslationUtil } from '../utils';
describe('TranslationUtil', () => {
let translationUtil: TranslationUtil;
let mockTranslator: (key: string, options?: any) => string;
beforeEach(() => {
translationUtil = new TranslationUtil();
mockTranslator = (key: string, options?: any) => {
// 简单的模拟翻译函数
if (options?.name) {
return key.replace('{name}', options.name);
}
return `translated_${key}`;
};
});
describe('translate', () => {
it('should handle simple translation', () => {
const result = translationUtil.translate('Hello World', mockTranslator);
expect(result).toBe('translated_Hello World');
});
it('should handle translation with options', () => {
const result = translationUtil.translate('Hello {name}', mockTranslator, { name: 'John' });
expect(result).toBe('Hello John');
});
it('should handle template compilation with single quotes', () => {
const template = "{{t('Hello World')}}";
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('translated_Hello World');
});
it('should handle template compilation with double quotes', () => {
const template = '{{t("User Name")}}';
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('translated_User Name');
});
it('should handle template compilation with backticks', () => {
const template = '{{t(`Email`)}}';
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('translated_Email');
});
it('should handle template with options', () => {
const template = '{{t(\'Hello {name}\', {"name": "John"})}}';
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('Hello John');
});
it('should handle template with whitespace', () => {
const template = "{{ t ( 'User Name' ) }}";
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('translated_User Name');
});
it('should handle mixed content', () => {
const template = "前缀 {{t('User Name')}} 后缀";
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('前缀 translated_User Name 后缀');
});
it('should handle multiple templates', () => {
const template = "{{t('Hello')}} {{t('World')}}";
const result = translationUtil.translate(template, mockTranslator);
expect(result).toBe('translated_Hello translated_World');
});
it('should handle invalid template gracefully', () => {
const template = "{{t('unclosed quote)}}";
const result = translationUtil.translate(template, mockTranslator);
// 由于模板格式不匹配,会被当作普通字符串翻译
expect(result).toBe("translated_{{t('unclosed quote)}}");
});
it('should return original value for non-string input', () => {
const result = translationUtil.translate(null as any, mockTranslator);
expect(result).toBe(null);
});
it('should return empty string for empty input', () => {
const result = translationUtil.translate('', mockTranslator);
expect(result).toBe('');
});
});
describe('caching', () => {
it('should cache template results', () => {
const template = "{{t('Hello World')}}";
// 第一次调用
const result1 = translationUtil.translate(template, mockTranslator);
expect(result1).toBe('translated_Hello World');
expect(translationUtil.getCacheSize()).toBe(1);
// 第二次调用应该使用缓存
const result2 = translationUtil.translate(template, mockTranslator);
expect(result2).toBe('translated_Hello World');
expect(translationUtil.getCacheSize()).toBe(1);
});
it('should not cache simple translations', () => {
const result1 = translationUtil.translate('Hello World', mockTranslator);
expect(result1).toBe('translated_Hello World');
expect(translationUtil.getCacheSize()).toBe(0);
});
it('should clear cache', () => {
const template = "{{t('Hello World')}}";
translationUtil.translate(template, mockTranslator);
expect(translationUtil.getCacheSize()).toBe(1);
translationUtil.clearCache();
expect(translationUtil.getCacheSize()).toBe(0);
});
});
describe('error handling', () => {
it('should handle translator function errors', () => {
const errorTranslator = () => {
throw new Error('Translation error');
};
const template = "{{t('Hello World')}}";
const result = translationUtil.translate(template, errorTranslator);
// 应该返回原始模板而不是抛出错误
expect(result).toBe("{{t('Hello World')}}");
});
it('should handle invalid JSON options', () => {
const template = "{{t('Hello', invalid json)}}";
const result = translationUtil.translate(template, mockTranslator);
// JSON 解析失败,但仍会调用翻译器,只是没有 options
expect(result).toBe('translated_Hello');
});
});
});

View File

@ -11,6 +11,7 @@ import { Button, Result, Typography } from 'antd';
import React, { FC, useState } from 'react';
import { FallbackProps } from 'react-error-boundary';
import { useFlowModel } from '../hooks/useFlowModel';
import { getT } from '../utils';
const { Paragraph, Text } = Typography;
@ -20,6 +21,7 @@ const { Paragraph, Text } = Typography;
const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
const [loading, setLoading] = useState(false);
const model = useFlowModel(); // 在这里安全地使用 Hook
const t = getT(model);
const handleCopyError = async () => {
setLoading(true);
@ -104,9 +106,9 @@ const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }
const subTitle = (
<span>
{'This is likely a NocoBase internals bug. Please open an issue at '}
{t('This is likely a NocoBase internals bug. Please open an issue at')}{' '}
<a href="https://github.com/nocobase/nocobase/issues" target="_blank" rel="noopener noreferrer">
here
{t('here')}
</a>
{model && (
<div style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>
@ -121,23 +123,23 @@ const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }
<Result
style={{ maxWidth: '60vw', margin: 'auto' }}
status="error"
title="Render Failed"
title={t('Render failed')}
subTitle={subTitle}
extra={[
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
Feedback
{t('Feedback')}
</Button>,
canDownloadLogs && (
<Button key="log" loading={loading} onClick={handleDownloadLogs}>
Download logs
{t('Download logs')}
</Button>
),
<Button key="copy" loading={loading} onClick={handleCopyError}>
Copy Error Info
{t('Copy error info')}
</Button>,
resetErrorBoundary && (
<Button key="retry" danger onClick={resetErrorBoundary}>
Try Again
{t('Try again')}
</Button>
),
].filter(Boolean)}
@ -197,18 +199,18 @@ export const FlowErrorFallback: FC<FallbackProps> & {
<Result
style={{ maxWidth: '60vw', margin: 'auto' }}
status="error"
title="Render Failed"
title="Render failed"
subTitle={subTitle}
extra={[
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
Feedback
</Button>,
<Button key="copy" loading={loading} onClick={handleCopyError}>
Copy Error Info
Copy error info
</Button>,
resetErrorBoundary && (
<Button key="retry" danger onClick={resetErrorBoundary}>
Try Again
Try again
</Button>
),
].filter(Boolean)}

View File

@ -20,6 +20,7 @@ import {
import { FlowModel } from '../../../../models';
import { StepDefinition } from '../../../../types';
import { openStepSettings } from './StepSettings';
import { getT } from '../../../../utils';
// Type definitions for better type safety
interface StepInfo {
@ -110,34 +111,35 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
flattenSubMenus = true,
}) => {
const { message } = App.useApp();
const t = getT(model);
// 分离处理函数以便更好的代码组织
const handleCopyUid = useCallback(async () => {
try {
await navigator.clipboard.writeText(model.uid);
message.success('UID 已复制到剪贴板');
message.success(t('UID copied to clipboard'));
} catch (error) {
console.error('复制失败:', error);
message.error('复制失败,请重试');
console.error(t('Copy failed'), ':', error);
message.error(t('Copy failed, please try again'));
}
}, [model.uid, message]);
const handleDelete = useCallback(() => {
Modal.confirm({
title: '确认删除',
title: t('Confirm delete'),
icon: <ExclamationCircleOutlined />,
content: '确定要删除此项吗?此操作不可撤销。',
okText: '确认删除',
content: t('Are you sure you want to delete this item? This action cannot be undone.'),
okText: t('Confirm'),
okType: 'primary',
cancelText: '取消',
cancelText: t('Cancel'),
async onOk() {
try {
await model.destroy();
} catch (error) {
console.error('删除操作失败:', error);
console.error(t('Delete operation failed'), ':', error);
Modal.error({
title: '删除失败',
content: '删除操作失败,请检查控制台获取详细信息。',
title: t('Delete failed'),
content: t('Delete operation failed, please check the console for details.'),
});
}
},
@ -181,7 +183,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
stepKey,
});
} catch (error) {
console.log('配置弹窗已取消或出错:', error);
console.log(t('Configuration popup cancelled or error'), ':', error);
}
},
[model],
@ -231,12 +233,14 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
// 如果step使用了action检查action是否有uiSchema
let hasActionUiSchema = false;
let stepTitle = actionStep.title;
if (actionStep.use) {
try {
const action = targetModel.flowEngine?.getAction?.(actionStep.use);
hasActionUiSchema = action && action.uiSchema != null;
stepTitle = stepTitle || action.title;
} catch (error) {
console.warn(`获取action '${actionStep.use}' 失败:`, error);
console.warn(t('Failed to get action {{action}}', { action: actionStep.use }), ':', error);
}
}
@ -253,7 +257,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
stepKey,
step: actionStep,
uiSchema: mergedUiSchema,
title: actionStep.title || stepKey,
title: t(stepTitle) || stepKey,
modelKey, // 添加模型标识
};
})
@ -263,7 +267,11 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
})
.filter(Boolean);
} catch (error) {
console.error(`获取模型 '${targetModel?.uid || 'unknown'}' 的可配置flows失败:`, error);
console.error(
t('Failed to get configurable flows for model {{model}}', { model: targetModel?.uid || 'unknown' }),
':',
error,
);
return [];
}
}, []);
@ -338,7 +346,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
// 在平铺模式下始终按流程分组
items.push({
key: groupKey,
label: flow.title || flow.key,
label: t(flow.title) || flow.key,
type: 'group',
});
@ -353,7 +361,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
items.push({
key: uniqueKey,
icon: <SettingOutlined />,
label: stepInfo.title,
label: t(stepInfo.title),
});
});
});
@ -382,7 +390,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
items.push({
key: groupKey,
label: flow.title || flow.key,
label: t(flow.title) || flow.key,
type: 'group',
});
@ -392,7 +400,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
items.push({
key: uniqueKey,
icon: <SettingOutlined />,
label: stepInfo.title,
label: t(stepInfo.title),
});
});
});
@ -408,7 +416,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
subMenuChildren.push({
key: uniqueKey,
icon: <SettingOutlined />,
label: stepInfo.title,
label: t(stepInfo.title),
});
});
});
@ -443,7 +451,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
items.push({
key: 'copy-uid',
icon: <CopyOutlined />,
label: '复制 UID',
label: t('Copy UID'),
});
}
@ -452,7 +460,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
items.push({
key: 'delete',
icon: <DeleteOutlined />,
label: '删除',
label: t('Delete'),
});
}
}
@ -467,7 +475,7 @@ export const DefaultSettingsIcon: React.FC<DefaultSettingsIconProps> = ({
// 渲染前验证模型
if (!model || !model.uid) {
console.warn('提供的模型无效');
console.warn(t('Invalid model provided'));
return null;
}

View File

@ -15,6 +15,7 @@ import { observer } from '@formily/react';
import { FlowModel } from '../../../../models';
import { useFlowModelById } from '../../../../hooks';
import { openStepSettingsDialog } from './StepSettingsDialog';
import { getT } from '../../../../utils';
// 右键菜单组件接口
interface ModelProvidedProps {
@ -70,25 +71,26 @@ const FlowsContextMenu: React.FC<FlowsContextMenuProps> = (props) => {
// 使用传入的model
const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => {
const t = getT(model);
const handleMenuClick = useCallback(
({ key }: { key: string }) => {
if (key === 'delete') {
// 处理删除操作
Modal.confirm({
title: '确认删除',
title: t('Confirm delete'),
icon: <ExclamationCircleOutlined />,
content: '确定要删除此项吗?此操作不可撤销。',
okText: '确认删除',
content: t('Are you sure you want to delete this item? This action cannot be undone.'),
okText: t('Confirm Delete'),
okType: 'danger',
cancelText: '取消',
cancelText: t('Cancel'),
onOk() {
try {
model.dispatchEvent('remove');
} catch (error) {
console.error('删除操作失败:', error);
console.error(t('Delete operation failed'), ':', error);
Modal.error({
title: '删除失败',
content: '删除操作失败,请检查控制台获取详细信息。',
title: t('Delete failed'),
content: t('Delete operation failed, please check the console for details.'),
});
}
},

View File

@ -16,6 +16,7 @@ import { ToolbarItemConfig } from '../../../../types';
import { useFlowModelById } from '../../../../hooks';
import { useFlowEngine } from '../../../../provider';
import { FlowEngine } from '../../../../flowEngine';
import { getT } from '../../../../utils';
import { Droppable } from '../../../dnd';
// 检测DOM中直接子元素是否包含button元素的辅助函数
@ -300,7 +301,8 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
}, []);
if (!model) {
return <Alert message="提供的模型无效" type="error" />;
const t = getT(model || ({} as FlowModel));
return <Alert message={t('Invalid model provided')} type="error" />;
}
// 如果未启用或没有children直接返回children
@ -357,9 +359,10 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
extraToolbarItems: extraToolbarItems,
}) => {
const model = useFlowModelById(uid, modelClassName);
const flowEngine = useFlowEngine();
if (!model) {
return <Alert message={`未找到ID为 ${uid} 的模型`} type="error" />;
return <Alert message={flowEngine.translate('Model with ID {{uid}} not found', { uid })} type="error" />;
}
return (

View File

@ -12,8 +12,9 @@ import { message, Button } from 'antd';
import React from 'react';
import { FlowModel } from '../../../../models';
import { StepDefinition } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
import { toJS } from '@formily/reactive';
/**
*
@ -116,11 +117,14 @@ export interface StepFormDialogProps {
const openRequiredParamsStepFormDialog = async ({
model,
dialogWidth = 800,
dialogTitle = '步骤参数配置',
dialogTitle,
}: StepFormDialogProps): Promise<any> => {
const t = getT(model);
const defaultTitle = dialogTitle || t('Step parameter configuration');
if (!model) {
message.error('提供的模型无效');
throw new Error('提供的模型无效');
message.error(t('Invalid model provided'));
throw new Error(t('Invalid model provided'));
}
// 创建一个Promise, 并最终返回当此弹窗关闭时此promise resolve或者reject
@ -171,8 +175,8 @@ const openRequiredParamsStepFormDialog = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema确保step的uiSchema优先级更高
const mergedUiSchema = { ...resolvedActionUiSchema };
Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@ -224,7 +228,11 @@ const openRequiredParamsStepFormDialog = async ({
// 解析 defaultParams
const resolvedActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const mergedParams = { ...resolvedActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
const mergedParams = {
...toJS(resolvedActionDefaultParams),
...toJS(resolvedDefaultParams),
...toJS(stepParams),
};
if (Object.keys(mergedParams).length > 0) {
if (!initialValues[flowKey]) {
@ -300,17 +308,24 @@ const openRequiredParamsStepFormDialog = async ({
// 创建分步表单实例(只有多个步骤时才需要)
const formStep = requiredSteps.length > 1 ? FormStep.createFormStep(0) : null;
const flowEngine = model.flowEngine || {};
const scopes = {
formStep,
totalSteps: requiredSteps.length,
requiredSteps,
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
};
// 创建FormDialog
const formDialog = FormDialog(
{
title: dialogTitle,
title: dialogTitle || t('Step parameter configuration'),
width: dialogWidth,
footer: null, // 移除默认的底部按钮,使用自定义的导航按钮
destroyOnClose: true,
},
(form) => {
const flowEngine = model.flowEngine || {};
const handleSubmit = async () => {
try {
await form.submit();
@ -329,7 +344,7 @@ const openRequiredParamsStepFormDialog = async ({
resolve(currentValues);
formDialog.close();
} catch (error) {
console.error('提交表单时出错:', error);
console.error(t('Error submitting form'), ':', error);
// reject(error);
// 这里不需要reject因为forConfirm会处理
}
@ -340,20 +355,8 @@ const openRequiredParamsStepFormDialog = async ({
resolve({});
};
return (
<>
<MultiStepContextProvider model={model} requiredSteps={requiredSteps} formStep={formStep}>
<SchemaField
schema={formSchema}
components={{
FormStep,
...flowEngine.flowSettings?.components,
}}
scope={{
formStep,
totalSteps: requiredSteps.length,
requiredSteps,
useStepSettingContext,
const dialogScopes = {
...scopes,
closeDialog: handleClose,
handleNext: () => {
// 验证当前步骤的表单
@ -365,12 +368,25 @@ const openRequiredParamsStepFormDialog = async ({
}
})
.catch((errors: any) => {
console.log('表单验证失败:', errors);
console.log(t('Form validation failed'), ':', errors);
// 可以在这里添加更详细的错误处理
});
},
...flowEngine.flowSettings?.scopes,
};
// 编译 formSchema 中的表达式
const compiledFormSchema = compileUiSchema(dialogScopes, formSchema);
return (
<>
<MultiStepContextProvider model={model} requiredSteps={requiredSteps} formStep={formStep}>
<SchemaField
schema={compiledFormSchema}
components={{
FormStep,
...flowEngine.flowSettings?.components,
}}
scope={dialogScopes}
/>
</MultiStepContextProvider>
<FormConsumer>
@ -388,7 +404,7 @@ const openRequiredParamsStepFormDialog = async ({
{/* 只有一个步骤时,只显示完成按钮 */}
{requiredSteps.length === 1 ? (
<Button type="primary" onClick={handleSubmit}>
{t('Complete configuration')}
</Button>
) : (
<>
@ -400,7 +416,7 @@ const openRequiredParamsStepFormDialog = async ({
}
}}
>
{t('Previous step')}
</Button>
<Button
disabled={!formStep?.allowNext}
@ -415,7 +431,7 @@ const openRequiredParamsStepFormDialog = async ({
}
})
.catch((errors: any) => {
console.log('表单验证失败:', errors);
console.log(t('Form validation failed'), ':', errors);
// 可以在这里添加更详细的错误处理
});
}}
@ -423,7 +439,7 @@ const openRequiredParamsStepFormDialog = async ({
display: (formStep?.current ?? 0) < requiredSteps.length - 1 ? 'inline-block' : 'none',
}}
>
{t('Next step')}
</Button>
<Button
disabled={formStep?.allowNext}
@ -433,7 +449,7 @@ const openRequiredParamsStepFormDialog = async ({
display: (formStep?.current ?? 0) >= requiredSteps.length - 1 ? 'inline-block' : 'none',
}}
>
{t('Complete configuration')}
</Button>
</>
)}
@ -447,10 +463,10 @@ const openRequiredParamsStepFormDialog = async ({
// 打开对话框
formDialog.open({
initialValues,
initialValues: compileUiSchema(scopes, initialValues),
});
} catch (error) {
reject(new Error(`导入 FormDialog 或 FormStep 失败: ${error.message}`));
reject(new Error(`${t('Failed to import FormDialog or FormStep')}: ${error.message}`));
}
})();
}).catch((e) => {

View File

@ -12,6 +12,7 @@ import { StepSettingsProps } from '../../../../types';
import { openStepSettingsDialog } from './StepSettingsDialog';
import { openStepSettingsDrawer } from './StepSettingsDrawer';
import { FlowModel } from '../../../../models';
import { getT } from '../../../../utils';
/**
*
@ -24,9 +25,11 @@ import { FlowModel } from '../../../../models';
* @returns Promise<any>
*/
const openStepSettings = async ({ model, flowKey, stepKey, width = 600, title }: StepSettingsProps): Promise<any> => {
const t = getT(model);
if (!model) {
message.error('提供的模型无效');
throw new Error('提供的模型无效');
message.error(t('Invalid model provided'));
throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@ -34,13 +37,13 @@ const openStepSettings = async ({ model, flowKey, stepKey, width = 600, title }:
const step = flow?.steps?.[stepKey];
if (!flow) {
message.error(`未找到Key为 ${flowKey} 的流程`);
throw new Error(`未找到Key为 ${flowKey} 的流程`);
message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
message.error(`未找到Key为 ${stepKey} 的步骤`);
throw new Error(`未找到Key为 ${stepKey} 的步骤`);
message.error(t('Step with key {{stepKey}} not found', { stepKey }));
throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
// 检查步骤的 settingMode 配置,默认为 'dialog'
@ -84,7 +87,7 @@ const isStepUsingDrawerMode = (model: FlowModel, flowKey: string, stepKey: strin
return step.settingMode === 'drawer';
} catch (error) {
console.warn('检查步骤设置模式时出错:', error);
console.warn('Error checking step setting mode:', error);
return false;
}
};
@ -107,7 +110,7 @@ const getStepSettingMode = (model: FlowModel, flowKey: string, stepKey: string):
return step.settingMode || 'dialog';
} catch (error) {
console.warn('获取步骤设置模式时出错:', error);
console.warn('Error getting step setting mode:', error);
return null;
}
};

View File

@ -11,8 +11,9 @@ import { createSchemaField, ISchema } from '@formily/react';
import { message } from 'antd';
import React from 'react';
import { StepSettingsDialogProps } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField();
@ -32,9 +33,11 @@ const openStepSettingsDialog = async ({
dialogWidth = 600,
dialogTitle,
}: StepSettingsDialogProps): Promise<any> => {
const t = getT(model);
if (!model) {
message.error('提供的模型无效');
throw new Error('提供的模型无效');
message.error(t('Invalid model provided'));
throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@ -42,16 +45,16 @@ const openStepSettingsDialog = async ({
const step = flow?.steps?.[stepKey];
if (!flow) {
message.error(`未找到Key为 ${flowKey} 的流程`);
throw new Error(`未找到Key为 ${flowKey} 的流程`);
message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
message.error(`未找到Key为 ${stepKey} 的步骤`);
throw new Error(`未找到Key为 ${stepKey} 的步骤`);
message.error(t('Step with key {{stepKey}} not found', { stepKey }));
throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
const title = dialogTitle || (step ? `${step.title || stepKey} - 配置` : `步骤配置 - ${stepKey}`);
let title = step.title;
// 创建参数解析上下文
const paramsContext = {
@ -72,6 +75,7 @@ const openStepSettingsDialog = async ({
actionUiSchema = action.uiSchema;
}
actionDefaultParams = action.defaultParams || {};
title = title || action.title;
}
// 解析动态 uiSchema
@ -79,8 +83,8 @@ const openStepSettingsDialog = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema确保step的uiSchema优先级更高
const mergedUiSchema = { ...resolvedActionUiSchema };
Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@ -90,17 +94,23 @@ const openStepSettingsDialog = async ({
// 如果没有可配置的UI Schema显示提示
if (Object.keys(mergedUiSchema).length === 0) {
message.info('此步骤没有可配置的参数');
message.info(t('This step has no configurable parameters'));
return {};
}
const flowEngine = model.flowEngine;
const scopes = {
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
};
// 获取初始值
const stepParams = model.getStepParams(flowKey, stepKey) || {};
// 解析 defaultParams
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
const initialValues = { ...resolveActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
// 构建表单Schema
const formSchema: ISchema = {
@ -122,21 +132,19 @@ const openStepSettingsDialog = async ({
try {
({ FormDialog } = await import('@formily/antd-v5'));
} catch (error) {
throw new Error(`导入 FormDialog 失败: ${error.message}`);
throw new Error(`${t('Failed to import FormDialog')}: ${error.message}`);
}
// 创建FormDialog
const formDialog = FormDialog(
{
title,
title: dialogTitle || `${t(title)} - ${t('Configuration')}`,
width: dialogWidth,
okText: '确认',
cancelText: '取消',
okText: t('OK'),
cancelText: t('Cancel'),
destroyOnClose: true,
},
(form) => {
const flowEngine = model.flowEngine;
// 创建上下文值
const contextValue: StepSettingContextType = {
model,
@ -148,17 +156,17 @@ const openStepSettingsDialog = async ({
stepKey,
};
// 编译 formSchema 中的表达式
const compiledFormSchema = compileUiSchema(scopes, formSchema);
return (
<StepSettingContextProvider value={contextValue}>
<SchemaField
schema={formSchema}
schema={compiledFormSchema}
components={{
...flowEngine.flowSettings?.components,
}}
scope={{
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
}}
scope={scopes}
/>
</StepSettingContextProvider>
);
@ -172,11 +180,11 @@ const openStepSettingsDialog = async ({
const currentValues = payload.values;
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
message.success('配置已保存');
message.success(t('Configuration saved'));
next(payload);
} catch (error) {
console.error('保存配置时出错:', error);
message.error('保存配置时出错,请检查控制台');
console.error(t('Error saving configuration'), ':', error);
message.error(t('Error saving configuration, please check console'));
throw error;
}
});
@ -185,7 +193,7 @@ const openStepSettingsDialog = async ({
// 打开对话框
return formDialog.open({
initialValues,
initialValues: compileUiSchema(scopes, initialValues),
});
};

View File

@ -10,9 +10,11 @@
import { createSchemaField, ISchema } from '@formily/react';
import { message, Button, Space } from 'antd';
import React, { useState } from 'react';
import { StepDefinition, StepSettingsDrawerProps } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema } from '../../../../utils';
import { useTranslation } from 'react-i18next';
import { StepSettingsDrawerProps } from '../../../../types';
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
import { toJS } from '@formily/reactive';
const SchemaField = createSchemaField();
@ -32,9 +34,11 @@ const openStepSettingsDrawer = async ({
drawerWidth = 600,
drawerTitle,
}: StepSettingsDrawerProps): Promise<any> => {
const t = getT(model);
if (!model) {
message.error('提供的模型无效');
throw new Error('提供的模型无效');
message.error(t('Invalid model provided'));
throw new Error(t('Invalid model provided'));
}
// 获取流程和步骤信息
@ -42,16 +46,16 @@ const openStepSettingsDrawer = async ({
const step = flow?.steps?.[stepKey];
if (!flow) {
message.error(`未找到Key为 ${flowKey} 的流程`);
throw new Error(`未找到Key为 ${flowKey} 的流程`);
message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
}
if (!step) {
message.error(`未找到Key为 ${stepKey} 的步骤`);
throw new Error(`未找到Key为 ${stepKey} 的步骤`);
message.error(t('Step with key {{stepKey}} not found', { stepKey }));
throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
}
const title = drawerTitle || (step ? `${step.title || stepKey} - 配置` : `步骤配置 - ${stepKey}`);
let title = step.title;
// 创建参数解析上下文
const paramsContext = {
@ -71,6 +75,7 @@ const openStepSettingsDrawer = async ({
actionUiSchema = action.uiSchema;
}
actionDefaultParams = action.defaultParams || {};
title = title || action.title;
}
// 解析动态 uiSchema
@ -78,8 +83,8 @@ const openStepSettingsDrawer = async ({
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
// 合并uiSchema确保step的uiSchema优先级更高
const mergedUiSchema = { ...resolvedActionUiSchema };
Object.entries(resolvedStepUiSchema).forEach(([fieldKey, schema]) => {
const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
if (mergedUiSchema[fieldKey]) {
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
} else {
@ -89,17 +94,23 @@ const openStepSettingsDrawer = async ({
// 如果没有可配置的UI Schema显示提示
if (Object.keys(mergedUiSchema).length === 0) {
message.info('此步骤没有可配置的参数');
message.info(t('This step has no configurable parameters'));
return {};
}
// 获取初始值
const stepParams = model.getStepParams(flowKey, stepKey) || {};
const flowEngine = model.flowEngine;
const scopes = {
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
};
// 解析 defaultParams
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
const initialValues = { ...resolveActionDefaultParams, ...resolvedDefaultParams, ...stepParams };
const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
// 构建表单Schema
const formSchema: ISchema = {
@ -122,13 +133,13 @@ const openStepSettingsDrawer = async ({
({ Form } = await import('@formily/antd-v5'));
({ createForm } = await import('@formily/core'));
} catch (error) {
throw new Error(`导入 Formily 组件失败: ${error.message}`);
throw new Error(`${t('Failed to import Formily components')}: ${error.message}`);
}
// 获取drawer API
const drawer = model.flowEngine?.context?.drawer;
if (!drawer) {
throw new Error('Drawer API 不可用,请确保在 FlowEngineGlobalsContextProvider 内使用');
throw new Error(t('Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider'));
}
return new Promise((resolve) => {
@ -137,11 +148,12 @@ const openStepSettingsDrawer = async ({
// 创建表单实例
const form = createForm({
initialValues,
initialValues: compileUiSchema(scopes, initialValues),
});
// 创建抽屉内容组件
const DrawerContent: React.FC = () => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
@ -156,13 +168,13 @@ const openStepSettingsDrawer = async ({
model.setStepParams(flowKey, stepKey, currentValues);
await model.save();
message.success('配置已保存');
message.success(t('Configuration saved'));
isResolved = true;
drawerRef.destroy();
resolve(currentValues);
} catch (error) {
console.error('保存配置时出错:', error);
message.error('保存配置时出错,请检查控制台');
console.error(t('Error saving configuration'), ':', error);
message.error(t('Error saving configuration, please check console'));
} finally {
setLoading(false);
}
@ -176,8 +188,6 @@ const openStepSettingsDrawer = async ({
drawerRef.destroy();
};
const flowEngine = model.flowEngine;
// 创建上下文值
const contextValue: StepSettingContextType = {
model,
@ -189,20 +199,20 @@ const openStepSettingsDrawer = async ({
stepKey,
};
// 编译 formSchema 中的表达式
const compiledFormSchema = compileUiSchema(scopes, formSchema);
return (
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
<Form form={form} layout="vertical">
<StepSettingContextProvider value={contextValue}>
<SchemaField
schema={formSchema}
schema={compiledFormSchema}
components={{
...flowEngine.flowSettings?.components,
}}
scope={{
useStepSettingContext,
...flowEngine.flowSettings?.scopes,
}}
scope={scopes}
/>
</StepSettingContextProvider>
</Form>
@ -217,9 +227,9 @@ const openStepSettingsDrawer = async ({
}}
>
<Space>
<Button onClick={handleCancel}></Button>
<Button onClick={handleCancel}>{t('Cancel')}</Button>
<Button type="primary" loading={loading} onClick={handleSubmit}>
{t('OK')}
</Button>
</Space>
</div>
@ -229,7 +239,7 @@ const openStepSettingsDrawer = async ({
// 打开抽屉
const drawerRef = drawer.open({
title,
title: drawerTitle || `${t(title)} - ${t('Configuration')}`,
width: drawerWidth,
content: <DrawerContent />,
onClose: () => {

View File

@ -14,6 +14,7 @@ import { ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton';
import { useTranslation } from 'react-i18next';
interface AddActionButtonProps {
/**
@ -48,6 +49,11 @@ interface AddActionButtonProps {
items?: SubModelItemsType;
}
const DefaultBtn = () => {
const { t } = useTranslation();
return <FlowSettingsButton icon={<SettingOutlined />}>{t('Configure actions')}</FlowSettingsButton>;
};
/**
*
*
@ -63,7 +69,7 @@ const AddActionButtonCore: React.FC<AddActionButtonProps> = ({
model,
subModelBaseClass = 'ActionFlowModel',
subModelKey = 'actions',
children = <FlowSettingsButton icon={<SettingOutlined />}>{'Configure actions'}</FlowSettingsButton>,
children = <DefaultBtn />,
subModelType = 'array',
items,
filter,

View File

@ -15,6 +15,7 @@ import { FlowModelOptions, ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType, mergeSubModelItems } from './AddSubModelButton';
import { useTranslation } from 'react-i18next';
export type BuildCreateModelOptionsType = {
defaultOptions: FlowModelOptions;
@ -65,6 +66,11 @@ function defaultBuildCreateModelOptions({ defaultOptions }: BuildCreateModelOpti
return defaultOptions;
}
const DefaultBtn = () => {
const { t } = useTranslation();
return <FlowSettingsButton icon={<SettingOutlined />}>{t('Configure fields')}</FlowSettingsButton>;
};
/**
*
*
@ -80,7 +86,7 @@ const AddFieldButtonCore: React.FC<AddFieldButtonProps> = ({
model,
subModelBaseClass = 'FieldFlowModel',
subModelKey = 'fields',
children = <FlowSettingsButton icon={<SettingOutlined />}>{'Configure fields'}</FlowSettingsButton>,
children = <DefaultBtn />,
subModelType = 'array',
collection,
buildCreateModelOptions = defaultBuildCreateModelOptions,

View File

@ -9,6 +9,7 @@
import { Dropdown, DropdownProps, Input, Menu, Spin, Empty, InputProps } from 'antd';
import React, { useEffect, useState, useMemo, useRef, FC } from 'react';
import { useFlowModel } from '../../hooks';
/**
* dropdown
@ -72,6 +73,7 @@ interface LazyDropdownMenuProps extends Omit<DropdownProps['menu'], 'items'> {
}
const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownMenuProps }> = ({ menu, ...props }) => {
const model = useFlowModel();
const [loadedChildren, setLoadedChildren] = useState<Record<string, Item[]>>({});
const [loadingKeys, setLoadingKeys] = useState<Set<string>>(new Set());
const [menuVisible, setMenuVisible] = useState(false);
@ -82,6 +84,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
const dropdownMaxHeight = useNiceDropdownMaxHeight([menuVisible]);
const [isSearching, setIsSearching] = useState(false);
const searchTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const t = model.translate;
// 清理定时器,避免内存泄露
useEffect(() => {
@ -223,7 +226,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
visible={menuVisible}
variant="borderless"
allowClear
placeholder={item.searchPlaceholder || 'search'}
placeholder={t(item.searchPlaceholder || 'Search')}
value={currentSearchValue}
onChange={(e) => {
e.stopPropagation();
@ -264,7 +267,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
key: `${item.key}-empty`,
label: (
<div style={{ padding: '16px', textAlign: 'center' as const }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="No Data" style={{ margin: 0 }} />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} style={{ margin: 0 }} />
</div>
),
disabled: true,
@ -278,7 +281,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
return {
type: 'group',
key: item.key,
label: item.label,
label: typeof item.label === 'string' ? t(item.label) : item.label,
children: groupChildren,
};
}
@ -289,7 +292,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
return {
key: item.key,
label: item.label,
label: typeof item.label === 'string' ? t(item.label) : item.label,
onClick: (info) => {
if (children) {
return;
@ -316,7 +319,7 @@ const LazyDropdown: React.FC<Omit<DropdownProps, 'menu'> & { menu: LazyDropdownM
key: `${keyPath}-empty`,
label: (
<div style={{ padding: '16px', textAlign: 'center' as const }}>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description="No Data" style={{ margin: 0 }} />
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('No data')} style={{ margin: 0 }} />
</div>
),
disabled: true,

View File

@ -96,7 +96,7 @@ export class DataSource {
}
get displayName() {
return this.options.displayName ? this.flowEngine?.t(this.options.displayName) : this.key;
return this.options.displayName ? this.flowEngine?.translate(this.options.displayName) : this.key;
}
get key() {
@ -272,7 +272,7 @@ export class Collection {
}
get title() {
return this.options.title ? this.flowEngine?.t(this.options.title) : this.name;
return this.options.title ? this.flowEngine?.translate(this.options.title) : this.name;
}
initInherits() {
@ -413,7 +413,7 @@ export class CollectionField {
get title() {
const titleValue = this.options?.title || this.options?.uiSchema?.title || this.options.name;
return this.flowEngine?.t(titleValue);
return this.flowEngine?.translate(titleValue);
}
set title(value: string) {

View File

@ -14,11 +14,13 @@ import {
ActionDefinition,
ActionOptions,
CreateModelOptions,
FlowContext,
FlowDefinition,
IFlowModelRepository,
ModelConstructor,
} from './types';
import { isInheritedFrom, TranslationUtil } from './utils';
import { isInheritedFrom } from './utils';
import { initFlowEngineLocale } from './locale';
interface ApplyFlowCacheEntry {
status: 'pending' | 'resolved' | 'rejected';
@ -36,16 +38,15 @@ export class FlowEngine {
private modelInstances: Map<string, any> = new Map();
/** @public Stores flow settings including components and scopes for formily settings. */
public flowSettings: FlowSettings = new FlowSettings();
context: Record<string, any> = {};
context: FlowContext['globals'] = {} as FlowContext['globals'];
private modelRepository: IFlowModelRepository | null = null;
private _applyFlowCache = new Map<string, ApplyFlowCacheEntry>();
/** @private Translation utility for template compilation and caching */
private _translationUtil = new TranslationUtil();
reactView: ReactView;
constructor() {
this.reactView = new ReactView(this);
this.flowSettings.registerScopes({ t: this.translate.bind(this) });
}
// 注册默认的 FlowModel
@ -58,6 +59,9 @@ export class FlowEngine {
setContext(context: any) {
this.context = { ...this.context, ...context };
if (this.context.i18n) {
initFlowEngineLocale(this.context.i18n);
}
}
getContext() {
@ -82,12 +86,20 @@ export class FlowEngine {
* flowEngine.t("前缀 {{ t('User Name') }} 后缀")
* flowEngine.t("{{t('Hello {name}', {name: 'John'})}}")
*/
public t(keyOrTemplate: string, options?: any): string {
return this._translationUtil.translate(
keyOrTemplate,
(key: string, opts?: any) => this.translateKey(key, opts),
options,
);
public translate(keyOrTemplate: string, options?: any): string {
if (!keyOrTemplate || typeof keyOrTemplate !== 'string') {
return keyOrTemplate;
}
// 先尝试一次翻译
let result = this.translateKey(keyOrTemplate, options);
// 检查翻译结果是否包含模板语法,如果有则进行模板编译
if (this.isTemplate(result)) {
result = this.compileTemplate(result);
}
return result;
}
/**
@ -102,6 +114,44 @@ export class FlowEngine {
return key;
}
/**
*
* @private
*/
private isTemplate(str: string): boolean {
return /\{\{\s*t\s*\(\s*["'`].*?["'`]\s*(?:,\s*.*?)?\s*\)\s*\}\}/g.test(str);
}
/**
*
* @private
*/
private compileTemplate(template: string): string {
return template.replace(
/\{\{\s*t\s*\(\s*["'`](.*?)["'`]\s*(?:,\s*((?:[^{}]|\{[^}]*\})*?))?\s*\)\s*\}\}/g,
(match, key, optionsStr) => {
try {
let templateOptions = {};
if (optionsStr) {
optionsStr = optionsStr.trim();
if (optionsStr.startsWith('{') && optionsStr.endsWith('}')) {
// 使用受限的 Function 构造器解析
try {
templateOptions = new Function('$root', `with($root) { return (${optionsStr}); }`)({});
} catch (parseError) {
return match;
}
}
}
return this.translateKey(key, templateOptions);
} catch (error) {
console.warn(`FlowEngine: Failed to compile template "${match}":`, error);
return match;
}
},
);
}
get applyFlowCache() {
return this._applyFlowCache;
}

View File

@ -0,0 +1,59 @@
{
"Invalid model provided": "Invalid model provided",
"Flow with key {{flowKey}} not found": "Flow with key {{flowKey}} not found",
"Step with key {{stepKey}} not found": "Step with key {{stepKey}} not found",
"Configuration": "Configuration",
"Step configuration": "Step configuration",
"This step has no configurable parameters": "This step has no configurable parameters",
"Failed to import Formily components": "Failed to import Formily components",
"Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider": "Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider",
"Configuration saved": "Configuration saved",
"Error saving configuration": "Error saving configuration",
"Error saving configuration, please check console": "Error saving configuration, please check console",
"Failed to import FormDialog": "Failed to import FormDialog",
"OK": "OK",
"Cancel": "Cancel",
"Step parameter configuration": "Step parameter configuration",
"Error submitting form": "Error submitting form",
"Complete configuration": "Complete configuration",
"Previous step": "Previous step",
"Form validation failed": "Form validation failed",
"Next step": "Next step",
"Failed to import FormDialog or FormStep": "Failed to import FormDialog or FormStep",
"UID copied to clipboard": "UID copied to clipboard",
"Copy failed": "Copy failed",
"Copy failed, please try again": "Copy failed, please try again",
"Confirm delete": "Confirm delete",
"Are you sure you want to delete this item? This action cannot be undone.": "Are you sure you want to delete this item? This action cannot be undone.",
"Delete operation failed": "Delete operation failed",
"Delete failed": "Delete failed",
"Delete operation failed, please check the console for details.": "Delete operation failed, please check the console for details.",
"Configuration popup cancelled or error": "Configuration popup cancelled or error",
"Failed to get action {{action}}": "Failed to get action {{action}}",
"Failed to get configurable flows for model {{model}}": "Failed to get configurable flows for model {{model}}",
"Copy UID": "Copy UID",
"Delete": "Delete",
"This is likely a NocoBase internals bug. Please open an issue at": "This is likely a NocoBase internals bug. Please open an issue at",
"here": "here",
"Render failed": "Render failed",
"Feedback": "Feedback",
"Download logs": "Download logs",
"Copy error info": "Copy error info",
"Try again": "Try again",
"Data blocks": "Data blocks",
"Filter blocks": "Filter blocks",
"Other blocks": "Other blocks",
"Invalid input parameters": "Invalid input parameters",
"Invalid subModelKey format": "Invalid subModelKey format: {{subModelKey}}",
"Submodel not found": "Submodel '{{subKey}}' not found",
"Expected array for subModel": "Expected array for '{{subKey}}', got {{type}}",
"Array index out of bounds": "Array index {{index}} out of bounds for '{{subKey}}'",
"Expected object for subModel": "Expected object for '{{subKey}}', got array",
"No createModelOptions found for item": "No createModelOptions found for item",
"createModelOptions must specify use property": "createModelOptions must specify \"use\" property",
"Failed to add sub model": "Failed to add sub model",
"Failed to destroy model after creation error": "Failed to destroy model after creation error",
"Add": "Add",
"Name": "Name",
"Model with ID {{uid}} not found": "Model with ID {{uid}} not found"
}

View File

@ -0,0 +1,38 @@
/**
* 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 enUS from './en-US.json';
import zhCN from './zh-CN.json';
import { FLOW_ENGINE_NAMESPACE } from '../utils';
export const locales = {
'en-US': enUS,
'zh-CN': zhCN,
};
/**
* Get translation for a key with fallback
*/
export function getFlowEngineTranslation(key: string, locale = 'en-US'): string {
const translations = locales[locale] || locales['en-US'];
return translations[key] || key;
}
/**
* Initialize flow-engine locale resources
* This should be called when the flow-engine is initialized
*/
export function initFlowEngineLocale(i18nInstance?: any) {
if (i18nInstance && typeof i18nInstance.addResourceBundle === 'function') {
// Register flow-engine translations to the flow-engine namespace
Object.entries(locales).forEach(([locale, resources]) => {
i18nInstance.addResourceBundle(locale, FLOW_ENGINE_NAMESPACE, resources, true, false);
});
}
}

View File

@ -0,0 +1,59 @@
{
"Invalid model provided": "提供的模型无效",
"Flow with key {{flowKey}} not found": "未找到key为 {{flowKey}} 的流程",
"Step with key {{stepKey}} not found": "未找到key为 {{stepKey}} 的步骤",
"Configuration": "配置",
"Step configuration": "步骤配置",
"This step has no configurable parameters": "此步骤没有可配置的参数",
"Failed to import Formily components": "导入 Formily 组件失败",
"Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider": "抽屉 API 不可用,请确保在 FlowEngineGlobalsContextProvider 中使用",
"Configuration saved": "配置已保存",
"Error saving configuration": "保存配置时出错",
"Error saving configuration, please check console": "保存配置时出错,请检查控制台",
"Failed to import FormDialog": "导入 FormDialog 失败",
"OK": "确定",
"Cancel": "取消",
"Step parameter configuration": "步骤参数配置",
"Error submitting form": "提交表单时出错",
"Complete configuration": "完成配置",
"Previous step": "上一步",
"Form validation failed": "表单验证失败",
"Next step": "下一步",
"Failed to import FormDialog or FormStep": "导入 FormDialog 或 FormStep 失败",
"UID copied to clipboard": "UID 已复制到剪贴板",
"Copy failed": "复制失败",
"Copy failed, please try again": "复制失败,请重试",
"Confirm delete": "确认删除",
"Are you sure you want to delete this item? This action cannot be undone.": "确定要删除此项吗?此操作不可撤销。",
"Delete operation failed": "删除操作失败",
"Delete failed": "删除失败",
"Delete operation failed, please check the console for details.": "删除操作失败,请检查控制台获取详细信息。",
"Configuration popup cancelled or error": "配置弹窗已取消或出错",
"Failed to get action {{action}}": "获取 action '{{action}}' 失败",
"Failed to get configurable flows for model {{model}}": "获取模型 '{{model}}' 的可配置 flows 失败",
"Copy UID": "复制 UID",
"Delete": "删除",
"This is likely a NocoBase internals bug. Please open an issue at": "这可能是 NocoBase 内部错误。请在以下地址提交问题",
"here": "这里",
"Render failed": "渲染失败",
"Feedback": "反馈",
"Download logs": "下载日志",
"Copy error info": "复制错误信息",
"Try again": "重试",
"Data blocks": "数据区块",
"Filter blocks": "筛选区块",
"Other blocks": "其他区块",
"Invalid input parameters": "输入参数无效",
"Invalid subModelKey format": "无效的 subModelKey 格式: {{subModelKey}}",
"Submodel not found": "未找到 Submodel '{{subKey}}'",
"Expected array for subModel": "期望 '{{subKey}}' 为数组,实际为 {{type}}",
"Array index out of bounds": "数组索引 {{index}} 超出 '{{subKey}}' 的边界",
"Expected object for subModel": "期望 '{{subKey}}' 为对象,实际为数组",
"No createModelOptions found for item": "未找到该项的 createModelOptions",
"createModelOptions must specify use property": "createModelOptions 必须指定 \"use\" 属性",
"Failed to add sub model": "添加子模型失败",
"Failed to destroy model after creation error": "创建错误后销毁模型失败",
"Add": "添加",
"Name": "名称",
"Model with ID {{uid}} not found": "未找到 ID 为 {{uid}} 的模型"
}

View File

@ -401,7 +401,7 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
console[level.toLowerCase()](logMessage, logMeta);
};
const globalContexts = currentFlowEngine.getContext() || {};
const globalContexts = currentFlowEngine.getContext();
const flowContext: FlowContext<this> = {
exit: () => {
throw new FlowExitException(flowKey, this.uid);
@ -875,6 +875,10 @@ export class FlowModel<Structure extends { parent?: any; subModels?: any } = Def
};
}
get translate() {
return this.flowEngine.translate.bind(this.flowEngine);
}
public setSharedContext(ctx: Record<string, any>) {
this._sharedContext = { ...this._sharedContext, ...ctx };
}

View File

@ -11,6 +11,7 @@ import { ISchema } from '@formily/json-schema';
import type { FlowEngine } from './flowEngine';
import type { FlowModel } from './models';
import { ReactView } from './ReactView';
import { APIClient } from '@nocobase/sdk';
/**
* T T
@ -99,7 +100,11 @@ export interface FlowContext<TModel extends FlowModel = FlowModel> {
reactView: ReactView;
stepResults: Record<string, any>; // Results from previous steps
shared: Record<string, any>; // Shared data within the flow (read/write)
globals: Record<string, any>; // Global context data (read-only)
globals: Record<string, any> & {
flowEngine: FlowEngine;
app: any;
api: APIClient;
};
extra: Record<string, any>; // Extra context passed to applyFlow (read-only)
model: TModel; // Current model instance with specific type
app: any; // Application instance (required)

View File

@ -9,9 +9,29 @@
import _ from 'lodash';
import type { ISchema } from '@formily/json-schema';
import { Schema } from '@formily/json-schema';
import type { FlowModel } from './models';
import { ActionDefinition, DeepPartial, FlowContext, FlowDefinition, ModelConstructor, ParamsContext } from './types';
// Flow Engine 命名空间常量
export const FLOW_ENGINE_NAMESPACE = 'flow-engine';
/**
* flow-engine
* @param model FlowModel
* @returns 使 flow-engine
*/
export function getT(model: FlowModel): (key: string, options?: any) => string {
if (model.flowEngine?.translate) {
return (key: string, options?: any) => {
// 自动添加 flow-engine 命名空间
return model.flowEngine.translate(key, { ns: [FLOW_ENGINE_NAMESPACE, 'client'], nsMode: 'fallback', ...options });
};
}
// 回退到原始键值
return (key: string) => key;
}
export function generateUid(): string {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
@ -155,95 +175,87 @@ export function defineAction(options: ActionDefinition) {
return options;
}
// 模块级全局缓存,与 useCompile 保持一致
const compileCache = {};
/**
*
* UI Schema
*
* @param scope t, randomString
* @param uiSchema UI Schema
* @param options
* @returns UI Schema
*/
export class TranslationUtil {
/** @private Simple template cache - template string -> compiled result */
private _templateCache = new Map<string, string>();
export function compileUiSchema(scope: Record<string, any>, uiSchema: any, options: { noCache?: boolean } = {}): any {
const { noCache = false } = options;
/**
*
* @param keyOrTemplate {{t('key', options)}}
* @param translator i18n.t
* @param options
* @returns
*/
public translate(keyOrTemplate: string, translator: (key: string, options?: any) => string, options?: any): string {
if (!keyOrTemplate || typeof keyOrTemplate !== 'string') {
return keyOrTemplate;
const hasVariable = (source: string): boolean => {
const reg = /\{\{.*?\}\}/g;
return reg.test(source);
};
const compile = (source: any): any => {
let shouldCompile = false;
let cacheKey: string;
// source is expression, for example: {{ t('Add new') }}
if (typeof source === 'string' && source.startsWith('{{')) {
shouldCompile = true;
cacheKey = source;
}
// 检查是否包含模板语法 {{t('...')}}
const hasTemplate = this.isTemplate(keyOrTemplate);
if (hasTemplate) {
// 模板编译模式 - 简单缓存
if (this._templateCache.has(keyOrTemplate)) {
return this._templateCache.get(keyOrTemplate)!;
}
// 解析模板
const result = this.compileTemplate(keyOrTemplate, translator);
// 简单缓存:直接存储
this._templateCache.set(keyOrTemplate, result);
return result;
} else {
// 简单翻译模式 - 直接调用翻译函数
return translator(keyOrTemplate, options);
}
}
/**
*
* @private
*/
private isTemplate(str: string): boolean {
return /\{\{\s*t\s*\(\s*["'`].*?["'`]\s*(?:,\s*.*?)?\s*\)\s*\}\}/g.test(str);
}
/**
*
* @private
*/
private compileTemplate(template: string, translator: (key: string, options?: any) => string): string {
return template.replace(
/\{\{\s*t\s*\(\s*["'`](.*?)["'`]\s*(?:,\s*((?:[^{}]|\{[^}]*\})*))?\s*\)\s*\}\}/g,
(match, key, optionsStr) => {
// source is Component Object, for example: { 'x-component': "Cascader", type: "array", title: "所属地区(行政区划)" }
if (source && typeof source === 'object' && !Array.isArray(source)) {
try {
let templateOptions = {};
if (optionsStr) {
optionsStr = optionsStr.trim();
if (optionsStr.startsWith('{') && optionsStr.endsWith('}')) {
cacheKey = JSON.stringify(source);
} catch (e) {
console.warn('Failed to stringify:', e);
return source;
}
if (compileCache[cacheKey]) return compileCache[cacheKey];
shouldCompile = hasVariable(cacheKey);
}
// source is Array, for example: [{ 'title': "{{ t('Admin') }}", name: 'admin' }, { 'title': "{{ t('Root') }}", name: 'root' }]
if (Array.isArray(source)) {
try {
templateOptions = JSON.parse(optionsStr);
} catch (jsonError) {
// JSON 解析失败,返回原始匹配字符串
return match;
cacheKey = JSON.stringify(source);
} catch (e) {
console.warn('Failed to stringify:', e);
return source;
}
if (compileCache[cacheKey]) return compileCache[cacheKey];
shouldCompile = hasVariable(cacheKey);
}
}
return translator(key, templateOptions);
if (shouldCompile) {
if (!cacheKey) {
try {
return Schema.compile(source, scope);
} catch (error) {
console.warn(`TranslationUtil: Failed to compile template "${match}":`, error);
return match;
console.warn('Failed to compile with Formily Schema.compile:', error);
return source;
}
}
try {
if (noCache) {
return Schema.compile(source, scope);
}
compileCache[cacheKey] = compileCache[cacheKey] || Schema.compile(source, scope);
return compileCache[cacheKey];
} catch (e) {
console.log('compileUiSchema error', source, e);
try {
return Schema.compile(source, scope);
} catch (error) {
return source;
}
}
},
);
}
/**
*
*/
public clearCache(): void {
this._templateCache.clear();
}
// source is: plain object、string、number、boolean、undefined、null
return source;
};
/**
*
*/
public getCacheSize(): number {
return this._templateCache.size;
}
return compile(uiSchema);
}

View File

@ -19,7 +19,7 @@ import * as antd from 'antd';
import { Card, Spin } from 'antd';
import React, { createRef } from 'react';
import ReactDOM from 'react-dom/client';
import { NAMESPACE } from './locale';
import { NAMESPACE, tStr } from './locale';
export class LowcodeBlockModel extends BlockModel {
ref = createRef<HTMLDivElement>();
declare resource: APIResource;
@ -125,15 +125,15 @@ ctx.element.innerHTML = \`
LowcodeBlockModel.registerFlow({
key: 'default',
title: 'Configuration',
title: tStr('Configuration'),
auto: true,
steps: {
executionStep: {
title: 'Code',
title: tStr('Code'),
uiSchema: {
code: {
type: 'string',
title: 'Execution Code',
title: tStr('Execution Code'),
'x-component': 'CodeEditor',
'x-component-props': {
minHeight: '400px',