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

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 (173) 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/__tests__/validation.test.ts +48 -0
  67. package/src/flow/actions/linkageRules.tsx +59 -16
  68. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  69. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  70. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +42 -23
  71. package/src/flow/common/Markdown/style.ts +4 -0
  72. package/src/flow/components/BlockItemCard.tsx +2 -2
  73. package/src/flow/components/FieldAssignValueInput.tsx +42 -621
  74. package/src/flow/components/FlowRoute.tsx +71 -29
  75. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  76. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +335 -0
  77. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +389 -0
  78. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  79. package/src/flow/components/field-value-variable/index.ts +12 -0
  80. package/src/flow/components/filter/VariableFilterItem.tsx +23 -6
  81. package/src/flow/components/filter/__tests__/VariableFilterItem.rightMetaTree.test.tsx +158 -0
  82. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
  83. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  84. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  85. package/src/flow/index.ts +2 -0
  86. package/src/flow/internal/components/Markdown/DisplayMarkdown.tsx +14 -9
  87. package/src/flow/internal/components/Markdown/__tests__/DisplayMarkdown.test.tsx +46 -0
  88. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  89. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  90. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  91. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  92. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  93. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  94. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  95. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  96. package/src/flow/models/base/GridModel.tsx +0 -1
  97. package/src/flow/models/base/PageModel/PageTabModel.tsx +67 -17
  98. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +509 -12
  99. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  100. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  101. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  102. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  103. package/src/flow/models/blocks/assign-form/AssignFormGridModel.tsx +22 -1
  104. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +36 -50
  105. package/src/flow/models/blocks/assign-form/__tests__/assignFieldValuesFlow.editor.test.tsx +140 -0
  106. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  107. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  108. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +27 -3
  109. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  110. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
  111. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  112. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  113. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.toggleFormFieldsCollapse.test.ts +29 -0
  114. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
  115. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  116. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  117. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +32 -4
  118. package/src/flow/models/blocks/filter-form/fields/FieldComponentProps.tsx +1 -1
  119. package/src/flow/models/blocks/filter-form/fields/__tests__/FieldComponentProps.options.test.tsx +61 -0
  120. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  121. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  122. package/src/flow/models/blocks/form/FormBlockModel.tsx +73 -5
  123. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  124. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +153 -2
  125. package/src/flow/models/blocks/form/__tests__/popupLinkage.test.tsx +175 -0
  126. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  127. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  128. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  129. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  130. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  131. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  132. package/src/flow/models/blocks/table/TableBlockModel.tsx +28 -2
  133. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  134. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  135. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  136. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  137. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  138. package/src/flow/models/blocks/table/utils.ts +7 -0
  139. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  140. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +12 -3
  141. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  142. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  143. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +29 -3
  144. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  145. package/src/flow/models/fields/AssociationFieldModel/SubTableFieldModel/SubTableField.tsx +5 -1
  146. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +191 -3
  147. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  148. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  149. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  150. package/src/flow/models/fields/DisplayAssociationField/DisplaySubTableFieldModel.tsx +42 -7
  151. package/src/flow/models/fields/DisplayAssociationField/__tests__/DisplaySubTableFieldModel.test.tsx +351 -0
  152. package/src/flow/models/fields/DisplayHtmlFieldModel.tsx +2 -1
  153. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  154. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  155. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  156. package/src/flow/models/fields/RichTextFieldModel/__tests__/RichTextFieldModel.test.tsx +74 -0
  157. package/src/flow/models/fields/RichTextFieldModel/index.tsx +27 -6
  158. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  159. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  160. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  161. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
  162. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
  163. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  164. package/src/flow/models/topbar/TopbarActionModel.tsx +5 -5
  165. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  166. package/src/index.ts +1 -0
  167. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  168. package/src/layout-manager/LayoutRoute.tsx +2 -1
  169. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  170. package/src/locale/languageCodes.ts +1 -0
  171. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  172. package/src/ui-operation/index.ts +33 -0
  173. package/src/ui-operation/ui-operation-codec.ts +54 -0
