@nocobase/client-v2 2.3.0-alpha.1 → 2.3.0-beta.10

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 (171) hide show
  1. package/es/APIClient.d.ts +12 -0
  2. package/es/components/form/ScanInput/useCodeScanner.d.ts +12 -2
  3. package/es/components/form/ScanInput/zxingWasmDecoder.d.ts +9 -0
  4. package/es/flow/actions/linkageRules.d.ts +25 -0
  5. package/es/flow/components/FieldAssignValueInput.d.ts +6 -30
  6. package/es/flow/components/FlowRoute.d.ts +2 -2
  7. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  8. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +32 -0
  9. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  10. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  11. package/es/flow/components/filter/VariableFilterItem.d.ts +7 -0
  12. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  13. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  14. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  15. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  16. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  17. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  18. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  19. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  20. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  21. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  22. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  23. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  24. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  25. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  26. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  27. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  28. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  29. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  30. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  31. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  32. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  33. package/es/flow-compat/passwordUtils.d.ts +1 -1
  34. package/es/index.d.ts +1 -0
  35. package/es/index.mjs +148 -145
  36. package/es/ui-operation/index.d.ts +15 -0
  37. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  38. package/lib/index.js +170 -167
  39. package/lib/locale/languageCodes.js +1 -0
  40. package/package.json +10 -8
  41. package/src/APIClient.ts +92 -0
  42. package/src/Application.tsx +3 -1
  43. package/src/__tests__/APIClient.test.tsx +58 -0
  44. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +7 -2
  45. package/src/__tests__/settings-center.test.tsx +270 -6
  46. package/src/acl/ACLProvider.tsx +2 -0
  47. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  48. package/src/collection-manager/__tests__/field-configure.test.ts +52 -0
  49. package/src/collection-manager/field-configure.ts +2 -2
  50. package/src/components/form/ScanInput/CodeScanner.tsx +64 -35
  51. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +107 -0
  52. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +294 -24
  53. package/src/components/form/ScanInput/__tests__/zxingWasmDecoder.test.ts +78 -0
  54. package/src/components/form/ScanInput/useCodeScanner.ts +281 -23
  55. package/src/components/form/ScanInput/zxingWasmDecoder.ts +64 -0
  56. package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
  57. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
  58. package/src/components/form/table/Table.tsx +81 -6
  59. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  60. package/src/flow/__tests__/FlowRoute.test.tsx +126 -8
  61. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  62. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  63. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  64. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  65. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  66. package/src/flow/actions/linkageRules.tsx +59 -16
  67. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  68. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  69. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  70. package/src/flow/common/Markdown/style.ts +4 -0
  71. package/src/flow/components/BlockItemCard.tsx +2 -2
  72. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  73. package/src/flow/components/FlowRoute.tsx +71 -29
  74. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  75. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  76. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  77. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  78. package/src/flow/components/field-value-variable/index.ts +12 -0
  79. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  80. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  82. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  83. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  84. package/src/flow/index.ts +2 -0
  85. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  86. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  87. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  88. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  89. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  90. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  91. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  92. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  93. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  94. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  95. package/src/flow/models/base/GridModel.tsx +0 -1
  96. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  97. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  98. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  99. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  100. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  102. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  103. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  104. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  105. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  106. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  107. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  108. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  109. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  110. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  111. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  117. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  118. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  119. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  120. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  121. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  122. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  123. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  124. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  125. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  126. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  127. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  128. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  129. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  130. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  131. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  132. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  133. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  134. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  136. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  137. package/src/flow/models/blocks/table/utils.ts +7 -0
  138. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  139. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  142. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  143. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  144. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  145. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  146. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  147. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  148. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  149. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  150. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  151. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  152. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  153. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  155. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  156. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  157. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  158. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  159. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  160. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  161. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  162. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  163. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  164. package/src/index.ts +1 -0
  165. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  166. package/src/layout-manager/LayoutRoute.tsx +2 -1
  167. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  168. package/src/locale/languageCodes.ts +1 -0
  169. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  170. package/src/ui-operation/index.ts +33 -0
  171. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -0,0 +1,314 @@
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 } from '@nocobase/flow-engine';
11
+ import { describe, expect, it, vi } from 'vitest';
12
+ import { ActionModel as ConfiguredActionModel } from '../../models/base/ActionModel';
13
+ import { ActionModel } from '../../models/base/ActionModelCore';
14
+ import { actionLinkageRules, linkageSetActionProps, updateLinkageRules } from '../linkageRules';
15
+
16
+ class TestActionModel extends ActionModel {}
17
+
18
+ describe('action linkage rules on action forks', () => {
19
+ it('does not snapshot unrelated master props when disabling an action', async () => {
20
+ const engine = new FlowEngine();
21
+ engine.registerModels({ TestActionModel });
22
+ const master = engine.createModel<TestActionModel>({
23
+ use: 'TestActionModel',
24
+ props: { title: 'Edit' },
25
+ });
26
+ const fork = master.createFork({ className: 'row-action' });
27
+
28
+ const ctx = {
29
+ flowKey: 'buttonSettings',
30
+ model: fork,
31
+ app: {
32
+ jsonLogic: {
33
+ apply: vi.fn(() => true),
34
+ },
35
+ },
36
+ t: (value: string) => value,
37
+ resolveJsonTemplate: vi.fn(async (value: unknown) => value),
38
+ getAction: (name: string) => {
39
+ if (name !== 'linkageSetActionProps') return undefined;
40
+ return {
41
+ handler: async (_ctx: unknown, params: { setProps: Function }) => {
42
+ params.setProps(fork, { disabled: true });
43
+ },
44
+ };
45
+ },
46
+ } as never;
47
+
48
+ await actionLinkageRules.handler(ctx, {
49
+ value: [
50
+ {
51
+ key: 'disable-edit',
52
+ enable: true,
53
+ condition: { logic: '$and', items: [] },
54
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
55
+ },
56
+ ],
57
+ });
58
+
59
+ expect(fork.localProps.title).toBeUndefined();
60
+ expect(fork.__originalProps.title).toBeUndefined();
61
+ expect(fork.localProps.disabled).toBe(true);
62
+
63
+ master.setProps('title', 'Updated');
64
+ await actionLinkageRules.handler(ctx, {
65
+ value: [
66
+ {
67
+ key: 'disable-edit',
68
+ enable: true,
69
+ condition: { logic: '$and', items: [] },
70
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
71
+ },
72
+ ],
73
+ });
74
+
75
+ expect(fork.getProps().title).toBe('Updated');
76
+ expect(fork.serialize().props).toMatchObject({ title: 'Updated' });
77
+ expect(fork.serialize().props).not.toHaveProperty('disabled');
78
+
79
+ const refreshedFork = master.createFork({ className: 'row-action' });
80
+ expect(refreshedFork.getProps().title).toBe('Updated');
81
+ });
82
+
83
+ it.each(['disable', 'delete'] as const)(
84
+ 'restores a row action when linkage rules are %sd without persisting disabled',
85
+ async (change) => {
86
+ const engine = new FlowEngine();
87
+ engine.registerModels({ ConfiguredActionModel });
88
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
89
+
90
+ let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
91
+ engine.setModelRepository({
92
+ save: async (model) => {
93
+ savedData = model.serialize();
94
+ return savedData;
95
+ },
96
+ } as Parameters<FlowEngine['setModelRepository']>[0]);
97
+
98
+ const enabledRules = [
99
+ {
100
+ key: 'disable-edit',
101
+ enable: true,
102
+ condition: { logic: '$and', items: [] },
103
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
104
+ },
105
+ ];
106
+ const master = engine.createModel<ConfiguredActionModel>({
107
+ use: 'ConfiguredActionModel',
108
+ props: { title: 'Edit' },
109
+ stepParams: {
110
+ buttonSettings: {
111
+ linkageRules: { value: enabledRules },
112
+ },
113
+ },
114
+ });
115
+ const fork = master.createFork({ className: 'row-action' });
116
+
117
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
118
+ expect(fork.getProps().disabled).toBe(true);
119
+
120
+ const nextRules =
121
+ change === 'disable'
122
+ ? updateLinkageRules(enabledRules, (rules) => {
123
+ rules[0].enable = false;
124
+ })
125
+ : [];
126
+ fork.setStepParams('buttonSettings', 'linkageRules', { value: nextRules });
127
+ await fork.saveStepParams();
128
+ await fork.rerender();
129
+
130
+ expect(savedData).toBeDefined();
131
+ expect(savedData?.props).not.toHaveProperty('disabled');
132
+ expect(savedData?.stepParams.buttonSettings.linkageRules.value).toEqual(nextRules);
133
+ expect(fork.getProps().disabled).toBeUndefined();
134
+
135
+ const reloadedEngine = new FlowEngine();
136
+ reloadedEngine.registerModels({ ConfiguredActionModel });
137
+ reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
138
+ const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
139
+ use: 'ConfiguredActionModel',
140
+ props: savedData?.props,
141
+ stepParams: savedData?.stepParams,
142
+ });
143
+ const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
144
+
145
+ await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
146
+ expect(reloadedFork.getProps().disabled).toBeUndefined();
147
+ },
148
+ );
149
+
150
+ it.each(['disabled', 'deleted'] as const)(
151
+ 'ignores a historical disabled prop after the linkage rule is %s',
152
+ async (state) => {
153
+ const engine = new FlowEngine();
154
+ engine.registerModels({ ConfiguredActionModel });
155
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
156
+ const rules =
157
+ state === 'disabled'
158
+ ? [
159
+ {
160
+ key: 'disable-edit',
161
+ enable: false,
162
+ condition: { logic: '$and', items: [] },
163
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
164
+ },
165
+ ]
166
+ : [];
167
+ const master = engine.createModel<ConfiguredActionModel>({
168
+ use: 'ConfiguredActionModel',
169
+ props: { title: 'Edit', disabled: true },
170
+ stepParams: {
171
+ buttonSettings: {
172
+ linkageRules: { value: rules },
173
+ },
174
+ },
175
+ });
176
+ const fork = master.createFork({ className: 'row-action' });
177
+
178
+ expect(master.getProps()).not.toHaveProperty('disabled');
179
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
180
+
181
+ expect(fork.getProps().disabled).toBeUndefined();
182
+ },
183
+ );
184
+
185
+ it('still applies an enabled linkage rule after dropping a historical disabled prop', async () => {
186
+ const engine = new FlowEngine();
187
+ engine.registerModels({ ConfiguredActionModel });
188
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
189
+ const master = engine.createModel<ConfiguredActionModel>({
190
+ use: 'ConfiguredActionModel',
191
+ props: { title: 'Edit', disabled: true },
192
+ stepParams: {
193
+ buttonSettings: {
194
+ linkageRules: {
195
+ value: [
196
+ {
197
+ key: 'disable-edit',
198
+ enable: true,
199
+ condition: { logic: '$and', items: [] },
200
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
201
+ },
202
+ ],
203
+ },
204
+ },
205
+ },
206
+ });
207
+ const fork = master.createFork({ className: 'row-action' });
208
+
209
+ expect(master.getProps()).not.toHaveProperty('disabled');
210
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
211
+
212
+ expect(fork.getProps().disabled).toBe(true);
213
+ });
214
+
215
+ it('preserves a default disabled state as runtime-only state', async () => {
216
+ class DefaultDisabledActionModel extends ConfiguredActionModel {
217
+ defaultProps = { ...this.defaultProps, disabled: true };
218
+ }
219
+ const engine = new FlowEngine();
220
+ engine.registerModels({ DefaultDisabledActionModel });
221
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
222
+ const master = engine.createModel<DefaultDisabledActionModel>({
223
+ use: 'DefaultDisabledActionModel',
224
+ props: { title: 'Edit' },
225
+ stepParams: {
226
+ buttonSettings: {
227
+ linkageRules: { value: [] },
228
+ },
229
+ },
230
+ });
231
+ const fork = master.createFork({ className: 'row-action' });
232
+
233
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
234
+
235
+ expect(fork.getProps().disabled).toBe(true);
236
+ expect(fork.serialize().props).not.toHaveProperty('disabled');
237
+ });
238
+
239
+ it('updates a disabled row action title through the real beforeRender flow', async () => {
240
+ const engine = new FlowEngine();
241
+ engine.registerModels({ ConfiguredActionModel });
242
+ engine.registerActions({ actionLinkageRules, linkageSetActionProps });
243
+ let savedData: ReturnType<ConfiguredActionModel['serialize']> | undefined;
244
+ engine.setModelRepository({
245
+ save: async (model) => {
246
+ savedData = model.serialize();
247
+ return savedData;
248
+ },
249
+ } as Parameters<FlowEngine['setModelRepository']>[0]);
250
+ const master = engine.createModel<ConfiguredActionModel>({
251
+ use: 'ConfiguredActionModel',
252
+ props: { title: 'Edit' },
253
+ stepParams: {
254
+ buttonSettings: {
255
+ general: { title: 'Edit' },
256
+ linkageRules: {
257
+ value: [
258
+ {
259
+ key: 'disable-edit',
260
+ enable: true,
261
+ condition: { logic: '$and', items: [] },
262
+ actions: [{ name: 'linkageSetActionProps', params: { value: 'disabled' } }],
263
+ },
264
+ ],
265
+ },
266
+ },
267
+ },
268
+ });
269
+ const fork = master.createFork({ className: 'row-action' });
270
+
271
+ await fork.dispatchEvent('beforeRender', undefined, { useCache: false });
272
+ expect(fork.getProps()).toMatchObject({ title: 'Edit', disabled: true });
273
+
274
+ fork.setStepParams('buttonSettings', 'general', { title: 'Updated' });
275
+ await fork.saveStepParams();
276
+ await fork.rerender();
277
+
278
+ expect(savedData?.props).not.toHaveProperty('disabled');
279
+ expect(savedData?.stepParams.buttonSettings.general.title).toBe('Updated');
280
+ expect(fork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
281
+
282
+ const reloadedEngine = new FlowEngine();
283
+ reloadedEngine.registerModels({ ConfiguredActionModel });
284
+ reloadedEngine.registerActions({ actionLinkageRules, linkageSetActionProps });
285
+ const reloadedMaster = reloadedEngine.createModel<ConfiguredActionModel>({
286
+ use: 'ConfiguredActionModel',
287
+ props: savedData?.props,
288
+ stepParams: savedData?.stepParams,
289
+ });
290
+ const reloadedFork = reloadedMaster.createFork({ className: 'row-action' });
291
+
292
+ await reloadedFork.dispatchEvent('beforeRender', undefined, { useCache: false });
293
+ expect(reloadedFork.getProps()).toMatchObject({ title: 'Updated', disabled: true });
294
+ });
295
+
296
+ it('creates a new rules value when disabling a linkage rule', () => {
297
+ const rules = [
298
+ {
299
+ key: 'disable-edit',
300
+ title: 'Linkage rule',
301
+ enable: true,
302
+ condition: { logic: '$and', items: [] },
303
+ actions: [],
304
+ },
305
+ ];
306
+ const nextRules = updateLinkageRules(rules, (next) => {
307
+ next[0].enable = false;
308
+ });
309
+
310
+ expect(nextRules).toEqual([expect.objectContaining({ key: 'disable-edit', enable: false })]);
311
+ expect(nextRules).not.toBe(rules);
312
+ expect(rules[0].enable).toBe(true);
313
+ });
314
+ });
@@ -225,6 +225,108 @@ describe('ensureFormValueDrivenDataScopeClear', () => {
225
225
  expect(onChange).toHaveBeenCalledWith(null);
226
226
  });
