diff --git a/CHANGELOG.md b/CHANGELOG.md index 00a0eda99b..e0058471f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,84 @@ 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.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03 + +### 🐛 Bug Fixes + +- **[client]** + - x-disabled property not taking effect on form fields ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh + + - field label display issue to prevent truncation by colon ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh + +- **[database]** When deleting one-to-many records, both `filter` and `filterByTk` are passed and `filter` includes an association field, the `filterByTk` is ignored ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile + +## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01 + +### 🚀 Improvements + +- **[database]** + - Add trim option for text field ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher + + - Add trim option for string field ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher + +- **[File manager]** Add trim option for text fields of storages collection ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher + +- **[Workflow]** Improve code ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher + +- **[Workflow: Approval]** Support to use block template for approval process form by @mytharcher + +### 🐛 Bug Fixes + +- **[database]** Avoid "datetimeNoTz" field changes when value not changed in updating record ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher + +- **[client]** + - association field (select) displaying N/A when exposing related collection fields ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh + + - Fix `disabled` property not works when `SchemaInitializerItem` has `items` ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher + + - cascade issue: 'The value of xxx cannot be in array format' when deleting and re-selecting ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh + +- **[Collection field: Many to many (array)]** Issue of filtering by fields in an association collection with a many to many (array) field ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile + +- **[Public forms]** View permissions include list and get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos + +- **[Authentication]** token assignment in `AuthProvider` ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile + +- **[Workflow]** Fix sync option display incorrectly ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher + +- **[Block: Map]** map management validation should not pass with space input ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh + +- **[Workflow: Approval]** + - Fix client variables to use in approval form by @mytharcher + + - Fix branch mode when `endOnReject` configured as `true` by @mytharcher + +## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29 + +### 🐛 Bug Fixes + +- **[Calendar]** missing data on boundary dates in weekly calendar view ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh + +- **[Auth: OIDC]** Incorrect redirection occurs when the callback path is the string 'null' by @2013xile + +- **[Workflow: Approval]** Fix approval node configuration is incorrect after schema changed by @mytharcher + +## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28 + +### 🚀 Improvements + +- **[Async task manager]** optimize import/export buttons in Pro ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos + +- **[Action: Export records Pro]** optimize import/export buttons in Pro by @katherinehhh + +- **[Migration manager]** allow skip automatic backup and restore for migration by @gchust + +### 🐛 Bug Fixes + +- **[client]** linkage conflict between same-named association fields in different sub-tables within the same form ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh + +- **[Action: Batch edit]** Click the batch edit button, configure the pop-up window, and then open it again, the pop-up window is blank ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe + ## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27 ### 🐛 Bug Fixes diff --git a/CHANGELOG.zh-CN.md b/CHANGELOG.zh-CN.md index e22c5a3d4a..e5cff93429 100644 --- a/CHANGELOG.zh-CN.md +++ b/CHANGELOG.zh-CN.md @@ -5,6 +5,84 @@ 格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/), 并且本项目遵循 [语义化版本](https://semver.org/spec/v2.0.0.html)。 +## [v1.6.16](https://github.com/nocobase/nocobase/compare/v1.6.15...v1.6.16) - 2025-04-03 + +### 🐛 修复 + +- **[client]** + - 表单字段设置不可编辑不起作用 ([#6610](https://github.com/nocobase/nocobase/pull/6610)) by @katherinehhh + + - 表单字段标题因冒号导致的截断问题 ([#6599](https://github.com/nocobase/nocobase/pull/6599)) by @katherinehhh + +- **[database]** 删除一对多记录时,同时传递 `filter` 和 `filterByTk` 参数,`filter` 包含关系字段时,`filterByTk` 参数失效 ([#6606](https://github.com/nocobase/nocobase/pull/6606)) by @2013xile + +## [v1.6.15](https://github.com/nocobase/nocobase/compare/v1.6.14...v1.6.15) - 2025-04-01 + +### 🚀 优化 + +- **[database]** + - 为多行文本类型字段增加去除首尾空白字符的选项 ([#6603](https://github.com/nocobase/nocobase/pull/6603)) by @mytharcher + + - 为单行文本增加自动去除首尾空白字符的选项 ([#6565](https://github.com/nocobase/nocobase/pull/6565)) by @mytharcher + +- **[文件管理器]** 为存储引擎表的文本字段增加去除首尾空白字符的选项 ([#6604](https://github.com/nocobase/nocobase/pull/6604)) by @mytharcher + +- **[工作流]** 优化代码 ([#6589](https://github.com/nocobase/nocobase/pull/6589)) by @mytharcher + +- **[工作流:审批]** 支持审批表单使用区块模板 by @mytharcher + +### 🐛 修复 + +- **[database]** 避免“日期时间(无时区)”字段在值未变动的更新时触发值改变 ([#6588](https://github.com/nocobase/nocobase/pull/6588)) by @mytharcher + +- **[client]** + - 关系字段(select)放出关系表字段时默认显示 N/A ([#6582](https://github.com/nocobase/nocobase/pull/6582)) by @katherinehhh + + - 修复 `SchemaInitializerItem` 配置了 `items` 时 `disabled` 属性无效的问题 ([#6597](https://github.com/nocobase/nocobase/pull/6597)) by @mytharcher + + - 级联组件删除后重新选择时出现 'The value of xxx cannot be in array format' ([#6585](https://github.com/nocobase/nocobase/pull/6585)) by @katherinehhh + +- **[数据表字段:多对多 (数组)]** 主表筛选带有多对多(数组)字段的关联表中的字段报错的问题 ([#6596](https://github.com/nocobase/nocobase/pull/6596)) by @2013xile + +- **[公开表单]** 查看权限包括 list 和 get ([#6607](https://github.com/nocobase/nocobase/pull/6607)) by @chenos + +- **[用户认证]** `AuthProvider` 中的 token 赋值 ([#6593](https://github.com/nocobase/nocobase/pull/6593)) by @2013xile + +- **[工作流]** 修复同步选项展示问题 ([#6595](https://github.com/nocobase/nocobase/pull/6595)) by @mytharcher + +- **[区块:地图]** 地图管理必填校验不应通过空格输入 ([#6575](https://github.com/nocobase/nocobase/pull/6575)) by @katherinehhh + +- **[工作流:审批]** + - 修复审批表单中的前端变量 by @mytharcher + + - 修复分支模式下配置拒绝则结束时的流程问题 by @mytharcher + +## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29 + +### 🐛 修复 + +- **[日历]** 日历区块以周为视图时,边界日期不显示数据 ([#6587](https://github.com/nocobase/nocobase/pull/6587)) by @katherinehhh + +- **[认证:OIDC]** 回调路径是字符串'null'时导致跳转不正确 by @2013xile + +- **[工作流:审批]** 修复审批节点界面配置变更后数据未同步的问题 by @mytharcher + +## [v1.6.13](https://github.com/nocobase/nocobase/compare/v1.6.12...v1.6.13) - 2025-03-28 + +### 🚀 优化 + +- **[异步任务管理器]** 优化 Pro 导入导出按钮异步逻辑 ([#6531](https://github.com/nocobase/nocobase/pull/6531)) by @chenos + +- **[操作:导出记录 Pro]** 优化 Pro 导入导出按钮 by @katherinehhh + +- **[迁移管理]** 允许执行迁移时跳过自动备份还原 by @gchust + +### 🐛 修复 + +- **[client]** 同一表单中不同关系字段的同名关系字段的联动互相影响 ([#6577](https://github.com/nocobase/nocobase/pull/6577)) by @katherinehhh + +- **[操作:批量编辑]** 点击批量编辑按钮,配置完弹窗再打开,弹窗是空白的 ([#6578](https://github.com/nocobase/nocobase/pull/6578)) by @zhangzhonghe + ## [v1.6.12](https://github.com/nocobase/nocobase/compare/v1.6.11...v1.6.12) - 2025-03-27 ### 🐛 修复 diff --git a/LICENSE.txt b/LICENSE.txt index b3c87c1e83..babf2bf053 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Updated Date: February 20, 2025 +Updated Date: April 1, 2025 NocoBase License Agreement @@ -88,7 +88,7 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr 6.6 Can sell plugins developed for Software in the Marketplace. -6.7 The User with an Enterprise Edition License can sell Upper Layer Application to their clients. +6.7 The User with a Professional or Enterprise Edition License can sell Upper Layer Application to their clients. 6.8 Not restricted by the AGPL-3.0 agreement. @@ -106,9 +106,9 @@ Except for Third-Party Open Source Software, the Company owns all copyrights, tr 7.4 It is not allowed to provide any form of no-code, zero-code, low-code platform SaaS products to the public using the original or modified Software. -7.5 It is not allowed for the User withot an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license. +7.5 It is not allowed for the User withot a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license. -7.6 It is not allowed for the User with an Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration. +7.6 It is not allowed for the User with a Professional or Enterprise Edition license to sell Upper Layer Application to clients without a Commercial license with access to further development and configuration. 7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace. diff --git a/README.ja-JP.md b/README.ja-JP.md index 2fff33bddd..49e5c1be42 100644 --- a/README.ja-JP.md +++ b/README.ja-JP.md @@ -2,14 +2,10 @@ https://github.com/user-attachments/assets/cf08bfe5-e6e6-453c-8b96-350a6a8bed17 -## ご協力ありがとうございます! +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## リリースノート - -リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。 +

