mirror of
https://gitee.com/nocobase/nocobase.git
synced 2025-05-09 23:49:27 +08:00
Merge branch 'develop' of github.com:nocobase/nocobase into feat-main-datasource-mssql
This commit is contained in:
commit
11578db51c
52
CHANGELOG.md
52
CHANGELOG.md
@ -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/)
|
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.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
|
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||||
|
|
||||||
### 🐛 Bug Fixes
|
### 🐛 Bug Fixes
|
||||||
|
@ -5,6 +5,58 @@
|
|||||||
格式基于 [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.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
|
## [v1.6.14](https://github.com/nocobase/nocobase/compare/v1.6.13...v1.6.14) - 2025-03-29
|
||||||
|
|
||||||
### 🐛 修复
|
### 🐛 修复
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
Updated Date: February 20, 2025
|
Updated Date: April 1, 2025
|
||||||
|
|
||||||
NocoBase License Agreement
|
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.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.
|
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.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.
|
7.7 It is not allowed to publicly sell plugins developed for Software outside of the Marketplace.
|
||||||
|
|
||||||
|
@ -63,7 +63,7 @@ export const SchemaInitializerItem = memo(
|
|||||||
className: className,
|
className: className,
|
||||||
label: children || compile(title),
|
label: children || compile(title),
|
||||||
onClick: (info) => {
|
onClick: (info) => {
|
||||||
if (info.key !== name) return;
|
if (disabled || info.key !== name) return;
|
||||||
if (closeInitializerMenuWhenClick) {
|
if (closeInitializerMenuWhenClick) {
|
||||||
setVisible?.(false);
|
setVisible?.(false);
|
||||||
}
|
}
|
||||||
@ -73,10 +73,10 @@ export const SchemaInitializerItem = memo(
|
|||||||
children: childrenItems,
|
children: childrenItems,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [name, style, className, children, title, onClick, icon, childrenItems]);
|
}, [name, disabled, style, className, children, title, onClick, icon, childrenItems]);
|
||||||
|
|
||||||
if (items && items.length > 0) {
|
if (items && items.length > 0) {
|
||||||
return <SchemaInitializerMenu items={menuItems}></SchemaInitializerMenu>;
|
return <SchemaInitializerMenu disabled={disabled} items={menuItems}></SchemaInitializerMenu>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -31,6 +31,12 @@ export class TextareaFieldInterface extends CollectionFieldInterface {
|
|||||||
titleUsable = true;
|
titleUsable = true;
|
||||||
properties = {
|
properties = {
|
||||||
...defaultProps,
|
...defaultProps,
|
||||||
|
trim: {
|
||||||
|
type: 'boolean',
|
||||||
|
'x-content': '{{t("Automatically remove heading and tailing spaces")}}',
|
||||||
|
'x-decorator': 'FormItem',
|
||||||
|
'x-component': 'Checkbox',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
schemaInitialize(schema: ISchema, { block }) {
|
schemaInitialize(schema: ISchema, { block }) {
|
||||||
if (['Table', 'Kanban'].includes(block)) {
|
if (['Table', 'Kanban'].includes(block)) {
|
||||||
|
@ -104,12 +104,31 @@ const CollectionFieldInternalField = (props) => {
|
|||||||
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
|
const dynamicProps = useDynamicComponentProps(uiSchema?.['x-use-component-props'], props);
|
||||||
|
|
||||||
useEffect(() => {
|
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.
|
* There seems to be a bug in formily where after setting a field to readPretty, switching to editable,
|
||||||
// This code is meant to fix this issue.
|
* 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) {
|
if (fieldSchema['x-read-pretty'] === true && !field.readPretty) {
|
||||||
field.readPretty = true;
|
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 = field.data || {};
|
||||||
field.data.dataSource = uiSchema?.enum;
|
field.data.dataSource = uiSchema?.enum;
|
||||||
}, [field, fieldSchema]);
|
}, [field, fieldSchema]);
|
||||||
|
@ -107,7 +107,7 @@ AssociationFilter.BlockDesigner = AssociationFilterBlockDesigner;
|
|||||||
AssociationFilter.useAssociationField = () => {
|
AssociationFilter.useAssociationField = () => {
|
||||||
const fieldSchema = useFieldSchema();
|
const fieldSchema = useFieldSchema();
|
||||||
const collection = useCollection();
|
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 {
|
export class AssociationFilterPlugin extends Plugin {
|
||||||
|
@ -45,16 +45,16 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<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%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-label="block-item-demo title"
|
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"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<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
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -84,7 +84,7 @@ describe('CollectionSelect', () => {
|
|||||||
class="ant-formily-item-control-content-component"
|
class="ant-formily-item-control-content-component"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
data-testid="select-collection"
|
||||||
role="button"
|
role="button"
|
||||||
>
|
>
|
||||||
@ -98,31 +98,27 @@ describe('CollectionSelect', () => {
|
|||||||
class="ant-select-selector"
|
class="ant-select-selector"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="ant-select-selection-wrap"
|
class="ant-select-selection-search"
|
||||||
>
|
>
|
||||||
<span
|
<input
|
||||||
class="ant-select-selection-search"
|
aria-autocomplete="list"
|
||||||
>
|
aria-controls="rc_select_TEST_OR_SSR_list"
|
||||||
<input
|
aria-expanded="false"
|
||||||
aria-autocomplete="list"
|
aria-haspopup="listbox"
|
||||||
aria-controls="rc_select_TEST_OR_SSR_list"
|
aria-owns="rc_select_TEST_OR_SSR_list"
|
||||||
aria-expanded="false"
|
autocomplete="off"
|
||||||
aria-haspopup="listbox"
|
class="ant-select-selection-search-input"
|
||||||
aria-owns="rc_select_TEST_OR_SSR_list"
|
id="rc_select_TEST_OR_SSR"
|
||||||
autocomplete="off"
|
role="button"
|
||||||
class="ant-select-selection-search-input"
|
type="search"
|
||||||
id="rc_select_TEST_OR_SSR"
|
value=""
|
||||||
role="button"
|
/>
|
||||||
type="search"
|
</span>
|
||||||
value=""
|
<span
|
||||||
/>
|
class="ant-select-selection-item"
|
||||||
</span>
|
title="Users"
|
||||||
<span
|
>
|
||||||
class="ant-select-selection-item"
|
Users
|
||||||
title="Users"
|
|
||||||
>
|
|
||||||
Users
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<span
|
||||||
@ -186,16 +182,16 @@ describe('CollectionSelect', () => {
|
|||||||
expect(container).toMatchInlineSnapshot(`
|
expect(container).toMatchInlineSnapshot(`
|
||||||
<div>
|
<div>
|
||||||
<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%;"
|
style="height: 100%;"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
aria-label="block-item-demo title"
|
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"
|
role="button"
|
||||||
>
|
>
|
||||||
<div
|
<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
|
<div
|
||||||
class="ant-formily-item-label"
|
class="ant-formily-item-label"
|
||||||
@ -226,7 +222,7 @@ describe('CollectionSelect', () => {
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="ant-tag css-dev-only-do-not-override-1rquknz"
|
class="ant-tag css-dev-only-do-not-override-qu8jc9"
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
</span>
|
</span>
|
||||||
|
@ -37,6 +37,15 @@ const formItemWrapCss = css`
|
|||||||
.ant-description-textarea img {
|
.ant-description-textarea img {
|
||||||
max-width: 100%;
|
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`
|
const formItemLabelCss = css`
|
||||||
@ -44,7 +53,7 @@ const formItemLabelCss = css`
|
|||||||
padding: 0px !important;
|
padding: 0px !important;
|
||||||
}
|
}
|
||||||
> .ant-formily-item-label {
|
> .ant-formily-item-label {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -83,7 +92,7 @@ export const FormItem: any = withDynamicSchemaProps(
|
|||||||
[formItemLabelCss]: showTitle === false,
|
[formItemLabelCss]: showTitle === false,
|
||||||
});
|
});
|
||||||
}, [showTitle]);
|
}, [showTitle]);
|
||||||
|
console.log(className);
|
||||||
// 联动规则中的“隐藏保留值”的效果
|
// 联动规则中的“隐藏保留值”的效果
|
||||||
if (field.data?.hidden) {
|
if (field.data?.hidden) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -55,4 +55,18 @@ describe('text field', () => {
|
|||||||
});
|
});
|
||||||
await Test.sync();
|
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',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -159,6 +159,7 @@ describe('has many repository', () => {
|
|||||||
name: 'posts',
|
name: 'posts',
|
||||||
fields: [
|
fields: [
|
||||||
{ type: 'string', name: 'title' },
|
{ type: 'string', name: 'title' },
|
||||||
|
{ type: 'belongsTo', name: 'user' },
|
||||||
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
{ type: 'belongsToMany', name: 'tags', through: 'posts_tags' },
|
||||||
{ type: 'hasMany', name: 'comments' },
|
{ type: 'hasMany', name: 'comments' },
|
||||||
{ type: 'string', name: 'status' },
|
{ type: 'string', name: 'status' },
|
||||||
@ -480,6 +481,51 @@ describe('has many repository', () => {
|
|||||||
).not.toBeNull();
|
).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 () => {
|
test('destroy by pk', async () => {
|
||||||
const u1 = await User.repository.create({
|
const u1 = await User.repository.create({
|
||||||
values: { name: 'u1' },
|
values: { name: 'u1' },
|
||||||
|
@ -29,26 +29,24 @@ export class DatetimeNoTzField extends Field {
|
|||||||
return DatetimeNoTzTypeMySQL;
|
return DatetimeNoTzTypeMySQL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return DataTypes.STRING;
|
return DataTypes.DATE;
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
beforeSave = async (instance, options) => {
|
||||||
const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options;
|
const { name, defaultToCurrentTime, onUpdateToCurrentTime } = this.options;
|
||||||
|
|
||||||
this.beforeSave = async (instance, options) => {
|
const value = instance.get(name);
|
||||||
const value = instance.get(name);
|
|
||||||
|
|
||||||
if (!value && instance.isNewRecord && defaultToCurrentTime) {
|
if (!value && instance.isNewRecord && defaultToCurrentTime) {
|
||||||
instance.set(name, new Date());
|
instance.set(name, new Date());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (onUpdateToCurrentTime) {
|
if (onUpdateToCurrentTime) {
|
||||||
instance.set(name, new Date());
|
instance.set(name, new Date());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
|
||||||
|
|
||||||
additionalSequelizeOptions(): {} {
|
additionalSequelizeOptions(): {} {
|
||||||
const { name } = this.options;
|
const { name } = this.options;
|
||||||
@ -57,17 +55,14 @@ export class DatetimeNoTzField extends Field {
|
|||||||
const timezone = this.database.options.rawTimezone || '+00:00';
|
const timezone = this.database.options.rawTimezone || '+00:00';
|
||||||
|
|
||||||
const isPg = this.database.inDialect('postgres');
|
const isPg = this.database.inDialect('postgres');
|
||||||
|
const isMySQLCompatibleDialect = this.database.isMySQLCompatibleDialect();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get() {
|
get() {
|
||||||
const val = this.getDataValue(name);
|
const val = this.getDataValue(name);
|
||||||
|
|
||||||
if (val instanceof Date) {
|
if (val instanceof Date) {
|
||||||
if (isPg) {
|
const momentVal = moment(val);
|
||||||
return moment(val).format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
}
|
|
||||||
// format to YYYY-MM-DD HH:mm:ss
|
|
||||||
const momentVal = moment(val).utcOffset(timezone);
|
|
||||||
return momentVal.format('YYYY-MM-DD HH:mm:ss');
|
return momentVal.format('YYYY-MM-DD HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -75,18 +70,24 @@ export class DatetimeNoTzField extends Field {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set(val) {
|
set(val) {
|
||||||
if (typeof val === 'string' && isIso8601(val)) {
|
if (val == null) {
|
||||||
const momentVal = moment(val).utcOffset(timezone);
|
return this.setDataValue(name, null);
|
||||||
val = momentVal.format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (val && val instanceof Date) {
|
const dateOffset = new Date().getTimezoneOffset();
|
||||||
// format to YYYY-MM-DD HH:mm:ss
|
const momentVal = moment(val);
|
||||||
const momentVal = moment(val).utcOffset(timezone);
|
if ((typeof val === 'string' && isIso8601(val)) || val instanceof Date) {
|
||||||
val = momentVal.format('YYYY-MM-DD HH:mm:ss');
|
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);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,20 @@ export class TextField extends Field {
|
|||||||
this.options.defaultValue = null;
|
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 {
|
export interface TextFieldOptions extends BaseColumnFieldOptions {
|
||||||
type: 'text';
|
type: 'text';
|
||||||
length?: 'tiny' | 'medium' | 'long';
|
length?: 'tiny' | 'medium' | 'long';
|
||||||
|
trim?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -72,7 +72,13 @@ export class HasManyRepository extends MultipleRelationRepository {
|
|||||||
const filterResult = this.parseFilter(options['filter'], options);
|
const filterResult = this.parseFilter(options['filter'], options);
|
||||||
|
|
||||||
if (filterResult.include && filterResult.include.length > 0) {
|
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);
|
where.push(filterResult.where);
|
||||||
|
@ -179,9 +179,15 @@ export abstract class MultipleRelationRepository extends RelationRepository {
|
|||||||
return false;
|
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({
|
const instances = await this.find({
|
||||||
filter: filter,
|
...options,
|
||||||
transaction,
|
transaction,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ export default defineCollection({
|
|||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'title',
|
name: 'title',
|
||||||
translation: true,
|
translation: true,
|
||||||
|
trim: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '英文标识',
|
title: '英文标识',
|
||||||
@ -28,6 +29,7 @@ export default defineCollection({
|
|||||||
type: 'uid',
|
type: 'uid',
|
||||||
name: 'name',
|
name: 'name',
|
||||||
unique: true,
|
unique: true,
|
||||||
|
trim: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
comment: '类型标识,如 local/ali-oss 等',
|
comment: '类型标识,如 local/ali-oss 等',
|
||||||
@ -51,12 +53,14 @@ export default defineCollection({
|
|||||||
type: 'text',
|
type: 'text',
|
||||||
name: 'path',
|
name: 'path',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
trim: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
comment: '访问地址前缀',
|
comment: '访问地址前缀',
|
||||||
type: 'string',
|
type: 'string',
|
||||||
name: 'baseUrl',
|
name: 'baseUrl',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
trim: true,
|
||||||
},
|
},
|
||||||
// TODO(feature): 需要使用一个实现了可设置默认值的字段
|
// TODO(feature): 需要使用一个实现了可设置默认值的字段
|
||||||
{
|
{
|
||||||
|
@ -147,6 +147,7 @@ export function AdminPublicFormPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Breadcrumb
|
<Breadcrumb
|
||||||
|
style={{ marginLeft: '10px' }}
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
title: <Link to={`/admin/settings/public-forms`}>{t('Public forms', { ns: NAMESPACE })}</Link>,
|
title: <Link to={`/admin/settings/public-forms`}>{t('Public forms', { ns: NAMESPACE })}</Link>,
|
||||||
|
@ -33,7 +33,7 @@ import {
|
|||||||
import { Input, Modal, Spin } from 'antd';
|
import { Input, Modal, Spin } from 'antd';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import React, { createContext, useContext, useEffect, useMemo, useState } from 'react';
|
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 { useParams } from 'react-router';
|
||||||
import { usePublicSubmitActionProps } from '../hooks';
|
import { usePublicSubmitActionProps } from '../hooks';
|
||||||
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
|
import { UnEnabledFormPlaceholder, UnFoundFormPlaceholder } from './UnEnabledFormPlaceholder';
|
||||||
@ -129,9 +129,6 @@ const PublicFormMessageProvider = ({ children }) => {
|
|||||||
</PublicFormMessageContext.Provider>
|
</PublicFormMessageContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
function isMobile() {
|
|
||||||
return window.matchMedia('(max-width: 768px)').matches;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AssociationFieldMobile = (props) => {
|
const AssociationFieldMobile = (props) => {
|
||||||
return <AssociationField {...props} popupMatchSelectWidth={true} />;
|
return <AssociationField {...props} popupMatchSelectWidth={true} />;
|
||||||
@ -165,7 +162,6 @@ const mobileComponents = {
|
|||||||
function InternalPublicForm() {
|
function InternalPublicForm() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const apiClient = useAPIClient();
|
const apiClient = useAPIClient();
|
||||||
const isMobileMedia = isMobile();
|
|
||||||
const { error, data, loading, run } = useRequest<any>(
|
const { error, data, loading, run } = useRequest<any>(
|
||||||
{
|
{
|
||||||
url: `publicForms:getMeta/${params.name}`,
|
url: `publicForms:getMeta/${params.name}`,
|
||||||
@ -243,7 +239,7 @@ function InternalPublicForm() {
|
|||||||
if (!data?.data) {
|
if (!data?.data) {
|
||||||
return <UnEnabledFormPlaceholder />;
|
return <UnEnabledFormPlaceholder />;
|
||||||
}
|
}
|
||||||
const components = isMobileMedia ? mobileComponents : {};
|
const components = isMobile ? mobileComponents : {};
|
||||||
return (
|
return (
|
||||||
<ACLCustomContext.Provider value={{ allowAll: true }}>
|
<ACLCustomContext.Provider value={{ allowAll: true }}>
|
||||||
<PublicAPIClientProvider>
|
<PublicAPIClientProvider>
|
||||||
|
@ -172,7 +172,7 @@ export class PluginPublicFormsServer extends Plugin {
|
|||||||
skip: true,
|
skip: true,
|
||||||
};
|
};
|
||||||
} else if (
|
} else if (
|
||||||
(actionName === 'list' && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
|
(['list', 'get'].includes(actionName) && ctx.PublicForm['targetCollections'].includes(resourceName)) ||
|
||||||
(collection?.options.template === 'file' && actionName === 'create') ||
|
(collection?.options.template === 'file' && actionName === 'create') ||
|
||||||
(resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) ||
|
(resourceName === 'storages' && ['getBasicInfo', 'createPresignedUrl'].includes(actionName)) ||
|
||||||
(resourceName === 'vditor' && ['check'].includes(actionName)) ||
|
(resourceName === 'vditor' && ['check'].includes(actionName)) ||
|
||||||
|
@ -189,7 +189,7 @@ export class ApprovalPassthroughModeNode {
|
|||||||
this.detailsConfigureFieldsButton = page.getByLabel(
|
this.detailsConfigureFieldsButton = page.getByLabel(
|
||||||
`schema-initializer-Grid-details:configureFields-${collectionName}`,
|
`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.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||||
this.actionsConfigureActionsButton = page.getByLabel(
|
this.actionsConfigureActionsButton = page.getByLabel(
|
||||||
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
|
'schema-initializer-ActionBar-ApprovalProcessAddActionButton-',
|
||||||
@ -262,7 +262,7 @@ export class ApprovalBranchModeNode {
|
|||||||
this.detailsConfigureFieldsButton = page.getByLabel(
|
this.detailsConfigureFieldsButton = page.getByLabel(
|
||||||
`schema-initializer-Grid-details:configureFields-${collectionName}`,
|
`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.actionsConfigureFieldsButton = page.getByLabel('schema-initializer-Grid-FormItemInitializers-approvalRecords');
|
||||||
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
|
this.actionsConfigureActionsButton = page.getByLabel('schema-initializer-ActionBar-');
|
||||||
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
this.addApproveButton = page.getByRole('menuitem', { name: 'Approve' }).getByRole('switch');
|
||||||
|
@ -359,6 +359,50 @@ describe('workflow > triggers > collection', () => {
|
|||||||
const executions = await workflow.getExecutions();
|
const executions = await workflow.getExecutions();
|
||||||
expect(executions.length).toBe(1);
|
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', () => {
|
describe('config.condition', () => {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user