mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-07-01 18:52:20 +08:00
Compare commits
11 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
3387366f1e | ||
|
88cb9884f6 | ||
|
22cd39552f | ||
|
026baab241 | ||
|
d1ff280d9f | ||
|
88591454ec | ||
|
3c555e9fc0 | ||
|
6afcbffdec | ||
|
8d91e1fded | ||
|
940002a876 | ||
|
a4eb026ea4 |
@ -48,6 +48,14 @@ DB_PASSWORD=nocobase
|
||||
# DB_LOGGING=on
|
||||
# DB_UNDERSCORED=false
|
||||
|
||||
# @see https://sequelize.org/api/v6/class/src/sequelize.js~sequelize#instance-constructor-constructor
|
||||
# DB_POOL_MAX=5
|
||||
# DB_POOL_MIN=0
|
||||
# DB_POOL_IDLE=10000
|
||||
# DB_POOL_ACQUIRE=60000
|
||||
# DB_POOL_EVICT=1000
|
||||
# DB_POOL_MAX_USES=0
|
||||
|
||||
# sqlite only
|
||||
# DB_STORAGE=storage/db/nocobase.sqlite
|
||||
|
||||
|
21
CHANGELOG.md
21
CHANGELOG.md
@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [v1.7.18](https://github.com/nocobase/nocobase/compare/v1.7.17...v1.7.18) - 2025-06-26
|
||||
|
||||
### 🚀 Improvements
|
||||
|
||||
- **[Workflow]** Optimize mobile style ([#7040](https://github.com/nocobase/nocobase/pull/7040)) by @mytharcher
|
||||
|
||||
- **[Public forms]** Optimize the performance of date components in public forms ([#7117](https://github.com/nocobase/nocobase/pull/7117)) by @zhangzhonghe
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **[Workflow]** Fix params of loading record in tasks ([#7123](https://github.com/nocobase/nocobase/pull/7123)) by @mytharcher
|
||||
|
||||
- **[WEB client]** Fix issue where blocks under pages were not displayed after setting role menu permissions ([#7112](https://github.com/nocobase/nocobase/pull/7112)) by @aaaaaajie
|
||||
|
||||
- **[Workflow: Approval]**
|
||||
- Fix applicant variable name in trigger by @mytharcher
|
||||
|
||||
- Fix mobile styles by @mytharcher
|
||||
|
||||
- Fix error thrown when approval related collection deleted by @mytharcher
|
||||
|
||||
## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
@ -5,6 +5,27 @@
|
||||
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
|
||||
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。
|
||||
|
||||
## [v1.7.18](https://github.com/nocobase/nocobase/compare/v1.7.17...v1.7.18) - 2025-06-26
|
||||
|
||||
### 🚀 优化
|
||||
|
||||
- **[工作流]** 优化移动端样式 ([#7040](https://github.com/nocobase/nocobase/pull/7040)) by @mytharcher
|
||||
|
||||
- **[公开表单]** 优化公开表单中日期组件的性能 ([#7117](https://github.com/nocobase/nocobase/pull/7117)) by @zhangzhonghe
|
||||
|
||||
### 🐛 修复
|
||||
|
||||
- **[工作流]** 修复待办中心加载记录的参数 ([#7123](https://github.com/nocobase/nocobase/pull/7123)) by @mytharcher
|
||||
|
||||
- **[WEB 客户端]** 修复设置角色菜单权限后页面下区块不显示的问题 ([#7112](https://github.com/nocobase/nocobase/pull/7112)) by @aaaaaajie
|
||||
|
||||
- **[工作流:审批]**
|
||||
- 修复审批触发器中申请人变量名的问题 by @mytharcher
|
||||
|
||||
- 修复移动端样式 by @mytharcher
|
||||
|
||||
- 修复审批关联表被删除后的报错 by @mytharcher
|
||||
|
||||
## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
|
||||
|
||||
### 🐛 修复
|
||||
|
@ -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) => (
|
||||
|
@ -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();
|
||||
|
@ -19,6 +19,7 @@ import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive'
|
||||
import { useSchemaComponentContext } from '../../hooks';
|
||||
import { AssociationFieldContext } from './context';
|
||||
import { FormItem, useSchemaOptionsContext } from '../../../schema-component';
|
||||
import { useCollectionRecord } from '../../../data-source';
|
||||
|
||||
export const AssociationFieldProvider = observer(
|
||||
(props) => {
|
||||
@ -28,6 +29,7 @@ export const AssociationFieldProvider = observer(
|
||||
const api = useAPIClient();
|
||||
const option = useSchemaOptionsContext();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const record = useCollectionRecord();
|
||||
|
||||
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
|
||||
useSchemaComponentContext();
|
||||
@ -71,7 +73,9 @@ export const AssociationFieldProvider = observer(
|
||||
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
if (record && !record.isNew) {
|
||||
return Promise.reject(null);
|
||||
}
|
||||
return api.request({
|
||||
resource: collectionField.target,
|
||||
action: Array.isArray(ids) ? 'list' : 'get',
|
||||
|
@ -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,
|
||||
|
@ -64,7 +64,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
|
||||
const localVariables = useLocalVariables();
|
||||
const collection = useCollection_deprecated();
|
||||
const record = useRecord();
|
||||
const { form } = useFormBlockContext();
|
||||
const { form, type } = useFormBlockContext();
|
||||
const { getFields } = useCollectionFilterOptionsV2(collection);
|
||||
const { isInSubForm, isInSubTable } = useFlag() || {};
|
||||
|
||||
@ -219,7 +219,6 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
|
||||
targetField,
|
||||
variables,
|
||||
]);
|
||||
|
||||
const handleSubmit: (values: any) => void = useCallback(
|
||||
(v) => {
|
||||
const schema: ISchema = {
|
||||
@ -227,7 +226,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
|
||||
};
|
||||
fieldSchema.default = v.default ?? null;
|
||||
if (!isVariable(v.default)) {
|
||||
field.setInitialValue?.(v.default);
|
||||
(record.__isNewRecord__ || type === 'create') && field.setInitialValue?.(v.default);
|
||||
}
|
||||
schema.default = v.default ?? null;
|
||||
dn.emit('patch', {
|
||||
|
@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
|
||||
import { useBlockContext } from '../../../block-provider';
|
||||
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
|
||||
import { CollectionFieldOptions_deprecated } from '../../../collection-manager';
|
||||
import { useDataBlockRequestData, useDataSource } from '../../../data-source';
|
||||
import { useCollection, useDataSource } from '../../../data-source';
|
||||
import { useFlag } from '../../../flag-provider/hooks/useFlag';
|
||||
import { useBaseVariable } from './useBaseVariable';
|
||||
|
||||
@ -100,6 +100,7 @@ export const useCurrentFormVariable = ({
|
||||
const { currentFormCtx, shouldDisplayCurrentForm } = useCurrentFormContext({ form: _form });
|
||||
const { t } = useTranslation();
|
||||
const { collectionName } = useFormBlockContext();
|
||||
const collection = useCollection();
|
||||
const dataSource = useDataSource();
|
||||
const currentFormSettings = useBaseVariable({
|
||||
collectionField,
|
||||
@ -108,7 +109,7 @@ export const useCurrentFormVariable = ({
|
||||
maxDepth: 4,
|
||||
name: '$nForm',
|
||||
title: t('Current form'),
|
||||
collectionName: collectionName,
|
||||
collectionName: collectionName || collection?.name,
|
||||
noDisabled,
|
||||
dataSource: dataSource?.key,
|
||||
returnFields: (fields, option) => {
|
||||
|
40
packages/core/database/src/__tests__/helpers.test.ts
Normal file
40
packages/core/database/src/__tests__/helpers.test.ts
Normal file
@ -0,0 +1,40 @@
|
||||
/**
|
||||
* 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 { parseDatabaseOptionsFromEnv } from '@nocobase/database';
|
||||
|
||||
describe('database helpers', () => {
|
||||
describe('parseDatabaseOptionsFromEnv()', () => {
|
||||
it('undefined pool options', async () => {
|
||||
const options1 = await parseDatabaseOptionsFromEnv();
|
||||
expect(options1).toMatchObject({
|
||||
pool: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('custom pool options', async () => {
|
||||
process.env.DB_POOL_MAX = '10';
|
||||
process.env.DB_POOL_MIN = '1';
|
||||
process.env.DB_POOL_IDLE = '5000';
|
||||
process.env.DB_POOL_ACQUIRE = '30000';
|
||||
process.env.DB_POOL_EVICT = '2000';
|
||||
process.env.DB_POOL_MAX_USES = '0'; // Set to 0 to test default behavior
|
||||
|
||||
const options2 = await parseDatabaseOptionsFromEnv();
|
||||
expect(options2.pool).toMatchObject({
|
||||
max: 10,
|
||||
min: 1,
|
||||
idle: 5000,
|
||||
acquire: 30000,
|
||||
evict: 2000,
|
||||
maxUses: Number.POSITIVE_INFINITY, // Default value
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -15,6 +15,7 @@ import { MysqlDialect } from './dialects/mysql-dialect';
|
||||
import { SqliteDialect } from './dialects/sqlite-dialect';
|
||||
import { MariadbDialect } from './dialects/mariadb-dialect';
|
||||
import { PostgresDialect } from './dialects/postgres-dialect';
|
||||
import { PoolOptions } from 'sequelize';
|
||||
|
||||
function getEnvValue(key, defaultValue?) {
|
||||
return process.env[key] || defaultValue;
|
||||
@ -73,6 +74,29 @@ function extractSSLOptionsFromEnv() {
|
||||
});
|
||||
}
|
||||
|
||||
function getPoolOptions(): PoolOptions {
|
||||
const options: PoolOptions = {};
|
||||
if (process.env.DB_POOL_MAX) {
|
||||
options.max = Number.parseInt(process.env.DB_POOL_MAX, 10);
|
||||
}
|
||||
if (process.env.DB_POOL_MIN) {
|
||||
options.min = Number.parseInt(process.env.DB_POOL_MIN, 10);
|
||||
}
|
||||
if (process.env.DB_POOL_IDLE) {
|
||||
options.idle = Number.parseInt(process.env.DB_POOL_IDLE, 10);
|
||||
}
|
||||
if (process.env.DB_POOL_ACQUIRE) {
|
||||
options.acquire = Number.parseInt(process.env.DB_POOL_ACQUIRE, 10);
|
||||
}
|
||||
if (process.env.DB_POOL_EVICT) {
|
||||
options.evict = Number.parseInt(process.env.DB_POOL_EVICT, 10);
|
||||
}
|
||||
if (process.env.DB_POOL_MAX_USES) {
|
||||
options.maxUses = Number.parseInt(process.env.DB_POOL_MAX_USES, 10) || Number.POSITIVE_INFINITY;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export async function parseDatabaseOptionsFromEnv(): Promise<IDatabaseOptions> {
|
||||
const databaseOptions: IDatabaseOptions = {
|
||||
logging: process.env.DB_LOGGING == 'on' ? customLogger : false,
|
||||
@ -87,6 +111,7 @@ export async function parseDatabaseOptionsFromEnv(): Promise<IDatabaseOptions> {
|
||||
tablePrefix: process.env.DB_TABLE_PREFIX,
|
||||
schema: process.env.DB_SCHEMA,
|
||||
underscored: process.env.DB_UNDERSCORED === 'true',
|
||||
pool: getPoolOptions(),
|
||||
};
|
||||
|
||||
const sslOptions = await extractSSLOptionsFromEnv();
|
||||
|
@ -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 }) }],
|
||||
};
|
||||
|
@ -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,14 @@ 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) {
|
||||
return dayjs(val);
|
||||
return dayjs.utc(val);
|
||||
}
|
||||
|
||||
if (dayjs.isDayjs(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;
|
||||
|
@ -17,6 +17,8 @@ import {
|
||||
useRequest,
|
||||
useResourceActionContext,
|
||||
useResourceContext,
|
||||
useFilterFieldProps,
|
||||
useFilterFieldOptions,
|
||||
} from '@nocobase/client';
|
||||
import React from 'react';
|
||||
import { i18nText } from '../../utils';
|
||||
@ -212,6 +214,16 @@ export const tableActionColumnSchema: ISchema = {
|
||||
},
|
||||
};
|
||||
|
||||
export const useFilterActionProps = () => {
|
||||
const { collection } = useResourceContext();
|
||||
const options = useFilterFieldOptions(collection.fields);
|
||||
const service = useResourceActionContext();
|
||||
return useFilterFieldProps({
|
||||
options: options,
|
||||
params: service.state?.params?.[0] || service.params,
|
||||
service,
|
||||
});
|
||||
};
|
||||
export const schema: ISchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
@ -245,6 +257,18 @@ export const schema: ISchema = {
|
||||
},
|
||||
},
|
||||
properties: {
|
||||
filter: {
|
||||
'x-component': 'Filter.Action',
|
||||
'x-use-component-props': useFilterActionProps,
|
||||
default: {
|
||||
$and: [{ displayName: { $includes: '' } }, { name: { $includes: '' } }],
|
||||
},
|
||||
title: "{{t('Filter')}}",
|
||||
'x-component-props': {
|
||||
icon: 'FilterOutlined',
|
||||
},
|
||||
'x-align': 'left',
|
||||
},
|
||||
delete: {
|
||||
type: 'void',
|
||||
title: '{{ t("Delete") }}',
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -86,6 +86,7 @@ export const executionSchema = {
|
||||
appends: ['workflow.id', 'workflow.title'],
|
||||
pageSize: 20,
|
||||
sort: ['-createdAt'],
|
||||
except: ['context', 'output'],
|
||||
filter: {},
|
||||
},
|
||||
},
|
||||
|
@ -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