## NocoBaseはなに? @@ -28,6 +24,16 @@ https://docs-cn.nocobase.com/ コミュニティ: https://forum.nocobase.com/ +チュートリアル: +https://www.nocobase.com/ja/tutorials + +顧客のストーリー: +https://www.nocobase.com/ja/blog/tags/customer-stories + +## リリースノート + +リリースノートは[ブログ](https://www.nocobase.com/ja/blog/timeline)で随時更新され、週ごとにまとめて公開しています。 + ## 他の製品との違い ### 1. データモデル駆動 diff --git a/README.md b/README.md index 1314bec051..a2cfdb2bc4 100644 --- a/README.md +++ b/README.md @@ -2,19 +2,14 @@ English | [中文](./README.zh-CN.md) | [日本語](./README.ja-JP.md) https://github.com/user-attachments/assets/a50c100a-4561-4e06-b2d2-d48098659ec0 -## We'd love your support! - +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## Release Notes - -Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary. +

## What is NocoBase -NocoBase is a scalability-first, open-source no-code development platform. +NocoBase is an extensibility-first, open-source no-code development platform. Instead of investing years of time and millions of dollars in research and development, deploy NocoBase in a few minutes and you'll have a private, controllable, and extremely scalable no-code development platform! Homepage: @@ -29,6 +24,17 @@ https://docs.nocobase.com/ Forum: https://forum.nocobase.com/ +Tutorials: +https://www.nocobase.com/en/tutorials + +Use Cases: +https://www.nocobase.com/en/blog/tags/customer-stories + + +## Release Notes + +Our [blog](https://www.nocobase.com/en/blog/timeline) is regularly updated with release notes and provides a weekly summary. + ## Distinctive features ### 1. Data model-driven diff --git a/README.zh-CN.md b/README.zh-CN.md index 66e9c281d7..728695efc7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -2,13 +2,10 @@ https://github.com/nocobase/nocobase/assets/1267426/29623e45-9a48-4598-bb9e-9dd173ade553 -## 感谢支持 +