227
227
 
228
+ it('does not clear a popup form field when data scope depends on the external parent item', () => {
229
+ const emitter = new EventEmitter();
230
+ const formBlock = {
231
+ uid: 'form-1',
232
+ disposed: false,
233
+ emitter,
234
+ context: {
235
+ form: {},
236
+ formValues: { staff_m2o: null },
237
+ },
238
+ };
239
+
240
+ const onChange = vi.fn();
241
+ const model: any = {
242
+ disposed: false,
243
+ props: {
244
+ value: { id: 10 },
245
+ onChange,
246
+ },
247
+ context: {
248
+ blockModel: formBlock,
249
+ },
250
+ };
251
+
252
+ const ctx: any = {
253
+ model,
254
+ flowKey: 'selectSettings',
255
+ };
256
+
257
+ const filter = {
258
+ logic: '$and',
259
+ items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.parentItem.value.org_m2o.id }}' }],
260
+ };
261
+
262
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
263
+
264
+ emitter.emit('formValuesChange', {
265
+ changedPaths: [['staff_m2o']],
266
+ allValues: { staff_m2o: { id: 10 } },
267
+ });
268
+
269
+ expect(onChange).not.toHaveBeenCalled();
270
+ });
271
+
272
+ it('maps popup current item dependencies to the popup form root', () => {
273
+ const emitter = new EventEmitter();
274
+ const formBlock = {
275
+ uid: 'form-1',
276
+ disposed: false,
277
+ emitter,
278
+ context: {
279
+ form: {},
280
+ formValues: {
281
+ org_m2o: { id: 1 },
282
+ staff_m2o: { id: 10 },
283
+ },
284
+ },
285
+ };
286
+
287
+ const onChange = vi.fn();
288
+ const model: any = {
289
+ disposed: false,
290
+ props: {
291
+ value: { id: 10 },
292
+ onChange,
293
+ },
294
+ context: {
295
+ blockModel: formBlock,
296
+ },
297
+ };
298
+
299
+ const ctx: any = {
300
+ model,
301
+ flowKey: 'selectSettings',
302
+ };
303
+
304
+ const filter = {
305
+ logic: '$and',
306
+ items: [{ path: 'orgId', operator: '$eq', value: '{{ ctx.item.value.org_m2o.id }}' }],
307
+ };
308
+
309
+ ensureFormValueDrivenDataScopeClear(ctx, filter);
310
+
311
+ emitter.emit('formValuesChange', {
312
+ changedPaths: [['staff_m2o']],
313
+ allValues: {
314
+ org_m2o: { id: 1 },
315
+ staff_m2o: { id: 10 },
316
+ },
317
+ });
318
+ expect(onChange).not.toHaveBeenCalled();
319
+
320
+ emitter.emit('formValuesChange', {
321
+ changedPaths: [['org_m2o']],
322
+ allValues: {
323
+ org_m2o: { id: 2 },
324
+ staff_m2o: { id: 10 },
325
+ },
326
+ });
327
+ expect(onChange).toHaveBeenCalledWith(null);
328
+ });
329
+
228
330
  it('does not clear a row field when a sibling field changes but the item dependency value is unchanged', () => {
229
331
  const emitter = new EventEmitter();
230
332
  const formBlock = {
@@ -0,0 +1,33 @@
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 { describe, expect, it } from 'vitest';
11
+ import { getActionLinkageStateOptions } from '../linkageRules';
12
+
13
+ describe('getActionLinkageStateOptions', () => {
14
+ const t = (key: string) => key;
15
+
16
+ it('returns all states for regular action models', () => {
17
+ expect(getActionLinkageStateOptions({}, t).map((option) => option.value)).toEqual([
18
+ 'visible',
19
+ 'hidden',
20
+ 'hiddenText',
21
+ 'enabled',
22
+ 'disabled',
23
+ ]);
24
+ });
25
+
26
+ it('filters states declared unsupported by the action model', () => {
27
+ expect(
28
+ getActionLinkageStateOptions({ supportedActionLinkageStates: ['visible', 'hidden'] }, t).map(
29
+ (option) => option.value,
30
+ ),
31
+ ).toEqual(['visible', 'hidden']);
32
+ });
33
+ });
@@ -18,6 +18,7 @@ import {
18
18
  subFormFieldLinkageRules,
19
19
  subFormLinkageSetFieldProps,
20
20
  } from '../linkageRules';
21
+ import { CreateFormModel, EditFormModel, FormGridModel, FormItemModel, JSItemModel } from '../../models';
21
22
 
22
23
  const createSubFormFieldModel = ({
23
24
  uid,
@@ -1056,6 +1057,77 @@ describe('linkageSetFieldProps action', () => {
1056
1057
  expect(await screen.findByText('Published')).toBeTruthy();
1057
1058
  });
1058
1059
 
1060
+ it.each([
1061
+ ['create form', CreateFormModel, linkageSetFieldProps, false],
1062
+ ['edit form', EditFormModel, linkageSetFieldProps, false],
1063
+ ['to-one subform', FlowModel, subFormLinkageSetFieldProps, false],
1064
+ ['to-many subform row', FlowModel, subFormLinkageSetFieldProps, true],
1065
+ ])('should exclude JS items from %s field state options', async (_, ModelClass, action, useGridFork) => {
1066
+ const engine = new FlowEngine();
1067
+ engine.registerModels({
1068
+ CreateFormModel,
1069
+ EditFormModel,
1070
+ FormGridModel,
1071
+ FormItemModel,
1072
+ JSItemModel,
1073
+ });
1074
+ const model = new ModelClass({
1075
+ flowEngine: engine,
1076
+ uid: 'form-model',
1077
+ use: ModelClass.name,
1078
+ }) as FlowModel;
1079
+ const grid = engine.createModel<FormGridModel>({
1080
+ use: 'FormGridModel',
1081
+ uid: 'form-grid',
1082
+ });
1083
+ const fieldModel = engine.createModel<FormItemModel>({
1084
+ use: 'FormItemModel',
1085
+ uid: 'name-field',
1086
+ props: { label: 'Name' },
1087
+ subModels: {
1088
+ field: {
1089
+ use: 'FlowModel',
1090
+ uid: 'name-field-component',
1091
+ },
1092
+ },
1093
+ });
1094
+ const jsItemModel = engine.createModel<JSItemModel>({
1095
+ use: 'JSItemModel',
1096
+ uid: 'js-item',
1097
+ props: { label: 'JS item' },
1098
+ });
1099
+ Object.assign(model.subModels, {
1100
+ grid: useGridFork
1101
+ ? grid.createFork({}, 'row-0')
1102
+ : Object.assign(grid, {
1103
+ subModels: {
1104
+ ...grid.subModels,
1105
+ items: [fieldModel, jsItemModel],
1106
+ },
1107
+ }),
1108
+ });
1109
+ Object.assign(model.subModels.grid.subModels, {
1110
+ items: [fieldModel, jsItemModel],
1111
+ });
1112
+ const ctx = model.context;
1113
+ const Comp: any = action.uiSchema.value['x-component'];
1114
+ const view = render(
1115
+ React.createElement(
1116
+ FlowSettingsContextProvider,
1117
+ { value: ctx },
1118
+ React.createElement(Comp, {
1119
+ value: { fields: [] },
1120
+ onChange: () => {},
1121
+ }),
1122
+ ),
1123
+ );
1124
+
1125
+ fireEvent.mouseDown(view.container.querySelector('.ant-select-selector') as Element);
1126
+
1127
+ expect(await screen.findByText('Name')).toBeTruthy();
1128
+ expect(screen.queryByText('JS item')).toBeNull();
1129
+ });
1130
+
1059
1131
  it('should only show options state for supported single field selection', () => {
1060
1132
  const supportedField: any = {
1061
1133
  uid: 'status-field',
@@ -68,6 +68,12 @@ interface LinkageRule {
68
68
  }[];
69
69
  }
70
70
 
71
+ export function updateLinkageRules<T>(rules: T[], updater: (nextRules: T[]) => void): T[] {
72
+ const nextRules = _.cloneDeep(rules);
73
+ updater(nextRules);
74
+ return nextRules;
75
+ }
76
+
71
77
  const previewValueForLog = (value: any) => {
72
78
  if (value == null) return value;
73
79
  const t = typeof value;
@@ -114,11 +120,16 @@ const isActionFlowModel = (model: any): boolean => {
114
120
  return false;
115
121
  };
116
122
 
123
+ const isFormFieldModel = (model: unknown): model is FlowModel => {
124
+ if (!model || typeof model !== 'object') return false;
125
+ return !!(model as FlowModel).subModels?.field;
126
+ };
127
+
117
128
  // 获取表单中所有字段的 model 实例的通用函数
118
129
  const getFormFields = (ctx: any) => {
119
130
  try {
120
131
  const fieldModels = ctx.model?.subModels?.grid?.subModels?.items || [];
121
- return fieldModels.map((model: any) => ({
132
+ return fieldModels.filter(isFormFieldModel).map((model) => ({
122
133
  label: model.props.label || model.props.name,
123
134
  value: model.uid,
124
135
  model,
@@ -340,8 +351,8 @@ const FieldStateEditor = ({
340
351
  const getFormFieldsByForkModel = (ctx: any) => {
341
352
  try {
342
353
  const fieldModels = ctx.model?.subModels?.grid?.subModels?.items || [];
343
- return fieldModels.map((model: any) => {
344
- const forkModel = Array.from(model.forks)[0] as any;
354
+ return fieldModels.filter(isFormFieldModel).map((model) => {
355
+ const forkModel = Array.from(model.forks)[0] as FlowModel | undefined;
345
356
 
346
357
  if (forkModel) {
347
358
  return {
@@ -654,6 +665,30 @@ export const linkageSetBlockProps = defineAction({
654
665
  },
655
666
  });
656
667
 
668
+ const ACTION_LINKAGE_STATE_OPTIONS = [
669
+ { label: 'Visible', value: 'visible' },
670
+ { label: 'Hidden', value: 'hidden' },
671
+ { label: 'Hidden text', value: 'hiddenText' },
672
+ { label: 'Enabled', value: 'enabled' },
673
+ { label: 'Disabled', value: 'disabled' },
674
+ ] as const;
675
+
676
+ type ActionLinkageState = (typeof ACTION_LINKAGE_STATE_OPTIONS)[number]['value'];
677
+
678
+ export function getActionLinkageStateOptions(
679
+ model: { supportedActionLinkageStates?: readonly ActionLinkageState[] },
680
+ t: (key: string) => string,
681
+ ) {
682
+ const supportedStates = model.supportedActionLinkageStates ? new Set(model.supportedActionLinkageStates) : undefined;
683
+
684
+ return ACTION_LINKAGE_STATE_OPTIONS.filter((option) => !supportedStates || supportedStates.has(option.value)).map(
685
+ (option) => ({
686
+ label: t(option.label),
687
+ value: option.value,
688
+ }),
689
+ );
690
+ }
691
+
657
692
  export const linkageSetActionProps = defineAction({
658
693
  name: 'linkageSetActionProps',
659
694
  title: tExpr('Set button state'),
@@ -674,13 +709,7 @@ export const linkageSetActionProps = defineAction({
674
709
  onChange={onChange}
675
710
  placeholder={t('Please select state')}
676
711
  style={{ width: '100%' }}
677
- options={[
678
- { label: t('Visible'), value: 'visible' },
679
- { label: t('Hidden'), value: 'hidden' },
680
- { label: t('Hidden text'), value: 'hiddenText' },
681
- { label: t('Enabled'), value: 'enabled' },
682
- { label: t('Disabled'), value: 'disabled' },
683
- ]}
712
+ options={getActionLinkageStateOptions(ctx.model, t)}
684
713
  allowClear
685
714
  />
686
715
  );
@@ -1500,13 +1529,26 @@ async function resolveLinkageRulesParamsPreservingRunJsScripts(ctx: FlowContext,
1500
1529
  }
1501
1530
 
1502
1531
  const LinkageRulesUI = observer(
1503
- (props: { readonly value: LinkageRule[]; supportedActions: string[]; title?: string }) => {
1504
- const { value: rules, supportedActions } = props;
1532
+ (props: {
1533
+ readonly value: LinkageRule[];
1534
+ onChange?: (value: LinkageRule[]) => void;
1535
+ supportedActions: string[];
1536
+ title?: string;
1537
+ }) => {
1538
+ const { value: rules = [], onChange, supportedActions } = props;
1505
1539
  const ctx = useFlowContext();
1506
1540
  const flowEngine = useFlowEngine();
1507
1541
  const t = ctx.model.translate.bind(ctx.model);
1508
1542
  const assignPriorityTip = t('Assignment takes precedence over form field assignment');
1509
1543
 
1544
+ const replaceRules = (updater: (nextRules: LinkageRule[]) => void) => {
1545
+ if (onChange) {
1546
+ onChange(updateLinkageRules(rules, updater));
1547
+ } else {
1548
+ updater(rules);
1549
+ }
1550
+ };
1551
+
1510
1552
  // 创建新规则的默认值
1511
1553
  const createNewRule = (): LinkageRule => ({
1512
1554
  key: uid(),
@@ -1523,7 +1565,7 @@ const LinkageRulesUI = observer(
1523
1565
 
1524
1566
  // 删除规则
1525
1567
  const handleDeleteRule = (index: number) => {
1526
- rules.splice(index, 1);
1568
+ replaceRules((nextRules) => nextRules.splice(index, 1));
1527
1569
  };
1528
1570
 
1529
1571
  // 上移规则
@@ -1562,7 +1604,9 @@ const LinkageRulesUI = observer(
1562
1604
 
1563
1605
  // 切换规则启用状态
1564
1606
  const handleToggleEnable = (index: number, enable: boolean) => {
1565
- rules[index].enable = enable;
1607
+ replaceRules((nextRules) => {
1608
+ nextRules[index].enable = enable;
1609
+ });
1566
1610
  };
1567
1611
 
1568
1612
  // 获取可用的动作类型
@@ -2222,7 +2266,7 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2222
2266
  }
2223
2267
 
2224
2268
  rememberOriginalProp(key, model.props?.[key]);
2225
- if (key === 'hiddenText') {
2269
+ if (key === 'hiddenText' && normalizedProps[key]) {
2226
2270
  rememberOriginalProp('title', model.props?.title);
2227
2271
  }
2228
2272
  if (key === 'required') {
@@ -2286,7 +2330,6 @@ const commonLinkageRulesHandler = async (ctx: FlowContext, params: any) => {
2286
2330
  const newProps = { ...model.__originalProps, ...patchProps };
2287
2331
  const prevHidden = !!model.hidden;
2288
2332
  const nextHidden = !!newProps.hiddenModel;
2289
-
2290
2333
  model.setProps(_.omit(newProps, ['hiddenModel', 'value', 'hiddenText', LINKAGE_ASSIGN_MODE_PROP]));
2291
2334
  syncFieldOptionsToForks(model, patchProps);
2292
2335
  if (typeof model.setHidden === 'function') {