mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
Compare commits
23 Commits
dec669a94a
...
3b39df9147
Author | SHA1 | Date | |
---|---|---|---|
|
3b39df9147 | ||
|
7561db0fa0 | ||
|
407408b36b | ||
|
318145dac9 | ||
|
750468bb74 | ||
|
df7434c970 | ||
|
c2d77907ae | ||
|
bf1886cff5 | ||
|
90fe98b4e5 | ||
|
026baab241 | ||
|
0278e09fef | ||
|
54d3867551 | ||
|
469bd5acee | ||
|
d1ff280d9f | ||
|
2424e93369 | ||
|
5c4619f712 | ||
|
88591454ec | ||
|
a4ca48a1f7 | ||
|
c2176861d8 | ||
|
3c555e9fc0 | ||
|
0a801f655e | ||
|
0dcfc2ec27 | ||
|
6afcbffdec |
@ -143,6 +143,17 @@ class Package {
|
||||
});
|
||||
console.log(chalk.greenBright(`Downloaded: ${this.packageName}@${version}`));
|
||||
} catch (error) {
|
||||
if (error.response.data && typeof error.response.data.pipe === 'function') {
|
||||
let errorMessageBuffer = '';
|
||||
error.response.data.on('data', (chunk) => {
|
||||
errorMessageBuffer += chunk.toString('utf8'); // 收集错误信息
|
||||
});
|
||||
error.response.data.on('end', () => {
|
||||
if (error.response.status === 403) {
|
||||
console.error(chalk.redBright('You do not have permission to download this package version.'));
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(chalk.redBright(`Download failed: ${this.packageName}`));
|
||||
}
|
||||
}
|
||||
@ -252,7 +263,13 @@ module.exports = (cli) => {
|
||||
NOCOBASE_PKG_USERNAME,
|
||||
NOCOBASE_PKG_PASSWORD,
|
||||
} = process.env;
|
||||
const { accessKeyId, accessKeySecret } = await getAccessKeyPair();
|
||||
let accessKeyId;
|
||||
let accessKeySecret;
|
||||
try {
|
||||
({ accessKeyId, accessKeySecret } = await getAccessKeyPair());
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (!(NOCOBASE_PKG_USERNAME && NOCOBASE_PKG_PASSWORD) && !(accessKeyId && accessKeySecret)) {
|
||||
return;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ const fs = require('fs-extra');
|
||||
const os = require('os');
|
||||
const moment = require('moment-timezone');
|
||||
const { keyDecrypt, getEnvAsync } = require('@nocobase/license-kit');
|
||||
const _ = require('lodash');
|
||||
const omit = require('lodash/omit');
|
||||
|
||||
exports.isPackageValid = (pkg) => {
|
||||
try {
|
||||
@ -491,10 +491,20 @@ exports.generatePlugins = function () {
|
||||
}
|
||||
};
|
||||
|
||||
async function isEnvMatch(keyData) {
|
||||
const env = await getEnvAsync();
|
||||
if (env?.container?.id && keyData?.instanceData?.container?.id) {
|
||||
return (
|
||||
JSON.stringify(omit(env, ['timestamp', 'container', 'hostname'])) ===
|
||||
JSON.stringify(omit(keyData?.instanceData, ['timestamp', 'container', 'hostname']))
|
||||
);
|
||||
}
|
||||
return JSON.stringify(omit(env, ['timestamp'])) === JSON.stringify(omit(keyData?.instanceData, ['timestamp']));
|
||||
}
|
||||
|
||||
exports.getAccessKeyPair = async function () {
|
||||
const keyFile = resolve(process.cwd(), 'storage/.license/license-key');
|
||||
if (!fs.existsSync(keyFile)) {
|
||||
// showLicenseInfo(LicenseKeyError.notExist);
|
||||
return {};
|
||||
}
|
||||
|
||||
@ -505,13 +515,13 @@ exports.getAccessKeyPair = async function () {
|
||||
keyData = JSON.parse(keyDataStr);
|
||||
} catch (error) {
|
||||
showLicenseInfo(LicenseKeyError.parseFailed);
|
||||
return {};
|
||||
throw new Error(LicenseKeyError.parseFailed.title);
|
||||
}
|
||||
|
||||
const currentEnv = await getEnvAsync();
|
||||
if (!_.isEqual(_.omit(keyData?.instanceData, ['timestamp']), _.omit(currentEnv, ['timestamp']))) {
|
||||
const isEnvMatched = await isEnvMatch(keyData);
|
||||
if (!isEnvMatched) {
|
||||
showLicenseInfo(LicenseKeyError.notMatch);
|
||||
return {};
|
||||
throw new Error(LicenseKeyError.notMatch.title);
|
||||
}
|
||||
|
||||
const { accessKeyId, accessKeySecret } = keyData;
|
||||
@ -520,21 +530,21 @@ exports.getAccessKeyPair = async function () {
|
||||
|
||||
const LicenseKeyError = {
|
||||
notExist: {
|
||||
title: 'License key not exist',
|
||||
title: 'License key not found',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then generate the license key on the service platform.',
|
||||
},
|
||||
parseFailed: {
|
||||
title: 'License key parse failed',
|
||||
title: 'Invalid license key format',
|
||||
content: 'Please check your license key, or regenerate the license key on the service platform.',
|
||||
},
|
||||
notMatch: {
|
||||
title: 'License key not matched',
|
||||
title: 'License key mismatch',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
notValid: {
|
||||
title: 'License key not valid',
|
||||
title: 'Invalid license key',
|
||||
content:
|
||||
'Please go to the license settings page to obtain the Instance ID for the current environment, and then regenerate the license key on the service platform.',
|
||||
},
|
||||
|
@ -278,6 +278,7 @@ export class Application {
|
||||
api: this.apiClient,
|
||||
i18n: this.i18n,
|
||||
router: this.router.router,
|
||||
flowEngine: this.flowEngine,
|
||||
});
|
||||
this.use(FlowEngineProvider, { engine: this.flowEngine });
|
||||
this.use(FlowEngineGlobalsContextProvider);
|
||||
|
@ -24,6 +24,7 @@ export class DateFieldInterface extends CollectionFieldInterface {
|
||||
'x-component': 'DatePicker',
|
||||
'x-component-props': {
|
||||
dateOnly: true,
|
||||
showTime: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { useField, useForm } from '@formily/react';
|
||||
import { Cascader, Input, Select, Spin, Table, Tag } from 'antd';
|
||||
import { last, omit } from 'lodash';
|
||||
import _, { last, omit } from 'lodash';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ResourceActionContext, useCompile } from '../../../';
|
||||
@ -120,9 +120,10 @@ const PreviewCom = (props) => {
|
||||
}, [databaseView]);
|
||||
|
||||
const handleFieldChange = (record, index) => {
|
||||
dataSource.splice(index, 1, record);
|
||||
setDataSource(dataSource);
|
||||
field.value = dataSource.map((v) => {
|
||||
const newDataSource = _.cloneDeep(dataSource);
|
||||
newDataSource[index] = record;
|
||||
setDataSource(newDataSource);
|
||||
field.value = newDataSource.map((v) => {
|
||||
const source = typeof v.source === 'string' ? v.source : v.source?.filter?.(Boolean)?.join('.');
|
||||
return {
|
||||
...v,
|
||||
@ -198,8 +199,7 @@ const PreviewCom = (props) => {
|
||||
style={{ width: '100%' }}
|
||||
popupMatchSelectWidth={false}
|
||||
onChange={(value) => {
|
||||
const interfaceConfig = getInterface(value);
|
||||
handleFieldChange({ ...item, interface: value, uiSchema: interfaceConfig?.default?.uiSchema }, index);
|
||||
handleFieldChange({ ...item, interface: value }, index);
|
||||
}}
|
||||
>
|
||||
{data.map((group) => (
|
||||
|
@ -47,7 +47,7 @@ export function Grid(props: {
|
||||
<Col key={cellIdx} span={spans[cellIdx]}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||||
{cell.map((uid) => (
|
||||
<div key={uid}>{renderItem(uid)}</div>
|
||||
<React.Fragment key={uid}>{renderItem(uid)}</React.Fragment>
|
||||
))}
|
||||
</div>
|
||||
</Col>
|
||||
|
@ -39,7 +39,9 @@ export const BlockItemCard = (props) => {
|
||||
<Card
|
||||
title={title}
|
||||
style={{ display: 'flex', flexDirection: 'column', height: '100%' }}
|
||||
bodyStyle={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
styles={{
|
||||
body: { flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden' },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Card>
|
||||
|
@ -9,7 +9,7 @@
|
||||
|
||||
import { QuestionCircleOutlined } from '@ant-design/icons';
|
||||
import { css } from '@emotion/css';
|
||||
import { FlowsFloatContextMenu } from '@nocobase/flow-engine';
|
||||
import { DragHandler, FlowsFloatContextMenu } from '@nocobase/flow-engine';
|
||||
import { tval } from '@nocobase/utils/client';
|
||||
import { TableColumnProps, Tooltip } from 'antd';
|
||||
import React from 'react';
|
||||
@ -24,6 +24,13 @@ export class TableColumnModel extends FieldModel {
|
||||
containerStyle={{ display: 'block', padding: '11px 8px', margin: '-11px -8px' }}
|
||||
showBorder={false}
|
||||
settingsMenuLevel={2}
|
||||
extraToolbarItems={[
|
||||
{
|
||||
key: 'drag-handler',
|
||||
component: DragHandler,
|
||||
sort: 1,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<div
|
||||
className={css`
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
AddActionButton,
|
||||
AddFieldButton,
|
||||
DndProvider,
|
||||
DragHandler,
|
||||
FlowModelRenderer,
|
||||
MultiRecordResource,
|
||||
useFlowEngine,
|
||||
@ -103,10 +104,21 @@ export class TableModel extends DataBlockModel<TableModelStructure> {
|
||||
appendItems={[
|
||||
{
|
||||
key: 'actions',
|
||||
label: tval('Actions column'),
|
||||
label: this.translate('Actions column'),
|
||||
createModelOptions: {
|
||||
use: 'TableActionsColumnModel',
|
||||
},
|
||||
toggleDetector: (ctx) => {
|
||||
// 检测是否已存在操作列
|
||||
const subModels = ctx.model.subModels.columns;
|
||||
const modelClass = ctx.model.flowEngine.getModelClass('TableActionsColumnModel');
|
||||
if (Array.isArray(subModels)) {
|
||||
return subModels.some((subModel) => {
|
||||
return subModel instanceof modelClass;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]}
|
||||
onModelCreated={async (model: TableColumnModel) => {
|
||||
@ -246,7 +258,17 @@ export class TableModel extends DataBlockModel<TableModelStructure> {
|
||||
// @ts-ignore
|
||||
if (action.props.position !== 'left') {
|
||||
return (
|
||||
<FlowModelRenderer model={action} showFlowSettings={{ showBackground: false, showBorder: false }} />
|
||||
<FlowModelRenderer
|
||||
model={action}
|
||||
showFlowSettings={{ showBackground: false, showBorder: false }}
|
||||
extraToolbarItems={[
|
||||
{
|
||||
key: 'drag-handler',
|
||||
component: DragHandler,
|
||||
sort: 1,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -203,6 +203,18 @@ export class TabulatorModel extends DataBlockModel<S> {
|
||||
createModelOptions: {
|
||||
use: 'TabulatorTableActionsColumnModel',
|
||||
},
|
||||
toggleDetector: (ctx) => {
|
||||
// 检测是否已存在操作列
|
||||
const subModels = ctx.model.subModels.columns;
|
||||
const modelClass = ctx.model.flowEngine.getModelClass('TabulatorTableActionsColumnModel');
|
||||
if (Array.isArray(subModels)) {
|
||||
return subModels.some((subModel) => {
|
||||
// 使用 flowEngine 的模型注册信息进行检测
|
||||
return subModel instanceof modelClass;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]}
|
||||
onModelCreated={async (model: TabulatorColumnModel) => {
|
||||
|
@ -66,7 +66,7 @@ export const createFormBlockSettings = new SchemaSettings({
|
||||
useVisible() {
|
||||
const { action } = useFormBlockContext();
|
||||
const schema = useFieldSchema();
|
||||
return !action && schema?.['x-acl-action'].includes('create');
|
||||
return !action && schema?.['x-acl-action']?.includes('create');
|
||||
},
|
||||
useComponentProps() {
|
||||
const { name } = useCollection_deprecated();
|
||||
|
@ -10,6 +10,7 @@
|
||||
import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client';
|
||||
import type { Dayjs } from 'dayjs';
|
||||
import dayjs from 'dayjs';
|
||||
import { dayjsable, formatDayjsValue } from '@formily/antd-v5/esm/__builtins__';
|
||||
|
||||
const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => {
|
||||
if (!dayjs.isDayjs(value)) return value;
|
||||
@ -89,7 +90,7 @@ export const handleDateChangeOnForm = (value, dateOnly, utc, picker, showTime, g
|
||||
return value;
|
||||
}
|
||||
if (dateOnly) {
|
||||
return dayjs(value).startOf(picker).format('YYYY-MM-DD');
|
||||
return formatDayjsValue(value, 'YYYY-MM-DD');
|
||||
}
|
||||
if (utc) {
|
||||
if (gmt) {
|
||||
@ -114,6 +115,7 @@ export const mapDatePicker = function () {
|
||||
const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props;
|
||||
const format = getDefaultFormat(props);
|
||||
const onChange = props.onChange;
|
||||
|
||||
return {
|
||||
...props,
|
||||
inputReadOnly: isMobileMedia,
|
||||
|
@ -33,7 +33,7 @@ const toDate = (date, options: any = {}) => {
|
||||
}
|
||||
|
||||
if (field.constructor.name === 'DateOnlyField') {
|
||||
val = moment(val).format('YYYY-MM-DD HH:mm:ss');
|
||||
val = moment.utc(val).format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
const eventObj = {
|
||||
@ -69,7 +69,6 @@ export default {
|
||||
const r = parseDate(value, {
|
||||
timezone: parseDateTimezone(ctx),
|
||||
});
|
||||
|
||||
if (typeof r === 'string') {
|
||||
return {
|
||||
[Op.eq]: toDate(r, { ctx }),
|
||||
@ -77,6 +76,9 @@ export default {
|
||||
}
|
||||
|
||||
if (Array.isArray(r)) {
|
||||
console.log(11111111, {
|
||||
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
|
||||
});
|
||||
return {
|
||||
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
|
||||
};
|
||||
|
536
packages/core/flow-engine/src/__tests__/flowSettings.test.ts
Normal file
536
packages/core/flow-engine/src/__tests__/flowSettings.test.ts
Normal file
@ -0,0 +1,536 @@
|
||||
/**
|
||||
* 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, test, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import { FlowSettings } from '../flowSettings';
|
||||
import { DefaultSettingsIcon } from '../components/settings/wrappers/contextual/DefaultSettingsIcon';
|
||||
import { DragHandler } from '../components/dnd';
|
||||
import { FlowModel } from '../models';
|
||||
import { FlowEngine } from '../flowEngine';
|
||||
|
||||
// Mock external dependencies
|
||||
vi.mock('../components/settings/wrappers/contextual/StepSettingsDialog', () => ({
|
||||
openStepSettingsDialog: vi.fn().mockResolvedValue({ success: true, data: 'test-result' }),
|
||||
}));
|
||||
|
||||
vi.mock('../components/settings/wrappers/contextual/DefaultSettingsIcon', () => ({
|
||||
DefaultSettingsIcon: vi.fn(() => 'DefaultSettingsIcon'),
|
||||
}));
|
||||
|
||||
vi.mock('../components/dnd', () => ({
|
||||
DragHandler: vi.fn(() => 'DragHandler'),
|
||||
}));
|
||||
|
||||
// Mock dynamic imports
|
||||
vi.mock('@formily/antd-v5', () => ({
|
||||
ArrayBase: vi.fn(() => 'ArrayBase'),
|
||||
ArrayCards: vi.fn(() => 'ArrayCards'),
|
||||
ArrayCollapse: vi.fn(() => 'ArrayCollapse'),
|
||||
ArrayItems: vi.fn(() => 'ArrayItems'),
|
||||
ArrayTable: vi.fn(() => 'ArrayTable'),
|
||||
ArrayTabs: vi.fn(() => 'ArrayTabs'),
|
||||
Cascader: vi.fn(() => 'Cascader'),
|
||||
Checkbox: vi.fn(() => 'Checkbox'),
|
||||
DatePicker: vi.fn(() => 'DatePicker'),
|
||||
Editable: vi.fn(() => 'Editable'),
|
||||
Form: vi.fn(() => 'Form'),
|
||||
FormDialog: vi.fn(() => 'FormDialog'),
|
||||
FormDrawer: vi.fn(() => 'FormDrawer'),
|
||||
FormButtonGroup: vi.fn(() => 'FormButtonGroup'),
|
||||
FormCollapse: vi.fn(() => 'FormCollapse'),
|
||||
FormGrid: vi.fn(() => 'FormGrid'),
|
||||
FormItem: vi.fn(() => 'FormItem'),
|
||||
FormLayout: vi.fn(() => 'FormLayout'),
|
||||
FormStep: vi.fn(() => 'FormStep'),
|
||||
FormTab: vi.fn(() => 'FormTab'),
|
||||
Input: vi.fn(() => 'Input'),
|
||||
NumberPicker: vi.fn(() => 'NumberPicker'),
|
||||
Password: vi.fn(() => 'Password'),
|
||||
PreviewText: vi.fn(() => 'PreviewText'),
|
||||
Radio: vi.fn(() => 'Radio'),
|
||||
Reset: vi.fn(() => 'Reset'),
|
||||
Select: vi.fn(() => 'Select'),
|
||||
SelectTable: vi.fn(() => 'SelectTable'),
|
||||
Space: vi.fn(() => 'Space'),
|
||||
Submit: vi.fn(() => 'Submit'),
|
||||
Switch: vi.fn(() => 'Switch'),
|
||||
TimePicker: vi.fn(() => 'TimePicker'),
|
||||
Transfer: vi.fn(() => 'Transfer'),
|
||||
TreeSelect: vi.fn(() => 'TreeSelect'),
|
||||
Upload: vi.fn(() => 'Upload'),
|
||||
}));
|
||||
|
||||
vi.mock('antd', () => ({
|
||||
Button: vi.fn(() => 'Button'),
|
||||
}));
|
||||
|
||||
describe('FlowSettings', () => {
|
||||
let flowSettings: FlowSettings;
|
||||
let consoleSpy: any;
|
||||
|
||||
beforeEach(() => {
|
||||
flowSettings = new FlowSettings();
|
||||
consoleSpy = {
|
||||
log: vi.spyOn(console, 'log').mockImplementation(() => {}),
|
||||
warn: vi.spyOn(console, 'warn').mockImplementation(() => {}),
|
||||
error: vi.spyOn(console, 'error').mockImplementation(() => {}),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
consoleSpy.log.mockRestore();
|
||||
consoleSpy.warn.mockRestore();
|
||||
consoleSpy.error.mockRestore();
|
||||
});
|
||||
|
||||
describe('Constructor & Initialization', () => {
|
||||
test('should initialize with default values', () => {
|
||||
expect(flowSettings.enabled).toBe(false);
|
||||
expect(flowSettings.components).toEqual({});
|
||||
expect(flowSettings.scopes).toEqual({});
|
||||
expect(flowSettings.toolbarItems).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('should add default toolbar items during construction', () => {
|
||||
const toolbarItems = flowSettings.getToolbarItems();
|
||||
|
||||
expect(toolbarItems).toHaveLength(1);
|
||||
|
||||
const settingsItem = toolbarItems.find((item) => item.key === 'settings-menu');
|
||||
|
||||
expect(settingsItem).toBeDefined();
|
||||
expect(settingsItem?.component).toBe(DefaultSettingsIcon);
|
||||
expect(settingsItem?.sort).toBe(0);
|
||||
});
|
||||
|
||||
test('should set up observable properties', () => {
|
||||
// Test that enabled property is reactive
|
||||
const initialEnabled = flowSettings.enabled;
|
||||
flowSettings.enable();
|
||||
expect(flowSettings.enabled).not.toBe(initialEnabled);
|
||||
expect(flowSettings.enabled).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component Registration', () => {
|
||||
test('should register single component', () => {
|
||||
const TestComponent = () => 'TestComponent';
|
||||
flowSettings.registerComponents({ TestComponent });
|
||||
|
||||
expect(flowSettings.components.TestComponent).toBe(TestComponent);
|
||||
});
|
||||
|
||||
test('should register multiple components', () => {
|
||||
const Component1 = () => 'Component1';
|
||||
const Component2 = () => 'Component2';
|
||||
|
||||
flowSettings.registerComponents({ Component1, Component2 });
|
||||
|
||||
expect(flowSettings.components.Component1).toBe(Component1);
|
||||
expect(flowSettings.components.Component2).toBe(Component2);
|
||||
});
|
||||
|
||||
test('should warn when overwriting existing component', () => {
|
||||
const Component1 = () => 'Component1';
|
||||
const Component1Updated = () => 'Component1Updated';
|
||||
|
||||
flowSettings.registerComponents({ Component1 });
|
||||
flowSettings.registerComponents({ Component1: Component1Updated });
|
||||
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
||||
"FlowSettings: Component with name 'Component1' is already registered and will be overwritten.",
|
||||
);
|
||||
expect(flowSettings.components.Component1).toBe(Component1Updated);
|
||||
});
|
||||
|
||||
test('should handle empty components object', () => {
|
||||
flowSettings.registerComponents({});
|
||||
expect(Object.keys(flowSettings.components)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Scope Registration', () => {
|
||||
test('should register single scope', () => {
|
||||
const testHook = () => 'testHook';
|
||||
flowSettings.registerScopes({ testHook });
|
||||
|
||||
expect(flowSettings.scopes.testHook).toBe(testHook);
|
||||
});
|
||||
|
||||
test('should register multiple scopes', () => {
|
||||
const hook1 = () => 'hook1';
|
||||
const variable1 = 'variable1';
|
||||
const function1 = () => 'function1';
|
||||
|
||||
flowSettings.registerScopes({ hook1, variable1, function1 });
|
||||
|
||||
expect(flowSettings.scopes.hook1).toBe(hook1);
|
||||
expect(flowSettings.scopes.variable1).toBe(variable1);
|
||||
expect(flowSettings.scopes.function1).toBe(function1);
|
||||
});
|
||||
|
||||
test('should warn when overwriting existing scope', () => {
|
||||
const scope1 = () => 'scope1';
|
||||
const scope1Updated = () => 'scope1Updated';
|
||||
|
||||
flowSettings.registerScopes({ scope1 });
|
||||
flowSettings.registerScopes({ scope1: scope1Updated });
|
||||
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
||||
"FlowSettings: Scope with name 'scope1' is already registered and will be overwritten.",
|
||||
);
|
||||
expect(flowSettings.scopes.scope1).toBe(scope1Updated);
|
||||
});
|
||||
|
||||
test('should handle empty scopes object', () => {
|
||||
flowSettings.registerScopes({});
|
||||
expect(Object.keys(flowSettings.scopes)).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enable/Disable Functionality', () => {
|
||||
test('should enable flow settings', () => {
|
||||
expect(flowSettings.enabled).toBe(false);
|
||||
|
||||
flowSettings.enable();
|
||||
|
||||
expect(flowSettings.enabled).toBe(true);
|
||||
});
|
||||
|
||||
test('should disable flow settings', () => {
|
||||
flowSettings.enable();
|
||||
expect(flowSettings.enabled).toBe(true);
|
||||
|
||||
flowSettings.disable();
|
||||
|
||||
expect(flowSettings.enabled).toBe(false);
|
||||
});
|
||||
|
||||
test('should handle multiple enable/disable calls', () => {
|
||||
flowSettings.enable();
|
||||
flowSettings.enable();
|
||||
expect(flowSettings.enabled).toBe(true);
|
||||
|
||||
flowSettings.disable();
|
||||
flowSettings.disable();
|
||||
expect(flowSettings.enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Resource Loading', () => {
|
||||
test('should load Antd components successfully', async () => {
|
||||
await flowSettings.load();
|
||||
|
||||
// Verify all expected components are registered
|
||||
const expectedComponents = [
|
||||
'Form',
|
||||
'FormDialog',
|
||||
'FormDrawer',
|
||||
'FormItem',
|
||||
'FormLayout',
|
||||
'FormGrid',
|
||||
'FormStep',
|
||||
'FormTab',
|
||||
'FormCollapse',
|
||||
'FormButtonGroup',
|
||||
'Input',
|
||||
'NumberPicker',
|
||||
'Password',
|
||||
'Select',
|
||||
'SelectTable',
|
||||
'Cascader',
|
||||
'TreeSelect',
|
||||
'Transfer',
|
||||
'DatePicker',
|
||||
'TimePicker',
|
||||
'Checkbox',
|
||||
'Radio',
|
||||
'Switch',
|
||||
'ArrayBase',
|
||||
'ArrayCards',
|
||||
'ArrayCollapse',
|
||||
'ArrayItems',
|
||||
'ArrayTable',
|
||||
'ArrayTabs',
|
||||
'Upload',
|
||||
'Space',
|
||||
'Editable',
|
||||
'PreviewText',
|
||||
'Button',
|
||||
'Submit',
|
||||
'Reset',
|
||||
];
|
||||
|
||||
expectedComponents.forEach((componentName) => {
|
||||
expect(flowSettings.components[componentName]).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Toolbar Item Management', () => {
|
||||
describe('addToolbarItem', () => {
|
||||
test('should add new toolbar item', () => {
|
||||
const TestIcon = () => 'TestIcon';
|
||||
const config = {
|
||||
key: 'test-item',
|
||||
component: TestIcon,
|
||||
sort: 10,
|
||||
};
|
||||
|
||||
flowSettings.addToolbarItem(config);
|
||||
|
||||
const items = flowSettings.getToolbarItems();
|
||||
const testItem = items.find((item) => item.key === 'test-item');
|
||||
|
||||
expect(testItem).toBeDefined();
|
||||
expect(testItem?.component).toBe(TestIcon);
|
||||
expect(testItem?.sort).toBe(10);
|
||||
});
|
||||
|
||||
test('should replace existing toolbar item with same key', () => {
|
||||
const TestIcon1 = () => 'TestIcon1';
|
||||
const TestIcon2 = () => 'TestIcon2';
|
||||
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'duplicate-key',
|
||||
component: TestIcon1,
|
||||
sort: 10,
|
||||
});
|
||||
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'duplicate-key',
|
||||
component: TestIcon2,
|
||||
sort: 20,
|
||||
});
|
||||
|
||||
expect(consoleSpy.warn).toHaveBeenCalledWith(
|
||||
"FlowSettings: Toolbar item with key 'duplicate-key' already exists and will be replaced.",
|
||||
);
|
||||
|
||||
const items = flowSettings.getToolbarItems();
|
||||
const duplicateItems = items.filter((item) => item.key === 'duplicate-key');
|
||||
|
||||
expect(duplicateItems).toHaveLength(1);
|
||||
expect(duplicateItems[0].component).toBe(TestIcon2);
|
||||
expect(duplicateItems[0].sort).toBe(20);
|
||||
});
|
||||
|
||||
test('should sort toolbar items by sort field (descending)', () => {
|
||||
const Icon1 = () => 'Icon1';
|
||||
const Icon2 = () => 'Icon2';
|
||||
const Icon3 = () => 'Icon3';
|
||||
|
||||
flowSettings.addToolbarItem({ key: 'item1', component: Icon1, sort: 10 });
|
||||
flowSettings.addToolbarItem({ key: 'item2', component: Icon2, sort: 30 });
|
||||
flowSettings.addToolbarItem({ key: 'item3', component: Icon3, sort: 20 });
|
||||
|
||||
const items = flowSettings.getToolbarItems();
|
||||
const customItems = items.filter((item) => ['item1', 'item2', 'item3'].includes(item.key));
|
||||
|
||||
expect(customItems[0].key).toBe('item2'); // sort: 30
|
||||
expect(customItems[1].key).toBe('item3'); // sort: 20
|
||||
expect(customItems[2].key).toBe('item1'); // sort: 10
|
||||
});
|
||||
|
||||
test('should handle items with undefined sort', () => {
|
||||
const TestIcon = () => 'TestIcon';
|
||||
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'no-sort-item',
|
||||
component: TestIcon,
|
||||
});
|
||||
|
||||
const items = flowSettings.getToolbarItems();
|
||||
const item = items.find((item) => item.key === 'no-sort-item');
|
||||
|
||||
expect(item).toBeDefined();
|
||||
expect(item?.sort).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addToolbarItems', () => {
|
||||
test('should add multiple toolbar items', () => {
|
||||
const Icon1 = () => 'Icon1';
|
||||
const Icon2 = () => 'Icon2';
|
||||
|
||||
const configs = [
|
||||
{ key: 'multi-item1', component: Icon1, sort: 10 },
|
||||
{ key: 'multi-item2', component: Icon2, sort: 20 },
|
||||
];
|
||||
|
||||
flowSettings.addToolbarItems(configs);
|
||||
|
||||
const items = flowSettings.getToolbarItems();
|
||||
|
||||
expect(items.find((item) => item.key === 'multi-item1')).toBeDefined();
|
||||
expect(items.find((item) => item.key === 'multi-item2')).toBeDefined();
|
||||
});
|
||||
|
||||
test('should handle empty array', () => {
|
||||
const initialLength = flowSettings.getToolbarItems().length;
|
||||
|
||||
flowSettings.addToolbarItems([]);
|
||||
|
||||
expect(flowSettings.getToolbarItems()).toHaveLength(initialLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeToolbarItem', () => {
|
||||
test('should remove existing toolbar item', () => {
|
||||
const TestIcon = () => 'TestIcon';
|
||||
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'remove-test',
|
||||
component: TestIcon,
|
||||
sort: 10,
|
||||
});
|
||||
|
||||
let items = flowSettings.getToolbarItems();
|
||||
expect(items.find((item) => item.key === 'remove-test')).toBeDefined();
|
||||
|
||||
flowSettings.removeToolbarItem('remove-test');
|
||||
|
||||
items = flowSettings.getToolbarItems();
|
||||
expect(items.find((item) => item.key === 'remove-test')).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should handle removal of non-existent item', () => {
|
||||
const initialLength = flowSettings.getToolbarItems().length;
|
||||
|
||||
flowSettings.removeToolbarItem('non-existent-key');
|
||||
|
||||
expect(flowSettings.getToolbarItems()).toHaveLength(initialLength);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToolbarItems', () => {
|
||||
test('should return copy of toolbar items', () => {
|
||||
const items1 = flowSettings.getToolbarItems();
|
||||
const items2 = flowSettings.getToolbarItems();
|
||||
|
||||
expect(items1).not.toBe(items2); // Different references
|
||||
expect(items1).toEqual(items2); // Same content
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearToolbarItems', () => {
|
||||
test('should clear all toolbar items', () => {
|
||||
const TestIcon = () => 'TestIcon';
|
||||
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'clear-test',
|
||||
component: TestIcon,
|
||||
sort: 10,
|
||||
});
|
||||
|
||||
expect(flowSettings.getToolbarItems().length).toBeGreaterThan(0);
|
||||
|
||||
flowSettings.clearToolbarItems();
|
||||
|
||||
expect(flowSettings.getToolbarItems()).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('should handle clearing empty toolbar items', () => {
|
||||
flowSettings.clearToolbarItems();
|
||||
flowSettings.clearToolbarItems();
|
||||
|
||||
expect(flowSettings.getToolbarItems()).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Step Settings Dialog', () => {
|
||||
test('should call openStepSettingsDialog with correct parameters', async () => {
|
||||
const { openStepSettingsDialog } = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
||||
|
||||
const props = {
|
||||
model: new FlowModel({ uid: 'test-model', flowEngine: new FlowEngine() }),
|
||||
flowKey: 'test-flow',
|
||||
stepKey: 'test-step',
|
||||
dialogWidth: 800,
|
||||
dialogTitle: 'Test Dialog',
|
||||
};
|
||||
|
||||
const result = await flowSettings.openStepSettingsDialog(props);
|
||||
|
||||
expect(openStepSettingsDialog).toHaveBeenCalledWith(props);
|
||||
expect(result).toEqual({ success: true, data: 'test-result' });
|
||||
});
|
||||
|
||||
test('should handle dialog errors', async () => {
|
||||
const { openStepSettingsDialog } = await import('../components/settings/wrappers/contextual/StepSettingsDialog');
|
||||
|
||||
(openStepSettingsDialog as any).mockRejectedValueOnce(new Error('Dialog error'));
|
||||
|
||||
const props = {
|
||||
model: new FlowModel({ uid: 'test-model', flowEngine: new FlowEngine() }),
|
||||
flowKey: 'test-flow',
|
||||
stepKey: 'test-step',
|
||||
};
|
||||
|
||||
await expect(flowSettings.openStepSettingsDialog(props)).rejects.toThrow('Dialog error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Complex Integration Scenarios', () => {
|
||||
test('should maintain state consistency during multiple operations', () => {
|
||||
// Initialize with components and scopes
|
||||
const TestComponent = () => 'TestComponent';
|
||||
const testScope = () => 'testScope';
|
||||
|
||||
flowSettings.registerComponents({ TestComponent });
|
||||
flowSettings.registerScopes({ testScope });
|
||||
|
||||
// Add toolbar items
|
||||
flowSettings.addToolbarItem({
|
||||
key: 'integration-test',
|
||||
component: TestComponent,
|
||||
sort: 15,
|
||||
});
|
||||
|
||||
// Enable/disable
|
||||
flowSettings.enable();
|
||||
expect(flowSettings.enabled).toBe(true);
|
||||
|
||||
// Verify all state is maintained
|
||||
expect(flowSettings.components.TestComponent).toBe(TestComponent);
|
||||
expect(flowSettings.scopes.testScope).toBe(testScope);
|
||||
expect(flowSettings.getToolbarItems().find((item) => item.key === 'integration-test')).toBeDefined();
|
||||
|
||||
flowSettings.disable();
|
||||
expect(flowSettings.enabled).toBe(false);
|
||||
|
||||
// State should still be maintained after disable
|
||||
expect(flowSettings.components.TestComponent).toBe(TestComponent);
|
||||
expect(flowSettings.scopes.testScope).toBe(testScope);
|
||||
});
|
||||
|
||||
test('should handle complex toolbar sorting scenarios', () => {
|
||||
// Clear default items for this test
|
||||
flowSettings.clearToolbarItems();
|
||||
|
||||
const items = [
|
||||
{ key: 'item-a', component: () => 'A', sort: 10 },
|
||||
{ key: 'item-b', component: () => 'B' }, // no sort (should be 0)
|
||||
{ key: 'item-c', component: () => 'C', sort: 30 },
|
||||
{ key: 'item-d', component: () => 'D', sort: 10 }, // duplicate sort
|
||||
{ key: 'item-e', component: () => 'E', sort: 5 },
|
||||
];
|
||||
|
||||
items.forEach((item) => flowSettings.addToolbarItem(item));
|
||||
|
||||
const sortedItems = flowSettings.getToolbarItems();
|
||||
const keys = sortedItems.map((item) => item.key);
|
||||
|
||||
// Expected order: item-c (30), then items with sort 10 (order of insertion: item-a, item-d), item-e (5), item-b (0)
|
||||
expect(keys).toEqual(['item-c', 'item-a', 'item-d', 'item-e', 'item-b']);
|
||||
});
|
||||
});
|
||||
});
|
@ -20,6 +20,7 @@ export const FlowSettingsButton: FC<ButtonProps> = (props) => {
|
||||
...props.style,
|
||||
borderColor: 'var(--colorSettings)',
|
||||
color: 'var(--colorSettings)',
|
||||
alignSelf: 'flex-start',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
@ -7,13 +7,15 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { createSchemaField, ISchema } from '@formily/react';
|
||||
import { message } from 'antd';
|
||||
import { FormButtonGroup } from '@formily/antd-v5';
|
||||
import { createForm } from '@formily/core';
|
||||
import { createSchemaField, FormProvider, ISchema } from '@formily/react';
|
||||
import { toJS } from '@formily/reactive';
|
||||
import { Button, message, Space } from 'antd';
|
||||
import React from 'react';
|
||||
import { StepSettingsDialogProps } from '../../../../types';
|
||||
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
|
||||
import { compileUiSchema, getT, resolveDefaultParams, resolveUiSchema } from '../../../../utils';
|
||||
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
|
||||
import { toJS } from '@formily/reactive';
|
||||
|
||||
const SchemaField = createSchemaField();
|
||||
|
||||
@ -32,8 +34,10 @@ const openStepSettingsDialog = async ({
|
||||
stepKey,
|
||||
dialogWidth = 600,
|
||||
dialogTitle,
|
||||
mode = 'dialog',
|
||||
}: StepSettingsDialogProps): Promise<any> => {
|
||||
const t = getT(model);
|
||||
const message = model.flowEngine.context.message;
|
||||
|
||||
if (!model) {
|
||||
message.error(t('Invalid model provided'));
|
||||
@ -127,25 +131,47 @@ const openStepSettingsDialog = async ({
|
||||
},
|
||||
};
|
||||
|
||||
// 动态导入FormDialog
|
||||
let FormDialog;
|
||||
try {
|
||||
({ FormDialog } = await import('@formily/antd-v5'));
|
||||
} catch (error) {
|
||||
throw new Error(`${t('Failed to import FormDialog')}: ${error.message}`);
|
||||
}
|
||||
const view = model.flowEngine.context[mode];
|
||||
|
||||
// 创建FormDialog
|
||||
const formDialog = FormDialog(
|
||||
{
|
||||
title: dialogTitle || `${t(title)} - ${t('Configuration')}`,
|
||||
width: dialogWidth,
|
||||
okText: t('OK'),
|
||||
cancelText: t('Cancel'),
|
||||
destroyOnClose: true,
|
||||
},
|
||||
(form) => {
|
||||
// 创建上下文值
|
||||
const form = createForm({
|
||||
initialValues: compileUiSchema(scopes, initialValues),
|
||||
});
|
||||
|
||||
const currentDialog = view.open({
|
||||
title: dialogTitle || t(title),
|
||||
width: dialogWidth,
|
||||
destroyOnClose: true,
|
||||
footer: (
|
||||
<Space align="end">
|
||||
<Button
|
||||
type="default"
|
||||
onClick={() => {
|
||||
currentDialog.close();
|
||||
}}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await form.submit();
|
||||
const currentValues = form.values;
|
||||
model.setStepParams(flowKey, stepKey, currentValues);
|
||||
await model.save();
|
||||
message.success(t('Configuration saved'));
|
||||
currentDialog.close();
|
||||
} catch (error) {
|
||||
console.error(t('Error saving configuration'), ':', error);
|
||||
message.error(t('Error saving configuration, please check console'));
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
content: (currentDialog) => {
|
||||
const contextValue: StepSettingContextType = {
|
||||
model,
|
||||
globals: model.flowEngine?.context || {},
|
||||
@ -155,45 +181,22 @@ const openStepSettingsDialog = async ({
|
||||
flowKey,
|
||||
stepKey,
|
||||
};
|
||||
|
||||
// 编译 formSchema 中的表达式
|
||||
const compiledFormSchema = compileUiSchema(scopes, formSchema);
|
||||
|
||||
return (
|
||||
<StepSettingContextProvider value={contextValue}>
|
||||
<SchemaField
|
||||
schema={compiledFormSchema}
|
||||
components={{
|
||||
...flowEngine.flowSettings?.components,
|
||||
}}
|
||||
scope={scopes}
|
||||
/>
|
||||
</StepSettingContextProvider>
|
||||
<FormProvider form={form}>
|
||||
<StepSettingContextProvider value={contextValue}>
|
||||
<SchemaField
|
||||
schema={compiledFormSchema}
|
||||
components={{
|
||||
...flowEngine.flowSettings?.components,
|
||||
}}
|
||||
scope={scopes}
|
||||
/>
|
||||
</StepSettingContextProvider>
|
||||
</FormProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// 设置保存回调
|
||||
formDialog.forConfirm(async (payload, next) => {
|
||||
try {
|
||||
// 获取表单当前值
|
||||
const currentValues = payload.values;
|
||||
model.setStepParams(flowKey, stepKey, currentValues);
|
||||
await model.save();
|
||||
message.success(t('Configuration saved'));
|
||||
next(payload);
|
||||
} catch (error) {
|
||||
console.error(t('Error saving configuration'), ':', error);
|
||||
message.error(t('Error saving configuration, please check console'));
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
formDialog.forCancel(async (payload, next) => next(payload));
|
||||
|
||||
// 打开对话框
|
||||
return formDialog.open({
|
||||
initialValues: compileUiSchema(scopes, initialValues),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -6,17 +6,8 @@
|
||||
* 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 { createSchemaField, ISchema } from '@formily/react';
|
||||
import { message, Button, Space } from 'antd';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { StepSettingsDrawerProps } from '../../../../types';
|
||||
import { resolveDefaultParams, resolveUiSchema, compileUiSchema, getT } from '../../../../utils';
|
||||
import { StepSettingContextProvider, StepSettingContextType, useStepSettingContext } from './StepSettingContext';
|
||||
import { toJS } from '@formily/reactive';
|
||||
|
||||
const SchemaField = createSchemaField();
|
||||
import { openStepSettingsDialog } from './StepSettingsDialog';
|
||||
|
||||
/**
|
||||
* StepSettingsDrawer组件 - 使用自定义 Drawer 显示单个步骤的配置界面
|
||||
@ -27,230 +18,8 @@ const SchemaField = createSchemaField();
|
||||
* @param props.drawerTitle 自定义抽屉标题,默认使用step的title
|
||||
* @returns Promise<any> 返回表单提交的值
|
||||
*/
|
||||
const openStepSettingsDrawer = async ({
|
||||
model,
|
||||
flowKey,
|
||||
stepKey,
|
||||
drawerWidth = 600,
|
||||
drawerTitle,
|
||||
}: StepSettingsDrawerProps): Promise<any> => {
|
||||
const t = getT(model);
|
||||
|
||||
if (!model) {
|
||||
message.error(t('Invalid model provided'));
|
||||
throw new Error(t('Invalid model provided'));
|
||||
}
|
||||
|
||||
// 获取流程和步骤信息
|
||||
const flow = model.getFlow(flowKey);
|
||||
const step = flow?.steps?.[stepKey];
|
||||
|
||||
if (!flow) {
|
||||
message.error(t('Flow with key {{flowKey}} not found', { flowKey }));
|
||||
throw new Error(t('Flow with key {{flowKey}} not found', { flowKey }));
|
||||
}
|
||||
|
||||
if (!step) {
|
||||
message.error(t('Step with key {{stepKey}} not found', { stepKey }));
|
||||
throw new Error(t('Step with key {{stepKey}} not found', { stepKey }));
|
||||
}
|
||||
|
||||
let title = step.title;
|
||||
|
||||
// 创建参数解析上下文
|
||||
const paramsContext = {
|
||||
model,
|
||||
globals: model.flowEngine?.context || {},
|
||||
app: model.flowEngine?.context?.app,
|
||||
};
|
||||
|
||||
const stepUiSchema = step.uiSchema || {};
|
||||
let actionDefaultParams = {};
|
||||
|
||||
// 如果step使用了action,也获取action的uiSchema
|
||||
let actionUiSchema = {};
|
||||
if (step.use) {
|
||||
const action = model.flowEngine?.getAction?.(step.use);
|
||||
if (action && action.uiSchema) {
|
||||
actionUiSchema = action.uiSchema;
|
||||
}
|
||||
actionDefaultParams = action.defaultParams || {};
|
||||
title = title || action.title;
|
||||
}
|
||||
|
||||
// 解析动态 uiSchema
|
||||
const resolvedActionUiSchema = await resolveUiSchema(actionUiSchema, paramsContext);
|
||||
const resolvedStepUiSchema = await resolveUiSchema(stepUiSchema, paramsContext);
|
||||
|
||||
// 合并uiSchema,确保step的uiSchema优先级更高
|
||||
const mergedUiSchema = { ...toJS(resolvedActionUiSchema) };
|
||||
Object.entries(toJS(resolvedStepUiSchema)).forEach(([fieldKey, schema]) => {
|
||||
if (mergedUiSchema[fieldKey]) {
|
||||
mergedUiSchema[fieldKey] = { ...mergedUiSchema[fieldKey], ...schema };
|
||||
} else {
|
||||
mergedUiSchema[fieldKey] = schema;
|
||||
}
|
||||
});
|
||||
|
||||
// 如果没有可配置的UI Schema,显示提示
|
||||
if (Object.keys(mergedUiSchema).length === 0) {
|
||||
message.info(t('This step has no configurable parameters'));
|
||||
return {};
|
||||
}
|
||||
|
||||
// 获取初始值
|
||||
const stepParams = model.getStepParams(flowKey, stepKey) || {};
|
||||
|
||||
const flowEngine = model.flowEngine;
|
||||
const scopes = {
|
||||
useStepSettingContext,
|
||||
...flowEngine.flowSettings?.scopes,
|
||||
};
|
||||
|
||||
// 解析 defaultParams
|
||||
const resolvedDefaultParams = await resolveDefaultParams(step.defaultParams, paramsContext);
|
||||
const resolveActionDefaultParams = await resolveDefaultParams(actionDefaultParams, paramsContext);
|
||||
const initialValues = { ...toJS(resolveActionDefaultParams), ...toJS(resolvedDefaultParams), ...toJS(stepParams) };
|
||||
|
||||
// 构建表单Schema
|
||||
const formSchema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
layout: {
|
||||
type: 'void',
|
||||
'x-component': 'FormLayout',
|
||||
'x-component-props': {
|
||||
layout: 'vertical',
|
||||
},
|
||||
properties: mergedUiSchema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// 动态导入Formily组件
|
||||
let Form, createForm;
|
||||
try {
|
||||
({ Form } = await import('@formily/antd-v5'));
|
||||
({ createForm } = await import('@formily/core'));
|
||||
} catch (error) {
|
||||
throw new Error(`${t('Failed to import Formily components')}: ${error.message}`);
|
||||
}
|
||||
|
||||
// 获取drawer API
|
||||
const drawer = model.flowEngine?.context?.drawer;
|
||||
if (!drawer) {
|
||||
throw new Error(t('Drawer API is not available, please ensure it is used within FlowEngineGlobalsContextProvider'));
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
// 用于跟踪Promise状态,避免重复调用resolve
|
||||
let isResolved = false;
|
||||
|
||||
// 创建表单实例
|
||||
const form = createForm({
|
||||
initialValues: compileUiSchema(scopes, initialValues),
|
||||
});
|
||||
|
||||
// 创建抽屉内容组件
|
||||
const DrawerContent: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
// 先获取表单当前值,然后验证
|
||||
const currentValues = form.values;
|
||||
await form.validate();
|
||||
|
||||
// 保存配置
|
||||
model.setStepParams(flowKey, stepKey, currentValues);
|
||||
await model.save();
|
||||
|
||||
message.success(t('Configuration saved'));
|
||||
isResolved = true;
|
||||
drawerRef.destroy();
|
||||
resolve(currentValues);
|
||||
} catch (error) {
|
||||
console.error(t('Error saving configuration'), ':', error);
|
||||
message.error(t('Error saving configuration, please check console'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
resolve(null);
|
||||
}
|
||||
drawerRef.destroy();
|
||||
};
|
||||
|
||||
// 创建上下文值
|
||||
const contextValue: StepSettingContextType = {
|
||||
model,
|
||||
globals: model.flowEngine?.context || {},
|
||||
app: model.flowEngine?.context?.app,
|
||||
step,
|
||||
flow,
|
||||
flowKey,
|
||||
stepKey,
|
||||
};
|
||||
|
||||
// 编译 formSchema 中的表达式
|
||||
const compiledFormSchema = compileUiSchema(scopes, formSchema);
|
||||
|
||||
return (
|
||||
<div style={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
|
||||
<div style={{ flex: 1, overflow: 'auto', padding: '16px' }}>
|
||||
<Form form={form} layout="vertical">
|
||||
<StepSettingContextProvider value={contextValue}>
|
||||
<SchemaField
|
||||
schema={compiledFormSchema}
|
||||
components={{
|
||||
...flowEngine.flowSettings?.components,
|
||||
}}
|
||||
scope={scopes}
|
||||
/>
|
||||
</StepSettingContextProvider>
|
||||
</Form>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
padding: '16px',
|
||||
borderTop: '1px solid #f0f0f0',
|
||||
display: 'flex',
|
||||
justifyContent: 'flex-end',
|
||||
gap: '8px',
|
||||
}}
|
||||
>
|
||||
<Space>
|
||||
<Button onClick={handleCancel}>{t('Cancel')}</Button>
|
||||
<Button type="primary" loading={loading} onClick={handleSubmit}>
|
||||
{t('OK')}
|
||||
</Button>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// 打开抽屉
|
||||
const drawerRef = drawer.open({
|
||||
title: drawerTitle || `${t(title)} - ${t('Configuration')}`,
|
||||
width: drawerWidth,
|
||||
content: <DrawerContent />,
|
||||
onClose: () => {
|
||||
// 只有在Promise还未被处理时才reject
|
||||
if (!isResolved) {
|
||||
isResolved = true;
|
||||
resolve(null);
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
const openStepSettingsDrawer = async (options: StepSettingsDrawerProps): Promise<any> => {
|
||||
return openStepSettingsDialog({ ...options, mode: 'drawer' });
|
||||
};
|
||||
|
||||
export { openStepSettingsDrawer };
|
||||
|
@ -129,7 +129,6 @@ const AddFieldButtonCore: React.FC<AddFieldButtonProps> = ({
|
||||
key: field.name,
|
||||
label: field.title,
|
||||
icon: fieldClass.meta?.icon,
|
||||
unique: true,
|
||||
createModelOptions: buildCreateModelOptions({
|
||||
defaultOptions,
|
||||
collectionField: field,
|
||||
|
@ -40,7 +40,6 @@ export interface SubModelItem {
|
||||
searchable?: boolean;
|
||||
searchPlaceholder?: string;
|
||||
keepDropdownOpen?: boolean;
|
||||
unique?: boolean;
|
||||
toggleDetector?: (ctx: AddSubModelContext) => boolean | Promise<boolean>;
|
||||
removeModelOptions?: {
|
||||
customRemove?: (ctx: AddSubModelContext, item: SubModelItem) => Promise<void>;
|
||||
@ -182,10 +181,10 @@ const createSwitchLabel = (originalLabel: string, isToggled: boolean) => (
|
||||
);
|
||||
|
||||
/**
|
||||
* 检查是否包含 unique 项
|
||||
* 检查是否包含可切换项
|
||||
*/
|
||||
const hasUniqueItems = (items: SubModelItem[]): boolean => {
|
||||
return items.some((item) => item.unique && item.toggleDetector && !item.children);
|
||||
const hasToggleItems = (items: SubModelItem[]): boolean => {
|
||||
return items.some((item) => item.toggleDetector && !item.children);
|
||||
};
|
||||
|
||||
/**
|
||||
@ -194,20 +193,20 @@ const hasUniqueItems = (items: SubModelItem[]): boolean => {
|
||||
const transformSubModelItems = async (items: SubModelItem[], context: AddSubModelContext): Promise<Item[]> => {
|
||||
if (items.length === 0) return [];
|
||||
|
||||
// 批量收集需要异步检测的 unique 项
|
||||
const uniqueItems: Array<{ item: SubModelItem; index: number }> = [];
|
||||
// 批量收集需要异步检测的可切换项
|
||||
const toggleItems: Array<{ item: SubModelItem; index: number }> = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const item = items[i];
|
||||
if (item.unique && item.toggleDetector && !item.children) {
|
||||
uniqueItems.push({ item, index: i });
|
||||
if (item.toggleDetector && !item.children) {
|
||||
toggleItems.push({ item, index: i });
|
||||
}
|
||||
}
|
||||
|
||||
// 批量执行 toggleDetector
|
||||
const toggleResults = await Promise.allSettled(uniqueItems.map(({ item }) => item.toggleDetector!(context)));
|
||||
const toggleResults = await Promise.allSettled(toggleItems.map(({ item }) => item.toggleDetector!(context)));
|
||||
|
||||
const toggleMap = new Map<number, boolean>();
|
||||
uniqueItems.forEach(({ index }, i) => {
|
||||
toggleItems.forEach(({ index }, i) => {
|
||||
const result = toggleResults[i];
|
||||
toggleMap.set(index, result.status === 'fulfilled' ? result.value : false);
|
||||
});
|
||||
@ -240,12 +239,11 @@ const transformSubModelItems = async (items: SubModelItem[], context: AddSubMode
|
||||
}
|
||||
|
||||
// 处理开关式菜单项
|
||||
if (item.unique && item.toggleDetector && !item.children) {
|
||||
if (item.toggleDetector && !item.children) {
|
||||
const isToggled = toggleMap.get(index) || false;
|
||||
const originalLabel = item.label || '';
|
||||
transformedItem.label = createSwitchLabel(originalLabel, isToggled);
|
||||
transformedItem.isToggled = isToggled;
|
||||
transformedItem.unique = true;
|
||||
}
|
||||
|
||||
return transformedItem;
|
||||
@ -265,8 +263,8 @@ const transformItems = (items: SubModelItemsType, context: AddSubModelContext):
|
||||
};
|
||||
}
|
||||
|
||||
const hasUnique = hasUniqueItems(items as SubModelItem[]);
|
||||
if (hasUnique) {
|
||||
const hasToggle = hasToggleItems(items as SubModelItem[]);
|
||||
if (hasToggle) {
|
||||
return () => transformSubModelItems(items as SubModelItem[], context);
|
||||
} else {
|
||||
let cachedResult: Item[] | null = null;
|
||||
@ -321,13 +319,23 @@ const createDefaultRemoveHandler = (config: {
|
||||
const createOpts = getCreateModelOptions(item);
|
||||
const targetModel = subModels.find((subModel) => {
|
||||
if (item.key && findFieldInStepParams(subModel, item.key)) return true;
|
||||
return (
|
||||
(subModel as any).constructor.name === createOpts?.use || (subModel as any).uid.includes(createOpts?.use)
|
||||
);
|
||||
|
||||
if (createOpts?.use) {
|
||||
try {
|
||||
const modelClass = config.model.flowEngine.getModelClass(createOpts.use);
|
||||
if (modelClass && subModel instanceof modelClass) {
|
||||
return true;
|
||||
}
|
||||
} catch (error) {
|
||||
// 如果获取模型类失败,继续使用 uid 匹配
|
||||
}
|
||||
return (subModel as any).uid.includes(createOpts.use);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (targetModel) {
|
||||
targetModel.remove();
|
||||
await targetModel.destroy();
|
||||
const index = subModels.indexOf(targetModel);
|
||||
if (index > -1) subModels.splice(index, 1);
|
||||
}
|
||||
@ -335,7 +343,7 @@ const createDefaultRemoveHandler = (config: {
|
||||
} else {
|
||||
const subModel = (model.subModels as any)[subModelKey] as FlowModel;
|
||||
if (subModel) {
|
||||
subModel.remove();
|
||||
await subModel.destroy();
|
||||
(model.subModels as any)[subModelKey] = undefined;
|
||||
}
|
||||
}
|
||||
@ -388,10 +396,9 @@ const AddSubModelButtonCore = function AddSubModelButton({
|
||||
const clickedItem = info.originalItem || info;
|
||||
const item = clickedItem.originalItem || (clickedItem as SubModelItem);
|
||||
const isToggled = clickedItem.isToggled;
|
||||
const isUnique = clickedItem.unique || item.unique;
|
||||
|
||||
// 处理 unique 菜单项的开关操作
|
||||
if (isUnique && item.toggleDetector && isToggled) {
|
||||
// 处理可切换菜单项的开关操作
|
||||
if (item.toggleDetector && isToggled) {
|
||||
try {
|
||||
if (item.removeModelOptions?.customRemove) {
|
||||
await item.removeModelOptions.customRemove(buildContext, item);
|
||||
|
@ -11,7 +11,6 @@ import { define, observable } from '@formily/reactive';
|
||||
import { openStepSettingsDialog } from './components/settings/wrappers/contextual/StepSettingsDialog';
|
||||
import { StepSettingsDialogProps, ToolbarItemConfig } from './types';
|
||||
import { DefaultSettingsIcon } from './components/settings/wrappers/contextual/DefaultSettingsIcon';
|
||||
import { DragHandler } from './components/dnd';
|
||||
|
||||
export class FlowSettings {
|
||||
public components: Record<string, any> = {};
|
||||
@ -38,18 +37,11 @@ export class FlowSettings {
|
||||
*/
|
||||
private addDefaultToolbarItems(): void {
|
||||
// 添加基础的配置菜单项目(原有的菜单功能)
|
||||
this.toolbarItems.push(
|
||||
{
|
||||
key: 'settings-menu',
|
||||
component: DefaultSettingsIcon,
|
||||
sort: 0, // 默认为0,作为第一个添加的项目
|
||||
},
|
||||
{
|
||||
key: 'drag-handler',
|
||||
component: DragHandler,
|
||||
sort: 1,
|
||||
},
|
||||
);
|
||||
this.toolbarItems.push({
|
||||
key: 'settings-menu',
|
||||
component: DefaultSettingsIcon,
|
||||
sort: 0, // 默认为0,作为第一个添加的项目
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -263,6 +263,7 @@ export interface StepSettingsDialogProps {
|
||||
stepKey: string;
|
||||
dialogWidth?: number | string;
|
||||
dialogTitle?: string;
|
||||
mode?: 'dialog' | 'drawer'; // 设置模式,默认为'dialog'
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,6 +34,13 @@ const DrawerComponent = forwardRef<unknown, DrawerComponentProps>(({ afterClose,
|
||||
setVisible(false);
|
||||
config.onClose?.(e);
|
||||
}}
|
||||
styles={{
|
||||
...config?.styles,
|
||||
footer: {
|
||||
textAlign: 'end',
|
||||
...config?.styles?.footer,
|
||||
},
|
||||
}}
|
||||
afterOpenChange={(open) => {
|
||||
if (!open) {
|
||||
afterClose?.();
|
||||
|
@ -15,6 +15,7 @@ export interface Str2momentOptions {
|
||||
picker?: 'year' | 'month' | 'week' | 'quarter';
|
||||
utcOffset?: number;
|
||||
utc?: boolean;
|
||||
dateOnly?: boolean;
|
||||
}
|
||||
|
||||
export type Str2momentValue = string | string[] | dayjs.Dayjs | dayjs.Dayjs[];
|
||||
@ -83,10 +84,13 @@ const toMoment = (val: any, options?: Str2momentOptions) => {
|
||||
return;
|
||||
}
|
||||
const offset = options.utcOffset;
|
||||
const { gmt, picker, utc = true } = options;
|
||||
const { gmt, picker, utc = true, dateOnly } = options;
|
||||
|
||||
if (dayjs(val).isValid()) {
|
||||
if (dateOnly) {
|
||||
return dayjs.utc(val, 'YYYY-MM-DD');
|
||||
}
|
||||
if (!utc) {
|
||||
console.log(888);
|
||||
return dayjs(val);
|
||||
}
|
||||
|
||||
|
@ -188,9 +188,11 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
|
||||
const field = getField?.(path);
|
||||
|
||||
if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') {
|
||||
if (value.type) {
|
||||
return getDayRangeByParams({ ...value, timezone: field?.timezone || timezone });
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
return dateValueWrapper(value, field?.timezone || timezone);
|
||||
}
|
||||
return value;
|
||||
|
@ -63,7 +63,7 @@ const useSubmitProps = () => {
|
||||
},
|
||||
});
|
||||
setLoading(false);
|
||||
message.success(t('License key saved successfully, please restart the server'));
|
||||
message.success(t('License key saved successfully, please re-run the plugin installation.'));
|
||||
} catch (e) {
|
||||
setLoading(false);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"License key saved successfully, please restart the server": "License key saved successfully, please restart the server",
|
||||
"License key saved successfully, please re-run the plugin installation.": "License key saved successfully, please re-run the plugin installation.",
|
||||
"License settings": "License settings",
|
||||
"Instance ID": "Instance ID",
|
||||
"License key": "License key",
|
||||
|
@ -1,5 +1,5 @@
|
||||
{
|
||||
"License key saved successfully, please restart the server": "授权密钥保存成功,请重启服务器",
|
||||
"License key saved successfully, please re-run the plugin installation.": "授权密钥保存成功,请重新执行插件安装操作",
|
||||
"License settings": "授权设置",
|
||||
"Instance ID": "实例 ID",
|
||||
"License key": "授权密钥",
|
||||
|
@ -30,13 +30,15 @@ import { WorkflowLink } from './WorkflowLink';
|
||||
import OpenDrawer from './components/OpenDrawer';
|
||||
import { workflowSchema } from './schemas/workflows';
|
||||
import { ExecutionStatusSelect, ExecutionStatusColumn } from './components/ExecutionStatus';
|
||||
import WorkflowPlugin, { ExecutionStatusOptions, RadioWithTooltip } from '.';
|
||||
import WorkflowPlugin from '.';
|
||||
import { RadioWithTooltip } from './components';
|
||||
import { useRefreshActionProps } from './hooks/useRefreshActionProps';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { TriggerOptionRender } from './components/TriggerOptionRender';
|
||||
import { CategoryTabs } from './WorkflowCategoryTabs';
|
||||
import { EnumerationField } from './components/EmunerationField';
|
||||
import { useWorkflowFilterActionProps } from './hooks/useWorkflowFilterActionProps';
|
||||
import { ExecutionStatusOptions } from './constants';
|
||||
|
||||
function SyncOptionSelect(props) {
|
||||
const field = useField<any>();
|
||||
|
@ -100,4 +100,5 @@ export default class extends Instruction {
|
||||
resultTitle: lang('Calculation result'),
|
||||
};
|
||||
}
|
||||
testable = true;
|
||||
}
|
||||
|
@ -194,4 +194,5 @@ export default class extends Instruction {
|
||||
</NodeDefaultView>
|
||||
);
|
||||
}
|
||||
testable = true;
|
||||
}
|
||||
|
@ -7,11 +7,11 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { CloseOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { CaretRightOutlined, CloseOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { createForm, Field } from '@formily/core';
|
||||
import { toJS } from '@formily/reactive';
|
||||
import { ISchema, observer, useField, useForm } from '@formily/react';
|
||||
import { Alert, App, Button, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
|
||||
import { Alert, App, Button, Collapse, Dropdown, Empty, Input, Space, Tag, Tooltip, message } from 'antd';
|
||||
import { cloneDeep, get, set } from 'lodash';
|
||||
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -26,7 +26,6 @@ import {
|
||||
cx,
|
||||
useAPIClient,
|
||||
useActionContext,
|
||||
useCancelAction,
|
||||
useCompile,
|
||||
usePlugin,
|
||||
useResourceActionContext,
|
||||
@ -330,6 +329,7 @@ const useRunAction = () => {
|
||||
async run() {
|
||||
const template = parse(node.config);
|
||||
const config = template(toJS(values.config));
|
||||
const logField = query('log').take() as Field;
|
||||
const resultField = query('result').take() as Field;
|
||||
resultField.setValue(null);
|
||||
resultField.setFeedback({});
|
||||
@ -352,6 +352,7 @@ const useRunAction = () => {
|
||||
messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')],
|
||||
});
|
||||
resultField.setValue(data.result);
|
||||
logField.setValue(data.log || '');
|
||||
} catch (err) {
|
||||
resultField.setFeedback({
|
||||
type: 'error',
|
||||
@ -359,7 +360,6 @@ const useRunAction = () => {
|
||||
});
|
||||
}
|
||||
field.data.loading = false;
|
||||
ctx.setFormValueChanged(false);
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -397,113 +397,163 @@ function TestFormFieldset({ value, onChange }) {
|
||||
);
|
||||
}
|
||||
|
||||
function LogCollapse({ value }) {
|
||||
return value ? (
|
||||
<Collapse
|
||||
ghost
|
||||
items={[
|
||||
{
|
||||
key: 'log',
|
||||
label: lang('Log'),
|
||||
children: (
|
||||
<Input.TextArea
|
||||
value={value}
|
||||
autoSize={{ minRows: 5, maxRows: 20 }}
|
||||
style={{ whiteSpace: 'pre', cursor: 'text', fontFamily: 'monospace', fontSize: '80%' }}
|
||||
disabled
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
className={css`
|
||||
.ant-collapse-item > .ant-collapse-header {
|
||||
padding: 0;
|
||||
}
|
||||
.ant-collapse-content > .ant-collapse-content-box {
|
||||
padding: 0;
|
||||
}
|
||||
`}
|
||||
/>
|
||||
) : null;
|
||||
}
|
||||
|
||||
function useCancelAction() {
|
||||
const form = useForm();
|
||||
const ctx = useActionContext();
|
||||
return {
|
||||
async run() {
|
||||
const resultField = form.query('result').take() as Field;
|
||||
resultField.setFeedback();
|
||||
form.setValues({ result: null, log: null });
|
||||
form.clearFormGraph('*');
|
||||
form.reset();
|
||||
ctx.setVisible(false);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function TestButton() {
|
||||
const node = useNodeContext();
|
||||
const { values } = useForm();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const template = parse(values);
|
||||
const keys = template.parameters.map((item) => item.key);
|
||||
const form = useMemo(() => createForm(), []);
|
||||
|
||||
const onOpen = useCallback(() => {
|
||||
setVisible(true);
|
||||
}, []);
|
||||
const setModalVisible = useCallback(
|
||||
(v: boolean) => {
|
||||
if (v) {
|
||||
setVisible(true);
|
||||
return;
|
||||
}
|
||||
const resultField = form.query('result').take() as Field;
|
||||
resultField?.setFeedback();
|
||||
form.setValues({ result: null, log: null });
|
||||
form.clearFormGraph('*');
|
||||
form.reset();
|
||||
setVisible(false);
|
||||
},
|
||||
[form],
|
||||
);
|
||||
|
||||
return (
|
||||
<NodeContext.Provider value={{ ...node, config: values }}>
|
||||
<VariableKeysContext.Provider value={keys}>
|
||||
<SchemaComponent
|
||||
components={{
|
||||
Alert,
|
||||
TestFormFieldset,
|
||||
}}
|
||||
scope={{
|
||||
useCancelAction,
|
||||
useRunAction,
|
||||
}}
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'testButton',
|
||||
title: '{{t("Test run")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
icon: 'CaretRightOutlined',
|
||||
// openSize: 'small',
|
||||
},
|
||||
properties: {
|
||||
modal: {
|
||||
type: 'void',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-decorator-props': {
|
||||
form,
|
||||
<ActionContextProvider value={{ visible, setVisible: setModalVisible }}>
|
||||
<Button icon={<CaretRightOutlined />} onClick={onOpen}>
|
||||
{lang('Test run')}
|
||||
</Button>
|
||||
<SchemaComponent
|
||||
components={{
|
||||
Alert,
|
||||
TestFormFieldset,
|
||||
LogCollapse,
|
||||
}}
|
||||
scope={{
|
||||
useCancelAction,
|
||||
useRunAction,
|
||||
}}
|
||||
schema={{
|
||||
type: 'void',
|
||||
name: 'modal',
|
||||
'x-decorator': 'FormV2',
|
||||
'x-decorator-props': {
|
||||
form,
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
title: `{{t("Test run", { ns: "workflow" })}}`,
|
||||
properties: {
|
||||
alert: {
|
||||
type: 'void',
|
||||
'x-component': 'Alert',
|
||||
'x-component-props': {
|
||||
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
|
||||
type: 'warning',
|
||||
showIcon: true,
|
||||
className: css`
|
||||
margin-bottom: 1em;
|
||||
`,
|
||||
},
|
||||
},
|
||||
'x-component': 'Action.Modal',
|
||||
title: `{{t("Test run", { ns: "workflow" })}}`,
|
||||
properties: {
|
||||
alert: {
|
||||
type: 'void',
|
||||
'x-component': 'Alert',
|
||||
'x-component-props': {
|
||||
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`,
|
||||
type: 'warning',
|
||||
showIcon: true,
|
||||
className: css`
|
||||
margin-bottom: 1em;
|
||||
`,
|
||||
},
|
||||
},
|
||||
config: {
|
||||
type: 'object',
|
||||
title: '{{t("Replace variables", { ns: "workflow" })}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'TestFormFieldset',
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
title: '{{t("Run")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useRunAction }}',
|
||||
},
|
||||
config: {
|
||||
type: 'object',
|
||||
title: '{{t("Replace variables", { ns: "workflow" })}}',
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'TestFormFieldset',
|
||||
},
|
||||
actions: {
|
||||
type: 'void',
|
||||
'x-component': 'ActionBar',
|
||||
properties: {
|
||||
submit: {
|
||||
type: 'void',
|
||||
title: '{{t("Run")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
type: 'primary',
|
||||
useAction: '{{ useRunAction }}',
|
||||
},
|
||||
},
|
||||
},
|
||||
result: {
|
||||
type: 'string',
|
||||
title: `{{t("Result", { ns: "workflow" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.JSON',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 5,
|
||||
maxRows: 20,
|
||||
},
|
||||
style: {
|
||||
whiteSpace: 'pre',
|
||||
cursor: 'text',
|
||||
},
|
||||
},
|
||||
result: {
|
||||
type: 'string',
|
||||
title: `{{t("Result", { ns: "workflow" })}}`,
|
||||
'x-decorator': 'FormItem',
|
||||
'x-component': 'Input.JSON',
|
||||
'x-component-props': {
|
||||
autoSize: {
|
||||
minRows: 5,
|
||||
maxRows: 20,
|
||||
},
|
||||
'x-pattern': 'disabled',
|
||||
},
|
||||
footer: {
|
||||
type: 'void',
|
||||
'x-component': 'Action.Modal.Footer',
|
||||
properties: {
|
||||
cancel: {
|
||||
type: 'void',
|
||||
title: '{{t("Close")}}',
|
||||
'x-component': 'Action',
|
||||
'x-component-props': {
|
||||
useAction: '{{ useCancelAction }}',
|
||||
},
|
||||
},
|
||||
style: {
|
||||
whiteSpace: 'pre',
|
||||
cursor: 'text',
|
||||
},
|
||||
},
|
||||
'x-pattern': 'disabled',
|
||||
},
|
||||
log: {
|
||||
type: 'string',
|
||||
'x-component': 'LogCollapse',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
/>
|
||||
}}
|
||||
/>
|
||||
</ActionContextProvider>
|
||||
</VariableKeysContext.Provider>
|
||||
</NodeContext.Provider>
|
||||
);
|
||||
|
@ -39,6 +39,22 @@ export class CalculationInstruction extends Instruction {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async test({ engine = 'math.js', expression = '' }) {
|
||||
const evaluator = <Evaluator | undefined>evaluators.get(engine);
|
||||
try {
|
||||
const result = evaluator && expression ? evaluator(expression) : null;
|
||||
return {
|
||||
result,
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
result: e.toString(),
|
||||
status: JOB_STATUS.ERROR,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CalculationInstruction;
|
||||
|
@ -7,7 +7,7 @@
|
||||
* For more information, please refer to: https://www.nocobase.com/agreement.
|
||||
*/
|
||||
|
||||
import { evaluators } from '@nocobase/evaluators';
|
||||
import { Evaluator, evaluators } from '@nocobase/evaluators';
|
||||
import { Instruction } from '.';
|
||||
import type Processor from '../Processor';
|
||||
import { JOB_STATUS } from '../constants';
|
||||
@ -80,6 +80,22 @@ export class ConditionInstruction extends Instruction {
|
||||
// pass control to upper scope by ending current scope
|
||||
return processor.exit(branchJob.status);
|
||||
}
|
||||
|
||||
async test({ engine, calculation, expression = '' }) {
|
||||
const evaluator = <Evaluator | undefined>evaluators.get(engine);
|
||||
try {
|
||||
const result = evaluator ? evaluator(expression) : logicCalculate(calculation);
|
||||
return {
|
||||
result,
|
||||
status: JOB_STATUS.RESOLVED,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
result: e.toString(),
|
||||
status: JOB_STATUS.ERROR,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ConditionInstruction;
|
||||
|
Loading…
x
Reference in New Issue
Block a user