@nocobase/client-v2 2.2.0-beta.9 → 3.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/es/APIClient.d.ts +1 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/acl/ACLProvider.d.ts +10 -0
  4. package/es/authRedirect.d.ts +5 -0
  5. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  7. package/es/components/AttachmentUpload.d.ts +51 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +2 -0
  18. package/es/flow/actions/index.d.ts +1 -1
  19. package/es/flow/actions/linkageRules.d.ts +3 -0
  20. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  21. package/es/flow/admin-shell/admin-layout/NocoBaseLogo.d.ts +10 -0
  22. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  23. package/es/flow/components/FieldAssignValueInput.d.ts +9 -0
  24. package/es/flow/components/FlowRoute.d.ts +2 -2
  25. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/index.d.ts +1 -0
  29. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  30. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  31. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  32. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  33. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  34. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  35. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  36. package/es/flow/models/blocks/table/TableColumnModel.d.ts +1 -0
  37. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  38. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  39. package/es/flow-compat/routeTypes.d.ts +1 -0
  40. package/es/index.d.ts +8 -1
  41. package/es/index.mjs +190 -141
  42. package/es/nocobase-buildin-plugin/currentUserAuthStatus.d.ts +11 -0
  43. package/es/nocobase-buildin-plugin/index.d.ts +9 -0
  44. package/es/settings-app/SettingsApplication.d.ts +18 -0
  45. package/es/settings-app/SettingsBrand.d.ts +16 -0
  46. package/es/settings-app/SettingsBuildInPlugin.d.ts +9 -0
  47. package/es/settings-app/SettingsDocumentRedirect.d.ts +10 -0
  48. package/es/settings-app/SettingsGroupNav.d.ts +17 -0
  49. package/es/settings-app/SettingsPluginSettingsManager.d.ts +14 -0
  50. package/es/settings-app/SettingsRouterManager.d.ts +15 -0
  51. package/es/settings-app/SettingsSearch.d.ts +17 -0
  52. package/es/settings-app/SettingsShell.d.ts +10 -0
  53. package/es/settings-app/isSettingsApp.d.ts +18 -0
  54. package/es/settings-app/runtimePaths.d.ts +18 -0
  55. package/es/settings-app/settingsDocumentPath.d.ts +14 -0
  56. package/es/settings-app/settingsTheme.d.ts +87 -0
  57. package/es/settings-app/useSettingsThemeConfig.d.ts +22 -0
  58. package/es/settings-center/AdminSettingsLayout.d.ts +2 -2
  59. package/es/settings-center/groups.d.ts +66 -0
  60. package/es/settings-center/useSettingsGroups.d.ts +36 -0
  61. package/es/settings-center/useSettingsSearch.d.ts +35 -0
  62. package/es/settings-center/utils.d.ts +24 -0
  63. package/es/theme/index.d.ts +1 -0
  64. package/es/theme/itemActive.d.ts +24 -0
  65. package/es/theme/type.d.ts +9 -0
  66. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  67. package/es/utils/index.d.ts +1 -0
  68. package/lib/index.js +195 -146
  69. package/lib/locale/languageCodes.js +3 -1
  70. package/package.json +9 -7
  71. package/src/APIClient.ts +1 -10
  72. package/src/Application.tsx +4 -0
  73. package/src/BaseApplication.tsx +11 -6
  74. package/src/__tests__/app.test.tsx +73 -0
  75. package/src/__tests__/authRedirect.test.ts +137 -0
  76. package/src/__tests__/browserChecker.test.ts +185 -6
  77. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  78. package/src/__tests__/mockSettingsApplication.ts +29 -0
  79. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +79 -10
  80. package/src/__tests__/plugin-manager.test.tsx +7 -6
  81. package/src/__tests__/settings-application.test.ts +164 -0
  82. package/src/__tests__/settings-auth-routes.test.ts +71 -0
  83. package/src/__tests__/settings-center.test.tsx +38 -37
  84. package/src/__tests__/settings-layout-root.test.tsx +260 -0
  85. package/src/__tests__/settings-runtime-paths.test.ts +99 -0
  86. package/src/__tests__/settings-shell.test.tsx +200 -0
  87. package/src/acl/ACLProvider.tsx +21 -0
  88. package/src/authRedirect.ts +72 -3
  89. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  90. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  91. package/src/collection-manager/field-validation.ts +1 -1
  92. package/src/components/AttachmentUpload.tsx +275 -0
  93. package/src/components/README.md +7 -1
  94. package/src/components/README.zh-CN.md +6 -1
  95. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  96. package/src/components/category-tabs/index.ts +10 -0
  97. package/src/components/form/JsonTextArea.tsx +4 -0
  98. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  99. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  100. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  101. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  102. package/src/components/form/TypedVariableInput.tsx +452 -101
  103. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  104. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  105. package/src/components/form/filter/CollectionFilterItem.tsx +32 -7
  106. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  107. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +38 -0
  108. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  109. package/src/components/form/filter/index.ts +2 -0
  110. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  111. package/src/components/index.ts +2 -0
  112. package/src/flow/FlowPage.tsx +9 -1
  113. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  114. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  115. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  116. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  117. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  118. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  119. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  120. package/src/flow/actions/index.ts +2 -0
  121. package/src/flow/actions/linkageRules.tsx +111 -16
  122. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  123. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +1 -57
  124. package/src/flow/admin-shell/admin-layout/NocoBaseLogo.tsx +77 -0
  125. package/src/flow/admin-shell/admin-layout/TopbarActionsBar.tsx +37 -14
  126. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  127. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +77 -13
  128. package/src/flow/common/Markdown/style.ts +4 -0
  129. package/src/flow/components/BlockItemCard.tsx +2 -2
  130. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  131. package/src/flow/components/FieldAssignValueInput.tsx +91 -20
  132. package/src/flow/components/FlowRoute.tsx +78 -32
  133. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  134. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  135. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  136. package/src/flow/components/filter/VariableFilterItem.tsx +158 -10
  137. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  138. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  139. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +70 -1
  140. package/src/flow/index.ts +3 -0
  141. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  142. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  143. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  144. package/src/flow/models/base/CollectionBlockModel.tsx +6 -2
  145. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  146. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  147. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  148. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  149. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  152. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  153. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  154. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  155. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  156. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +27 -12
  157. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +36 -0
  158. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  159. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  160. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  161. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  162. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  163. package/src/flow/models/blocks/table/TableBlockModel.tsx +4 -2
  164. package/src/flow/models/blocks/table/TableColumnModel.tsx +30 -1
  165. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  166. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  167. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +2 -1
  168. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  169. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  170. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  171. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  172. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  173. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  174. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +20 -10
  175. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +24 -12
  176. package/src/flow/models/topbar/TopbarActionModel.tsx +52 -180
  177. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  178. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  179. package/src/flow-compat/routeTypes.ts +1 -0
  180. package/src/index.ts +14 -1
  181. package/src/locale/languageCodes.ts +3 -1
  182. package/src/nocobase-buildin-plugin/currentUserAuthStatus.ts +58 -0
  183. package/src/nocobase-buildin-plugin/index.tsx +110 -70
  184. package/src/settings-app/SettingsApplication.ts +31 -0
  185. package/src/settings-app/SettingsBrand.tsx +108 -0
  186. package/src/settings-app/SettingsBuildInPlugin.ts +10 -0
  187. package/src/settings-app/SettingsDocumentRedirect.tsx +26 -0
  188. package/src/settings-app/SettingsGroupNav.tsx +124 -0
  189. package/src/settings-app/SettingsPluginSettingsManager.ts +38 -0
  190. package/src/settings-app/SettingsRouterManager.ts +92 -0
  191. package/src/settings-app/SettingsSearch.tsx +238 -0
  192. package/src/settings-app/SettingsShell.tsx +167 -0
  193. package/src/settings-app/isSettingsApp.ts +21 -0
  194. package/src/settings-app/runtimePaths.ts +104 -0
  195. package/src/settings-app/settingsDocumentPath.ts +66 -0
  196. package/src/settings-app/settingsTheme.ts +301 -0
  197. package/src/settings-app/useSettingsThemeConfig.ts +100 -0
  198. package/src/settings-center/AdminSettingsLayout.tsx +122 -141
  199. package/src/settings-center/SystemSettingsPage.tsx +4 -170
  200. package/src/settings-center/groups.ts +108 -0
  201. package/src/settings-center/useSettingsGroups.ts +105 -0
  202. package/src/settings-center/useSettingsSearch.ts +173 -0
  203. package/src/settings-center/utils.tsx +50 -1
  204. package/src/theme/index.tsx +1 -0
  205. package/src/theme/itemActive.ts +31 -0
  206. package/src/theme/type.ts +10 -0
  207. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  208. package/src/utils/index.tsx +1 -0
