@nocobase/client-v2 2.2.0-beta.9 → 2.2.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 (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -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/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -11,7 +11,7 @@ import React from 'react';
11
11
  import { describe, it, expect, vi, beforeEach } from 'vitest';
12
12
  import { render, screen, fireEvent, waitFor } from '@testing-library/react';
13
13
  import { VariableFilterItem, VariableFilterItemValue } from '../VariableFilterItem';
14
- import { FlowEngine, FlowModel } from '@nocobase/flow-engine';
14
+ import { FlowEngine, FlowModel, type Converters, type MetaTreeNode } from '@nocobase/flow-engine';
15
15
  import { observable } from '@formily/reactive';
16
16
  import { createMockFlowApp, TestCollectionFieldInterface } from '../../../__tests__/helpers/mockFlowApp';
17
17
 
@@ -47,6 +47,10 @@ vi.mock('@nocobase/flow-engine', async () => {
47
47
  return { ...actual, VariableInput: MockVariableInput };
48
48
  });
49
49
 
50
+ vi.mock('../../../models/blocks/filter-form/fields/date-time/components/DateFilterDynamicComponent', () => ({
51
+ DateFilterDynamicComponent: () => <div data-testid="date-filter-dynamic-component" />,
52
+ }));
53
+
50
54
  const getRenderedSelectTexts = (root: ParentNode = document.body) =>
51
55
  Array.from(root.querySelectorAll('.ant-select-selection-item')).map((node) => (node.textContent || '').trim());
52
56
 
@@ -158,6 +162,196 @@ describe('VariableFilterItem', () => {
158
162
  expect(value.value).toBe('abc');
159
163
  });
160
164
 
165
+ it.each([
166
+ { operator: '$includes', xComponent: 'Input' },
167
+ { operator: '$eq', xComponent: 'Select' },
168
+ { operator: '$in', xComponent: 'Select' },
169
+ ])('uses a silent app registry lookup for the local $xComponent control', async ({ operator, xComponent }) => {
170
+ const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
171
+ const model = CreateModel();
172
+ const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
173
+ const getComponent = vi.spyOn(app, 'getComponent');
174
+
175
+ (globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'status';
176
+ (globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
177
+ interface: 'input',
178
+ uiSchema: {
179
+ 'x-component': xComponent,
180
+ enum: [{ label: 'Draft', value: 'draft' }],
181
+ 'x-filter-operators': [
182
+ {
183
+ value: operator,
184
+ label: 'Operator',
185
+ selected: true,
186
+ schema: {
187
+ 'x-component': xComponent,
188
+ 'x-component-props': xComponent === 'Select' ? { mode: operator === '$in' ? 'tags' : null } : {},
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ paths: ['collection', 'status'],
194
+ name: 'status',
195
+ title: 'Status',
196
+ type: 'string',
197
+ };
198
+
199
+ render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
200
+ fireEvent.click(screen.getByTestId('variable-input'));
201
+
202
+ await waitFor(() => {
203
+ expect(value.operator).toBe(operator);
204
+ });
205
+ if (xComponent === 'Select' && operator === '$eq') {
206
+ expect(getComponent).not.toHaveBeenCalled();
207
+ } else {
208
+ expect(getComponent).toHaveBeenCalledWith(xComponent, false);
209
+ }
210
+ });
211
+
212
+ it('silently handles the DatePicker field schema before the date operator component takes over', async () => {
213
+ const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
214
+ const model = CreateModel();
215
+ const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
216
+ const getComponent = vi.spyOn(app, 'getComponent');
217
+
218
+ (globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'createdAt';
219
+ (globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
220
+ interface: 'createdAt',
221
+ uiSchema: {
222
+ 'x-component': 'DatePicker',
223
+ 'x-filter-operators': [
224
+ {
225
+ value: '$dateOn',
226
+ label: 'Is',
227
+ selected: true,
228
+ schema: {
229
+ 'x-component': 'DateFilterDynamicComponent',
230
+ 'x-component-props': { isRange: false },
231
+ },
232
+ },
233
+ ],
234
+ },
235
+ paths: ['collection', 'createdAt'],
236
+ name: 'createdAt',
237
+ title: 'Created at',
238
+ type: 'date',
239
+ };
240
+
241
+ render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
242
+ fireEvent.click(screen.getByTestId('variable-input'));
243
+
244
+ await waitFor(() => {
245
+ expect(value.operator).toBe('$dateOn');
246
+ });
247
+ expect(getComponent).toHaveBeenCalledWith('DatePicker', false);
248
+ expect(await screen.findByTestId('date-filter-dynamic-component')).toBeInTheDocument();
249
+ });
250
+
251
+ it('continues resolving plugin-provided keyword controls from the app registry', async () => {
252
+ const value = observable({ path: '', operator: '', value: '' }) as VariableFilterItemValue;
253
+ const model = CreateModel();
254
+ const app = model.context.app as unknown as ReturnType<typeof createMockFlowApp>;
255
+ const MultipleKeywordsInput: React.FC = () => <div data-testid="multiple-keywords-input" />;
256
+ app.addComponents({ MultipleKeywordsInput });
257
+ const getComponent = vi.spyOn(app, 'getComponent');
258
+
259
+ (globalThis as { __TEST_PATH__?: string }).__TEST_PATH__ = 'name';
260
+ (globalThis as { __TEST_META__?: MetaTreeNode }).__TEST_META__ = {
261
+ interface: 'input',
262
+ uiSchema: {
263
+ 'x-component': 'Input',
264
+ 'x-filter-operators': [
265
+ {
266
+ value: '$in',
267
+ label: 'Is any of',
268
+ selected: true,
269
+ schema: { 'x-component': 'MultipleKeywordsInput' },
270
+ },
271
+ ],
272
+ },
273
+ paths: ['collection', 'name'],
274
+ name: 'name',
275
+ title: 'Name',
276
+ type: 'string',
277
+ };
278
+
279
+ render(<VariableFilterItem value={value} model={model} rightAsVariable={false} />);
280
+ fireEvent.click(screen.getByTestId('variable-input'));
281
+
282
+ expect(await screen.findByTestId('multiple-keywords-input')).toBeTruthy();
283
+ expect(getComponent).toHaveBeenCalledWith('MultipleKeywordsInput');
284
+ });
285
+
286
+ it('passes disabled through to the left selector, operator select, and right variable input', async () => {
287
+ const value: VariableFilterItemValue = { path: '', operator: '', value: '' };
288
+ const model = CreateModel();
289
+
290
+ render(<VariableFilterItem value={value} model={model} rightAsVariable disabled />);
291
+
292
+ const leftVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
293
+ expect(leftVariableInputProps.disabled).toBe(true);
294
+
295
+ fireEvent.click(screen.getAllByTestId('variable-input')[0]);
296
+
297
+ const variableInputs = screen.getAllByTestId('variable-input');
298
+ expect(variableInputs.length).toBeGreaterThanOrEqual(2);
299
+ const rightVariableInputProps = (globalThis as any).__LAST_VARIABLE_INPUT_PROPS__;
300
+ expect(rightVariableInputProps.disabled).toBe(true);
301
+
302
+ const operatorSelect = document.body.querySelector('.ant-select') as HTMLDivElement | null;
303
+ expect(operatorSelect).not.toBeNull();
304
+ expect(operatorSelect).toHaveClass('ant-select-disabled');
305
+ });
306
+
307
+ it('composes custom right variable converters with constant, null, and core fallbacks', () => {
308
+ const value: VariableFilterItemValue = {
309
+ path: 'name',
310
+ operator: '$eq',
311
+ value: '{{$context.data.id}}',
312
+ };
313
+ const model = CreateModel();
314
+ const rightVariableConverters: Pick<Converters, 'resolvePathFromValue' | 'resolveValueFromPath'> = {
315
+ resolvePathFromValue: (input) => (input === '{{$context.data.id}}' ? ['$context', 'data', 'id'] : undefined),
316
+ resolveValueFromPath: (metaTreeNode) =>
317
+ metaTreeNode.paths?.[0] === '$context' ? `{{${metaTreeNode.paths.join('.')}}}` : undefined,
318
+ };
319
+
320
+ render(
321
+ <VariableFilterItem
322
+ value={value}
323
+ model={model}
324
+ rightAsVariable
325
+ rightVariableConverters={rightVariableConverters}
326
+ />,
327
+ );
328
+
329
+ const variableInputProps = (
330
+ globalThis as {
331
+ __LAST_VARIABLE_INPUT_PROPS__?: {
332
+ converters?: Converters;
333
+ value?: unknown;
334
+ };
335
+ }
336
+ ).__LAST_VARIABLE_INPUT_PROPS__;
337
+ const converters = variableInputProps?.converters;
338
+ const constantNode: MetaTreeNode = { name: 'constant', title: 'Constant', type: 'string', paths: ['constant'] };
339
+ const nullNode: MetaTreeNode = { name: 'null', title: 'Null', type: 'object', paths: ['null'] };
340
+ const workflowNode: MetaTreeNode = {
341
+ name: 'id',
342
+ title: 'ID',
343
+ type: 'number',
344
+ paths: ['$context', 'data', 'id'],
345
+ };
346
+
347
+ expect(variableInputProps?.value).toBe('{{$context.data.id}}');
348
+ expect(converters?.resolvePathFromValue?.('{{$context.data.id}}')).toEqual(['$context', 'data', 'id']);
349
+ expect(converters?.resolvePathFromValue?.('{{ ctx.$context.data.id }}')).toEqual(['$context', 'data', 'id']);
350
+ expect(converters?.resolveValueFromPath?.(constantNode)).toBe('');
351
+ expect(converters?.resolveValueFromPath?.(nullNode)).toBeNull();
352
+ expect(converters?.resolveValueFromPath?.(workflowNode)).toBe('{{$context.data.id}}');
353
+ });
354
+
161
355
  it('uses scoped context dataSourceManager when app dataSourceManager has no field interface manager', async () => {
162
356
  const value = observable({ path: '', operator: '', value: '' }) as any;
163
357
  const model = CreateModel();
@@ -8,12 +8,13 @@
8
8
  */
9
9
 
10
10
  import { useFlowModel } from '@nocobase/flow-engine';
11
- import { Result } from 'antd';
11
+ import { useMemoizedFn } from 'ahooks';
12
+ import { Button, Result } from 'antd';
12
13
  import _ from 'lodash';
13
14
  import React, { useMemo } from 'react';
14
15
  import { useTranslation } from 'react-i18next';
15
16
  import { BlockItemCard } from '../BlockItemCard';
16
- import { BlockModel } from '../../models/base/BlockModel';
17
+ import type { BlockModel } from '../../models/base/BlockModel';
17
18
 
18
19
  function getResourceSettingsInitParams(model: any) {
19
20
  if (typeof model?.getResourceSettingsInitParams === 'function') {
@@ -43,26 +44,46 @@ function getBlockResourceInfo(model: any, t: (key: string) => string) {
43
44
  const dataSourceName = dataSource ? t(dataSource.displayName || dataSource.key) : dataSourceKey;
44
45
  const collectionLabel = collectionName ? `${t(collectionName) || collectionName}` : '';
45
46
  const dataSourceLabel = dataSourceName ? `${t(dataSourceName)} > ` : '';
47
+ const isDataSourceLoadFailed = ['loading-failed', 'reloading-failed'].includes(dataSource?.status);
46
48
 
47
49
  return {
48
50
  dataSourceName,
49
51
  nameValue: `${dataSourceLabel}${collectionLabel}`,
50
- isDataSourceUnavailable: Boolean(dataSourceKey && !dataSource),
52
+ isDataSourceLoadFailed,
53
+ isDataSourceUnavailable: Boolean(dataSourceKey && (!dataSource || isDataSourceLoadFailed)),
51
54
  };
52
55
  }
53
56
 
54
- function DataSourceUnavailablePlaceholder({ dataSourceName }: { dataSourceName?: string }) {
57
+ function getTemporarilyUnavailableMessage(t: (key: string, options?: Record<string, string>) => string, name?: string) {
58
+ return name
59
+ ? t('The data source "{{name}}" is temporarily unavailable. Please try again later or contact an administrator.', {
60
+ name,
61
+ })
62
+ : t('The data source is temporarily unavailable. Please try again later or contact an administrator.');
63
+ }
64
+
65
+ function DataSourceUnavailablePlaceholder({
66
+ dataSourceName,
67
+ isLoadFailed,
68
+ }: {
69
+ dataSourceName?: string;
70
+ isLoadFailed?: boolean;
71
+ }) {
55
72
  const { t } = useTranslation();
56
- const subTitle = dataSourceName
57
- ? t(
58
- 'The data source "{{name}}" used by this block is disabled or unavailable. Enable the data source to display this block.',
59
- { name: dataSourceName },
60
- )
61
- : t('The data source used by this block is disabled or unavailable. Enable the data source to display this block.');
73
+ const subTitle = isLoadFailed
74
+ ? getTemporarilyUnavailableMessage(t, dataSourceName)
75
+ : dataSourceName
76
+ ? t(
77
+ 'The data source "{{name}}" used by this block is disabled or unavailable. Enable the data source to display this block.',
78
+ { name: dataSourceName },
79
+ )
80
+ : t(
81
+ 'The data source used by this block is disabled or unavailable. Enable the data source to display this block.',
82
+ );
62
83
 
63
84
  return (
64
- <BlockItemCard>
65
- <Result status="403" subTitle={subTitle}></Result>
85
+ <BlockItemCard role="alert" aria-live="polite">
86
+ <Result status={isLoadFailed ? 'warning' : '403'} subTitle={subTitle}></Result>
66
87
  </BlockItemCard>
67
88
  );
68
89
  }
@@ -70,7 +91,7 @@ function DataSourceUnavailablePlaceholder({ dataSourceName }: { dataSourceName?:
70
91
  export const BlockPlaceholder = () => {
71
92
  const { t } = useTranslation();
72
93
  const model: BlockModel = useFlowModel();
73
- const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
94
+ const { dataSourceName, isDataSourceLoadFailed, isDataSourceUnavailable, nameValue } = useMemo(() => {
74
95
  return getBlockResourceInfo(model, t);
75
96
  }, [model, t]);
76
97
 
@@ -86,10 +107,7 @@ export const BlockPlaceholder = () => {
86
107
  }, [actionName, nameValue, t]);
87
108
 
88
109
  if (isDataSourceUnavailable) {
89
- if (!model.context.flowSettingsEnabled) {
90
- return null;
91
- }
92
- return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
110
+ return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} isLoadFailed={isDataSourceLoadFailed} />;
93
111
  }
94
112
 
95
113
  return (
@@ -101,8 +119,8 @@ export const BlockPlaceholder = () => {
101
119
 
102
120
  export function BlockDeletePlaceholder() {
103
121
  const { t } = useTranslation();
104
- const model: any = useFlowModel();
105
- const { dataSourceName, isDataSourceUnavailable, nameValue } = useMemo(() => {
122
+ const model: BlockModel = useFlowModel();
123
+ const { dataSourceName, isDataSourceLoadFailed, isDataSourceUnavailable, nameValue } = useMemo(() => {
106
124
  return getBlockResourceInfo(model, t);
107
125
  }, [model, t]);
108
126
 
@@ -115,10 +133,7 @@ export function BlockDeletePlaceholder() {
115
133
  }, [nameValue, t]);
116
134
 
117
135
  if (isDataSourceUnavailable) {
118
- if (!model.context.flowSettingsEnabled) {
119
- return null;
120
- }
121
- return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} />;
136
+ return <DataSourceUnavailablePlaceholder dataSourceName={dataSourceName} isLoadFailed={isDataSourceLoadFailed} />;
122
137
  }
123
138
 
124
139
  return (
@@ -127,3 +142,35 @@ export function BlockDeletePlaceholder() {
127
142
  </BlockItemCard>
128
143
  );
129
144
  }
145
+
146
+ type RefreshableBlockModel = BlockModel & {
147
+ refresh?: () => Promise<unknown> | unknown;
148
+ };
149
+
150
+ export function BlockResourceErrorPlaceholder() {
151
+ const { t } = useTranslation();
152
+ const model = useFlowModel() as RefreshableBlockModel;
153
+ const { dataSourceName } = useMemo(() => getBlockResourceInfo(model, t), [model, t]);
154
+ const handleRetry = useMemoizedFn(async () => {
155
+ try {
156
+ await model.refresh?.();
157
+ } catch {
158
+ // The resource retains the latest error and keeps this placeholder visible.
159
+ }
160
+ });
161
+
162
+ return (
163
+ <BlockItemCard ref={model.context.ref} {...model.decoratorProps} role="alert" aria-live="polite">
164
+ <Result
165
+ status="warning"
166
+ title={t('Data loading failed')}
167
+ subTitle={getTemporarilyUnavailableMessage(t, dataSourceName)}
168
+ extra={
169
+ <Button type="primary" onClick={handleRetry}>
170
+ {t('Try again')}
171
+ </Button>
172
+ }
173
+ />
174
+ </BlockItemCard>
175
+ );
176
+ }
@@ -8,9 +8,9 @@
8
8
  */
9
9
 
10
10
  import { FlowModelProvider } from '@nocobase/flow-engine';
11
- import { render, screen } from '@testing-library/react';
11
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
12
12
  import React from 'react';
13
- import { BlockDeletePlaceholder } from '../BlockPlaceholder';
13
+ import { BlockDeletePlaceholder, BlockResourceErrorPlaceholder } from '../BlockPlaceholder';
14
14
 
15
15
  vi.mock('react-i18next', () => ({
16
16
  useTranslation: () => ({
@@ -23,8 +23,9 @@ vi.mock('react-i18next', () => ({
23
23
  function createModel(options: {
24
24
  dataSourceKey?: string;
25
25
  collectionName?: string;
26
- dataSource?: { key: string; displayName?: string };
26
+ dataSource?: { key: string; displayName?: string; status?: string };
27
27
  flowSettingsEnabled?: boolean;
28
+ refresh?: () => Promise<unknown>;
28
29
  }) {
29
30
  const model: any = {
30
31
  context: {
@@ -38,6 +39,7 @@ function createModel(options: {
38
39
  dataSourceKey: options.dataSourceKey,
39
40
  collectionName: options.collectionName,
40
41
  }),
42
+ refresh: options.refresh,
41
43
  };
42
44
  model.context.blockModel = model;
43
45
  return model;
@@ -64,25 +66,47 @@ describe('BlockPlaceholder', () => {
64
66
  ).toBeInTheDocument();
65
67
  });
66
68
 
67
- it('should hide data source unavailable block outside configuration mode', () => {
69
+ it('should render data source unavailable placeholder outside configuration mode', () => {
68
70
  const model = createModel({
69
71
  dataSourceKey: 'external-mysql',
70
72
  collectionName: 'orders',
71
73
  flowSettingsEnabled: false,
72
74
  });
73
75
 
74
- const { container } = render(
76
+ render(
75
77
  <FlowModelProvider model={model}>
76
78
  <BlockDeletePlaceholder />
77
79
  </FlowModelProvider>,
78
80
  );
79
81
 
80
82
  expect(
81
- screen.queryByText(
83
+ screen.getByText(
82
84
  'The data source "external-mysql" used by this block is disabled or unavailable. Enable the data source to display this block.',
83
85
  ),
86
+ ).toBeInTheDocument();
87
+ });
88
+
89
+ it('should treat a failed data source as unavailable', () => {
90
+ const model = createModel({
91
+ dataSourceKey: 'external-mysql',
92
+ collectionName: 'orders',
93
+ dataSource: { key: 'external-mysql', displayName: 'External MySQL', status: 'loading-failed' },
94
+ });
95
+
96
+ render(
97
+ <FlowModelProvider model={model}>
98
+ <BlockDeletePlaceholder />
99
+ </FlowModelProvider>,
100
+ );
101
+
102
+ expect(
103
+ screen.getByText(
104
+ 'The data source "External MySQL" is temporarily unavailable. Please try again later or contact an administrator.',
105
+ ),
106
+ ).toBeInTheDocument();
107
+ expect(
108
+ screen.queryByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
84
109
  ).not.toBeInTheDocument();
85
- expect(container).toBeEmptyDOMElement();
86
110
  });
87
111
 
88
112
  it('should keep collection deleted placeholder when the data source is available', () => {
@@ -102,4 +126,30 @@ describe('BlockPlaceholder', () => {
102
126
  screen.getByText('The Collection "External MySQL > orders" may have been deleted. Please remove this Block.'),
103
127
  ).toBeInTheDocument();
104
128
  });
129
+
130
+ it('should render a retryable resource error placeholder', async () => {
131
+ const refresh = vi.fn().mockRejectedValue(new Error('still unavailable'));
132
+ const model = createModel({
133
+ dataSourceKey: 'external-mysql',
134
+ collectionName: 'orders',
135
+ dataSource: { key: 'external-mysql', displayName: 'External MySQL' },
136
+ refresh,
137
+ });
138
+
139
+ render(
140
+ <FlowModelProvider model={model}>
141
+ <BlockResourceErrorPlaceholder />
142
+ </FlowModelProvider>,
143
+ );
144
+
145
+ expect(screen.getByText('Data loading failed')).toBeInTheDocument();
146
+ expect(
147
+ screen.getByText(
148
+ 'The data source "External MySQL" is temporarily unavailable. Please try again later or contact an administrator.',
149
+ ),
150
+ ).toBeInTheDocument();
151
+
152
+ fireEvent.click(screen.getByRole('button', { name: 'Try again' }));
153
+ await waitFor(() => expect(refresh).toHaveBeenCalledTimes(1));
154
+ });
105
155
  });
package/src/flow/index.ts CHANGED
@@ -22,12 +22,14 @@ import { Markdown } from './common/Markdown/Markdown';
22
22
  import { LiquidEngine } from './common/Liquid';
23
23
  import type { PreviewRunJSResult } from './components/code-editor/runjsDiagnostics';
24
24
  import { TextAreaWithContextSelector } from './components/TextAreaWithContextSelector';
25
+ import { registerDeviceTypeContext } from './internal/registerDeviceTypeContext';
25
26
 
26
27
  export class PluginFlowEngine<TApp extends BaseApplication<any> = BaseApplication<any>> extends Plugin<
27
28
  PluginOptions<any>,
28
29
  TApp
29
30
  > {
30
31
  async load() {
32
+ registerDeviceTypeContext(this.flowEngine);
31
33
  this.app.addComponents({ FlowRoute });
32
34
  this.app.flowEngine.setModelRepository(new FlowModelRepository(this.app));
33
35
  const filteredModels = Object.fromEntries(
@@ -98,6 +100,7 @@ export * from './FlowPage';
98
100
  export * from './models';
99
101
  export * from './utils';
100
102
  export * from './actions';
103
+ export { FieldAssignValueInput } from './components/FieldAssignValueInput';
101
104
  export * from './system-settings';
102
105
  export * from './admin-shell/admin-layout';
103
106
  export * from './admin-shell/BaseLayoutModel';
@@ -0,0 +1,39 @@
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 type { FlowEngine } from '@nocobase/flow-engine';
11
+ import { deviceType } from 'react-device-detect';
12
+
13
+ export const registerDeviceTypeContext = (flowEngine: FlowEngine) => {
14
+ flowEngine.context.defineProperty('deviceType', {
15
+ get: () => (deviceType === 'browser' ? 'computer' : deviceType),
16
+ cache: false,
17
+ meta: {
18
+ type: 'string',
19
+ title: flowEngine.translate('Current device type'),
20
+ interface: 'select',
21
+ uiSchema: {
22
+ enum: [
23
+ { label: flowEngine.translate('Computer'), value: 'computer' },
24
+ { label: flowEngine.translate('Mobile'), value: 'mobile' },
25
+ { label: flowEngine.translate('Tablet'), value: 'tablet' },
26
+ { label: flowEngine.translate('SmartTv'), value: 'smarttv' },
27
+ { label: flowEngine.translate('Console'), value: 'console' },
28
+ { label: flowEngine.translate('Wearable'), value: 'wearable' },
29
+ { label: flowEngine.translate('Embedded'), value: 'embedded' },
30
+ ],
31
+ 'x-component': 'Select',
32
+ },
33
+ },
34
+ info: {
35
+ description: 'Current device type (computer/mobile/tablet/...).',
36
+ detail: 'string',
37
+ },
38
+ });
39
+ };
@@ -20,7 +20,7 @@ type OperatorMeta = {
20
20
  };
21
21
 
22
22
  type ComponentRegistry = {
23
- getComponent?: (name: string) => ComponentType<Record<string, unknown>> | undefined;
23
+ getComponent?: (name: string, isShowError?: boolean) => ComponentType<Record<string, unknown>> | undefined;
24
24
  };
25
25
 
26
26
  type OperatorComponentFieldModel = {
@@ -67,6 +67,7 @@ export function resolveOperatorComponent(
67
67
  app: ComponentRegistry | undefined,
68
68
  operator: string,
69
69
  operators?: OperatorMeta[],
70
+ isShowError = true,
70
71
  ) {
71
72
  if (!operators || !Array.isArray(operators)) return null;
72
73
  const op = operators.find((item) => item?.value === operator);
@@ -77,7 +78,7 @@ export function resolveOperatorComponent(
77
78
  if (xComp === 'DateFilterDynamicComponent') {
78
79
  Comp = DateFilterDynamicComponent as ComponentType<Record<string, unknown>>;
79
80
  } else {
80
- Comp = app?.getComponent?.(xComp);
81
+ Comp = isShowError ? app?.getComponent?.(xComp) : app?.getComponent?.(xComp, false);
81
82
  }
82
83
  if (!Comp) return null;
83
84
  const props = isRecord(schema?.['x-component-props']) ? schema['x-component-props'] : {};
@@ -22,6 +22,11 @@ import { applyUpdateRecordAction } from './UpdateRecordActionUtils';
22
22
  // import { RemoteFlowModelRenderer } from '../../FlowPage';
23
23
 
24
24
  const SETTINGS_FLOW_KEY = 'assignSettings';
25
+ const AFTER_SUCCESS_DEFAULT_PARAMS = {
26
+ successMessage: tExpr('Saved successfully'),
27
+ manualClose: false,
28
+ actionAfterSuccess: 'stay',
29
+ };
25
30
 
26
31
  function Info() {
27
32
  const ctx = useFlowSettingsContext();
@@ -101,6 +106,10 @@ UpdateRecordActionModel.registerFlow({
101
106
  tipComponent: Info,
102
107
  validateBeforeSave: true,
103
108
  }),
109
+ afterSuccess: {
110
+ use: 'afterSuccess',
111
+ defaultParams: AFTER_SUCCESS_DEFAULT_PARAMS,
112
+ },
104
113
  },
105
114
  });
106
115
 
@@ -113,7 +122,15 @@ UpdateRecordActionModel.registerFlow({
113
122
  return getAssignFieldValuesDefaultParams(ctx, SETTINGS_FLOW_KEY);
114
123
  },
115
124
  async handler(ctx, params) {
116
- await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
125
+ const updated = await applyUpdateRecordAction(ctx, params, { settingsFlowKey: SETTINGS_FLOW_KEY });
126
+ if (!updated) {
127
+ return;
128
+ }
129
+ const savedAfterSuccess = ctx.model.getStepParams(SETTINGS_FLOW_KEY, 'afterSuccess') || {};
130
+ await ctx.runAction('afterSuccess', {
131
+ ...AFTER_SUCCESS_DEFAULT_PARAMS,
132
+ ...savedAfterSuccess,
133
+ });
117
134
  },
118
135
  },
119
136
  },
@@ -8,9 +8,15 @@
8
8
  */
9
9
 
10
10
  import { MultiRecordResource, SingleRecordResource } from '@nocobase/flow-engine';
11
+ import type { AxiosRequestConfig } from 'axios';
11
12
  import { dispatchEventDeep } from '../../utils';
12
13
  import { resolveAssignFieldValues } from '../blocks/assign-form/assignFieldValuesFlow';
13
14
 
15
+ type UpdateRecordActionParams = {
16
+ assignedValues?: unknown;
17
+ requestConfig?: AxiosRequestConfig;
18
+ };
19
+
14
20
  export async function refreshLinkageRulesAfterUpdate(ctx: any) {
15
21
  const blockModel = ctx?.blockModel || ctx?.model?.context?.blockModel || ctx?.model;
16
22
  const actionModel = ctx?.model;
@@ -48,11 +54,11 @@ export async function refreshLinkageRulesAfterUpdate(ctx: any) {
48
54
 
49
55
  export async function applyUpdateRecordAction(
50
56
  ctx: any,
51
- params: any,
57
+ params: UpdateRecordActionParams,
52
58
  options?: {
53
59
  settingsFlowKey?: string;
54
60
  },
55
- ) {
61
+ ): Promise<boolean> {
56
62
  const settingsFlowKey = options?.settingsFlowKey || 'assignSettings';
57
63
 
58
64
  // 统一接入二次确认:如果启用则弹窗;未配置时默认不启用
@@ -62,25 +68,31 @@ export async function applyUpdateRecordAction(
62
68
 
63
69
  const assignedValues = await resolveAssignFieldValues(ctx, params?.assignedValues, 'UpdateRecordAction');
64
70
  if (!assignedValues) {
65
- return;
71
+ return false;
66
72
  }
67
73
 
68
74
  if (!assignedValues || typeof assignedValues !== 'object' || !Object.keys(assignedValues).length) {
69
75
  ctx.message.warning(ctx.t('No assigned fields configured'));
70
- return;
76
+ return false;
71
77
  }
72
78
  const collection = ctx.collection?.name;
73
79
  const filterByTk = ctx.collection?.getFilterByTK?.(ctx.record);
74
80
  if (!collection || typeof filterByTk === 'undefined' || filterByTk === null) {
75
81
  ctx.message.error(ctx.t('Record is required to perform this action'));
76
- return;
82
+ return false;
77
83
  }
84
+ let updated = false;
78
85
  if (ctx.resource instanceof SingleRecordResource) {
79
86
  await ctx.resource.save(assignedValues, params.requestConfig);
87
+ updated = true;
80
88
  } else if (ctx.resource instanceof MultiRecordResource) {
81
89
  await ctx.resource.update(filterByTk, assignedValues, params.requestConfig);
90
+ updated = true;
91
+ }
92
+ if (!updated) {
93
+ return false;
82
94
  }
83
95
 
84
96
  await refreshLinkageRulesAfterUpdate(ctx);
85
- ctx.message.success(ctx.t('Saved successfully'));
97
+ return true;
86
98
  }