Compare commits

...

11 Commits

Author SHA1 Message Date
Junyi
3387366f1e
fix(plugin-workflow): except json field for load performance (#7138) 2025-07-01 10:59:30 +08:00
Junyi
88cb9884f6
refactor(database): add pool options from env (#7133)
* refactor(database): add pool options from env

* Update packages/core/database/src/helpers.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* fix(database): only inject env configured

* fix(server): simplify database options

* revert(server): revert create database api

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-01 10:40:15 +08:00
Zeke Zhang
22cd39552f
fix: update collection name handling in useCurrentFormVariable (#7125) 2025-07-01 10:01:50 +08:00
Katherine
026baab241
fix: filtering error on DateOnly or Datetime (without time zone) using Exact day variable (#7113)
* fix: filtering error on DateOnly or Datetime (without time zone) fields using Exact day variable

* fix: bug

* fix: bug

* fix: bug
2025-06-30 22:41:20 +11:00
ajie
d1ff280d9f
fix: setting field displayName in connected view does not take effect (#7130) 2025-06-30 17:34:17 +08:00
Junyi
88591454ec
fix(plugin-workflow): fix cycling import (#7134) 2025-06-30 15:25:57 +08:00
Junyi
3c555e9fc0
refactor(plugin-workflow): add log for node testing (#7129) 2025-06-30 11:45:39 +08:00
Junyi
6afcbffdec
fix(client): fault tolerance for settings based on x-acl-action (#7128) 2025-06-29 09:34:21 +08:00
Katherine
8d91e1fded
fix: association field default value overrides existing data in sub-table (#7120)
* fix: association field default value overrides existing data in sub-table

* fix: bug

* fix: bug
2025-06-27 14:15:56 +08:00
Katherine
940002a876
refactor: add filter support to multi-app management (#7124) 2025-06-27 14:15:34 +08:00
nocobase[bot]
a4eb026ea4 docs: update changelogs 2025-06-26 14:00:06 +00:00
23 changed files with 355 additions and 113 deletions

View File

@ -48,6 +48,14 @@ DB_PASSWORD=nocobase
# DB_LOGGING=on # DB_LOGGING=on
# DB_UNDERSCORED=false # 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 # sqlite only
# DB_STORAGE=storage/db/nocobase.sqlite # DB_STORAGE=storage/db/nocobase.sqlite

View File

@ -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/) 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). 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 ## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
### 🐛 Bug Fixes ### 🐛 Bug Fixes

View File

@ -5,6 +5,27 @@
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 并且本项目遵循 [语义化版本](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 ## [v1.7.17](https://github.com/nocobase/nocobase/compare/v1.7.16...v1.7.17) - 2025-06-23
### 🐛 修复 ### 🐛 修复

View File

@ -24,6 +24,7 @@ export class DateFieldInterface extends CollectionFieldInterface {
'x-component': 'DatePicker', 'x-component': 'DatePicker',
'x-component-props': { 'x-component-props': {
dateOnly: true, dateOnly: true,
showTime: false,
}, },
}, },
}; };

View File

@ -9,7 +9,7 @@
import { useField, useForm } from '@formily/react'; import { useField, useForm } from '@formily/react';
import { Cascader, Input, Select, Spin, Table, Tag } from 'antd'; 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 React, { useContext, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ResourceActionContext, useCompile } from '../../../'; import { ResourceActionContext, useCompile } from '../../../';
@ -120,9 +120,10 @@ const PreviewCom = (props) => {
}, [databaseView]); }, [databaseView]);
const handleFieldChange = (record, index) => { const handleFieldChange = (record, index) => {
dataSource.splice(index, 1, record); const newDataSource = _.cloneDeep(dataSource);
setDataSource(dataSource); newDataSource[index] = record;
field.value = dataSource.map((v) => { setDataSource(newDataSource);
field.value = newDataSource.map((v) => {
const source = typeof v.source === 'string' ? v.source : v.source?.filter?.(Boolean)?.join('.'); const source = typeof v.source === 'string' ? v.source : v.source?.filter?.(Boolean)?.join('.');
return { return {
...v, ...v,
@ -198,8 +199,7 @@ const PreviewCom = (props) => {
style={{ width: '100%' }} style={{ width: '100%' }}
popupMatchSelectWidth={false} popupMatchSelectWidth={false}
onChange={(value) => { onChange={(value) => {
const interfaceConfig = getInterface(value); handleFieldChange({ ...item, interface: value }, index);
handleFieldChange({ ...item, interface: value, uiSchema: interfaceConfig?.default?.uiSchema }, index);
}} }}
> >
{data.map((group) => ( {data.map((group) => (

View File

@ -66,7 +66,7 @@ export const createFormBlockSettings = new SchemaSettings({
useVisible() { useVisible() {
const { action } = useFormBlockContext(); const { action } = useFormBlockContext();
const schema = useFieldSchema(); const schema = useFieldSchema();
return !action && schema?.['x-acl-action'].includes('create'); return !action && schema?.['x-acl-action']?.includes('create');
}, },
useComponentProps() { useComponentProps() {
const { name } = useCollection_deprecated(); const { name } = useCollection_deprecated();

View File

@ -19,6 +19,7 @@ import { useKeepAlive } from '../../../route-switch/antd/admin-layout/KeepAlive'
import { useSchemaComponentContext } from '../../hooks'; import { useSchemaComponentContext } from '../../hooks';
import { AssociationFieldContext } from './context'; import { AssociationFieldContext } from './context';
import { FormItem, useSchemaOptionsContext } from '../../../schema-component'; import { FormItem, useSchemaOptionsContext } from '../../../schema-component';
import { useCollectionRecord } from '../../../data-source';
export const AssociationFieldProvider = observer( export const AssociationFieldProvider = observer(
(props) => { (props) => {
@ -28,6 +29,7 @@ export const AssociationFieldProvider = observer(
const api = useAPIClient(); const api = useAPIClient();
const option = useSchemaOptionsContext(); const option = useSchemaOptionsContext();
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const record = useCollectionRecord();
// 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染 // 这里有点奇怪,在 Table 切换显示的组件时,这个组件并不会触发重新渲染,所以增加这个 Hooks 让其重新渲染
useSchemaComponentContext(); useSchemaComponentContext();
@ -71,7 +73,9 @@ export const AssociationFieldProvider = observer(
if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) { if (_.isUndefined(ids) || _.isNil(ids) || _.isNaN(ids)) {
return Promise.reject(null); return Promise.reject(null);
} }
if (record && !record.isNew) {
return Promise.reject(null);
}
return api.request({ return api.request({
resource: collectionField.target, resource: collectionField.target,
action: Array.isArray(ids) ? 'list' : 'get', action: Array.isArray(ids) ? 'list' : 'get',

View File

@ -10,6 +10,7 @@
import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client'; import { getDefaultFormat, str2moment, toGmt, toLocal, getPickerFormat } from '@nocobase/utils/client';
import type { Dayjs } from 'dayjs'; import type { Dayjs } from 'dayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { dayjsable, formatDayjsValue } from '@formily/antd-v5/esm/__builtins__';
const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => { const toStringByPicker = (value, picker = 'date', timezone: 'gmt' | 'local') => {
if (!dayjs.isDayjs(value)) return value; if (!dayjs.isDayjs(value)) return value;
@ -89,7 +90,7 @@ export const handleDateChangeOnForm = (value, dateOnly, utc, picker, showTime, g
return value; return value;
} }
if (dateOnly) { if (dateOnly) {
return dayjs(value).startOf(picker).format('YYYY-MM-DD'); return formatDayjsValue(value, 'YYYY-MM-DD');
} }
if (utc) { if (utc) {
if (gmt) { if (gmt) {
@ -114,6 +115,7 @@ export const mapDatePicker = function () {
const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props; const { dateOnly, showTime, picker = 'date', utc, gmt, underFilter } = props;
const format = getDefaultFormat(props); const format = getDefaultFormat(props);
const onChange = props.onChange; const onChange = props.onChange;
return { return {
...props, ...props,
inputReadOnly: isMobileMedia, inputReadOnly: isMobileMedia,

View File

@ -64,7 +64,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
const localVariables = useLocalVariables(); const localVariables = useLocalVariables();
const collection = useCollection_deprecated(); const collection = useCollection_deprecated();
const record = useRecord(); const record = useRecord();
const { form } = useFormBlockContext(); const { form, type } = useFormBlockContext();
const { getFields } = useCollectionFilterOptionsV2(collection); const { getFields } = useCollectionFilterOptionsV2(collection);
const { isInSubForm, isInSubTable } = useFlag() || {}; const { isInSubForm, isInSubTable } = useFlag() || {};
@ -219,7 +219,6 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
targetField, targetField,
variables, variables,
]); ]);
const handleSubmit: (values: any) => void = useCallback( const handleSubmit: (values: any) => void = useCallback(
(v) => { (v) => {
const schema: ISchema = { const schema: ISchema = {
@ -227,7 +226,7 @@ export const SchemaSettingsDefaultValue = function DefaultValueConfigure(props:
}; };
fieldSchema.default = v.default ?? null; fieldSchema.default = v.default ?? null;
if (!isVariable(v.default)) { if (!isVariable(v.default)) {
field.setInitialValue?.(v.default); (record.__isNewRecord__ || type === 'create') && field.setInitialValue?.(v.default);
} }
schema.default = v.default ?? null; schema.default = v.default ?? null;
dn.emit('patch', { dn.emit('patch', {

View File

@ -13,7 +13,7 @@ import { useTranslation } from 'react-i18next';
import { useBlockContext } from '../../../block-provider'; import { useBlockContext } from '../../../block-provider';
import { useFormBlockContext } from '../../../block-provider/FormBlockProvider'; import { useFormBlockContext } from '../../../block-provider/FormBlockProvider';
import { CollectionFieldOptions_deprecated } from '../../../collection-manager'; 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 { useFlag } from '../../../flag-provider/hooks/useFlag';
import { useBaseVariable } from './useBaseVariable'; import { useBaseVariable } from './useBaseVariable';
@ -100,6 +100,7 @@ export const useCurrentFormVariable = ({
const { currentFormCtx, shouldDisplayCurrentForm } = useCurrentFormContext({ form: _form }); const { currentFormCtx, shouldDisplayCurrentForm } = useCurrentFormContext({ form: _form });
const { t } = useTranslation(); const { t } = useTranslation();
const { collectionName } = useFormBlockContext(); const { collectionName } = useFormBlockContext();
const collection = useCollection();
const dataSource = useDataSource(); const dataSource = useDataSource();
const currentFormSettings = useBaseVariable({ const currentFormSettings = useBaseVariable({
collectionField, collectionField,
@ -108,7 +109,7 @@ export const useCurrentFormVariable = ({
maxDepth: 4, maxDepth: 4,
name: '$nForm', name: '$nForm',
title: t('Current form'), title: t('Current form'),
collectionName: collectionName, collectionName: collectionName || collection?.name,
noDisabled, noDisabled,
dataSource: dataSource?.key, dataSource: dataSource?.key,
returnFields: (fields, option) => { returnFields: (fields, option) => {

View 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
});
});
});
});

View File

@ -15,6 +15,7 @@ import { MysqlDialect } from './dialects/mysql-dialect';
import { SqliteDialect } from './dialects/sqlite-dialect'; import { SqliteDialect } from './dialects/sqlite-dialect';
import { MariadbDialect } from './dialects/mariadb-dialect'; import { MariadbDialect } from './dialects/mariadb-dialect';
import { PostgresDialect } from './dialects/postgres-dialect'; import { PostgresDialect } from './dialects/postgres-dialect';
import { PoolOptions } from 'sequelize';
function getEnvValue(key, defaultValue?) { function getEnvValue(key, defaultValue?) {
return process.env[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> { export async function parseDatabaseOptionsFromEnv(): Promise<IDatabaseOptions> {
const databaseOptions: IDatabaseOptions = { const databaseOptions: IDatabaseOptions = {
logging: process.env.DB_LOGGING == 'on' ? customLogger : false, logging: process.env.DB_LOGGING == 'on' ? customLogger : false,
@ -87,6 +111,7 @@ export async function parseDatabaseOptionsFromEnv(): Promise<IDatabaseOptions> {
tablePrefix: process.env.DB_TABLE_PREFIX, tablePrefix: process.env.DB_TABLE_PREFIX,
schema: process.env.DB_SCHEMA, schema: process.env.DB_SCHEMA,
underscored: process.env.DB_UNDERSCORED === 'true', underscored: process.env.DB_UNDERSCORED === 'true',
pool: getPoolOptions(),
}; };
const sslOptions = await extractSSLOptionsFromEnv(); const sslOptions = await extractSSLOptionsFromEnv();

View File

@ -33,7 +33,7 @@ const toDate = (date, options: any = {}) => {
} }
if (field.constructor.name === 'DateOnlyField') { 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 = { const eventObj = {
@ -69,7 +69,6 @@ export default {
const r = parseDate(value, { const r = parseDate(value, {
timezone: parseDateTimezone(ctx), timezone: parseDateTimezone(ctx),
}); });
if (typeof r === 'string') { if (typeof r === 'string') {
return { return {
[Op.eq]: toDate(r, { ctx }), [Op.eq]: toDate(r, { ctx }),
@ -77,6 +76,9 @@ export default {
} }
if (Array.isArray(r)) { if (Array.isArray(r)) {
console.log(11111111, {
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
});
return { return {
[Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }], [Op.and]: [{ [Op.gte]: toDate(r[0], { ctx }) }, { [Op.lt]: toDate(r[1], { ctx }) }],
}; };

View File

@ -15,6 +15,7 @@ export interface Str2momentOptions {
picker?: 'year' | 'month' | 'week' | 'quarter'; picker?: 'year' | 'month' | 'week' | 'quarter';
utcOffset?: number; utcOffset?: number;
utc?: boolean; utc?: boolean;
dateOnly?: boolean;
} }
export type Str2momentValue = string | string[] | dayjs.Dayjs | dayjs.Dayjs[]; export type Str2momentValue = string | string[] | dayjs.Dayjs | dayjs.Dayjs[];
@ -83,10 +84,14 @@ const toMoment = (val: any, options?: Str2momentOptions) => {
return; return;
} }
const offset = options.utcOffset; const offset = options.utcOffset;
const { gmt, picker, utc = true } = options; const { gmt, picker, utc = true, dateOnly } = options;
if (dayjs(val).isValid()) { if (dayjs(val).isValid()) {
if (dateOnly) {
return dayjs.utc(val, 'YYYY-MM-DD');
}
if (!utc) { if (!utc) {
return dayjs(val); return dayjs.utc(val);
} }
if (dayjs.isDayjs(val)) { if (dayjs.isDayjs(val)) {

View File

@ -188,9 +188,11 @@ export const parseFilter = async (filter: any, opts: ParseFilterOptions = {}) =>
const field = getField?.(path); const field = getField?.(path);
if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') { if (field?.constructor.name === 'DateOnlyField' || field?.constructor.name === 'DatetimeNoTzField') {
if (value.type) {
return getDayRangeByParams({ ...value, timezone: field?.timezone || timezone });
}
return value; return value;
} }
return dateValueWrapper(value, field?.timezone || timezone); return dateValueWrapper(value, field?.timezone || timezone);
} }
return value; return value;

View File

@ -17,6 +17,8 @@ import {
useRequest, useRequest,
useResourceActionContext, useResourceActionContext,
useResourceContext, useResourceContext,
useFilterFieldProps,
useFilterFieldOptions,
} from '@nocobase/client'; } from '@nocobase/client';
import React from 'react'; import React from 'react';
import { i18nText } from '../../utils'; 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 = { export const schema: ISchema = {
type: 'object', type: 'object',
properties: { properties: {
@ -245,6 +257,18 @@ export const schema: ISchema = {
}, },
}, },
properties: { 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: { delete: {
type: 'void', type: 'void',
title: '{{ t("Delete") }}', title: '{{ t("Delete") }}',

View File

@ -30,13 +30,15 @@ import { WorkflowLink } from './WorkflowLink';
import OpenDrawer from './components/OpenDrawer'; import OpenDrawer from './components/OpenDrawer';
import { workflowSchema } from './schemas/workflows'; import { workflowSchema } from './schemas/workflows';
import { ExecutionStatusSelect, ExecutionStatusColumn } from './components/ExecutionStatus'; import { ExecutionStatusSelect, ExecutionStatusColumn } from './components/ExecutionStatus';
import WorkflowPlugin, { ExecutionStatusOptions, RadioWithTooltip } from '.'; import WorkflowPlugin from '.';
import { RadioWithTooltip } from './components';
import { useRefreshActionProps } from './hooks/useRefreshActionProps'; import { useRefreshActionProps } from './hooks/useRefreshActionProps';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { TriggerOptionRender } from './components/TriggerOptionRender'; import { TriggerOptionRender } from './components/TriggerOptionRender';
import { CategoryTabs } from './WorkflowCategoryTabs'; import { CategoryTabs } from './WorkflowCategoryTabs';
import { EnumerationField } from './components/EmunerationField'; import { EnumerationField } from './components/EmunerationField';
import { useWorkflowFilterActionProps } from './hooks/useWorkflowFilterActionProps'; import { useWorkflowFilterActionProps } from './hooks/useWorkflowFilterActionProps';
import { ExecutionStatusOptions } from './constants';
function SyncOptionSelect(props) { function SyncOptionSelect(props) {
const field = useField<any>(); const field = useField<any>();

View File

@ -100,4 +100,5 @@ export default class extends Instruction {
resultTitle: lang('Calculation result'), resultTitle: lang('Calculation result'),
}; };
} }
testable = true;
} }

View File

@ -194,4 +194,5 @@ export default class extends Instruction {
</NodeDefaultView> </NodeDefaultView>
); );
} }
testable = true;
} }

View File

@ -7,11 +7,11 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * 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 { createForm, Field } from '@formily/core';
import { toJS } from '@formily/reactive'; import { toJS } from '@formily/reactive';
import { ISchema, observer, useField, useForm } from '@formily/react'; 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 { cloneDeep, get, set } from 'lodash';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react'; import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -26,7 +26,6 @@ import {
cx, cx,
useAPIClient, useAPIClient,
useActionContext, useActionContext,
useCancelAction,
useCompile, useCompile,
usePlugin, usePlugin,
useResourceActionContext, useResourceActionContext,
@ -330,6 +329,7 @@ const useRunAction = () => {
async run() { async run() {
const template = parse(node.config); const template = parse(node.config);
const config = template(toJS(values.config)); const config = template(toJS(values.config));
const logField = query('log').take() as Field;
const resultField = query('result').take() as Field; const resultField = query('result').take() as Field;
resultField.setValue(null); resultField.setValue(null);
resultField.setFeedback({}); resultField.setFeedback({});
@ -352,6 +352,7 @@ const useRunAction = () => {
messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')], messages: data.status > 0 ? [lang('Resolved')] : [lang('Failed')],
}); });
resultField.setValue(data.result); resultField.setValue(data.result);
logField.setValue(data.log || '');
} catch (err) { } catch (err) {
resultField.setFeedback({ resultField.setFeedback({
type: 'error', type: 'error',
@ -359,7 +360,6 @@ const useRunAction = () => {
}); });
} }
field.data.loading = false; 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() { function TestButton() {
const node = useNodeContext(); const node = useNodeContext();
const { values } = useForm(); const { values } = useForm();
const [visible, setVisible] = useState(false);
const template = parse(values); const template = parse(values);
const keys = template.parameters.map((item) => item.key); const keys = template.parameters.map((item) => item.key);
const form = useMemo(() => createForm(), []); 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 ( return (
<NodeContext.Provider value={{ ...node, config: values }}> <NodeContext.Provider value={{ ...node, config: values }}>
<VariableKeysContext.Provider value={keys}> <VariableKeysContext.Provider value={keys}>
<SchemaComponent <ActionContextProvider value={{ visible, setVisible: setModalVisible }}>
components={{ <Button icon={<CaretRightOutlined />} onClick={onOpen}>
Alert, {lang('Test run')}
TestFormFieldset, </Button>
}} <SchemaComponent
scope={{ components={{
useCancelAction, Alert,
useRunAction, TestFormFieldset,
}} LogCollapse,
schema={{ }}
type: 'void', scope={{
name: 'testButton', useCancelAction,
title: '{{t("Test run")}}', useRunAction,
'x-component': 'Action', }}
'x-component-props': { schema={{
icon: 'CaretRightOutlined', type: 'void',
// openSize: 'small', name: 'modal',
}, 'x-decorator': 'FormV2',
properties: { 'x-decorator-props': {
modal: { form,
type: 'void', },
'x-decorator': 'FormV2', 'x-component': 'Action.Modal',
'x-decorator-props': { title: `{{t("Test run", { ns: "workflow" })}}`,
form, 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', config: {
title: `{{t("Test run", { ns: "workflow" })}}`, type: 'object',
properties: { title: '{{t("Replace variables", { ns: "workflow" })}}',
alert: { 'x-decorator': 'FormItem',
type: 'void', 'x-component': 'TestFormFieldset',
'x-component': 'Alert', },
'x-component-props': { actions: {
message: `{{t("Test run will do the actual data manipulating or API calling, please use with caution.", { ns: "workflow" })}}`, type: 'void',
type: 'warning', 'x-component': 'ActionBar',
showIcon: true, properties: {
className: css` submit: {
margin-bottom: 1em; type: 'void',
`, title: '{{t("Run")}}',
}, 'x-component': 'Action',
}, 'x-component-props': {
config: { type: 'primary',
type: 'object', useAction: '{{ useRunAction }}',
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', result: {
title: `{{t("Result", { ns: "workflow" })}}`, type: 'string',
'x-decorator': 'FormItem', title: `{{t("Result", { ns: "workflow" })}}`,
'x-component': 'Input.JSON', 'x-decorator': 'FormItem',
'x-component-props': { 'x-component': 'Input.JSON',
autoSize: { 'x-component-props': {
minRows: 5, autoSize: {
maxRows: 20, minRows: 5,
}, maxRows: 20,
style: {
whiteSpace: 'pre',
cursor: 'text',
},
}, },
'x-pattern': 'disabled', style: {
}, whiteSpace: 'pre',
footer: { cursor: 'text',
type: 'void',
'x-component': 'Action.Modal.Footer',
properties: {
cancel: {
type: 'void',
title: '{{t("Close")}}',
'x-component': 'Action',
'x-component-props': {
useAction: '{{ useCancelAction }}',
},
},
}, },
}, },
'x-pattern': 'disabled',
},
log: {
type: 'string',
'x-component': 'LogCollapse',
}, },
}, },
}, }}
}} />
/> </ActionContextProvider>
</VariableKeysContext.Provider> </VariableKeysContext.Provider>
</NodeContext.Provider> </NodeContext.Provider>
); );

View File

@ -86,6 +86,7 @@ export const executionSchema = {
appends: ['workflow.id', 'workflow.title'], appends: ['workflow.id', 'workflow.title'],
pageSize: 20, pageSize: 20,
sort: ['-createdAt'], sort: ['-createdAt'],
except: ['context', 'output'],
filter: {}, filter: {},
}, },
}, },

View File

@ -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; export default CalculationInstruction;

View File

@ -7,7 +7,7 @@
* For more information, please refer to: https://www.nocobase.com/agreement. * For more information, please refer to: https://www.nocobase.com/agreement.
*/ */
import { evaluators } from '@nocobase/evaluators'; import { Evaluator, evaluators } from '@nocobase/evaluators';
import { Instruction } from '.'; import { Instruction } from '.';
import type Processor from '../Processor'; import type Processor from '../Processor';
import { JOB_STATUS } from '../constants'; import { JOB_STATUS } from '../constants';
@ -80,6 +80,22 @@ export class ConditionInstruction extends Instruction {
// pass control to upper scope by ending current scope // pass control to upper scope by ending current scope
return processor.exit(branchJob.status); 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; export default ConditionInstruction;