@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
@@ -7,16 +7,17 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import React, { useRef, useState, useEffect } from 'react';
11
- import { useParseMarkdown, convertToText } from './util';
12
- import { useMarkdownStyles } from './style';
10
+ import React, { useEffect, useRef, useState } from 'react';
13
11
  import { Tooltip } from 'antd';
12
+ import { useMarkdownStyles } from './style';
13
+ import { convertToText, useParseMarkdown } from './util';
14
14
 
15
15
  export const DisplayMarkdown = (props) => {
16
- const { textOnly, overflowMode, style } = props;
16
+ const { textOnly, overflowMode, sanitizeHtml, style } = props;
17
17
  const markdownClass = useMarkdownStyles();
18
18
  const { html = '' } = useParseMarkdown(props.value);
19
- const text: any = convertToText(html);
19
+ const displayHtml = sanitizeHtml ? sanitizeHtml(html) : html;
20
+ const text = convertToText(displayHtml);
20
21
  const isEllipsis = overflowMode === 'ellipsis';
21
22
 
22
23
  const contentRef = useRef<HTMLDivElement>(null);
@@ -29,7 +30,7 @@ export const DisplayMarkdown = (props) => {
29
30
  const overflowed = el.scrollWidth > el.clientWidth || el.scrollHeight > el.clientHeight;
30
31
  setIsOverflowed(overflowed);
31
32
  }
32
- }, [html, style, overflowMode]);
33
+ }, [displayHtml, style, overflowMode]);
33
34
 
34
35
  // 通用样式(用于 ellipsis 模式)
35
36
  const baseStyle: React.CSSProperties = {
@@ -48,11 +49,15 @@ export const DisplayMarkdown = (props) => {
48
49
  : {}),
49
50
  };
50
51
 
