Merge branch 'develop' of github.com:nocobase/nocobase into feat-main-datasource-mssql

This commit is contained in:
aaaaaajie 2025-04-04 00:08:58 +08:00
commit 11578db51c
22 changed files with 347 additions and 84 deletions

View File

@ -5,6 +5,58 @@ 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

View File

@ -5,6 +5,58 @@
格式基于 [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
### 🐛 修复

View File

@ -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.

View File

@ -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 <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>;
return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
}
return (
<div

View File

@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
titleUsable = true;
properties = {
...defaultProps,
trim: {
type: 'boolean',
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
'x-decorator': 'FormItem',
'x-component': 'Checkbox',
},
};
schemaInitialize(schema: ISchema, { block }) {
if (['Table', 'Kanban'].includes(block)) {

View File

@ -104,12 +104,31 @@ const CollectionFieldInternalField = (props) => {
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
useEffect(() => {
// There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
// then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
// This code is meant to fix this issue.
/**
* There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
* then back to readPretty, and refreshing the page, the field remains in editable state. The expected state is readPretty.
* This code is meant to fix this issue.
*/
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
field.readPretty = true;
}
/**
* This solves the issue: After creating a form and setting a field to "read-only", the field remains editable when refreshing the page and reopening the dialog.
*
* Note: This might be a bug in Formily
* When both x-disabled and x-read-pretty exist in the Schema:
* - If x-disabled appears before x-read-pretty in the Schema JSON, the disabled state becomes ineffective
* - The reason is that during field instance initialization, field.disabled is set before field.readPretty, which causes the pattern value to be changed to 'editable'
* - This issue is related to the order of JSON fields, which might return different orders in different environments (databases), thus making the issue inconsistent to reproduce
*
* Reference to Formily source code:
* 1. Setting readPretty may cause pattern to be changed to 'editable': https://github.com/alibaba/formily/blob/d4bb96c40e7918210b1bd7d57b8fadee0cfe4b26/packages/core/src/models/BaseField.ts#L208-L224
* 2. The execution order of the each method depends on the order of JSON fields: https://github.com/alibaba/formily/blob/123d536b6076196e00b4e02ee160d72480359f54/packages/json-schema/src/schema.ts#L486-L519
*/
if (fieldSchema['x-disabled'] === true) {
field.disabled = true;
}
field.data = field.data || {};
field.data.dataSource = uiSchema?.enum;
}, [field, fieldSchema]);

View File

@ -107,7 +107,7 @@ AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner;
AssociationFilter.useAssociationField = () => {
const fieldSchema = useFieldSchema();
const collection = useCollection();
return React.useMemo(() => collection.getField(fieldSchema.name as any), [fieldSchema.name]);
return React.useMemo(() => collection?.getField(fieldSchema?.name as any), [fieldSchema?.name]);
};
export class AssociationFilterPlugin extends Plugin {

View File

@ -45,16 +45,16 @@ describe('CollectionSelect', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-1rquknz ant-app"
class="css-dev-only-do-not-override-qu8jc9 ant-app"
style="height: 100%;"
>
<div
aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-qu8jc9"
role="button"
>
<div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
class="css-vij405 ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-qu8jc9"
>
<div
class="ant-formily-item-label"
@ -84,7 +84,7 @@ describe('CollectionSelect', () => {
class="ant-formily-item-control-content-component"
>
<div
class="ant-select ant-select-outlined css-dev-only-do-not-override-1rquknz ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
class="ant-select css-dev-only-do-not-override-qu8jc9 ant-select-focused ant-select-single ant-select-show-arrow ant-select-show-search"
data-testid="select-collection"
role="button"
>
@ -96,9 +96,6 @@ describe('CollectionSelect', () => {
</span>
<div
class="ant-select-selector"
>
<span
class="ant-select-selection-wrap"
>
<span
class="ant-select-selection-search"
@ -123,7 +120,6 @@ describe('CollectionSelect', () => {
>
Users
</span>
</span>
</div>
<span
aria-hidden="true"
@ -186,16 +182,16 @@ describe('CollectionSelect', () => {
expect(container).toMatchInlineSnapshot(`
<div>
<div
class="css-dev-only-do-not-override-1rquknz ant-app"
class="css-dev-only-do-not-override-qu8jc9 ant-app"
style="height: 100%;"
>
<div
aria-label="block-item-demo title"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-1rquknz"
class="nb-block-item nb-form-item css-9qorhu ant-nb-block-item css-dev-only-do-not-override-qu8jc9"
role="button"
>
<div
class="css-1yh5po ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-1rquknz"
class="css-vij405 ant-formily-item ant-formily-item-layout-horizontal ant-formily-item-feedback-layout-loose ant-formily-item-label-align-right ant-formily-item-control-align-left css-dev-only-do-not-override-qu8jc9"
>
<div
class="ant-formily-item-label"
@ -226,7 +222,7 @@ describe('CollectionSelect', () => {
>
<div>
<span
class="ant-tag css-dev-only-do-not-override-1rquknz"
class="ant-tag css-dev-only-do-not-override-qu8jc9"
>
Users
</span>

View File

@ -37,6 +37,15 @@ const formItemWrapCss = css`
.ant-description-textarea img {
max-width: 100%;
}
&.ant-formily-item-layout-vertical .ant-formily-item-label {
display: inline;
.ant-formily-item-label-tooltip-icon {
display: inline;
}
.ant-formily-item-label-content {
display: inline;
}
}
`;
const formItemLabelCss = css`
@ -44,7 +53,7 @@ const formItemLabelCss = css`
padding: 0px !important;
}
> .ant-formily-item-label {
display: none;
display: none !important;
}
`;
@ -83,7 +92,7 @@ export const FormItem: any = withDynamicSchemaProps(
[formItemLabelCss]: showTitle === false,
});
}, [showTitle]);
console.log(className);
// 联动规则中的“隐藏保留值”的效果
if (field.data?.hidden) {
return null;

View File

@ -55,4 +55,18 @@ describe('text field', () => {
});
await Test.sync();
});
it('trim', async () => {
const collection = db.collection({
name: 'tests',
fields: [{ type: 'text', name: 'name', trim: true }],
});
await db.sync();
const model = await collection.model.create({
name: ' n1\n ',
});
expect(model.toJSON()).toMatchObject({
name: 'n1',
});
});
});

View File

@ -159,6 +159,7 @@ describe('has many repository', () => {
name: 'posts',
fields: [
{ type: 'string', name: 'title' },
{ type: 'belongsTo', name: 'user' },
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
{ type: 'hasMany', name: 'comments' },
{ type: 'string', name: 'status' },
@ -480,6 +481,51 @@ describe('has many repository', () => {
).not.toBeNull();
});
test('destroy by pk and filter with association', async () => {
const u1 = await User.repository.create({
values: { name: 'u1' },
});
const UserPostRepository = new HasManyRepository(User, 'posts', u1.id);
const p1 = await UserPostRepository.create({
values: {
title: 't1',
status: 'published',
user: u1,
},
});
const p2 = await UserPostRepository.create({
values: {
title: 't2',
status: 'draft',
user: u1,
},
});
await UserPostRepository.destroy({
filterByTk: p1.id,
filter: {
user: {
id: u1.id,
},
},
});
expect(
await UserPostRepository.findOne({
filterByTk: p1.id,
}),
).toBeNull();
expect(
await UserPostRepository.findOne({
filterByTk: p2.id,
}),
).not.toBeNull();
});
test('destroy by pk', async () => {
const u1 = await User.repository.create({
values: { name: 'u1' },

View File

@ -29,13 +29,12 @@ export class DatetimeNoTzField extends Field {
return DatetimeNoTzTypeMySQL;
}
return DataTypes.STRING;
return DataTypes.DATE;
}
init() {
beforeSave = async (instance, options) => {
const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options;
this.beforeSave = async (instance, options) => {
const value = instance.get(name);
if (!value && instance.isNewRecord && defaultToCurrentTime) {
@ -48,7 +47,6 @@ export class DatetimeNoTzField extends Field {
return;
}
};
}
additionalSequelizeOptions(): {} {
const { name } = this.options;
@ -57,17 +55,14 @@ export class DatetimeNoTzField extends Field {
const timezone = this.database.options.rawTimezone || '+00:00';
const isPg = this.database.inDialect('postgres');
const isMySQLCompatibleDialect = this.database.isMySQLCompatibleDialect();
return {
get() {
const val = this.getDataValue(name);
if (val instanceof Date) {
if (isPg) {
return moment(val).format('YYYY-MM-DD HH:mm:ss');
}
// format to YYYY-MM-DD HH:mm:ss
const momentVal = moment(val).utcOffset(timezone);
const momentVal = moment(val);
return momentVal.format('YYYY-MM-DD HH:mm:ss');
}
@ -75,18 +70,24 @@ export class DatetimeNoTzField extends Field {
},
set(val) {
if (typeof val === 'string' && isIso8601(val)) {
const momentVal = moment(val).utcOffset(timezone);
val = momentVal.format('YYYY-MM-DD HH:mm:ss');
if (val == null) {
return this.setDataValue(name, null);
}
if (val && val instanceof Date) {
// format to YYYY-MM-DD HH:mm:ss
const momentVal = moment(val).utcOffset(timezone);
val = momentVal.format('YYYY-MM-DD HH:mm:ss');
const dateOffset = new Date().getTimezoneOffset();
const momentVal = moment(val);
if ((typeof val === 'string' && isIso8601(val)) || val instanceof Date) {
momentVal.utcOffset(timezone);
momentVal.utcOffset(-dateOffset, true);
}
return this.setDataValue(name, val);
if (isMySQLCompatibleDialect) {
momentVal.millisecond(0);
}
const date = momentVal.toDate();
return this.setDataValue(name, date);
},
};
}

View File

@ -23,9 +23,20 @@ export class TextField extends Field {
this.options.defaultValue = null;
}
}
additionalSequelizeOptions() {
const { name, trim } = this.options;
return {
set(value) {
this.setDataValue(name, trim ? value?.trim() : value);
},
};
}
}
export interface TextFieldOptions extends BaseColumnFieldOptions {
type: 'text';
length?: 'tiny' | 'medium' | 'long';
trim?: boolean;
}

View File

@ -72,7 +72,13 @@ export class HasManyRepository extends MultipleRelationRepository {
const filterResult = this.parseFilter(options['filter'], options);
if (filterResult.include && filterResult.include.length > 0) {
return await this.destroyByFilter(options['filter'], transaction);
return await this.destroyByFilter(
{
filter: options['filter'],
filterByTk: options['filterByTk'],
},
transaction,
);
}
where.push(filterResult.where);

View File

@ -179,9 +179,15 @@ export abstract class MultipleRelationRepository extends RelationRepository {
return false;
}
protected async destroyByFilter(filter: Filter, transaction?: Transaction) {
protected async destroyByFilter(
options: {
filter?: Filter;
filterByTk?: TargetKey | TargetKey[];
},
transaction?: Transaction,
) {
const instances = await this.find({
filter: filter,
...options,
transaction,
});

View File

@ -21,6 +21,7 @@ export default defineCollection({
type: 'string',
name: 'title',
translation: true,
trim: true,
},
{
title: '英文标识',
@ -28,6 +29,7 @@ export default defineCollection({
type: 'uid',
name: 'name',
unique: true,
trim: true,
},
{
comment: '类型标识,如 local/ali-oss 等',
@ -51,12 +53,14 @@ export default defineCollection({
type: 'text',
name: 'path',
defaultValue: '',
trim: true,
},
{
comment: '访问地址前缀',
type: 'string',
name: 'baseUrl',
defaultValue: '',
trim: true,
},
// TODO(feature): 需要使用一个实现了可设置默认值的字段
{

View File

@ -147,6 +147,7 @@ export function AdminPublicFormPage() {
}}
>
<Breadcrumb
style={{ marginLeft: '10px' }}
items={[
{
title: <Link to={`/admin/settings/public-forms`}>{t('Public forms', { ns: NAMESPACE })}</Link>,

View File

@ -33,7 +33,7 @@ import {
import { Input, Modal, Spin } from 'antd';
import { useTranslation } from 'react-i18next';
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { isDesktop } from 'react-device-detect';
import { isDesktop, isMobile } from 'react-device-detect';
import { useParams } from 'react-router';
import { usePublicSubmitActionProps } from '../hooks';
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
@ -129,9 +129,6 @@ const PublicFormMessageProvider = ({ children }) => {
</PublicFormMessageContext.Provider>
);
};
function isMobile() {
return window.matchMedia('(max-width: 768px)').matches;
}
const AssociationFieldMobile = (props) => {
return <AssociationField {...props} popupMatchSelectWidth={true} />;
@ -165,7 +162,6 @@ const mobileComponents = {
function InternalPublicForm() {
const params = useParams();
const apiClient = useAPIClient();
const isMobileMedia = isMobile();
const { error, data, loading, run } = useRequest<any>(
{
url: `publicForms:getMeta/${params.name}`,
@ -243,7 +239,7 @@ function InternalPublicForm() {
if (!data?.data) {
return <UnEnabledFormPlaceholder />;
}
const components = isMobileMedia ? mobileComponents : {};
const components = isMobile ? mobileComponents : {};
return (
<ACLCustomContext.Provider value={{ allowAll: true }}>
<PublicAPIClientProvider>

View File

@ -172,7 +172,7 @@ export class PluginPublicFormsServer extends Plugin {
skip: true,
};
} else if (
(actionName === 'list' && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
(['list', 'get'].includes(actionName) && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
(collection?.options.template === 'file' && actionName === 'create') ||
(resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) ||
(resourceName === 'vditor' && ['check'].includes(actionName)) ||

View File

@ -189,7 +189,7 @@ export class ApprovalPassthroughModeNode {
this.detailsConfigureFieldsButton = page.getByLabel(
`schema-initializer-Grid-details:configureFields-${collectionName}`,
);
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' });
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
this.actionsConfigureActionsButton = page.getByLabel(
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
@ -262,7 +262,7 @@ export class ApprovalBranchModeNode {
this.detailsConfigureFieldsButton = page.getByLabel(
`schema-initializer-Grid-details:configureFields-${collectionName}`,
);
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' }).getByRole('switch');
this.addActionsMenu = page.getByRole('menuitem', { name: 'Process form' });
this.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');

View File

@ -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', () => {