nocobase%2Fnocobase | Trendshift - NocoBase - Scalability-first, open-source no-code platform | Product Hunt - -## 发布日志 -我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。 +

## NocoBase 是什么 @@ -27,6 +24,15 @@ https://docs-cn.nocobase.com/ 社区: https://forum.nocobase.com/ +教程: +https://www.nocobase.com/cn/tutorials + +用户故事: +https://www.nocobase.com/cn/blog/tags/customer-stories + +## 发布日志 +我们的[博客](https://www.nocobase.com/cn/blog/timeline)会及时更新发布日志,并每周进行汇总。 + ## 与众不同之处 ### 1. 数据模型驱动 diff --git a/packages/core/client/src/api-client/APIClient.ts b/packages/core/client/src/api-client/APIClient.ts index 3fe2711a2a..f85c766e10 100644 --- a/packages/core/client/src/api-client/APIClient.ts +++ b/packages/core/client/src/api-client/APIClient.ts @@ -150,6 +150,9 @@ export class APIClient extends APIClientSDK { } return [{ message }]; } + if (error?.response?.data?.error) { + return [error?.response?.data?.error]; + } return ( error?.response?.data?.errors || error?.response?.data?.messages || diff --git a/packages/core/client/src/application/components/defaultComponents.tsx b/packages/core/client/src/application/components/defaultComponents.tsx index aeb2c475b9..31801e97ed 100644 --- a/packages/core/client/src/application/components/defaultComponents.tsx +++ b/packages/core/client/src/application/components/defaultComponents.tsx @@ -11,10 +11,11 @@ import React, { FC } from 'react'; import { MainComponent } from './MainComponent'; const Loading: FC = () =>
Loading...
; -const AppError: FC<{ error: Error }> = ({ error }) => { +const AppError: FC<{ error: Error & { title?: string } }> = ({ error }) => { + const title = error?.title || 'App Error'; return (
-
App Error
+
{title}
{error?.message} {process.env.__TEST__ && error?.stack}
diff --git a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx index 436fc54f37..dfdedad636 100644 --- a/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx +++ b/packages/core/client/src/application/schema-initializer/components/SchemaInitializerItem.tsx @@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo( className: className, label: children || compile(title), onClick: (info) => { - if (info.key !== name) return; + if (disabled || info.key !== name) return; if (closeInitializerMenuWhenClick) { setVisible?.(false); } @@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo( children: childrenItems, }, ]; - }, [name, style, className, children, title, onClick, icon, childrenItems]); + }, [name, disabled, style, className, children, title, onClick, icon, childrenItems]); if (items && items.length > 0) { - return ; + return ; } return (
void; } -export function SelectWithTitle({ title, defaultValue, onChange, options, fieldNames }: SelectWithTitleProps) { +export function SelectWithTitle({ + title, + defaultValue, + onChange, + options, + fieldNames, + ...others +}: SelectWithTitleProps) { const [open, setOpen] = useState(false); const timerRef = useRef(null); return ( @@ -36,6 +43,7 @@ export function SelectWithTitle({ title, defaultValue, onChange, options, fieldN > {title} + - {typeof typeValue === 'number' ? : null} + {typeof typeValue === 'number' ? ( + + ) : null} {typeValue === 'cron' ? ( onChange(`0 ${v}`)} clearButton={false} locale={window['cronLocale']} + disabled={disabled} /> ) : null} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts new file mode 100644 index 0000000000..bd93f359c9 --- /dev/null +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Dispatcher.ts @@ -0,0 +1,12 @@ +/** + * 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. + */ + +export default class Dispatcher { + constructor() {} +} diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts index d45449c56a..f15008b422 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Plugin.ts @@ -10,6 +10,7 @@ import path from 'path'; import { randomUUID } from 'crypto'; +import { Snowflake } from 'nodejs-snowflake'; import { Transaction, Transactionable } from 'sequelize'; import LRUCache from 'lru-cache'; @@ -61,6 +62,7 @@ export default class PluginWorkflowServer extends Plugin { triggers: Registry = new Registry(); functions: Registry = new Registry(); enabledCache: Map = new Map(); + snowflake: Snowflake; private ready = false; private executing: Promise | null = null; @@ -219,6 +221,14 @@ export default class PluginWorkflowServer extends Plugin { WorkflowRepository, WorkflowTasksRepository, }); + + const PluginRepo = this.db.getRepository('applicationPlugins'); + const pluginRecord = await PluginRepo.findOne({ + filter: { name: this.name }, + }); + this.snowflake = new Snowflake({ + custom_epoch: pluginRecord?.createdAt.getTime(), + }); } /** @@ -376,11 +386,16 @@ export default class PluginWorkflowServer extends Plugin { const prev = workflow.previous(); if (prev.config) { trigger.off({ ...workflow.get(), ...prev }); + this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id} based on configuration before updated`); } trigger.on(workflow); + this.getLogger(workflow.id).info(`toggle ON workflow ${workflow.id}`); + this.enabledCache.set(workflow.id, workflow); } else { trigger.off(workflow); + this.getLogger(workflow.id).info(`toggle OFF workflow ${workflow.id}`); + this.enabledCache.delete(workflow.id); } if (!silent) { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts index 1a6d20dd79..ebb510e863 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/Processor.ts @@ -56,15 +56,9 @@ export default class Processor { */ nodesMap = new Map(); - /** - * @experimental - */ - jobsMap = new Map(); - - /** - * @experimental - */ - jobsMapByNodeKey: { [key: string]: any } = {}; + private jobsMapByNodeKey: { [key: string]: JobModel } = {}; + private jobResultsMapByNodeKey: { [key: string]: any } = {}; + private jobsToSave: Map = new Map(); /** * @experimental @@ -100,10 +94,9 @@ export default class Processor { private makeJobs(jobs: Array) { jobs.forEach((job) => { - this.jobsMap.set(job.id, job); - const node = this.nodesMap.get(job.nodeId); - this.jobsMapByNodeKey[node.key] = job.result; + this.jobsMapByNodeKey[node.key] = job; + this.jobResultsMapByNodeKey[node.key] = job.result; }); } @@ -192,11 +185,11 @@ export default class Processor { } if (!(job instanceof Model)) { - job.upstreamId = prevJob instanceof Model ? prevJob.get('id') : null; + // job.upstreamId = prevJob instanceof Model ? prevJob.get('id') : null; job.nodeId = node.id; job.nodeKey = node.key; } - const savedJob = await this.saveJob(job); + const savedJob = this.saveJob(job); this.logger.info( `execution (${this.execution.id}) run instruction [${node.type}] for node (${node.id}) finished as status: ${savedJob.status}`, @@ -258,6 +251,27 @@ export default class Processor { } public async exit(s?: number) { + if (this.jobsToSave.size) { + const newJobs = []; + for (const job of this.jobsToSave.values()) { + if (job.isNewRecord) { + newJobs.push(job); + } else { + await job.save({ transaction: this.mainTransaction }); + } + } + if (newJobs.length) { + const JobsModel = this.options.plugin.db.getModel('jobs'); + await JobsModel.bulkCreate( + newJobs.map((job) => job.toJSON()), + { transaction: this.mainTransaction }, + ); + for (const job of newJobs) { + job.isNewRecord = false; + } + } + this.jobsToSave.clear(); + } if (typeof s === 'number') { const status = (this.constructor).StatusMap[s] ?? Math.sign(s); await this.execution.update({ status }, { transaction: this.mainTransaction }); @@ -269,33 +283,30 @@ export default class Processor { return null; } - // TODO(optimize) /** * @experimental */ - async saveJob(payload: JobModel | Record): Promise { + saveJob(payload: JobModel | Record): JobModel { const { database } = this.execution.constructor; - const { mainTransaction: transaction } = this; const { model } = database.getCollection('jobs'); let job; if (payload instanceof model) { - job = await payload.save({ transaction }); - } else if (payload.id) { - job = await model.findByPk(payload.id, { transaction }); - await job.update(payload, { transaction }); + job = payload; + job.set('updatedAt', new Date()); } else { - job = await model.create( - { - ...payload, - executionId: this.execution.id, - }, - { transaction }, - ); + job = model.build({ + ...payload, + id: this.options.plugin.snowflake.getUniqueID().toString(), + createdAt: new Date(), + updatedAt: new Date(), + executionId: this.execution.id, + }); } - this.jobsMap.set(job.id, job); + this.jobsToSave.set(job.id, job); this.lastSavedJob = job; - this.jobsMapByNodeKey[job.nodeKey] = job.result; + this.jobsMapByNodeKey[job.nodeKey] = job; + this.jobResultsMapByNodeKey[job.nodeKey] = job.result; return job; } @@ -357,32 +368,20 @@ export default class Processor { * @experimental */ findBranchParentJob(job: JobModel, node: FlowNodeModel): JobModel | null { - for (let j: JobModel | undefined = job; j; j = this.jobsMap.get(j.upstreamId)) { - if (j.nodeId === node.id) { - return j; - } - } - return null; + return this.jobsMapByNodeKey[node.key]; } /** * @experimental */ findBranchLastJob(node: FlowNodeModel, job: JobModel): JobModel | null { - const allJobs = Array.from(this.jobsMap.values()); + const allJobs = Object.values(this.jobsMapByNodeKey); const branchJobs = []; for (let n = this.findBranchEndNode(node); n && n !== node.upstream; n = n.upstream) { branchJobs.push(...allJobs.filter((item) => item.nodeId === n.id)); } - branchJobs.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()); - for (let i = branchJobs.length - 1; i >= 0; i -= 1) { - for (let j = branchJobs[i]; j && j.id !== job.id; j = this.jobsMap.get(j.upstreamId)) { - if (j.upstreamId === job.id) { - return branchJobs[i]; - } - } - } - return null; + branchJobs.sort((a, b) => a.updatedAt.getTime() - b.updatedAt.getTime()); + return branchJobs[branchJobs.length - 1] || null; } /** @@ -403,13 +402,13 @@ export default class Processor { for (let n = includeSelfScope ? node : this.findBranchParentNode(node); n; n = this.findBranchParentNode(n)) { const instruction = this.options.plugin.instructions.get(n.type); if (typeof instruction?.getScope === 'function') { - $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobsMapByNodeKey[n.key], this); + $scopes[n.id] = $scopes[n.key] = instruction.getScope(n, this.jobResultsMapByNodeKey[n.key], this); } } return { $context: this.execution.context, - $jobsMapByNodeKey: this.jobsMapByNodeKey, + $jobsMapByNodeKey: this.jobResultsMapByNodeKey, $system: systemFns, $scopes, $env: this.options.plugin.app.environment.getVariables(), diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts index f67b7a84e4..cff13e5d64 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/__tests__/triggers/collection.test.ts @@ -359,6 +359,50 @@ describe('workflow > triggers > collection', () => { const executions = await workflow.getExecutions(); expect(executions.length).toBe(1); }); + + it('datetime field not changed', async () => { + const workflow = await WorkflowModel.create({ + enabled: true, + sync: true, + type: 'collection', + config: { + mode: 2, + collection: 'posts', + changed: ['createdAt'], + }, + }); + + const post = await PostRepo.create({ values: { title: 't1' } }); + await PostRepo.update({ filterByTk: post.id, values: { ...post.get(), title: 't2' } }); + + const executions = await workflow.getExecutions(); + expect(executions.length).toBe(0); + }); + + it('datetimeNoTz field not changed', async () => { + db.getCollection('posts').addField('dateOnly', { + type: 'datetimeNoTz', + }); + + await db.sync(); + + const workflow = await WorkflowModel.create({ + enabled: true, + sync: true, + type: 'collection', + config: { + mode: 2, + collection: 'posts', + changed: ['dateOnly'], + }, + }); + + const post = await PostRepo.create({ values: { title: 't1', dateOnly: '2020-01-01 00:00:00' } }); + await PostRepo.update({ filterByTk: post.id, values: { ...post.get(), title: 't2' } }); + + const executions = await workflow.getExecutions(); + expect(executions.length).toBe(0); + }); }); describe('config.condition', () => { diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts index 8e7d6eba9d..3fe7020538 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/DateFieldScheduleTrigger.ts @@ -104,50 +104,54 @@ export default class DateFieldScheduleTrigger { // caching workflows in range, default to 5min cacheCycle = 300_000; + onAfterStart = () => { + if (this.timer) { + return; + } + + this.timer = setInterval(() => this.reload(), this.cacheCycle); + + this.reload(); + }; + + onBeforeStop = () => { + if (this.timer) { + clearInterval(this.timer); + } + + for (const [key, timer] of this.cache.entries()) { + clearTimeout(timer); + this.cache.delete(key); + } + }; + constructor(public workflow: Plugin) { - workflow.app.on('afterStart', async () => { - if (this.timer) { - return; - } - - this.timer = setInterval(() => this.reload(), this.cacheCycle); - - this.reload(); - }); - - workflow.app.on('beforeStop', () => { - if (this.timer) { - clearInterval(this.timer); - } - - for (const [key, timer] of this.cache.entries()) { - clearTimeout(timer); - this.cache.delete(key); - } - }); + workflow.app.on('afterStart', this.onAfterStart); + workflow.app.on('beforeStop', this.onBeforeStop); } - async reload() { + reload() { + for (const [key, timer] of this.cache.entries()) { + clearTimeout(timer); + this.cache.delete(key); + } + const workflows = Array.from(this.workflow.enabledCache.values()).filter( (item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.DATE_FIELD, ); - // NOTE: clear cached jobs in last cycle - this.cache = new Map(); - - this.inspect(workflows); + workflows.forEach((workflow) => { + this.inspect(workflow); + }); } - inspect(workflows: WorkflowModel[]) { + async inspect(workflow: WorkflowModel) { const now = new Date(); - - workflows.forEach(async (workflow) => { - const records = await this.loadRecordsToSchedule(workflow, now); - this.workflow.getLogger(workflow.id).info(`[Schedule on date field] ${records.length} records to schedule`); - records.forEach((record) => { - const nextTime = this.getRecordNextTime(workflow, record); - this.schedule(workflow, record, nextTime, Boolean(nextTime)); - }); + const records = await this.loadRecordsToSchedule(workflow, now); + this.workflow.getLogger(workflow.id).info(`[Schedule on date field] ${records.length} records to schedule`); + records.forEach((record) => { + const nextTime = this.getRecordNextTime(workflow, record); + this.schedule(workflow, record, nextTime, Boolean(nextTime)); }); } @@ -233,8 +237,6 @@ export default class DateFieldScheduleTrigger { [Op.gte]: new Date(endTimestamp), }, }); - } else { - this.workflow.getLogger(id).warn(`[Schedule on date field] "endsOn.field" is not configured`); } } } @@ -367,7 +369,7 @@ export default class DateFieldScheduleTrigger { } on(workflow: WorkflowModel) { - this.inspect([workflow]); + this.inspect(workflow); const { collection } = workflow.config; const [dataSourceName, collectionName] = parseCollectionName(collection); diff --git a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts index 542746ed9a..61d8a56d58 100644 --- a/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts +++ b/packages/plugins/@nocobase/plugin-workflow/src/server/triggers/ScheduleTrigger/StaticScheduleTrigger.ts @@ -18,36 +18,39 @@ const MAX_SAFE_INTERVAL = 2147483647; export default class StaticScheduleTrigger { private timers: Map = new Map(); - constructor(public workflow: Plugin) { - workflow.app.on('afterStart', async () => { - const workflows = Array.from(this.workflow.enabledCache.values()).filter( - (item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.STATIC, - ); - - this.inspect(workflows); - }); - - workflow.app.on('beforeStop', () => { - for (const timer of this.timers.values()) { - clearInterval(timer); - } - }); - } - - inspect(workflows: WorkflowModel[]) { - const now = new Date(); + onAfterStart = () => { + const workflows = Array.from(this.workflow.enabledCache.values()).filter( + (item) => item.type === 'schedule' && item.config.mode === SCHEDULE_MODE.STATIC, + ); workflows.forEach((workflow) => { - const nextTime = this.getNextTime(workflow, now); - if (nextTime) { - this.workflow - .getLogger(workflow.id) - .info(`caching scheduled workflow will run at: ${new Date(nextTime).toISOString()}`); - } else { - this.workflow.getLogger(workflow.id).info('workflow will not be scheduled'); - } - this.schedule(workflow, nextTime, nextTime >= now.getTime()); + this.inspect(workflow); }); + }; + + onBeforeStop = () => { + for (const timer of this.timers.values()) { + clearInterval(timer); + } + }; + + constructor(public workflow: Plugin) { + workflow.app.on('afterStart', this.onAfterStart); + workflow.app.on('beforeStop', this.onBeforeStop); + } + + inspect(workflow: WorkflowModel) { + const now = new Date(); + + const nextTime = this.getNextTime(workflow, now); + if (nextTime) { + this.workflow + .getLogger(workflow.id) + .info(`caching scheduled workflow will run at: ${new Date(nextTime).toISOString()}`); + } else { + this.workflow.getLogger(workflow.id).info('workflow will not be scheduled'); + } + this.schedule(workflow, nextTime, nextTime >= now.getTime()); } getNextTime({ config, stats }: WorkflowModel, currentDate: Date, nextSecond = false) { @@ -130,7 +133,7 @@ export default class StaticScheduleTrigger { } on(workflow) { - this.inspect([workflow]); + this.inspect(workflow); } off(workflow) { diff --git a/packages/presets/nocobase/package.json b/packages/presets/nocobase/package.json index 478fb8fde1..7240fe7b63 100644 --- a/packages/presets/nocobase/package.json +++ b/packages/presets/nocobase/package.json @@ -45,6 +45,7 @@ "@nocobase/plugin-gantt": "1.7.0-alpha.10", "@nocobase/plugin-graph-collection-manager": "1.7.0-alpha.10", "@nocobase/plugin-kanban": "1.7.0-alpha.10", + "@nocobase/plugin-locale-tester": "1.7.0-alpha.10", "@nocobase/plugin-localization": "1.7.0-alpha.10", "@nocobase/plugin-logger": "1.7.0-alpha.10", "@nocobase/plugin-map": "1.7.0-alpha.10",