@@ -86,6 +86,10 @@ export class FormValueRuntime {
86
86
 
87
87
  this.ruleEngine = new RuleEngine({
88
88
  getBlockModelUid: () => String(this.model?.uid),
89
+ getAssignRulesModelUid: () => {
90
+ const grid = this.model?.subModels?.grid;
91
+ return !Array.isArray(grid) && grid?.uid ? String(grid.uid) : undefined;
92
+ },
89
93
  getActionName: () => this.model?.getAclActionName?.() ?? this.model?.context?.actionName,
90
94
  getBlockContext: () => this.model?.context,
91
95
  getEngine: () => this.model?.context?.engine,
@@ -190,13 +194,32 @@ export class FormValueRuntime {
190
194
  return values;
191
195
  }
192
196
 
193
- private toMirrorSnapshot(value: any) {
194
- const raw = isObservable(value) ? toJS(value) : value;
195
- return _.cloneDeepWith(raw, (item) => {
196
- if (!item || typeof item !== 'object') return undefined;
197
- if (Array.isArray(item) || _.isPlainObject(item)) return undefined;
198
- return item;
199
- });
197
+ private toMirrorSnapshot<T>(value: T): T {
198
+ const cloneValue = (input: unknown): unknown =>
199
+ _.cloneDeepWith(input, (item: unknown) => {
200
+ if (isObservable(item)) {
201
+ const plainItem = toJS(item);
202
+ if (plainItem !== item) return cloneValue(plainItem);
203
+ }
204
+
205
+ // Tracking form-value array proxies hide `constructor`, while Lodash's array
206
+ // clone relies on it being callable.
207
+ if (Array.isArray(item) && typeof item.constructor !== 'function') {
208
+ const plainArray = new Array(item.length);
209
+ for (let index = 0; index < item.length; index++) {
210
+ if (Object.hasOwn(item, index)) {
211
+ plainArray[index] = cloneValue(item[index]);
212
+ }
213
+ }
214
+ return plainArray;
215
+ }
216
+
217
+ if (!item || typeof item !== 'object') return undefined;
218
+ if (Array.isArray(item) || _.isPlainObject(item)) return undefined;
219
+ return item;
220
+ });
221
+
222
+ return cloneValue(value) as T;
200
223
  }
201
224
 
202
225
  canApplyDefaultValuePatch(namePath: NamePath, resolved: any) {
@@ -940,11 +963,12 @@ export class FormValueRuntime {
940
963
  const changedPaths: NamePath[] = [];
941
964
 
942
965
  if (!Array.isArray(patch)) {
943
- const patchEntries = Object.entries(patch || {}).filter(([pathKey, rawValue]) => {
944
- if (shouldSkipByLinkageScope(pathKey)) return false;
945
- const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
946
- return !_.isEqual(this.getFormValueAtPath([pathKey]), value);
947
- });
966
+ const patchEntries = Object.entries(patch || {})
967
+ .map(([pathKey, rawValue]) => [pathKey, this.toMirrorSnapshot(rawValue)] as const)
968
+ .filter(([pathKey, value]) => {
969
+ if (shouldSkipByLinkageScope(pathKey)) return false;
970
+ return !_.isEqual(this.getFormValueAtPath([pathKey]), value);
971
+ });
948
972
  const patchToApply = Object.fromEntries(patchEntries);
949
973
  const patchKeys = patchEntries.map(([pathKey]) => pathKey);
950
974
  if (!patchKeys.length) {
@@ -955,8 +979,7 @@ export class FormValueRuntime {
955
979
  }
956
980
  this.suppressFormCallbackDepth++;
957
981
  try {
958
- for (const [pathKey, rawValue] of patchEntries) {
959
- const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
982
+ for (const [pathKey, value] of patchEntries) {
960
983
  if (typeof form.setFieldValue === 'function') {
961
984
  form.setFieldValue(pathKey, value);
962
985
  } else if (typeof (form as any).setFields === 'function') {
@@ -1017,7 +1040,7 @@ export class FormValueRuntime {
1017
1040
  const namePath = this.resolveNamePath(callerCtx, item.path);
1018
1041
  const pathKey = namePathToPathKey(namePath);
1019
1042
  const rawValue = item.value;
1020
- const value = isObservable(rawValue) ? toJS(rawValue) : rawValue;
1043
+ const value = this.toMirrorSnapshot(rawValue);
1021
1044
 
1022
1045
  if (shouldSkipByLinkageScope(pathKey)) continue;
1023
1046
 
@@ -1156,17 +1179,18 @@ export class FormValueRuntime {
1156
1179
  if (!form) return;
1157
1180
  if (source === 'override' && this.findUserEditedHit(pathKey)) return;
1158
1181
 
1182
+ const value = this.toMirrorSnapshot(nextValue);
1159
1183
  const prevValue = _.get(this.valuesMirror, namePath);
1160
- if (_.isEqual(prevValue, nextValue)) return;
1184
+ if (_.isEqual(prevValue, value)) return;
1161
1185
 
1162
1186
  this.writeSeq += 1;
1163
1187
  const writeSeq = this.writeSeq;
1164
1188
 
1165
1189
  this.suppressFormCallbackDepth++;
1166
1190
  try {
1167
- form.setFieldValue?.(namePath, nextValue);
1168
- _.set(this.valuesMirror, namePath, this.toMirrorSnapshot(nextValue));
1169
- this.syncMountedFieldModelValue(namePath, nextValue);
1191
+ form.setFieldValue?.(namePath, value);
1192
+ _.set(this.valuesMirror, namePath, this.toMirrorSnapshot(value));
1193
+ this.syncMountedFieldModelValue(namePath, value);
1170
1194
  this.bumpChangeTick();
1171
1195
  } finally {
1172
1196
  this.suppressFormCallbackDepth--;
@@ -235,7 +235,7 @@ export class JSBlockModel extends BlockModel {
235
235
  const cardProps = {
236
236
  ...rest,
237
237
  height,
238
- style,
238
+ ...(style === undefined ? {} : { style }),
239
239
  ...(beforeContent === undefined ? {} : { beforeContent }),
240
240
  ...(afterContent === undefined ? {} : { afterContent }),
241
241
  };
@@ -9,13 +9,14 @@
9
9
 
10
10
  import { LockOutlined, QuestionCircleOutlined } from '@ant-design/icons';
11
11
  import { css } from '@emotion/css';
12
- import type { PropertyMetaFactory } from '@nocobase/flow-engine';
12
+ import type { FlowModelOptions, PropertyMetaFactory } from '@nocobase/flow-engine';
13
13
  import {
14
14
  Droppable,
15
15
  tExpr,
16
16
  FlowsFloatContextMenu,
17
17
  DragHandler,
18
18
  MemoFlowModelRenderer,
19
+ createCurrentRecordMetaFactory,
19
20
  createRecordMetaFactory,
20
21
  createRecordResolveOnServerWithLocal,
21
22
  ElementProxy,
@@ -51,6 +52,14 @@ function getRecordRenderSignature(record: any) {
51
52
  export class JSColumnModel extends TableCustomColumnModel {
52
53
  // Stable per‑instance render component to avoid remounts across re-renders
53
54
  private _RenderComponent?: React.ComponentType;
55
+
56
+ onInit(options: FlowModelOptions): void {
57
+ super.onInit(options);
58
+ this.context.defineProperty('record', {
59
+ meta: createCurrentRecordMetaFactory(this.context, () => this.context.collection),
60
+ });
61
+ }
62
+
54
63
  renderHiddenInConfig() {
55
64
  return (
56
65
  <Tooltip
@@ -36,7 +36,15 @@ import { BlockSceneEnum } from '../../base/BlockModel';
36
36
  import { CollectionBlockModel } from '../../base/CollectionBlockModel';
37
37
  import { QuickEditFormModel } from '../form/QuickEditFormModel';
38
38
  import { TableColumnModel } from './TableColumnModel';
39
- import { extractIndex, adjustColumnOrder, setNestedValue, extractIds, getRowKey, useBlockHeight } from './utils';
39
+ import {
40
+ extractIndex,
41
+ adjustColumnOrder,
42
+ setNestedValue,
43
+ extractIds,
44
+ extractRowKeys,
45
+ getRowKey,
46
+ useBlockHeight,
47
+ } from './utils';
40
48
  import { resolveTableSorterField } from './sortUtils';
41
49
  import { commonConditionHandler, ConditionBuilder } from '../../../components/ConditionBuilder';
42
50
  import { BulkDeleteActionModel } from '../../actions/BulkDeleteActionModel';
@@ -222,6 +230,22 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
222
230
  return super.resource as MultiRecordResource;
223
231
  }
224
232
 
233
+ resetAfterFilterChange() {
234
+ if (!this.props.treeTable) {
235
+ return;
236
+ }
237
+
238
+ const expandAll = !!this.props.defaultExpandAllRows;
239
+ const expandedRowKeys = expandAll ? extractRowKeys(this.resource.getData(), this.collection.filterTargetKey) : [];
240
+
241
+ this.setProps('expandedRowKeys', expandedRowKeys);
242
+ this.mapSubModels('actions', (action) => {
243
+ if ('setExpandFlag' in action && typeof action.setExpandFlag === 'function') {
244
+ action.setExpandFlag(expandAll);
245
+ }
246
+ });
247
+ }
248
+
225
249
  private readonly columns = observable.ref([]);
226
250
  private disposeAutorun: () => void;
227
251
 
@@ -244,7 +268,9 @@ export class TableBlockModel extends CollectionBlockModel<TableBlockModelStructu
244
268
  }
245
269
 
246
270
  createResource(ctx, params) {
247
- return this.context.createResource(MultiRecordResource);
271
+ const resource = this.context.createResource(MultiRecordResource);
272
+ resource.addRequestHeader('X-With-ACL-Meta', 'true');
273
+ return resource;
248
274
  }
249
275
 
250
276
  /**
@@ -27,6 +27,7 @@ import {
27
27
  ModelRenderMode,
28
28
  useFlowModel,
29
29
  } from '@nocobase/flow-engine';
30
+ import type { FlowModel } from '@nocobase/flow-engine';
30
31
  import { useTranslation } from 'react-i18next';
31
32
  import { TableColumnProps, Tooltip, Space, InputNumber, Button, Divider } from 'antd';
32
33
  import { get, omit, capitalize } from 'lodash';
@@ -61,19 +62,28 @@ export function FieldDeletePlaceholder(props: any) {
61
62
  );
62
63
  }
63
64
 
64
- function FieldWithoutPermissionPlaceholder() {
65
+ type FieldPermissionPlaceholderModel = FlowModel & {
66
+ fieldPath?: string;
67
+ forbidden?: { actionName?: string } | null;
68
+ };
69
+
70
+ export function FieldWithoutPermissionPlaceholder({
71
+ targetModel,
72
+ }: {
73
+ targetModel?: FieldPermissionPlaceholderModel;
74
+ } = {}) {
65
75
  const { t } = useTranslation();
66
- const model: any = useFlowModel();
67
- const blockModel = model.context.blockModel;
68
- const collection = model.context.collectionField?.collection || blockModel.collection;
69
- const dataSource = collection.dataSource;
76
+ const contextModel = useFlowModel<FieldPermissionPlaceholderModel>();
77
+ const model = targetModel || contextModel;
78
+ const collection = model.context.collectionField?.collection || model.context.blockModel?.collection;
79
+ const dataSource = collection?.dataSource;
70
80
  const name = model.context.collectionField?.name || model.fieldPath;
71
81
  const nameValue = useMemo(() => {
72
- const dataSourcePrefix = `${t(dataSource.displayName || dataSource.key)} > `;
82
+ const dataSourcePrefix = dataSource ? `${t(dataSource.displayName || dataSource.key)} > ` : '';
73
83
  const collectionPrefix = collection ? `${t(collection.title) || collection.name || collection.tableName} > ` : '';
74
84
  return `${dataSourcePrefix}${collectionPrefix}${name}`;
75
- }, [collection, dataSource.displayName, dataSource.key, name, t]);
76
- const { actionName } = model.forbidden;
85
+ }, [collection, dataSource, name, t]);
86
+ const actionName = model.forbidden?.actionName || 'view';
77
87
  const messageValue = useMemo(() => {
78
88
  return t(
79
89
  `The current user only has the UI configuration permission, but don't have "{{actionName}}" permission for field "{{name}}"`,
@@ -194,6 +204,35 @@ export class TableColumnModel extends DisplayItemModel {
194
204
  .filter(Boolean);
195
205
  }
196
206
 
207
+ canQuickEdit(record: Record<string, unknown>): boolean {
208
+ if (!this.props.editable) {
209
+ return false;
210
+ }
211
+ if (this.context.skipAclCheck) {
212
+ return true;
213
+ }
214
+
215
+ const blockModel = this.context.blockModel;
216
+ const collection = blockModel?.collection;
217
+ const resource = blockModel?.resource;
218
+ const collectionField = this.collectionField;
219
+ if (!collection || !resource || !collectionField) {
220
+ return false;
221
+ }
222
+
223
+ const filterByTk = collection.getFilterByTK(record);
224
+ const recordPkValue = typeof filterByTk === 'string' || typeof filterByTk === 'number' ? filterByTk : undefined;
225
+
226
+ return this.context.acl.can({
227
+ dataSourceKey: collection.dataSourceKey,
228
+ resourceName: resource.getResourceName() || collection.name,
229
+ actionName: 'update',
230
+ recordPkValue,
231
+ allowedActions: resource.getMeta('allowedActions'),
232
+ fields: [collectionField.name],
233
+ });
234
+ }
235
+
197
236
  getColumnProps(): TableColumnProps & { sortField?: string } {
198
237
  if (!this.props.width) {
199
238
  return;
@@ -250,7 +289,7 @@ export class TableColumnModel extends DisplayItemModel {
250
289
  record,
251
290
  recordIndex: record?.__index || recordIndex,
252
291
  width: this.props.width - 16,
253
- editable: this.props.editable,
292
+ editable: this.canQuickEdit(record),
254
293
  dataIndex: this.props.dataIndex,
255
294
  title: this.props.title,
256
295
  overflowMode: this.props.overflowMode,
@@ -11,17 +11,62 @@ import { FlowEngine } from '@nocobase/flow-engine';
11
11
  import { describe, expect, it, vi } from 'vitest';
12
12
  import { JSColumnModel } from '../JSColumnModel';
13
13
 
14
- describe('JSColumnModel row refresh', () => {
14
+ describe('JSColumnModel', () => {
15
+ it('declares current record metadata before rendering any row', async () => {
16
+ const engine = new FlowEngine();
17
+ engine.registerModels({ JSColumnModel });
18
+ const model = engine.createModel<JSColumnModel>({
19
+ use: 'JSColumnModel',
20
+ uid: 'js-column-record-meta',
21
+ props: {
22
+ width: 200,
23
+ title: 'JS column',
24
+ },
25
+ });
26
+
27
+ engine.context.dataSourceManager.getDataSource('main').addCollection({
28
+ name: 'users',
29
+ filterTargetKey: 'id',
30
+ fields: [
31
+ { name: 'id', type: 'integer', interface: 'number' },
32
+ { name: 'age', type: 'integer', interface: 'integer' },
33
+ ],
34
+ });
35
+
36
+ const collection = engine.context.dataSourceManager.getCollection('main', 'users');
37
+ model.context.defineProperty('collection', {
38
+ value: collection,
39
+ });
40
+
41
+ const recordNode = model.context.getPropertyMetaTree().find((node) => node.name === 'record');
42
+ const recordFields =
43
+ typeof recordNode?.children === 'function' ? await recordNode.children() : recordNode?.children || [];
44
+
45
+ expect(model.context.record).toBeUndefined();
46
+ expect(recordNode).toMatchObject({
47
+ name: 'record',
48
+ title: 'Current record',
49
+ paths: ['record'],
50
+ });
51
+ expect(recordFields).toEqual(
52
+ expect.arrayContaining([
53
+ expect.objectContaining({ name: 'id', paths: ['record', 'id'] }),
54
+ expect.objectContaining({ name: 'age', paths: ['record', 'age'] }),
55
+ ]),
56
+ );
57
+ });
58
+
15
59
  it('changes renderer key and invalidates beforeRender cache when row content changes', () => {
16
60
  const engine = new FlowEngine();
17
- const model = new JSColumnModel({
61
+ engine.registerModels({ JSColumnModel });
62
+ const model = engine.createModel<JSColumnModel>({
63
+ use: 'JSColumnModel',
18
64
  uid: 'js-column-row-refresh',
19
- flowEngine: engine,
20
65
  props: {
21
66
  width: 200,
22
67
  title: 'JS column',
23
68
  },
24
- } as any);
69
+ });
25
70
 
26
71
  engine.context.dataSourceManager.getDataSource('main').addCollection({
27
72
  name: 'users',
@@ -39,12 +84,14 @@ describe('JSColumnModel row refresh', () => {
39
84
  });
40
85
 
41
86
  const column = model.getColumnProps();
42
- const first = column.render(null, { id: 1, age: 3, workyears: 39.2 }, 0) as any;
87
+ const firstRecord = { id: 1, age: 3, workyears: 39.2 };
88
+ const first = column.render(null, firstRecord, 0) as any;
43
89
  const fork = model.getFork('1') as any;
44
90
  const invalidateFlowCache = vi.fn();
45
91
  fork.invalidateFlowCache = invalidateFlowCache;
46
92
  const second = column.render(null, { id: 1, age: 37, workyears: 39.2 }, 0) as any;
47
93
 
94
+ expect(fork.context.record).toEqual({ id: 1, age: 37, workyears: 39.2 });
48
95
  expect(first.key).not.toBe(second.key);
49
96
  expect(invalidateFlowCache).toHaveBeenCalledWith('beforeRender');
50
97
  });
@@ -0,0 +1,78 @@
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, vi } from 'vitest';
11
+ import { TableBlockModel } from '../TableBlockModel';
12
+
13
+ describe('TableBlockModel filter reset', () => {
14
+ it('collapses the filtered tree when rows are not expanded by default', () => {
15
+ const setExpandFlag = vi.fn();
16
+ const setProps = vi.fn();
17
+ const model = {
18
+ props: { treeTable: true, defaultExpandAllRows: false, expandedRowKeys: [1, 2] },
19
+ resource: { getData: () => [{ id: 3, children: [{ id: 4 }] }] },
20
+ setProps,
21
+ mapSubModels: (_key, callback) => callback({ setExpandFlag }),
22
+ };
23
+
24
+ TableBlockModel.prototype.resetAfterFilterChange.call(model);
25
+
26
+ expect(setProps).toHaveBeenCalledWith('expandedRowKeys', []);
27
+ expect(setExpandFlag).toHaveBeenCalledWith(false);
28
+ });
29
+
30
+ it('expands the filtered tree when rows are expanded by default', () => {
31
+ const setExpandFlag = vi.fn();
32
+ const setProps = vi.fn();
33
+ const model = {
34
+ props: { treeTable: true, defaultExpandAllRows: true, expandedRowKeys: [1, 2] },
35
+ collection: { filterTargetKey: 'id' },
36
+ resource: { getData: () => [{ id: 3, children: [{ id: 4 }] }] },
37
+ setProps,
38
+ mapSubModels: (_key, callback) => callback({ setExpandFlag }),
39
+ };
40
+
41
+ TableBlockModel.prototype.resetAfterFilterChange.call(model);
42
+
43
+ expect(setProps).toHaveBeenCalledWith('expandedRowKeys', [3, 4]);
44
+ expect(setExpandFlag).toHaveBeenCalledWith(true);
45
+ });
46
+
47
+ it('uses the collection filter target key when expanding the filtered tree', () => {
48
+ const setProps = vi.fn();
49
+ const model = {
50
+ props: { treeTable: true, defaultExpandAllRows: true },
51
+ collection: { filterTargetKey: ['tenant', 'slug'] },
52
+ resource: {
53
+ getData: () => [{ tenant: 'acme', slug: 'root', children: [{ tenant: 'acme', slug: 'child' }] }],
54
+ },
55
+ setProps,
56
+ mapSubModels: vi.fn(),
57
+ };
58
+
59
+ TableBlockModel.prototype.resetAfterFilterChange.call(model);
60
+
61
+ expect(setProps).toHaveBeenCalledWith('expandedRowKeys', ['acme-root', 'acme-child']);
62
+ });
63
+
64
+ it('does not change expansion state for a regular table', () => {
65
+ const setProps = vi.fn();
66
+ const mapSubModels = vi.fn();
67
+ const model = {
68
+ props: { treeTable: false },
69
+ setProps,
70
+ mapSubModels,
71
+ };
72
+
73
+ TableBlockModel.prototype.resetAfterFilterChange.call(model);
74
+
75
+ expect(setProps).not.toHaveBeenCalled();
76
+ expect(mapSubModels).not.toHaveBeenCalled();
77
+ });
78
+ });
@@ -9,8 +9,20 @@
9
9
 
10
10
  import { FlowEngine, MultiRecordResource } from '@nocobase/flow-engine';
11
11
  import { describe, expect, it } from 'vitest';
12
+ import { TableBlockModel } from '../TableBlockModel';
12
13
 
13
14
  describe('TableBlockModel quick edit refresh', () => {
15
+ it('requests record-level ACL metadata for table data', () => {
16
+ const engine = new FlowEngine();
17
+ const model = {
18
+ context: engine.context,
19
+ };
20
+
21
+ const resource = TableBlockModel.prototype.createResource.call(model, {}, {});
22
+
23
+ expect(resource.getRequestOptions().headers['X-With-ACL-Meta']).toBe('true');
24
+ });
25
+
14
26
  it('updates table data through a new array reference after quick editing a flat row', () => {
15
27
  const engine = new FlowEngine();
16
28
  const resource = engine.context.createResource(MultiRecordResource);
@@ -66,6 +66,39 @@ describe('TableColumnModel sorter settings', () => {
66
66
  expect(setProps).toHaveBeenCalledWith('editable', false);
67
67
  });
68
68
 
69
+ it('checks record update scope and field permission before enabling quick edit', () => {
70
+ const can = vi.fn(() => false);
71
+ const model = {
72
+ props: { editable: true },
73
+ collectionField: { name: 'title' },
74
+ context: {
75
+ skipAclCheck: false,
76
+ acl: { can },
77
+ blockModel: {
78
+ collection: {
79
+ dataSourceKey: 'main',
80
+ name: 'posts',
81
+ getFilterByTK: () => 2,
82
+ },
83
+ resource: {
84
+ getResourceName: () => 'posts',
85
+ getMeta: () => ({ update: [1] }),
86
+ },
87
+ },
88
+ },
89
+ };
90
+
91
+ expect(TableColumnModel.prototype.canQuickEdit.call(model, { id: 2 })).toBe(false);
92
+ expect(can).toHaveBeenCalledWith({
93
+ dataSourceKey: 'main',
94
+ resourceName: 'posts',
95
+ actionName: 'update',
96
+ recordPkValue: 2,
97
+ allowedActions: { update: [1] },
98
+ fields: ['title'],
99
+ });
100
+ });
101
+
69
102
  it('hides sortable setting for association fields', async () => {
70
103
  const engine = new FlowEngine();
71
104
  const model = new TableColumnModel({ uid: 'table-column-association-sorter', flowEngine: engine } as any);
@@ -67,6 +67,13 @@ export function extractIds(data) {
67
67
  return ids;
68
68
  }
69
69
 
70
+ export function extractRowKeys(data: Record<string, unknown>[], key: string | string[]) {
71
+ return data.flatMap((item) => {
72
+ const children = Array.isArray(item.children) ? extractRowKeys(item.children, key) : [];
73
+ return [getRowKey(item, key), ...children];
74
+ });
75
+ }
76
+
70
77
  /**
71
78
  * 生成表格行唯一 key
72
79
  * @param record 当前行数据
@@ -8,7 +8,13 @@
8
8
  */
9
9
 
10
10
  import { Cascader, Space, Button } from 'antd';
11
- import { CollectionField, EditableItemModel, tExpr, MultiRecordResource } from '@nocobase/flow-engine';
11
+ import {
12
+ CollectionField,
13
+ EditableItemModel,
14
+ FilterableItemModel,
15
+ MultiRecordResource,
16
+ tExpr,
17
+ } from '@nocobase/flow-engine';
12
18
  import { DeleteOutlined } from '@ant-design/icons';
13
19
  import { css, cx } from '@emotion/css';
14
20
  import { debounce, castArray, omit, last, isEqual } from 'lodash';
@@ -31,6 +37,23 @@ function normalizeLabelKeys(labelKeyOrKeys?: string | string[]): string[] {
31
37
  return Array.from(new Set(list.filter((item): item is string => !!item && typeof item === 'string')));
32
38
  }
33
39
 
40
+ function normalizeCascadeFieldKey(key: unknown): string | undefined {
41
+ if (typeof key === 'string' && key) {
42
+ return key;
43
+ }
44
+ if (Array.isArray(key) && key.length === 1 && typeof key[0] === 'string') {
45
+ return key[0];
46
+ }
47
+ return undefined;
48
+ }
49
+
50
+ function getDefaultCascadeFieldNames(field: CollectionField) {
51
+ const targetCollection = field?.targetCollection;
52
+ const value = normalizeCascadeFieldKey(targetCollection?.filterTargetKey) || 'id';
53
+ const label = targetCollection?.titleCollectionField?.name || value;
54
+ return { label, value };
55
+ }
56
+
34
57
  function getRecordDisplayLabel(item: any, labelKeyOrKeys?: string | string[]): string | null {
35
58
  if (!isPlainRecord(item)) return null;
36
59
  const labelKeys = normalizeLabelKeys(labelKeyOrKeys);
@@ -815,6 +838,15 @@ EditableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o',
815
838
  order: 60,
816
839
  });
817
840
 
841
+ FilterableItemModel.bindModelToInterface('CascadeSelectFieldModel', ['m2o', 'o2o', 'oho', 'obo'], {
842
+ when: (ctx, field) => field.targetCollection?.template === 'tree',
843
+ isDefault: true,
844
+ order: 60,
845
+ defaultProps: (ctx, field) => ({
846
+ fieldNames: getDefaultCascadeFieldNames(field),
847
+ }),
848
+ });
849
+
818
850
  EditableItemModel.bindModelToInterface('CascadeSelectListFieldModel', ['m2m', 'o2m', 'mbm'], {
819
851
  when: (ctx, field) => field.targetCollection?.template === 'tree',
820
852
  isDefault: true,
@@ -19,7 +19,7 @@ import {
19
19
  createCurrentRecordMetaFactory,
20
20
  useFlowModel,
21
21
  } from '@nocobase/flow-engine';
22
- import { Table, Button, Space } from 'antd';
22
+ import { Table, Button, Space, theme } from 'antd';
23
23
  import { uid } from '@formily/shared';
24
24
  import classNames from 'classnames';
25
25
  import { DragEndEvent } from '@dnd-kit/core';
@@ -28,6 +28,7 @@ import { observer } from '@formily/reactive-react';
28
28
  import React, { useEffect, useMemo, useState, useRef } from 'react';
29
29
  import { useTranslation } from 'react-i18next';
30
30
  import { buildRecordPickerPopupContextInputArgs, RecordPickerContent } from '../RecordPickerFieldModel';
31
+ import { buildOpenerUids } from '../recordSelectShared';
31
32
  import { AssociationFieldModel } from '../AssociationFieldModel';
32
33
  import { adjustColumnOrder } from '../../../blocks/table/utils';
33
34
  import { isSubTableColumnFieldComponentContext } from '../SubTableFieldModel/SubTableColumnModel';
@@ -189,6 +190,7 @@ const AddFieldColumn = ({ model }) => {
189
190
  };
190
191
 
191
192
  const DisplayTable = (props) => {
193
+ const { token } = theme.useToken();
192
194
  const {
193
195
  pageSize,
194
196
  value,
@@ -268,7 +270,7 @@ const DisplayTable = (props) => {
268
270
  },
269
271
  },
270
272
  ...baseColumns,
271
- { key: 'empty' },
273
+ { key: 'empty', render: () => <div style={{ minHeight: token.controlHeight }} /> },
272
274
  ].filter(Boolean),
273
275
  ) as any[];
274
276
 
@@ -282,7 +284,7 @@ const DisplayTable = (props) => {
282
284
  }
283
285
 
284
286
  return cols;
285
- }, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model]);
287
+ }, [baseColumns, enableIndexColumn, currentPage, currentPageSize, isConfigMode, model, token.controlHeight]);
286
288
  return (
287
289
  <Table
288
290
  tableLayout="fixed"
@@ -653,6 +655,11 @@ PopupSubTableFieldModel.registerFlow({
653
655
  const parentItemOptions = ctx?.getPropertyOptions?.('item');
654
656
  const itemIndex = Array.isArray(ctx.model?.props?.value) ? ctx.model.props.value.length : 0;
655
657
  const itemLength = itemIndex + 1;
658
+ const associationName = ctx.collectionField?.resourceName;
659
+ const sourceId = parentItem?.value
660
+ ? ctx.collectionField?.collection?.getFilterByTK?.(parentItem.value)
661
+ : undefined;
662
+ const openerUids = buildOpenerUids(ctx, ctx.inputArgs);
656
663
  ctx.viewer.open({
657
664
  type: openMode,
658
665
  width: sizeToWidthMap[openMode][size],
@@ -663,12 +670,14 @@ PopupSubTableFieldModel.registerFlow({
663
670
  scene: 'subForm',
664
671
  dataSourceKey: ctx.collection.dataSourceKey,
665
672
  collectionName: ctx.collectionField?.target,
673
+ ...(associationName && sourceId != null ? { associationName, sourceId } : {}),
666
674
  collectionField: ctx.collectionField,
667
675
  parentItem,
668
676
  parentItemMeta: parentItemOptions?.meta,
669
677
  parentItemResolver: parentItemOptions?.resolveOnServer,
670
678
  itemIndex,
671
679
  itemLength,
680
+ openerUids,
672
681
  },
673
682
  content: () => <EditFormContent model={ctx.model} scene="create" />,
674
683
  styles: {