@@ -1,3 +1,12 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
1
10
  import { render, screen } from '@nocobase/test/client';
2
11
  import { FlowEngine, FlowEngineProvider } from '@nocobase/flow-engine';
3
12
  import { App, ConfigProvider } from 'antd';
@@ -35,3 +44,43 @@ describe('ActionModel rendering', () => {
35
44
  expect(screen.getByRole('button', { name: 'Open details' })).toBeInTheDocument();
36
45
  });
37
46
  });
47
+
48
+ describe('ActionModel disabled state persistence', () => {
49
+ it('drops disabled from persisted props during initialization', () => {
50
+ const engine = new FlowEngine();
51
+ engine.registerModels({ NoIconActionModel });
52
+ const model = engine.createModel<NoIconActionModel>({
53
+ use: 'NoIconActionModel',
54
+ props: { title: 'Open details', disabled: true },
55
+ });
56
+
57
+ expect(model.getProps()).not.toHaveProperty('disabled');
58
+ });
59
+
60
+ it('keeps runtime disabled state without serializing it', () => {
61
+ const engine = new FlowEngine();
62
+ engine.registerModels({ NoIconActionModel });
63
+ const model = engine.createModel<NoIconActionModel>({
64
+ use: 'NoIconActionModel',
65
+ props: { title: 'Open details' },
66
+ });
67
+
68
+ model.setProps('disabled', true);
69
+
70
+ expect(model.getProps().disabled).toBe(true);
71
+ expect(model.serialize().props).toEqual({ title: 'Open details' });
72
+ });
73
+
74
+ it('does not serialize fork-local disabled state', () => {
75
+ const engine = new FlowEngine();
76
+ engine.registerModels({ NoIconActionModel });
77
+ const model = engine.createModel<NoIconActionModel>({
78
+ use: 'NoIconActionModel',
79
+ props: { title: 'Open details' },
80
+ });
81
+ const fork = model.createFork({ disabled: true });
82
+
83
+ expect(fork.getProps().disabled).toBe(true);
84
+ expect(fork.serialize().props).toEqual({ title: 'Open details' });
85
+ });
86
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowEngine, type FlowModelContext, MultiRecordResource, VIEW_ACTIVATED_EVENT } from '@nocobase/flow-engine';
11
+ import { beforeEach, describe, expect, it } from 'vitest';
12
+ import { BlockGridModel } from '../BlockGridModel';
13
+ import { CollectionBlockModel } from '../CollectionBlockModel';
14
+
15
+ class TestBlockGridModel extends BlockGridModel {
16
+ mountForTest() {
17
+ super.onMount();
18
+ }
19
+
20
+ unmountForTest() {
21
+ super.onUnmount();
22
+ }
23
+ }
24
+
25
+ class TestMultiRecordResource extends MultiRecordResource<Record<string, unknown>> {
26
+ refreshCalls = 0;
27
+
28
+ override async refresh(): Promise<void> {
29
+ this.refreshCalls += 1;
30
+ this.emit('refresh');
31
+ }
32
+ }
33
+
34
+ class TestCollectionBlockModel extends CollectionBlockModel {
35
+ createResource(ctx: FlowModelContext) {
36
+ return ctx.createResource(TestMultiRecordResource);
37
+ }
38
+
39
+ mountForTest() {
40
+ super.onMount();
41
+ }
42
+ }
43
+
44
+ describe('BlockGridModel - select scene activation', () => {
45
+ let engine: FlowEngine;
46
+
47
+ beforeEach(() => {
48
+ engine = new FlowEngine();
49
+ engine.context.defineProperty('location', { value: { search: '' } });
50
+ engine.registerModels({ TestBlockGridModel, TestCollectionBlockModel });
51
+
52
+ engine.dataSourceManager.getDataSource('main').addCollection({
53
+ name: 'roles',
54
+ filterTargetKey: 'id',
55
+ fields: [
56
+ { name: 'id', type: 'integer', interface: 'number' },
57
+ { name: 'name', type: 'string', interface: 'input' },
58
+ ],
59
+ });
60
+ });
61
+
62
+ function createModels(scene = 'select') {
63
+ engine.context.defineProperty('view', { value: { inputArgs: { scene } } });
64
+ const grid = engine.createModel<TestBlockGridModel>({ use: 'TestBlockGridModel' });
65
+ const block = grid.addSubModel<TestCollectionBlockModel>('items', {
66
+ use: 'TestCollectionBlockModel',
67
+ stepParams: {
68
+ resourceSettings: {
69
+ init: {
70
+ dataSourceKey: 'main',
71
+ collectionName: 'roles',
72
+ },
73
+ },
74
+ },
75
+ });
76
+ const resource = block.context.resource as TestMultiRecordResource;
77
+
78
+ block.mountForTest();
79
+ grid.mountForTest();
80
+ return { grid, resource };
81
+ }
82
+
83
+ it('refreshes dirty collection blocks once when a nested view closes', async () => {
84
+ const { grid, resource } = createModels();
85
+
86
+ engine.markDataSourceDirty('main', 'roles');
87
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
88
+ await Promise.resolve();
89
+
90
+ expect(resource.refreshCalls).toBe(1);
91
+
92
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
93
+ await Promise.resolve();
94
+
95
+ expect(resource.refreshCalls).toBe(1);
96
+ grid.unmountForTest();
97
+ });
98
+
99
+ it('does not refresh without dirty data or after the grid unmounts', async () => {
100
+ const { grid, resource } = createModels();
101
+
102
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
103
+ await Promise.resolve();
104
+ expect(resource.refreshCalls).toBe(0);
105
+
106
+ grid.unmountForTest();
107
+ engine.markDataSourceDirty('main', 'roles');
108
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
109
+ await Promise.resolve();
110
+
111
+ expect(resource.refreshCalls).toBe(0);
112
+ });
113
+
114
+ it('does not handle activation outside the select scene', async () => {
115
+ const { grid, resource } = createModels('list');
116
+
117
+ engine.markDataSourceDirty('main', 'roles');
118
+ engine.emitter.emit(VIEW_ACTIVATED_EVENT);
119
+ await Promise.resolve();
120
+
121
+ expect(resource.refreshCalls).toBe(0);
122
+ grid.unmountForTest();
123
+ });
124
+ });
@@ -0,0 +1,41 @@
1
+ /**
2
+ * This file is part of the NocoBase (R) project.
3
+ * Copyright (c) 2020-2024 NocoBase Co., Ltd.
4
+ * Authors: NocoBase Team.
5
+ *
6
+ * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
+ * For more information, please refer to: https://www.nocobase.com/agreement.
8
+ */
9
+
10
+ import { FlowEngine, FlowModelContext, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { describe, expect, it } from 'vitest';
12
+ import { CollectionBlockModel } from '../CollectionBlockModel';
13
+
14
+ class AddAppendsCollectionBlockModel extends CollectionBlockModel {
15
+ createResource(ctx: FlowModelContext) {
16
+ return ctx.createResource(MultiRecordResource);
17
+ }
18
+ }
19
+
20
+ describe('CollectionBlockModel addAppends', () => {
21
+ it('does not throw for a nested field path when the collection is unavailable', () => {
22
+ const engine = new FlowEngine();
23
+ engine.registerModels({ AddAppendsCollectionBlockModel });
24
+
25
+ const model = engine.createModel<AddAppendsCollectionBlockModel>({
26
+ uid: 'missing-collection-add-appends-block',
27
+ use: 'AddAppendsCollectionBlockModel',
28
+ stepParams: {
29
+ resourceSettings: {
30
+ init: {
31
+ dataSourceKey: 'main',
32
+ collectionName: 'missing_collection',
33
+ },
34
+ },
35
+ },
36
+ });
37
+
38
+ expect(model.collection).toBeUndefined();
39
+ expect(() => model.addAppends('createdBy.departments')).not.toThrow();
40
+ });
41
+ });
@@ -15,8 +15,10 @@ import {
15
15
  DndProvider,
16
16
  DragHandler,
17
17
  Droppable,
18
+ isCtxDateExpression,
18
19
  isRunJSValue,
19
20
  normalizeRunJSValue,
21
+ parseCtxDateExpression,
20
22
  tExpr,
21
23
  FlowModelRenderer,
22
24
  FlowSettingsButton,
@@ -44,6 +46,15 @@ import { normalizeFilterValueByOperator } from './valueNormalization';
44
46
 
45
47
  const RELATION_FIELD_TYPES = ['belongsTo', 'hasOne', 'hasMany', 'belongsToMany', 'belongsToArray'];
46
48
  const NUMERIC_FIELD_TYPES = ['integer', 'float', 'double', 'decimal'];
49
+ const DATE_FILTER_OPERATORS = new Set([
50
+ '$dateOn',
51
+ '$dateNotOn',
52
+ '$dateBefore',
53
+ '$dateAfter',
54
+ '$dateNotBefore',
55
+ '$dateNotAfter',
56
+ '$dateBetween',
57
+ ]);
47
58
 
48
59
  function getFilterFormFieldMetaType(field: CollectionField) {
49
60
  if (RELATION_FIELD_TYPES.includes(field.type)) {
@@ -66,6 +77,14 @@ function getFilterFormFieldMetaType(field: CollectionField) {
66
77
  }
67
78
  }
68
79
 
80
+ function parseDateFilterDefaultValue(operator: string | undefined, rawValue: unknown) {
81
+ if (!operator || !DATE_FILTER_OPERATORS.has(operator) || !isCtxDateExpression(rawValue)) {
82
+ return undefined;
83
+ }
84
+
85
+ return parseCtxDateExpression(rawValue);
86
+ }
87
+
69
88
  function shouldShowFilterFormFieldMeta(field: CollectionField) {
70
89
  return Boolean(field?.interface);
71
90
  }
@@ -431,6 +450,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
431
450
 
432
451
  private canApplyFormDefaultValue(name: string, current: any, force?: boolean) {
433
452
  if (force) return true;
453
+ if (this.userEditedFieldNames.has(name)) return false;
434
454
  if (isEmptyValue(current)) return true;
435
455
  if (!this.lastDefaultValueByFieldName.has(name)) return false;
436
456
  return isEqual(current, this.lastDefaultValueByFieldName.get(name));
@@ -488,7 +508,7 @@ export class FilterFormBlockModel extends FilterBlockModel<{
488
508
  const rules = (params?.value || []) as any[];
489
509
  if (!Array.isArray(rules) || rules.length === 0) return appliedValues;
490
510
 
491
- const resolveValue = async (raw: any) => {
511
+ const resolveValue = async (raw: any, operator?: string) => {
492
512
  // RunJS support
493
513
  if (isRunJSValue(raw)) {
494
514
  const { code, version } = normalizeRunJSValue(raw);
@@ -496,6 +516,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
496
516
  return ret?.success ? ret.value : undefined;
497
517
  }
498
518
 
519
+ const parsedDateFilterValue = parseDateFilterDefaultValue(operator, raw);
520
+ if (typeof parsedDateFilterValue !== 'undefined') {
521
+ return parsedDateFilterValue;
522
+ }
523
+
499
524
  return await (this.context as any).resolveJsonTemplate?.(raw);
500
525
  };
501
526
 
@@ -519,11 +544,11 @@ export class FilterFormBlockModel extends FilterBlockModel<{
519
544
 
520
545
  const current = (form as any).getFieldValue?.(name);
521
546
 
522
- const resolved = await resolveValue(rule.value);
547
+ const operator = getDefaultOperator(itemModel as any);
548
+ const resolved = await resolveValue(rule.value, operator);
523
549
  if (options?.refreshSeq && options.refreshSeq !== this.defaultValuesRefreshSeq) return appliedValues;
524
550
  if (typeof resolved === 'undefined') continue;
525
551
 
526
- const operator = getDefaultOperator(itemModel as any);
527
552
  const normalized = normalizeFilterValueByOperator(operator, resolved);
528
553
  const mode = this.normalizeFieldValueMode(rule.mode);
529
554
  if (mode === 'default' && !this.canApplyFormDefaultValue(String(name), current, force)) continue;
@@ -62,9 +62,25 @@ function isRecord(value: unknown): value is Record<string, unknown> {
62
62
  return !!value && typeof value === 'object' && !Array.isArray(value);
63
63
  }
64
64
 
65
+ interface CollectionFieldWithAssociationMarker {
66
+ isAssociationField?: () => boolean;
67
+ target?: unknown;
68
+ }
69
+
70
+ function hasAssociationMarker(value: unknown): value is CollectionFieldWithAssociationMarker {
71
+ return isRecord(value) && (typeof value.isAssociationField === 'function' || 'target' in value);
72
+ }
73
+
74
+ const isAssociationCollectionField = (collectionField: unknown) => {
75
+ if (!hasAssociationMarker(collectionField)) {
76
+ return false;
77
+ }
78
+ return collectionField.isAssociationField ? collectionField.isAssociationField() : Boolean(collectionField.target);
79
+ };
80
+
65
81
  const normalizeAssociationDefaultFilterValue = (value: any, fieldModel: any) => {
66
82
  const collectionField = fieldModel?.context?.collectionField;
67
- if (!collectionField?.isAssociationField?.()) {
83
+ if (!isAssociationCollectionField(collectionField)) {
68
84
  return value;
69
85
  }
70
86
 
@@ -171,8 +187,7 @@ const buildFilterFormFieldItem = ({
171
187
  if (!binding) {
172
188
  return;
173
189
  }
174
- const isAssociation =
175
- typeof field?.isAssociationField === 'function' ? field.isAssociationField() : Boolean(field?.target);
190
+ const isAssociation = isAssociationCollectionField(field);
176
191
  const fieldModel =
177
192
  isAssociation && ctxWithFlags.engine?.getModelClass?.('FilterFormRecordSelectFieldModel')
178
193
  ? 'FilterFormRecordSelectFieldModel'
@@ -507,10 +522,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
507
522
  const formValue = this.context.form?.getFieldValue(this.props.name);
508
523
  const modelValue = fieldModel.getFilterValue ? fieldModel.getFilterValue() : formValue;
509
524
  const collectionField = (fieldModel as any)?.context?.collectionField;
510
- const isAssociationField =
511
- typeof collectionField?.isAssociationField === 'function'
512
- ? collectionField.isAssociationField()
513
- : !!collectionField?.target;
525
+ const isAssociationField = isAssociationCollectionField(collectionField);
514
526
  const shouldUseFormValue =
515
527
  isAssociationField &&
516
528
  !this.mounted &&
@@ -547,11 +559,7 @@ export class FilterFormItemModel extends FilterableItemModel<{
547
559
  return value;
548
560
  }
549
561
  const collectionField = (fieldModel as any)?.context?.collectionField;
550
- const isAssociation =
551
- typeof collectionField?.isAssociationField === 'function'
552
- ? collectionField.isAssociationField()
553
- : !!collectionField?.target;
554
- if (!isAssociation) {
562
+ if (!isAssociationCollectionField(collectionField)) {
555
563
  return value;
556
564
  }
557
565
  const normalizedFieldNames = normalizeAssociationFieldNames(
@@ -798,6 +806,13 @@ FilterFormItemModel.registerFlow({
798
806
  },
799
807
  defaultOperator: {
800
808
  use: 'defaultOperator',
809
+ hideInSettings(ctx) {
810
+ const collectionField =
811
+ ctx.collectionField ||
812
+ ctx.model?.context?.collectionField ||
813
+ ctx.model?.subModels?.field?.context?.collectionField;
814
+ return isAssociationCollectionField(collectionField);
815
+ },
801
816
  },
802
817
  operatorComponentProps: {
803
818
  use: 'operatorComponentProps',
@@ -33,6 +33,42 @@ class DummyCollectionBlockModel extends CollectionBlockModel {
33
33
  }
34
34
 
35
35
  describe('FilterFormItemModel defineChildren association fields', () => {
36
+ it('hides default operator setting for association filter fields', () => {
37
+ const engine = new FlowEngine();
38
+ engine.registerModels({
39
+ FilterFormItemModel,
40
+ });
41
+
42
+ const filterItem = engine.createModel<FilterFormItemModel>({
43
+ uid: 'association-filter-item-settings',
44
+ use: 'FilterFormItemModel',
45
+ });
46
+
47
+ const defaultOperatorStep = filterItem.getFlow('filterFormItemSettings')?.steps?.defaultOperator as {
48
+ hideInSettings?: (ctx: {
49
+ collectionField?: unknown;
50
+ model?: {
51
+ subModels?: {
52
+ field?: {
53
+ context?: {
54
+ collectionField?: unknown;
55
+ };
56
+ };
57
+ };
58
+ };
59
+ }) => boolean;
60
+ };
61
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { isAssociationField: () => true } })).toBe(true);
62
+ expect(
63
+ defaultOperatorStep?.hideInSettings?.({
64
+ model: { subModels: { field: { context: { collectionField: { target: 'departments' } } } } },
65
+ }),
66
+ ).toBe(true);
67
+ expect(defaultOperatorStep?.hideInSettings?.({ collectionField: { interface: 'input', type: 'string' } })).toBe(
68
+ false,
69
+ );
70
+ });
71
+
36
72
  it('groups association target fields and supports recursive paths', async () => {
37
73
  const engine = new FlowEngine();
38
74
  engine.registerModels({
@@ -17,7 +17,15 @@ import { FilterFormBlockModel } from '../FilterFormBlockModel';
17
17
  function resolveTemplateValue(raw: any, values: Record<string, any>): any {
18
18
  if (typeof raw === 'string') {
19
19
  const matched = raw.match(/^\{\{\s*ctx\.formValues\.([^}]+?)\s*\}\}$/);
20
- return matched ? values[matched[1]] : raw;
20
+ if (matched) {
21
+ return values[matched[1]];
22
+ }
23
+
24
+ if (/^\{\{\s*ctx\.date\.relative\.past\.day\.n7\s*\}\}$/.test(raw)) {
25
+ return '2026-06-15';
26
+ }
27
+
28
+ return raw;
21
29
  }
22
30
  if (Array.isArray(raw)) {
23
31
  return raw.map((item) => resolveTemplateValue(item, values));
@@ -30,7 +38,7 @@ function resolveTemplateValue(raw: any, values: Record<string, any>): any {
30
38
 
31
39
  function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<string, any> = {}) {
32
40
  const values = { ...initialValues };
33
- const createItem = (fieldPath: string, uid: string) => ({
41
+ const createItem = (fieldPath: string, uid: string, operator?: string) => ({
34
42
  uid,
35
43
  fieldPath,
36
44
  props: { name: `${fieldPath}_${uid}` },
@@ -44,7 +52,7 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
44
52
  return undefined;
45
53
  },
46
54
  subModels: {
47
- field: {},
55
+ field: operator ? { operator } : {},
48
56
  },
49
57
  });
50
58
  const model = {
@@ -76,7 +84,11 @@ function createFilterFormDefaultValuesModel(rules: any[], initialValues: Record<
76
84
  subModels: {
77
85
  grid: {
78
86
  subModels: {
79
- items: [createItem('nickname', 'nick'), createItem('username', 'user')],
87
+ items: [
88
+ createItem('nickname', 'nick'),
89
+ createItem('username', 'user'),
90
+ createItem('birthdate_tz', 'birthdate', '$dateOn'),
91
+ ],
80
92
  },
81
93
  },
82
94
  },
@@ -301,6 +313,40 @@ describe('filter-form defaultValues wiring', () => {
301
313
  expect(values.username_user).toBe('Manual');
302
314
  });
303
315
 
316
+ it('does not reapply a filter form default value after user clears the field', async () => {
317
+ const { model, values } = createFilterFormDefaultValuesModel([
318
+ {
319
+ key: 'username-default',
320
+ enable: true,
321
+ targetPath: 'username',
322
+ mode: 'default',
323
+ value: 'admin',
324
+ },
325
+ ]);
326
+
327
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
328
+ expect(values.username_user).toBe('admin');
329
+
330
+ values.username_user = undefined;
331
+ (model as any).handleFilterFormValuesChange({ username_user: undefined }, { username_user: undefined });
332
+
333
+ await waitFor(() => {
334
+ expect(model.dispatchEvent).toHaveBeenCalledWith(
335
+ 'formValuesChange',
336
+ {
337
+ changedValues: {
338
+ username_user: undefined,
339
+ },
340
+ allValues: {
341
+ username_user: undefined,
342
+ },
343
+ },
344
+ { debounce: true },
345
+ );
346
+ });
347
+ expect(values.username_user).toBeUndefined();
348
+ });
349
+
304
350
  it('applies fixed values even when the target filter field already has a value', async () => {
305
351
  const { model, values } = createFilterFormDefaultValuesModel(
306
352
  [
@@ -320,6 +366,22 @@ describe('filter-form defaultValues wiring', () => {
320
366
  expect(values.username_user).toBe('Bob');
321
367
  });
322
368
 
369
+ it('preserves relative date descriptors for date filter default values', async () => {
370
+ const { model, values } = createFilterFormDefaultValuesModel([
371
+ {
372
+ key: 'birthdate-default',
373
+ enable: true,
374
+ targetPath: 'birthdate_tz',
375
+ mode: 'assign',
376
+ value: '{{ ctx.date.relative.past.day.n7 }}',
377
+ },
378
+ ]);
379
+
380
+ await FilterFormBlockModel.prototype.applyFormDefaultValues.call(model as any);
381
+
382
+ expect(values.birthdate_tz_birthdate).toEqual({ type: 'past', unit: 'day', number: 7 });
383
+ });
384
+
323
385
  it('applies override values until the target filter field is changed by user', async () => {
324
386
  const { model, values } = createFilterFormDefaultValuesModel(
325
387
  [