51
- const content = (
52
+ const content = textOnly ? (
53
+ <div ref={contentRef} className={`${markdownClass} nb-markdown`} style={baseStyle}>
54
+ {text}
55
+ </div>
56
+ ) : (
52
57
  <div
53
58
  ref={contentRef}
54
59
  className={`${markdownClass} nb-markdown`}
55
- dangerouslySetInnerHTML={{ __html: textOnly ? text : html }}
60
+ dangerouslySetInnerHTML={{ __html: displayHtml }}
56
61
  style={baseStyle}
57
62
  />
58
63
  );
@@ -61,7 +66,7 @@ export const DisplayMarkdown = (props) => {
61
66
  <Tooltip
62
67
  title={
63
68
  <div
64
- dangerouslySetInnerHTML={{ __html: html }}
69
+ dangerouslySetInnerHTML={{ __html: displayHtml }}
65
70
  style={{
66
71
  maxHeight: 500,
67
72
  overflowY: 'auto',
@@ -0,0 +1,46 @@
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 { render } from '@nocobase/test/client';
11
+ import { sanitizeRichTextHtml } from '@nocobase/utils/client';
12
+ import React from 'react';
13
+ import { DisplayMarkdown } from '../DisplayMarkdown';
14
+
15
+ vi.mock('../util', () => ({
16
+ useParseMarkdown: (value: string) => ({ html: value }),
17
+ convertToText: (value: string) => {
18
+ const element = document.createElement('div');
19
+ element.innerHTML = value;
20
+ return element.textContent || '';
21
+ },
22
+ }));
23
+
24
+ describe('DisplayMarkdown', () => {
25
+ it('sanitizes HTML mode', () => {
26
+ const { container } = render(
27
+ <DisplayMarkdown
28
+ value={'<p>safe</p><img src="https://example.com/image.png" onerror="alert(1)"><script>alert(1)</script>'}
29
+ sanitizeHtml={sanitizeRichTextHtml}
30
+ textOnly={false}
31
+ />,
32
+ );
33
+
34
+ expect(container.querySelector('p')).toHaveTextContent('safe');
35
+ expect(container.querySelector('img')).not.toHaveAttribute('onerror');
36
+ expect(container.querySelector('script')).toBeNull();
37
+ });
38
+
39
+ it('renders decoded text as a text node', () => {
40
+ const payload = '&lt;img src=x onerror=alert(document.domain)&gt;';
41
+ const { container } = render(<DisplayMarkdown value={payload} sanitizeHtml={sanitizeRichTextHtml} textOnly />);
42
+
43
+ expect(container).toHaveTextContent('<img src=x onerror=alert(document.domain)>');
44
+ expect(container.querySelector('img')).toBeNull();
45
+ });
46
+ });
@@ -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
  }
@@ -8,8 +8,9 @@
8
8
  */
9
9
 
10
10
  import { beforeEach, describe, expect, it, vi } from 'vitest';
11
- import { MultiRecordResource } from '@nocobase/flow-engine';
11
+ import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
12
12
  import { applyUpdateRecordAction } from '../UpdateRecordActionUtils';
13
+ import { UpdateRecordActionModel } from '../UpdateRecordActionModel';
13
14
  import { dispatchEventDeep } from '../../../utils';
14
15
 
15
16
  vi.mock('../../../utils', () => ({
@@ -21,7 +22,7 @@ describe('UpdateRecordActionModel apply action', () => {
21
22
  vi.clearAllMocks();
22
23
  });
23
24
 
24
- it('dispatches paginationChange for action and block after successful update', async () => {
25
+ it('dispatches paginationChange and returns success after updating the record', async () => {
25
26
  const resource: any = Object.create(MultiRecordResource.prototype);
26
27
  resource.update = vi.fn(async () => ({}));
27
28
  resource.refresh = vi.fn(async () => {});
@@ -52,12 +53,13 @@ describe('UpdateRecordActionModel apply action', () => {
52
53
  t: (value: string) => value,
53
54
  };
54
55
 
55
- await applyUpdateRecordAction(ctx, {
56
+ const updated = await applyUpdateRecordAction(ctx, {
56
57
  assignedValues: {
57
58
  marital_status: '已婚',
58
59
  },
59
60
  });
60
61
 
62
+ expect(updated).toBe(true);
61
63
  expect(resource.update).toHaveBeenCalledWith(1, { marital_status: '已婚' }, undefined);
62
64
  expect(resource.refresh).not.toHaveBeenCalled();
63
65
 
@@ -67,6 +69,76 @@ describe('UpdateRecordActionModel apply action', () => {
67
69
  expect(paginationCalls.length).toBeGreaterThan(0);
68
70
  expect(paginationCalls.some(([model]: [any]) => model === ctx.model)).toBe(true);
69
71
  expect(paginationCalls.some(([model]: [any]) => model === blockModel)).toBe(true);
70
- expect(ctx.message.success).toHaveBeenCalledWith('Saved successfully');
72
+ expect(ctx.message.success).not.toHaveBeenCalled();
73
+ });
74
+
75
+ it('runs the configured after-success action only after a successful update', async () => {
76
+ const engine = new FlowEngine();
77
+ const action = new UpdateRecordActionModel({ uid: 'update-record-action', flowEngine: engine } as any);
78
+ action.setStepParams('assignSettings', 'confirm', { enable: false });
79
+ action.setStepParams('assignSettings', 'afterSuccess', {
80
+ successMessage: 'Record updated',
81
+ });
82
+
83
+ const resource: any = Object.create(MultiRecordResource.prototype);
84
+ resource.update = vi.fn(async () => ({}));
85
+ const blockModel: any = { uid: 'details-block' };
86
+ const runAction = vi.fn(async () => {});
87
+ const ctx: any = {
88
+ model: action,
89
+ blockModel,
90
+ runAction,
91
+ collection: {
92
+ name: 'users',
93
+ getFilterByTK: vi.fn(() => 1),
94
+ },
95
+ record: { id: 1 },
96
+ resource,
97
+ message: {
98
+ success: vi.fn(),
99
+ warning: vi.fn(),
100
+ error: vi.fn(),
101
+ },
102
+ t: (value: string) => value,
103
+ };
104
+ const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
105
+
106
+ await handler(ctx, { assignedValues: { status: 'active' } });
107
+
108
+ expect(runAction).toHaveBeenNthCalledWith(1, 'confirm', { enable: false });
109
+ expect(runAction).toHaveBeenNthCalledWith(2, 'afterSuccess', {
110
+ successMessage: 'Record updated',
111
+ manualClose: false,
112
+ actionAfterSuccess: 'stay',
113
+ });
114
+ });
115
+
116
+ it('does not run the after-success action when no fields are assigned', async () => {
117
+ const engine = new FlowEngine();
118
+ const action = new UpdateRecordActionModel({ uid: 'update-record-action-empty', flowEngine: engine } as any);
119
+ const runAction = vi.fn(async () => {});
120
+ const ctx: any = {
121
+ model: action,
122
+ runAction,
123
+ collection: {
124
+ name: 'users',
125
+ getFilterByTK: vi.fn(() => 1),
126
+ },
127
+ record: { id: 1 },
128
+ resource: Object.create(MultiRecordResource.prototype),
129
+ message: {
130
+ success: vi.fn(),
131
+ warning: vi.fn(),
132
+ error: vi.fn(),
133
+ },
134
+ t: (value: string) => value,
135
+ };
136
+ const handler = action.getFlow('apply')?.getStep('apply')?.serialize().handler;
137
+
138
+ await handler(ctx, { assignedValues: {} });
139
+
140
+ expect(runAction).toHaveBeenCalledTimes(1);
141
+ expect(runAction).toHaveBeenCalledWith('confirm', { enable: false });
142
+ expect(ctx.message.warning).toHaveBeenCalledWith('No assigned fields configured');
71
143
  });
72
144
  });
@@ -7,7 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { DefaultStructure, FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
10
+ import { FlowModel, tExpr, useFlowModel } from '@nocobase/flow-engine';
11
+ import type { DefaultStructure, FlowModelOptions } from '@nocobase/flow-engine';
11
12
  import { Icon } from '../../../components';
12
13
  import { Button, Tooltip } from 'antd';
13
14
  import type { ButtonProps } from 'antd/es/button';
@@ -87,8 +88,10 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
87
88
  return null;
88
89
  }
89
90
 
90
- onInit(options: any): void {
91
+ onInit(options: FlowModelOptions<T>): void {
91
92
  super.onInit(options);
93
+ // Action disabled state is computed at runtime and must never be restored from persisted props.
94
+ delete this.props.disabled;
92
95
  this.context.defineProperty('actionName', {
93
96
  get: () => {
94
97
  return this.getAclActionName();
@@ -97,6 +100,12 @@ export class ActionModel<T extends DefaultStructure = DefaultStructure> extends
97
100
  });
98
101
  }
99
102
 
103
+ serialize() {
104
+ const data = super.serialize();
105
+ delete data.props.disabled;
106
+ return data;
107
+ }
108
+
100
109
  getInputArgs() {
101
110
  const inputArgs: Record<string, any> = {};
102
111
  const defaultKeys: string[] = [];
@@ -14,8 +14,10 @@ import {
14
14
  FlowSettingsButton,
15
15
  buildSubModelGroups,
16
16
  buildSubModelItems,
17
+ type FlowModel,
17
18
  type SubModelItem,
18
19
  type SubModelItemsType,
20
+ VIEW_ACTIVATED_EVENT,
19
21
  } from '@nocobase/flow-engine';
20
22
  import React from 'react';
21
23
  import { FilterManager } from '../blocks/filter-manager/FilterManager';
@@ -24,6 +26,8 @@ import { GridModel } from './GridModel';
24
26
  const SELECT_SCENE_ALLOWED_OTHER_BLOCK_MODELS = ['JSBlockModel', 'IframeBlockModel', 'MarkdownBlockModel'] as const;
25
27
 
26
28
  export class BlockGridModel extends GridModel {
29
+ private viewActivatedListener?: () => void;
30
+
27
31
  dragOverlayConfig: DragOverlayConfig = {
28
32
  // 列内插入
29
33
  columnInsert: {
@@ -50,6 +54,28 @@ export class BlockGridModel extends GridModel {
50
54
  });
51
55
  }
52
56
 
57
+ onMount() {
58
+ super.onMount();
59
+ if (this.context.view?.inputArgs?.scene !== 'select' || this.viewActivatedListener) {
60
+ return;
61
+ }
62
+
63
+ this.viewActivatedListener = () => {
64
+ this.mapSubModels('items', (item) => {
65
+ (item as FlowModel & { onActive?: () => void }).onActive?.();
66
+ });
67
+ };
68
+ this.flowEngine.emitter.on(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
69
+ }
70
+
71
+ protected onUnmount() {
72
+ if (this.viewActivatedListener) {
73
+ this.flowEngine.emitter.off(VIEW_ACTIVATED_EVENT, this.viewActivatedListener);
74
+ this.viewActivatedListener = undefined;
75
+ }
76
+ super.onUnmount();
77
+ }
78
+
53
79
  get subModelBaseClasses() {
54
80
  const inputArgs = this.context.view?.inputArgs ?? {};
55
81
  if (inputArgs.collectionName && !inputArgs.filterByTk) {
@@ -16,10 +16,13 @@ import {
16
16
  tExpr,
17
17
  FlowModelContext,
18
18
  MultiRecordResource,
19
+ ResourceError,
19
20
  SingleRecordResource,
20
21
  } from '@nocobase/flow-engine';
21
22
  import type { FlowEngine } from '@nocobase/flow-engine';
22
23
  import _ from 'lodash';
24
+ import React from 'react';
25
+ import { BlockResourceErrorPlaceholder } from '../../components/placeholders/BlockPlaceholder';
23
26
  import { createDefaultCollectionBlockTitle } from '../../utils/blockUtils';
24
27
  import { areCapabilitiesSupported, getBlockCapabilityNamesFromModelClass } from '../../utils/actionCapability';
25
28
  import { FilterManager } from '../blocks/filter-manager/FilterManager';
@@ -82,6 +85,15 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
82
85
  const engine = this.context.engine as FlowEngine;
83
86
  const currentVersion = this.getDirtyTrackingVersion(engine, dataSourceKey, resource, params);
84
87
 
88
+ if (resource instanceof MultiRecordResource && this.getDataLoadingMode() === 'manual' && !this.hasActiveFilters()) {
89
+ resource.setData([]);
90
+ resource.setMeta({ count: 0, hasNext: false });
91
+ resource.setPage(1);
92
+ resource.loading = false;
93
+ this.lastSeenDirtyVersion = currentVersion;
94
+ return;
95
+ }
96
+
85
97
  if (forceRefresh) {
86
98
  if (this.dirtyRefreshing) return;
87
99
  this.dirtyRefreshing = true;
@@ -539,6 +551,18 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
539
551
  return this.resource.refresh();
540
552
  }
541
553
 
554
+ isExternalDataSource() {
555
+ const dataSourceKey = this.getResourceSettingsInitParams()?.dataSourceKey;
556
+ return Boolean(dataSourceKey && dataSourceKey !== 'main');
557
+ }
558
+
559
+ render() {
560
+ if (this.isExternalDataSource() && this.collection && this.resource?.getError()) {
561
+ return <BlockResourceErrorPlaceholder />;
562
+ }
563
+ return super.render();
564
+ }
565
+
542
566
  protected defaultBlockTitle() {
543
567
  const blockLabel = this.translate(this.constructor['meta']?.label || this.constructor.name);
544
568
  const params = this.getResourceSettingsInitParams();
@@ -569,9 +593,13 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
569
593
  }
570
594
  if (fieldPath.includes('.')) {
571
595
  // 关系数据
596
+ const collection = this.collection;
597
+ if (!collection) {
598
+ return;
599
+ }
572
600
  const [field1, field2] = fieldPath.split('.');
573
601
  const associationField = this.context.dataSourceManager.getCollectionField(
574
- `${this.collection.dataSourceKey}.${this.collection.name}.${field1}`,
602
+ `${collection.dataSourceKey}.${collection.name}.${field1}`,
575
603
  ) as CollectionField;
576
604
  if (!associationField) {
577
605
  return;
@@ -583,7 +611,7 @@ export class CollectionBlockModel<T = DefaultStructure> extends DataBlockModel<T
583
611
  }
584
612
 
585
613
  const collectionField = this.context.dataSourceManager.getCollectionField(
586
- `${this.collection.dataSourceKey}.${targetCollectionName}.${field2}`,
614
+ `${collection.dataSourceKey}.${targetCollectionName}.${field2}`,
587
615
  ) as CollectionField;
588
616
 
589
617
  if (collectionField && collectionField.isAssociationField()) {
@@ -693,7 +721,14 @@ CollectionBlockModel.registerFlow({
693
721
  if (ctx.model.isManualRefresh) {
694
722
  ctx.model.resource.loading = false;
695
723
  } else {
696
- await ctx.model.resource.refresh();
724
+ try {
725
+ await resource.refresh();
726
+ } catch (error) {
727
+ if (blockModel.isExternalDataSource() && error instanceof ResourceError && resource.getError() === error) {
728
+ return;
729
+ }
730
+ throw error;
731
+ }
697
732
  }
698
733
  },
699
734
  },
@@ -922,7 +922,6 @@ export class GridModel<T extends { subModels: { items: FlowModel[] } } = Default
922
922
  const baseLayout = this.context.isMobileLayout
923
923
  ? normalizeGridLayout({
924
924
  rows: transformRowsToSingleColumn(projectLayoutToLegacyRows(rawLayout).rows),
925
- itemUids: this.getItemUids(),
926
925
  })
927
926
  : rawLayout;
928
927
  const baseProjection = projectLayoutToLegacyRows(baseLayout);