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

View File

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

View File

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

View File

@ -8,22 +8,23 @@
*/ */
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { tval } from '@nocobase/utils/client';
import { Variable } from '../../schema-component/antd/variable/Variable'; import { Variable } from '../../schema-component/antd/variable/Variable';
export const openLinkAction = { export const openLinkAction = {
title: '编辑链接', title: tval('Edit link'),
uiSchema: { uiSchema: {
url: { url: {
title: 'URL', title: tval('URL'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': Variable.TextArea, 'x-component': Variable.TextArea,
description: 'Do not concatenate search params in the URL', description: tval('Do not concatenate search params in the URL'),
}, },
params: { params: {
type: 'array', type: 'array',
'x-component': 'ArrayItems', 'x-component': 'ArrayItems',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
title: `Search parameters`, title: tval('Search parameters'),
items: { items: {
type: 'object', type: 'object',
properties: { properties: {
@ -48,7 +49,7 @@ export const openLinkAction = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': { 'x-component-props': {
placeholder: `{{t("Name")}}`, placeholder: tval('Name'),
}, },
}, },
value: { value: {
@ -56,7 +57,7 @@ export const openLinkAction = {
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': Variable.TextArea, 'x-component': Variable.TextArea,
'x-component-props': { 'x-component-props': {
placeholder: `{{t("Value")}}`, placeholder: tval('Value'),
useTypedConstant: true, useTypedConstant: true,
changeOnSelect: true, changeOnSelect: true,
}, },
@ -73,14 +74,14 @@ export const openLinkAction = {
properties: { properties: {
add: { add: {
type: 'void', type: 'void',
title: 'Add parameter', title: tval('Add parameter'),
'x-component': 'ArrayItems.Addition', 'x-component': 'ArrayItems.Addition',
}, },
}, },
}, },
openInNewWindow: { openInNewWindow: {
type: 'boolean', type: 'boolean',
'x-content': 'Open in new window', 'x-content': tval('Open in new window'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', '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 React from 'react';
import { tval } from '@nocobase/utils/client';
import { FlowPage } from '../FlowPage'; import { FlowPage } from '../FlowPage';
export const openModeAction = { export const openModeAction = {
title: '打开方式', title: tval('Open mode'),
uiSchema: { uiSchema: {
mode: { mode: {
type: 'string', type: 'string',
title: '打开方式', title: tval('Open mode'),
enum: [ enum: [
{ label: 'Drawer', value: 'drawer' }, { label: tval('Drawer'), value: 'drawer' },
{ label: 'Modal', value: 'modal' }, { label: tval('Modal'), value: 'modal' },
], ],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
}, },
size: { size: {
type: 'string', type: 'string',
title: '弹窗尺寸', title: tval('Popup size'),
enum: [ enum: [
{ label: '小', value: 'small' }, { label: tval('Small'), value: 'small' },
{ label: '中', value: 'medium' }, { label: tval('Medium'), value: 'medium' },
{ label: '大', value: 'large' }, { label: tval('Large'), value: 'large' },
], ],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
@ -57,7 +67,7 @@ export const openModeAction = {
}; };
currentDrawer = ctx.globals[params.mode].open({ currentDrawer = ctx.globals[params.mode].open({
title: '命令式 Drawer', title: tval('Imperative Drawer'),
width: sizeToWidthMap[params.size], width: sizeToWidthMap[params.size],
content: <DrawerContent />, content: <DrawerContent />,
}); });

View File

@ -8,31 +8,32 @@
*/ */
import { defineAction } from '@nocobase/flow-engine'; import { defineAction } from '@nocobase/flow-engine';
import { tval } from '@nocobase/utils/client';
import React from 'react'; import React from 'react';
import { FlowPage } from '../FlowPage'; import { FlowPage } from '../FlowPage';
export const openView = defineAction({ export const openView = defineAction({
name: 'openView', name: 'openView',
title: '打开方式配置', title: tval('Open mode configuration'),
uiSchema: { uiSchema: {
mode: { mode: {
type: 'string', type: 'string',
title: '打开方式', title: tval('Open mode'),
enum: [ enum: [
{ label: 'Drawer', value: 'drawer' }, { label: tval('Drawer'), value: 'drawer' },
{ label: 'Dialog', value: 'dialog' }, { label: tval('Dialog'), value: 'dialog' },
{ label: 'Page', value: 'page' }, { label: tval('Page'), value: 'page' },
], ],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Radio.Group', 'x-component': 'Radio.Group',
}, },
size: { size: {
type: 'string', type: 'string',
title: '弹窗尺寸', title: tval('Popup size'),
enum: [ enum: [
{ label: '小', value: 'small' }, { label: tval('Small'), value: 'small' },
{ label: '中', value: 'medium' }, { label: tval('Medium'), value: 'medium' },
{ label: '大', value: 'large' }, { label: tval('Large'), value: 'large' },
], ],
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Radio.Group', '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 = { export const refreshOnCompleteAction = {
title: '执行后刷新数据', title: tval('Refresh data after execution'),
uiSchema: { uiSchema: {
enable: { enable: {
type: 'boolean', type: 'boolean',
title: 'Enable refresh', title: tval('Enable refresh'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', 'x-component': 'Checkbox',
}, },
@ -16,7 +27,7 @@ export const refreshOnCompleteAction = {
async handler(ctx, params) { async handler(ctx, params) {
if (params.enable) { if (params.enable) {
await ctx.extra.currentResource.refresh(); 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 = { export const secondaryConfirmationAction = {
title: '二次确认', title: tval('Secondary confirmation'),
uiSchema: { uiSchema: {
enable: { enable: {
type: 'boolean', type: 'boolean',
title: 'Enable secondary confirmation', title: tval('Enable secondary confirmation'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Checkbox', 'x-component': 'Checkbox',
}, },
title: { title: {
type: 'string', type: 'string',
title: 'Title', title: tval('Title'),
default: 'Delete record', default: tval('Delete record'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
}, },
content: { content: {
type: 'string', type: 'string',
title: 'Content', title: tval('Content'),
default: 'Are you sure you want to delete it?', default: tval('Are you sure you want to delete it?'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
}, },
@ -25,14 +36,14 @@ export const secondaryConfirmationAction = {
defaultParams(ctx) { defaultParams(ctx) {
return { return {
enable: true, enable: true,
title: 'Delete record', title: tval('Delete record'),
content: 'Are you sure you want to delete it?', content: tval('Are you sure you want to delete it?'),
}; };
}, },
async handler(ctx, params) { async handler(ctx, params) {
if (params.enable) { if (params.enable) {
const confirmed = await ctx.globals.modal.confirm({ const confirmed = await ctx.globals.modal.confirm({
title: params.title, title: ctx.globals.flowEngine.translate(params.title),
content: params.content, content: params.content,
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,22 +9,23 @@
import { MultiRecordResource } from '@nocobase/flow-engine'; import { MultiRecordResource } from '@nocobase/flow-engine';
import type { ButtonProps } from 'antd/es/button'; import type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { RecordActionModel } from '../base/ActionModel'; import { RecordActionModel } from '../base/ActionModel';
export class DeleteActionModel extends RecordActionModel { export class DeleteActionModel extends RecordActionModel {
defaultProps: ButtonProps = { defaultProps: ButtonProps = {
type: 'link', type: 'link',
title: 'Delete', title: tval('Delete'),
}; };
} }
DeleteActionModel.define({ DeleteActionModel.define({
title: 'Delete', title: tval('Delete'),
}); });
DeleteActionModel.registerFlow({ DeleteActionModel.registerFlow({
key: 'handleClick', key: 'handleClick',
title: '点击事件', title: tval('Click event'),
on: { on: {
eventName: 'click', eventName: 'click',
}, },
@ -34,17 +35,18 @@ DeleteActionModel.registerFlow({
}, },
delete: { delete: {
async handler(ctx, params) { async handler(ctx, params) {
const t = ctx.model.translate;
if (!ctx.shared?.currentBlockModel?.resource) { 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; return;
} }
if (!ctx.shared.currentRecord) { 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; return;
} }
const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource; const resource = ctx.shared.currentBlockModel.resource as MultiRecordResource;
await resource.destroy(ctx.shared.currentRecord); 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 type { ButtonProps } from 'antd/es/button';
import { tval } from '@nocobase/utils/client';
import { openModeAction } from '../../actions/openModeAction'; import { openModeAction } from '../../actions/openModeAction';
import { RecordActionModel } from '../base/ActionModel'; import { RecordActionModel } from '../base/ActionModel';
export class DuplicateActionModel extends RecordActionModel { export class DuplicateActionModel extends RecordActionModel {
defaultProps: ButtonProps = { defaultProps: ButtonProps = {
type: 'link', type: 'link',
title: 'Duplicate', title: tval('Duplicate'),
}; };
} }
DuplicateActionModel.define({ DuplicateActionModel.define({
title: 'Duplicate', title: tval('Duplicate'),
hide: true, hide: true,
}); });
DuplicateActionModel.registerFlow({ DuplicateActionModel.registerFlow({
key: 'handleClick', key: 'handleClick',
title: '点击事件', title: tval('Click event'),
on: { on: {
eventName: 'click', eventName: 'click',
}, },
steps: { steps: {
open: openModeAction, open: openModeAction,
duplicateMode: { duplicateMode: {
title: '复制方式', title: tval('Duplicate mode'),
uiSchema: { uiSchema: {
// TODO // TODO
duplicateMode: { duplicateMode: {
@ -40,15 +41,15 @@ DuplicateActionModel.registerFlow({
'x-component': 'Select', 'x-component': 'Select',
enum: [ enum: [
{ {
label: '快速复制', label: tval('Quick duplicate'),
value: 'quickDuplicate', value: 'quickDuplicate',
}, },
{ {
label: '表单复制', label: tval('Form duplicate'),
value: 'formDuplicate', value: 'formDuplicate',
}, },
], ],
title: '复制方式', title: tval('Duplicate mode'),
}, },
}, },
defaultParams(ctx) { defaultParams(ctx) {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@
*/ */
import { ButtonProps } from 'antd'; import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel'; import { ActionModel } from '../../base/ActionModel';
import { DataBlockModel } from '../../base/BlockModel'; import { DataBlockModel } from '../../base/BlockModel';
import { FormModel } from './FormModel'; import { FormModel } from './FormModel';
@ -16,14 +17,14 @@ export class FormActionModel extends ActionModel {}
export class FormSubmitActionModel extends FormActionModel { export class FormSubmitActionModel extends FormActionModel {
defaultProps: ButtonProps = { defaultProps: ButtonProps = {
children: 'Submit', title: tval('Submit'),
type: 'primary', type: 'primary',
htmlType: 'submit', htmlType: 'submit',
}; };
} }
FormSubmitActionModel.define({ FormSubmitActionModel.define({
title: 'Submit', title: tval('Submit'),
}); });
FormSubmitActionModel.registerFlow({ FormSubmitActionModel.registerFlow({
@ -35,7 +36,7 @@ FormSubmitActionModel.registerFlow({
step1: { step1: {
async handler(ctx, params) { async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.resource) { 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; return;
} }
const currentBlockModel = ctx.shared.currentBlockModel as FormModel; 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 { AddActionButton, AddFieldButton, FlowModelRenderer, SingleRecordResource } from '@nocobase/flow-engine';
import { Card } from 'antd'; import { Card } from 'antd';
import React from 'react'; import React from 'react';
import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel'; import { DataBlockModel } from '../../base/BlockModel';
import { EditableFieldModel } from '../../fields/EditableField/EditableFieldModel'; import { EditableFieldModel } from '../../fields/EditableField/EditableFieldModel';
@ -80,20 +81,20 @@ FormModel.registerFlow({
uiSchema: { uiSchema: {
dataSourceKey: { dataSourceKey: {
type: 'string', type: 'string',
title: 'Data Source Key', title: tval('Data Source Key'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': { 'x-component-props': {
placeholder: 'Enter data source key', placeholder: tval('Enter data source key'),
}, },
}, },
collectionName: { collectionName: {
type: 'string', type: 'string',
title: 'Collection Name', title: tval('Collection Name'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': { 'x-component-props': {
placeholder: 'Enter collection name', placeholder: tval('Enter collection name'),
}, },
}, },
}, },
@ -126,7 +127,7 @@ FormModel.registerFlow({
}); });
FormModel.define({ FormModel.define({
title: 'Form', title: tval('Form'),
group: 'Content', group: 'Content',
defaultOptions: { defaultOptions: {
use: 'FormModel', use: 'FormModel',

View File

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

View File

@ -12,6 +12,7 @@ import { observer } from '@formily/reactive-react';
import { AddActionButton, FlowModel, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine'; import { AddActionButton, FlowModel, FlowModelRenderer, FlowsFloatContextMenu } from '@nocobase/flow-engine';
import { Skeleton, Space } from 'antd'; import { Skeleton, Space } from 'antd';
import React from 'react'; import React from 'react';
import { tval } from '@nocobase/utils/client';
import { ActionModel } from '../../base/ActionModel'; import { ActionModel } from '../../base/ActionModel';
import { SupportedFieldInterfaces } from '../../base/FieldModel'; 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> </FlowsFloatContextMenu>
), ),
render: this.render(), render: this.render(),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,6 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { DatePicker } from '@formily/antd-v5'; import { DatePicker } from '@formily/antd-v5';
import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from '../EditableFieldModel'; import { EditableFieldModel } from '../EditableFieldModel';
export class DateTimeFieldModel extends EditableFieldModel { export class DateTimeFieldModel extends EditableFieldModel {
@ -35,11 +36,11 @@ DateTimeFieldModel.registerFlow({
key: 'key3', key: 'key3',
auto: true, auto: true,
sort: 1000, sort: 1000,
title: 'Specific properties', title: tval('Specific properties'),
steps: { steps: {
dateFormat: { dateFormat: {
use: 'dateDisplayFormat', 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 { Field, Form } from '@formily/core';
import { FieldContext } from '@formily/react'; import { FieldContext } from '@formily/react';
import React from 'react'; import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FieldModel } from '../../base/FieldModel'; import { FieldModel } from '../../base/FieldModel';
import { ReactiveField } from '../../../formily/ReactiveField'; import { ReactiveField } from '../../../formily/ReactiveField';
import { FormModel } from '../..'; import { FormModel } from '../..';
@ -107,7 +108,7 @@ export class EditableFieldModel extends FieldModel<Structure> {
EditableFieldModel.registerFlow({ EditableFieldModel.registerFlow({
key: 'init', key: 'init',
auto: true, auto: true,
title: 'Basic', title: tval('Basic'),
sort: 150, sort: 150,
steps: { steps: {
createField: { createField: {
@ -125,13 +126,13 @@ EditableFieldModel.registerFlow({
}, },
}, },
editTitle: { editTitle: {
title: 'Edit Title', title: tval('Edit Title'),
uiSchema: { uiSchema: {
title: { title: {
'x-component': 'Input', 'x-component': 'Input',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
placeholder: 'Enter field title', placeholder: tval('Enter field title'),
}, },
}, },
}, },
@ -145,7 +146,7 @@ EditableFieldModel.registerFlow({
}, },
}, },
initialValue: { initialValue: {
title: 'Set default value', title: tval('Set default value'),
uiSchema: { uiSchema: {
defaultValue: { defaultValue: {
'x-component': 'Input', 'x-component': 'Input',
@ -158,14 +159,14 @@ EditableFieldModel.registerFlow({
}, },
}, },
required: { required: {
title: 'Required', title: tval('Required'),
uiSchema: { uiSchema: {
required: { required: {
'x-component': 'Switch', 'x-component': 'Switch',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
checkedChildren: 'Yes', checkedChildren: tval('Yes'),
unCheckedChildren: 'No', unCheckedChildren: tval('No'),
}, },
}, },
}, },
@ -174,14 +175,14 @@ EditableFieldModel.registerFlow({
}, },
}, },
displayLabel: { displayLabel: {
title: 'Display label', title: tval('Display label'),
uiSchema: { uiSchema: {
displayLabel: { displayLabel: {
'x-component': 'Switch', 'x-component': 'Switch',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
checkedChildren: 'Yes', checkedChildren: tval('Yes'),
unCheckedChildren: 'No', unCheckedChildren: tval('No'),
}, },
}, },
}, },
@ -193,7 +194,7 @@ EditableFieldModel.registerFlow({
}, },
}, },
editDescription: { editDescription: {
title: 'Edit description', title: tval('Edit description'),
uiSchema: { uiSchema: {
description: { description: {
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
@ -205,7 +206,7 @@ EditableFieldModel.registerFlow({
}, },
}, },
editTooltip: { editTooltip: {
title: 'Edit tooltip', title: tval('Edit tooltip'),
uiSchema: { uiSchema: {
tooltip: { tooltip: {
'x-component': 'Input.TextArea', 'x-component': 'Input.TextArea',
@ -217,7 +218,7 @@ EditableFieldModel.registerFlow({
}, },
}, },
pattern: { pattern: {
title: 'Pattern', title: tval('Pattern'),
uiSchema: { uiSchema: {
pattern: { pattern: {
'x-component': 'Select', 'x-component': 'Select',
@ -225,19 +226,19 @@ EditableFieldModel.registerFlow({
enum: [ enum: [
{ {
value: 'editable', value: 'editable',
label: 'Editable', label: tval('Editable'),
}, },
{ {
value: 'disabled', value: 'disabled',
label: 'Disabled', label: tval('Disabled'),
}, },
{ {
value: 'readOnly', value: 'readOnly',
label: 'ReadOnly', label: tval('ReadOnly'),
}, },
{ {
value: 'readPretty', value: 'readPretty',
label: 'ReadPretty', label: tval('ReadPretty'),
}, },
], ],
}, },

View File

@ -29,11 +29,11 @@ NanoIDEditableFieldModel.registerFlow({
const { size, customAlphabet } = ctx.model.collectionField.options || { size: 21 }; const { size, customAlphabet } = ctx.model.collectionField.options || { size: 21 };
function isValidNanoid(value) { function isValidNanoid(value) {
if (value?.length !== size) { 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++) { for (let i = 0; i < value.length; i++) {
if (customAlphabet?.indexOf(value[i]) === -1) { 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 { Password } from '@formily/antd-v5';
import { tval } from '@nocobase/utils/client';
import { EditableFieldModel } from './EditableFieldModel'; import { EditableFieldModel } from './EditableFieldModel';
export class PasswordEditableFieldModel extends EditableFieldModel { export class PasswordEditableFieldModel extends EditableFieldModel {
@ -21,14 +22,17 @@ PasswordEditableFieldModel.registerFlow({
key: 'key3', key: 'key3',
auto: true, auto: true,
sort: 1000, sort: 1000,
title: 'Group3', title: tval('Password Options'),
steps: { steps: {
placeholder: { placeholder: {
title: 'Placeholder', title: tval('Placeholder'),
uiSchema: { uiSchema: {
checkStrength: { placeholder: {
'x-component': 'Input', 'x-component': 'Input',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': {
placeholder: tval('Enter placeholder text'),
},
}, },
}, },
handler(ctx, params) { handler(ctx, params) {
@ -36,14 +40,14 @@ PasswordEditableFieldModel.registerFlow({
}, },
}, },
checkStrength: { checkStrength: {
title: 'Check strength', title: tval('Check strength'),
uiSchema: { uiSchema: {
checkStrength: { checkStrength: {
'x-component': 'Switch', 'x-component': 'Switch',
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component-props': {
checkedChildren: 'Yes', checkedChildren: tval('Yes'),
unCheckedChildren: 'No', unCheckedChildren: tval('No'),
}, },
}, },
}, },

View File

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

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@ import { FormProvider } from '@formily/react';
import { AddActionButton, AddFieldButton, Collection, FlowModelRenderer } from '@nocobase/flow-engine'; import { AddActionButton, AddFieldButton, Collection, FlowModelRenderer } from '@nocobase/flow-engine';
import { Card } from 'antd'; import { Card } from 'antd';
import React from 'react'; import React from 'react';
import { tval } from '@nocobase/utils/client';
import { FilterBlockModel } from '../../base/BlockModel'; import { FilterBlockModel } from '../../base/BlockModel';
import { FilterFormFieldModel } from './FilterFormFieldModel'; import { FilterFormFieldModel } from './FilterFormFieldModel';
@ -69,7 +70,7 @@ export class FilterFormModel extends FilterBlockModel {
FilterFormModel.define({ FilterFormModel.define({
hide: true, hide: true,
title: 'Form', title: tval('Form'),
}); });
FilterFormModel.registerFlow({ FilterFormModel.registerFlow({
@ -82,20 +83,20 @@ FilterFormModel.registerFlow({
uiSchema: { uiSchema: {
dataSourceKey: { dataSourceKey: {
type: 'string', type: 'string',
title: 'Data Source Key', title: tval('Data Source Key'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': { 'x-component-props': {
placeholder: 'Enter data source key', placeholder: tval('Enter data source key'),
}, },
}, },
collectionName: { collectionName: {
type: 'string', type: 'string',
title: 'Collection Name', title: tval('Collection Name'),
'x-decorator': 'FormItem', 'x-decorator': 'FormItem',
'x-component': 'Input', 'x-component': 'Input',
'x-component-props': { '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 { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
import { ButtonProps } from 'antd'; import { ButtonProps } from 'antd';
import { tval } from '@nocobase/utils/client';
import { DataBlockModel } from '../../base/BlockModel'; import { DataBlockModel } from '../../base/BlockModel';
import { FilterFormActionModel } from './FilterFormActionModel'; import { FilterFormActionModel } from './FilterFormActionModel';
export class FilterFormResetActionModel extends FilterFormActionModel { export class FilterFormResetActionModel extends FilterFormActionModel {
defaultProps: ButtonProps = { defaultProps: ButtonProps = {
children: 'Reset', children: tval('Reset'),
}; };
} }
@ -27,7 +28,7 @@ FilterFormResetActionModel.registerFlow({
step1: { step1: {
async handler(ctx, params) { async handler(ctx, params) {
if (!ctx.shared?.currentBlockModel?.form) { 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; return;
} }
const currentBlockModel = ctx.shared.currentBlockModel; const currentBlockModel = ctx.shared.currentBlockModel;

View File

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

View File

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

View File

@ -895,5 +895,87 @@
"Refresh data blocks": "Refresh data blocks", "Refresh data blocks": "Refresh data blocks",
"Select data blocks to refresh": "Select data blocks to refresh", "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.", "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 Month":"日历月",
"Calendar Year":"日历年", "Calendar Year":"日历年",
"Scan to input":"扫码录入", "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 React, { FC, useState } from 'react';
import { FallbackProps } from 'react-error-boundary'; import { FallbackProps } from 'react-error-boundary';
import { useFlowModel } from '../hooks/useFlowModel'; import { useFlowModel } from '../hooks/useFlowModel';
import { getT } from '../utils';
const { Paragraph, Text } = Typography; const { Paragraph, Text } = Typography;
@ -20,6 +21,7 @@ const { Paragraph, Text } = Typography;
const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }) => { const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }) => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const model = useFlowModel(); // 在这里安全地使用 Hook const model = useFlowModel(); // 在这里安全地使用 Hook
const t = getT(model);
const handleCopyError = async () => { const handleCopyError = async () => {
setLoading(true); setLoading(true);
@ -104,9 +106,9 @@ const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }
const subTitle = ( const subTitle = (
<span> <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"> <a href="https://github.com/nocobase/nocobase/issues" target="_blank" rel="noopener noreferrer">
here {t('here')}
</a> </a>
{model && ( {model && (
<div style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}> <div style={{ marginTop: '8px', fontSize: '12px', color: '#999' }}>
@ -121,23 +123,23 @@ const FlowErrorFallbackInner: FC<FallbackProps> = ({ error, resetErrorBoundary }
<Result <Result
style={{ maxWidth: '60vw', margin: 'auto' }} style={{ maxWidth: '60vw', margin: 'auto' }}
status="error" status="error"
title="Render Failed" title={t('Render failed')}
subTitle={subTitle} subTitle={subTitle}
extra={[ extra={[
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank"> <Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
Feedback {t('Feedback')}
</Button>, </Button>,
canDownloadLogs && ( canDownloadLogs && (
<Button key="log" loading={loading} onClick={handleDownloadLogs}> <Button key="log" loading={loading} onClick={handleDownloadLogs}>
Download logs {t('Download logs')}
</Button> </Button>
), ),
<Button key="copy" loading={loading} onClick={handleCopyError}> <Button key="copy" loading={loading} onClick={handleCopyError}>
Copy Error Info {t('Copy error info')}
</Button>, </Button>,
resetErrorBoundary && ( resetErrorBoundary && (
<Button key="retry" danger onClick={resetErrorBoundary}> <Button key="retry" danger onClick={resetErrorBoundary}>
Try Again {t('Try again')}
</Button> </Button>
), ),
].filter(Boolean)} ].filter(Boolean)}
@ -197,18 +199,18 @@ export const FlowErrorFallback: FC<FallbackProps> & {
<Result <Result
style={{ maxWidth: '60vw', margin: 'auto' }} style={{ maxWidth: '60vw', margin: 'auto' }}
status="error" status="error"
title="Render Failed" title="Render failed"
subTitle={subTitle} subTitle={subTitle}
extra={[ extra={[
<Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank"> <Button type="primary" key="feedback" href="https://github.com/nocobase/nocobase/issues" target="_blank">
Feedback Feedback
</Button>, </Button>,
<Button key="copy" loading={loading} onClick={handleCopyError}> <Button key="copy" loading={loading} onClick={handleCopyError}>
Copy Error Info Copy error info
</Button>, </Button>,
resetErrorBoundary && ( resetErrorBoundary && (
<Button key="retry" danger onClick={resetErrorBoundary}> <Button key="retry" danger onClick={resetErrorBoundary}>
Try Again Try again
</Button> </Button>
), ),
].filter(Boolean)} ].filter(Boolean)}

View File

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

View File

@ -15,6 +15,7 @@ import { observer } from '@formily/react';
import { FlowModel } from '../../../../models'; import { FlowModel } from '../../../../models';
import { useFlowModelById } from '../../../../hooks'; import { useFlowModelById } from '../../../../hooks';
import { openStepSettingsDialog } from './StepSettingsDialog'; import { openStepSettingsDialog } from './StepSettingsDialog';
import { getT } from '../../../../utils';
// 右键菜单组件接口 // 右键菜单组件接口
interface ModelProvidedProps { interface ModelProvidedProps {
@ -70,25 +71,26 @@ const FlowsContextMenu: React.FC<FlowsContextMenuProps> = (props) => {
// 使用传入的model // 使用传入的model
const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer( const FlowsContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => { ({ model, children, enabled = true, position = 'right', showDeleteButton = true }) => {
const t = getT(model);
const handleMenuClick = useCallback( const handleMenuClick = useCallback(
({ key }: { key: string }) => { ({ key }: { key: string }) => {
if (key === 'delete') { if (key === 'delete') {
// 处理删除操作 // 处理删除操作
Modal.confirm({ Modal.confirm({
title: '确认删除', title: t('Confirm delete'),
icon: <ExclamationCircleOutlined />, icon: <ExclamationCircleOutlined />,
content: '确定要删除此项吗?此操作不可撤销。', content: t('Are you sure you want to delete this item? This action cannot be undone.'),
okText: '确认删除', okText: t('Confirm Delete'),
okType: 'danger', okType: 'danger',
cancelText: '取消', cancelText: t('Cancel'),
onOk() { onOk() {
try { try {
model.dispatchEvent('remove'); model.dispatchEvent('remove');
} catch (error) { } catch (error) {
console.error('删除操作失败:', error); console.error(t('Delete operation failed'), ':', error);
Modal.error({ Modal.error({
title: '删除失败', title: t('Delete failed'),
content: '删除操作失败,请检查控制台获取详细信息。', 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 { useFlowModelById } from '../../../../hooks';
import { useFlowEngine } from '../../../../provider'; import { useFlowEngine } from '../../../../provider';
import { FlowEngine } from '../../../../flowEngine'; import { FlowEngine } from '../../../../flowEngine';
import { getT } from '../../../../utils';
import { Droppable } from '../../../dnd'; import { Droppable } from '../../../dnd';
// 检测DOM中直接子元素是否包含button元素的辅助函数 // 检测DOM中直接子元素是否包含button元素的辅助函数
@ -300,7 +301,8 @@ const FlowsFloatContextMenuWithModel: React.FC<ModelProvidedProps> = observer(
}, []); }, []);
if (!model) { if (!model) {
return <Alert message="提供的模型无效" type="error" />; const t = getT(model || ({} as FlowModel));
return <Alert message={t('Invalid model provided')} type="error" />;
} }
// 如果未启用或没有children直接返回children // 如果未启用或没有children直接返回children
@ -357,9 +359,10 @@ const FlowsFloatContextMenuWithModelById: React.FC<ModelByIdProps> = observer(
extraToolbarItems: extraToolbarItems, extraToolbarItems: extraToolbarItems,
}) => { }) => {
const model = useFlowModelById(uid, modelClassName); const model = useFlowModelById(uid, modelClassName);
const flowEngine = useFlowEngine();
if (!model) { if (!model) {
return <Alert message={`未找到ID为 ${uid} 的模型`} type="error" />; return <Alert message={flowEngine.translate('Model with ID {{uid}} not found', { uid })} type="error" />;
} }
return ( return (

View File

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

View File

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

View File

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

View File

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

View File

@ -14,6 +14,7 @@ import { ModelConstructor } from '../../types';
import { FlowSettingsButton } from '../common/FlowSettingsButton'; import { FlowSettingsButton } from '../common/FlowSettingsButton';
import { withFlowDesignMode } from '../common/withFlowDesignMode'; import { withFlowDesignMode } from '../common/withFlowDesignMode';
import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton'; import { AddSubModelButton, SubModelItemsType } from './AddSubModelButton';
import { useTranslation } from 'react-i18next';
interface AddActionButtonProps { interface AddActionButtonProps {
/** /**
@ -48,6 +49,11 @@ interface AddActionButtonProps {
items?: SubModelItemsType; 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, model,
subModelBaseClass = 'ActionFlowModel', subModelBaseClass = 'ActionFlowModel',
subModelKey = 'actions', subModelKey = 'actions',
children = <FlowSettingsButton icon={<SettingOutlined />}>{'Configure actions'}</FlowSettingsButton>, children = <DefaultBtn />,
subModelType = 'array', subModelType = 'array',
items, items,
filter, filter,

View File

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

View File

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

View File

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

View File

@ -14,11 +14,13 @@ import {
ActionDefinition, ActionDefinition,
ActionOptions, ActionOptions,
CreateModelOptions, CreateModelOptions,
FlowContext,
FlowDefinition, FlowDefinition,
IFlowModelRepository, IFlowModelRepository,
ModelConstructor, ModelConstructor,
} from './types'; } from './types';
import { isInheritedFrom, TranslationUtil } from './utils'; import { isInheritedFrom } from './utils';
import { initFlowEngineLocale } from './locale';
interface ApplyFlowCacheEntry { interface ApplyFlowCacheEntry {
status: 'pending' | 'resolved' | 'rejected'; status: 'pending' | 'resolved' | 'rejected';
@ -36,16 +38,15 @@ export class FlowEngine {
private modelInstances: Map<string, any> = new Map(); private modelInstances: Map<string, any> = new Map();
/** @public Stores flow settings including components and scopes for formily settings. */ /** @public Stores flow settings including components and scopes for formily settings. */
public flowSettings: FlowSettings = new FlowSettings(); public flowSettings: FlowSettings = new FlowSettings();
context: Record<string, any> = {}; context: FlowContext['globals'] = {} as FlowContext['globals'];
private modelRepository: IFlowModelRepository | null = null; private modelRepository: IFlowModelRepository | null = null;
private _applyFlowCache = new Map<string, ApplyFlowCacheEntry>(); private _applyFlowCache = new Map<string, ApplyFlowCacheEntry>();
/** @private Translation utility for template compilation and caching */
private _translationUtil = new TranslationUtil();
reactView: ReactView; reactView: ReactView;
constructor() { constructor() {
this.reactView = new ReactView(this); this.reactView = new ReactView(this);
this.flowSettings.registerScopes({ t: this.translate.bind(this) });
} }
// 注册默认的 FlowModel // 注册默认的 FlowModel
@ -58,6 +59,9 @@ export class FlowEngine {
setContext(context: any) { setContext(context: any) {
this.context = { ...this.context, ...context }; this.context = { ...this.context, ...context };
if (this.context.i18n) {
initFlowEngineLocale(this.context.i18n);
}
} }
getContext() { getContext() {
@ -82,12 +86,20 @@ export class FlowEngine {
* flowEngine.t("前缀 {{ t('User Name') }} 后缀") * flowEngine.t("前缀 {{ t('User Name') }} 后缀")
* flowEngine.t("{{t('Hello {name}', {name: 'John'})}}") * flowEngine.t("{{t('Hello {name}', {name: 'John'})}}")
*/ */
public t(keyOrTemplate: string, options?: any): string { public translate(keyOrTemplate: string, options?: any): string {
return this._translationUtil.translate( if (!keyOrTemplate || typeof keyOrTemplate !== 'string') {
keyOrTemplate, return keyOrTemplate;
(key: string, opts?: any) => this.translateKey(key, opts), }
options,
); // 先尝试一次翻译
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; 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() { get applyFlowCache() {
return this._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); console[level.toLowerCase()](logMessage, logMeta);
}; };
const globalContexts = currentFlowEngine.getContext() || {}; const globalContexts = currentFlowEngine.getContext();
const flowContext: FlowContext<this> = { const flowContext: FlowContext<this> = {
exit: () => { exit: () => {
throw new FlowExitException(flowKey, this.uid); 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>) { public setSharedContext(ctx: Record<string, any>) {
this._sharedContext = { ...this._sharedContext, ...ctx }; this._sharedContext = { ...this._sharedContext, ...ctx };
} }

View File

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

View File

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

View File

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