@nocobase/client-v2 2.2.0-beta.8 → 2.2.0

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 (271) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +3 -0
  3. package/es/RouteRepository.d.ts +8 -0
  4. package/es/RouterManager.d.ts +15 -0
  5. package/es/authRedirect.d.ts +1 -0
  6. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  7. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  8. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  9. package/es/components/category-tabs/index.d.ts +9 -0
  10. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  11. package/es/components/form/TypedVariableInput.d.ts +31 -5
  12. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  13. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  14. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  15. package/es/components/form/filter/index.d.ts +2 -0
  16. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  17. package/es/components/index.d.ts +1 -0
  18. package/es/entry-actions/EntryActionManager.d.ts +24 -0
  19. package/es/entry-actions/index.d.ts +9 -0
  20. package/es/flow/actions/index.d.ts +1 -1
  21. package/es/flow/actions/linkageRules.d.ts +27 -0
  22. package/es/flow/admin-shell/BaseLayoutModel.d.ts +6 -0
  23. package/es/flow/admin-shell/BaseLayoutRouteCoordinator.d.ts +7 -0
  24. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  25. package/es/flow/admin-shell/admin-layout/AppListRender.d.ts +2 -1
  26. package/es/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.d.ts +24 -0
  27. package/es/flow/admin-shell/admin-layout/index.d.ts +1 -0
  28. package/es/flow/admin-shell/admin-layout/useApplications.d.ts +7 -2
  29. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  30. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  31. package/es/flow/components/FlowRoute.d.ts +2 -2
  32. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  33. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  34. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  35. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  36. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  37. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  38. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  39. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  40. package/es/flow/index.d.ts +1 -0
  41. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  42. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  43. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  44. package/es/flow/models/base/ActionModelCore.d.ts +6 -2
  45. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  46. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  47. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  48. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  49. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  50. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  51. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  52. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  53. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  54. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  55. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  56. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  57. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  58. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  59. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  60. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  61. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  62. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  63. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  64. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  65. package/es/flow/routeTransientInputArgs.d.ts +14 -0
  66. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  67. package/es/flow-compat/passwordUtils.d.ts +1 -1
  68. package/es/flow-compat/routeTypes.d.ts +1 -0
  69. package/es/index.d.ts +7 -0
  70. package/es/index.mjs +301 -177
  71. package/es/ui-operation/index.d.ts +15 -0
  72. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  73. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  74. package/es/utils/index.d.ts +1 -0
  75. package/es/utils/markdownSanitize.d.ts +11 -0
  76. package/lib/index.js +313 -189
  77. package/lib/locale/languageCodes.js +3 -1
  78. package/package.json +9 -7
  79. package/src/APIClient.ts +90 -7
  80. package/src/Application.tsx +7 -1
  81. package/src/BaseApplication.tsx +13 -6
  82. package/src/RouteRepository.ts +25 -0
  83. package/src/RouterManager.tsx +142 -1
  84. package/src/__tests__/APIClient.test.tsx +58 -0
  85. package/src/__tests__/RouteRepository.test.ts +23 -0
  86. package/src/__tests__/RouterManager.test.ts +125 -0
  87. package/src/__tests__/app.test.tsx +73 -0
  88. package/src/__tests__/authRedirect.test.ts +17 -0
  89. package/src/__tests__/browserChecker.test.ts +61 -0
  90. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  91. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  92. package/src/__tests__/settings-center.test.tsx +270 -6
  93. package/src/acl/ACLProvider.tsx +2 -0
  94. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  95. package/src/authRedirect.ts +38 -0
  96. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  97. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  98. package/src/collection-manager/field-validation.ts +1 -1
  99. package/src/components/README.md +7 -1
  100. package/src/components/README.zh-CN.md +6 -1
  101. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  102. package/src/components/category-tabs/index.ts +10 -0
  103. package/src/components/form/JsonTextArea.tsx +4 -0
  104. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  105. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  106. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  107. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  108. package/src/components/form/TypedVariableInput.tsx +452 -101
  109. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  110. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  111. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  112. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  113. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  114. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  115. package/src/components/form/filter/index.ts +2 -0
  116. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  117. package/src/components/form/table/Table.tsx +81 -6
  118. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  119. package/src/components/index.ts +1 -0
  120. package/src/entry-actions/EntryActionManager.ts +76 -0
  121. package/src/entry-actions/index.ts +10 -0
  122. package/src/flow/FlowPage.tsx +38 -3
  123. package/src/flow/__tests__/FlowPage.test.tsx +201 -27
  124. package/src/flow/__tests__/FlowRoute.test.tsx +675 -12
  125. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  126. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  127. package/src/flow/actions/__tests__/dataScopeFilter.test.ts +62 -0
  128. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  129. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  130. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  131. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  132. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  133. package/src/flow/actions/__tests__/openView.defineProps.route.test.tsx +80 -20
  134. package/src/flow/actions/dataScopeFilter.ts +10 -1
  135. package/src/flow/actions/dateTimeFormat.tsx +2 -2
  136. package/src/flow/actions/index.ts +2 -0
  137. package/src/flow/actions/linkageRules.tsx +144 -26
  138. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  139. package/src/flow/actions/openView.tsx +21 -1
  140. package/src/flow/admin-shell/BaseLayoutModel.tsx +124 -0
  141. package/src/flow/admin-shell/BaseLayoutRouteCoordinator.ts +184 -42
  142. package/src/flow/admin-shell/__tests__/AdminLayoutRouteCoordinator.test.ts +1016 -119
  143. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +41 -5
  144. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.test.tsx +177 -1
  145. package/src/flow/admin-shell/admin-layout/AdminLayoutEntryGuard.tsx +20 -0
  146. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  147. package/src/flow/admin-shell/admin-layout/AppListRender.tsx +13 -2
  148. package/src/flow/admin-shell/admin-layout/AppSwitcherActionPanelModel.tsx +289 -0
  149. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  150. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  151. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutModel.test.tsx +436 -2
  152. package/src/flow/admin-shell/admin-layout/__tests__/TopbarActionsBar.test.tsx +48 -0
  153. package/src/flow/admin-shell/admin-layout/index.ts +1 -0
  154. package/src/flow/admin-shell/admin-layout/useApplications.tsx +81 -12
  155. package/src/flow/common/Markdown/Display.tsx +14 -3
  156. package/src/flow/common/Markdown/Edit.tsx +19 -7
  157. package/src/flow/common/Markdown/style.ts +4 -0
  158. package/src/flow/components/BlockItemCard.tsx +2 -2
  159. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  160. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  161. package/src/flow/components/FlowRoute.tsx +204 -46
  162. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  163. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  164. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  165. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  166. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  167. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  168. package/src/flow/components/field-value-variable/index.ts +12 -0
  169. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  170. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  171. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  172. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  173. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  174. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  175. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  176. package/src/flow/index.ts +3 -0
  177. package/src/flow/internal/components/Markdown/util.ts +2 -1
  178. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  179. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  180. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  181. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  182. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  183. package/src/flow/models/base/ActionModel.tsx +10 -8
  184. package/src/flow/models/base/ActionModelCore.tsx +16 -2
  185. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  186. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  187. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  188. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  189. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  190. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  191. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  192. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  193. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  194. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  195. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  196. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  197. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  198. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  199. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  200. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  201. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  202. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  203. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  204. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  205. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  206. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  207. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  208. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  209. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  210. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  211. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  212. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  213. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  214. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  215. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  216. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  217. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  218. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  219. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  220. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  221. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  222. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  223. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  224. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  225. package/src/flow/models/blocks/table/TableColumnModel.tsx +54 -11
  226. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  227. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  228. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  229. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +84 -0
  230. package/src/flow/models/blocks/table/utils.ts +7 -0
  231. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  232. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  233. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  234. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  235. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  236. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +54 -149
  237. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  238. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  239. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  240. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  241. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  242. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  243. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  244. package/src/flow/models/fields/TextareaFieldModel.tsx +42 -19
  245. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  246. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  247. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  248. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  249. package/src/flow/models/fields/__tests__/TextareaFieldModel.test.tsx +32 -1
  250. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  251. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  252. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  253. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  254. package/src/flow/models/topbar/TopbarActionModel.tsx +78 -3
  255. package/src/flow/routeTransientInputArgs.ts +27 -0
  256. package/src/flow/utils/__tests__/dateTimeFormat.test.ts +42 -0
  257. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  258. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  259. package/src/flow-compat/routeTypes.ts +1 -0
  260. package/src/index.ts +7 -0
  261. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  262. package/src/layout-manager/LayoutRoute.tsx +2 -1
  263. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  264. package/src/locale/languageCodes.ts +3 -1
  265. package/src/nocobase-buildin-plugin/index.tsx +19 -1
  266. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  267. package/src/ui-operation/index.ts +33 -0
  268. package/src/ui-operation/ui-operation-codec.ts +54 -0
  269. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  270. package/src/utils/index.tsx +1 -0
  271. package/src/utils/markdownSanitize.ts +88 -0
@@ -22,7 +22,7 @@ import { css } from '@emotion/css';
22
22
  import { debounce } from 'lodash';
23
23
  import { useRequest } from 'ahooks';
24
24
  import { PlusOutlined } from '@ant-design/icons';
25
- import React, { useEffect, useMemo, useRef, useState } from 'react';
25
+ import React, { useMemo, useRef, useState } from 'react';
26
26
  import { useTranslation } from 'react-i18next';
27
27
  import { SkeletonFallback } from '../../../components/SkeletonFallback';
28
28
  import { AssociationFieldModel } from './AssociationFieldModel';
@@ -32,6 +32,7 @@ import {
32
32
  normalizeAssociationFieldNames,
33
33
  resolveOptions,
34
34
  toSelectValue,
35
+ useAssociationValueHydration,
35
36
  type AssociationOption,
36
37
  type LazySelectProps,
37
38
  } from './recordSelectShared';
@@ -41,81 +42,6 @@ import { ActionWithoutPermission } from '../../base/ActionModel';
41
42
  import { EditFormModel } from '../../blocks';
42
43
  import { hasAncestorModel } from './recordSelectSettingsUtils';
43
44
 
44
- function isPlainObject(val: unknown): val is Record<string, any> {
45
- return !!val && typeof val === 'object' && !Array.isArray(val);
46
- }
47
-
48
- type HydrateStatus = 'pending' | 'done';
49
-
50
- type HydrationCandidate = {
51
- item: AssociationOption;
52
- tk: any;
53
- tkKey: string;
54
- };
55
-
56
- export function collectAssociationHydrationCandidates(options: {
57
- value: LazySelectProps['value'];
58
- isMultiple: boolean;
59
- valueKey: string;
60
- labelKey: string;
61
- statusMap: Map<string, HydrateStatus>;
62
- }): HydrationCandidate[] {
63
- const { value, isMultiple, valueKey, labelKey, statusMap } = options;
64
- const list = isMultiple ? (Array.isArray(value) ? value : []) : value != null ? [value] : [];
65
-
66
- const activeTkKeys = new Set<string>();
67
- for (const item of list) {
68
- if (!isPlainObject(item)) continue;
69
- const tk = item?.[valueKey];
70
- if (tk == null) continue;
71
- const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
72
- if (!tkKey) continue;
73
- activeTkKeys.add(tkKey);
74
- }
75
-
76
- for (const key of Array.from(statusMap.keys())) {
77
- if (!activeTkKeys.has(key)) {
78
- statusMap.delete(key);
79
- }
80
- }
81
-
82
- const candidates: HydrationCandidate[] = [];
83
- for (const item of list) {
84
- if (!isPlainObject(item)) continue;
85
- const tk = item?.[valueKey];
86
- if (tk == null) continue;
87
- if (item?.[labelKey] != null) continue;
88
-
89
- const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
90
- if (!tkKey) continue;
91
-
92
- const status = statusMap.get(tkKey);
93
- if (status === 'pending' || status === 'done') continue;
94
-
95
- statusMap.set(tkKey, 'pending');
96
- candidates.push({ item, tk, tkKey });
97
- }
98
-
99
- return candidates;
100
- }
101
-
102
- export function getAssociationHydrationNamePath(model: any) {
103
- return model?.context?.fieldPathArray ?? model?.context?.fieldPath ?? model?.props?.name;
104
- }
105
-
106
- export function getAssociationHydrationSetterContext(model: any) {
107
- if (typeof model?.context?.setFormValue === 'function') {
108
- return model.context;
109
- }
110
-
111
- return model?.context?.blockModel?.context;
112
- }
113
-
114
- function markAssociationHydrationDone(statusMap: Map<string, HydrateStatus>, tkKey: string | null | undefined) {
115
- if (!tkKey) return;
116
- statusMap.set(tkKey, 'done');
117
- }
118
-
119
45
  function RemoteModelRenderer({ options }) {
120
46
  const ctx = useFlowViewContext();
121
47
  const { data, loading } = useRequest(
@@ -198,6 +124,48 @@ const useFieldPermissionMessage = (model, allowEdit) => {
198
124
  return messageValue;
199
125
  };
200
126
 
127
+ const recordSelectClassName = css`
128
+ min-width: 0;
129
+
130
+ .ant-select-selector {
131
+ min-width: 0;
132
+ overflow: hidden;
133
+ }
134
+
135
+ .ant-select-selection-search,
136
+ .ant-select-selection-item,
137
+ .ant-select-selection-placeholder,
138
+ .ant-select-selection-overflow,
139
+ .ant-select-selection-overflow-item {
140
+ min-width: 0;
141
+ max-width: 100%;
142
+ }
143
+
144
+ .ant-select-selection-item,
145
+ .ant-select-selection-item-content {
146
+ overflow: hidden;
147
+ text-overflow: ellipsis;
148
+ white-space: nowrap;
149
+ }
150
+ `;
151
+
152
+ const recordSelectLabelClassName = css`
153
+ display: block;
154
+ min-width: 0;
155
+ max-width: 100%;
156
+ overflow: hidden;
157
+ text-overflow: ellipsis;
158
+ white-space: nowrap;
159
+
160
+ * {
161
+ min-width: 0;
162
+ max-width: 100%;
163
+ overflow: hidden;
164
+ text-overflow: ellipsis;
165
+ white-space: nowrap !important;
166
+ }
167
+ `;
168
+
201
169
  const LazySelect = (props: Readonly<LazySelectProps>) => {
202
170
  const {
203
171
  fieldNames,
@@ -210,6 +178,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
210
178
  onChange,
211
179
  allowCreate = true,
212
180
  allowEdit = true,
181
+ className,
213
182
  ...others
214
183
  } = props;
215
184
  const model: any = useFlowModel();
@@ -238,66 +207,13 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
238
207
  };
239
208
  const isConfigMode = !!model.context.flowSettingsEnabled;
240
209
  const { t } = useTranslation();
241
- const hydrateStatusRef = useRef<Map<string, HydrateStatus>>(new Map());
242
-
243
- useEffect(() => {
244
- const resource: any = model?.resource;
245
- if (!resource || typeof resource.get !== 'function') return;
246
- const valueKey = normalizedFieldNames.value;
247
- const labelKey = normalizedFieldNames.label;
248
- if (!valueKey || !labelKey) return;
249
-
250
- const current = value;
251
- const candidates = collectAssociationHydrationCandidates({
252
- value: current,
253
- isMultiple,
254
- valueKey,
255
- labelKey,
256
- statusMap: hydrateStatusRef.current,
257
- });
258
- if (!candidates.length) return;
259
-
260
- const namePath = getAssociationHydrationNamePath(model);
261
- const setterCtx: any = getAssociationHydrationSetterContext(model);
262
-
263
- candidates.forEach(({ item, tk, tkKey }) => {
264
- void (async () => {
265
- try {
266
- const record = await resource.get(tk);
267
- if (!record || typeof record !== 'object') {
268
- return;
269
- }
270
-
271
- const merge = { ...(item as any), ...(record as any) };
272
- if (merge?.[labelKey] == null) {
273
- return;
274
- }
275
-
276
- const nextValue = isMultiple
277
- ? (Array.isArray(current) ? current : []).map((v: any) => {
278
- if (!isPlainObject(v)) return v;
279
- return v?.[valueKey] === tk ? merge : v;
280
- })
281
- : merge;
282
-
283
- if (setterCtx && typeof setterCtx.setFormValue === 'function' && namePath != null) {
284
- await setterCtx.setFormValue(namePath, nextValue, {
285
- source: 'default',
286
- markExplicit: false,
287
- triggerEvent: false,
288
- });
289
- return;
290
- }
291
-
292
- onChange(nextValue as any);
293
- } catch (error) {
294
- // ignore
295
- } finally {
296
- markAssociationHydrationDone(hydrateStatusRef.current, tkKey);
297
- }
298
- })();
299
- });
300
- }, [isMultiple, model, normalizedFieldNames.label, normalizedFieldNames.value, onChange, value]);
210
+ useAssociationValueHydration({
211
+ model,
212
+ value,
213
+ isMultiple,
214
+ fieldNames: normalizedFieldNames,
215
+ onChange,
216
+ });
301
217
 
302
218
  const QuickAddContent = ({ searchText }) => {
303
219
  return (
@@ -317,6 +233,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
317
233
  <Select
318
234
  style={{ width: '100%' }}
319
235
  {...others}
236
+ className={[recordSelectClassName, className].filter(Boolean).join(' ')}
320
237
  allowClear
321
238
  showSearch
322
239
  maxTagCount="responsive"
@@ -409,19 +326,7 @@ const LazySelect = (props: Readonly<LazySelectProps>) => {
409
326
  }}
410
327
  popupMatchSelectWidth
411
328
  labelRender={(data) => {
412
- return (
413
- <div
414
- className={css`
415
- div {
416
- white-space: nowrap !important;
417
- overflow: hidden;
418
- text-overflow: ellipsis;
419
- }
420
- `}
421
- >
422
- {data.label}
423
- </div>
424
- );
329
+ return <div className={recordSelectLabelClassName}>{data.label}</div>;
425
330
  }}
426
331
  dropdownRender={(menu) => {
427
332
  const isFullMatch = realOptions.some((v) => v[normalizedFieldNames.label] === others.searchText);
@@ -7,7 +7,8 @@
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
9
 
10
- import { FlowContext } from '@nocobase/flow-engine';
10
+ import { FlowContext, FlowEngine, FlowModel } from '@nocobase/flow-engine';
11
+ import { renderHook, waitFor } from '@testing-library/react';
11
12
  import { describe, expect, it, vi } from 'vitest';
12
13
  import {
13
14
  buildCurrentItemTitle,
@@ -21,6 +22,7 @@ import {
21
22
  } from '../itemChain';
22
23
  import { injectRecordPickerPopupContext } from '@nocobase/client-v2';
23
24
  import {
25
+ RecordPickerFieldModel,
24
26
  canRecordPickerSelectMultiple,
25
27
  getRecordPickerClearedValue,
26
28
  getRecordPickerEmptyValue,
@@ -28,8 +30,12 @@ import {
28
30
  normalizeRecordPickerValue,
29
31
  shouldClearRecordPickerValueOnMultipleChange,
30
32
  } from '../RecordPickerFieldModel';
31
- import { getAssociationHydrationNamePath, getAssociationHydrationSetterContext } from '../RecordSelectFieldModel';
32
-
33
+ import {
34
+ collectAssociationHydrationCandidates,
35
+ useAssociationValueHydration,
36
+ getAssociationHydrationNamePath,
37
+ getAssociationHydrationSetterContext,
38
+ } from '../recordSelectShared';
33
39
  function createMockCollection() {
34
40
  return {
35
41
  name: 'users',
@@ -78,6 +84,118 @@ describe('RecordPickerFieldModel item context', () => {
78
84
  expect(normalizeRecordPickerValue(undefined, fieldNames, false)).toBeUndefined();
79
85
  });
80
86
 
87
+ it('queues ID-only picker values for association label hydration', () => {
88
+ const statusMap = new Map<string, 'pending' | 'done'>();
89
+ const fieldNames = { label: 'name', value: 'id' };
90
+
91
+ expect(
92
+ collectAssociationHydrationCandidates({
93
+ value: [{ id: 1 }, { id: 2, name: 'Already hydrated' }],
94
+ isMultiple: true,
95
+ valueKey: fieldNames.value,
96
+ labelKey: fieldNames.label,
97
+ statusMap,
98
+ }),
99
+ ).toEqual([{ item: { id: 1 }, tk: 1, tkKey: '1' }]);
100
+ expect(statusMap).toEqual(new Map([['1', 'pending']]));
101
+
102
+ expect(
103
+ collectAssociationHydrationCandidates({
104
+ value: { id: 1 },
105
+ isMultiple: false,
106
+ valueKey: fieldNames.value,
107
+ labelKey: fieldNames.label,
108
+ statusMap,
109
+ }),
110
+ ).toEqual([]);
111
+ });
112
+
113
+ it('hydrates an ID-only picker value back into the form', async () => {
114
+ const resource = {
115
+ get: vi.fn().mockResolvedValue({ id: 1, name: '北京启明教育' }),
116
+ };
117
+ const setFormValue = vi.fn();
118
+ const onChange = vi.fn();
119
+ const model = {
120
+ resource,
121
+ props: { name: 'company' },
122
+ context: {
123
+ fieldPathArray: ['company'],
124
+ setFormValue,
125
+ },
126
+ };
127
+
128
+ const { rerender } = renderHook(() =>
129
+ useAssociationValueHydration({
130
+ model,
131
+ value: { id: 1 },
132
+ isMultiple: false,
133
+ fieldNames: { label: 'name', value: 'id' },
134
+ onChange,
135
+ }),
136
+ );
137
+
138
+ await waitFor(() => {
139
+ expect(setFormValue).toHaveBeenCalledWith(
140
+ ['company'],
141
+ { id: 1, name: '北京启明教育' },
142
+ { source: 'default', markExplicit: false, triggerEvent: false },
143
+ );
144
+ });
145
+ rerender();
146
+
147
+ expect(resource.get).toHaveBeenCalledTimes(1);
148
+ expect(onChange).not.toHaveBeenCalled();
149
+ });
150
+
151
+ it('initializes a target resource before hydrating an ID-only picker value', async () => {
152
+ const engine = new FlowEngine();
153
+ engine.registerModels({ RecordPickerFieldModel });
154
+
155
+ const formItem = engine.createModel<FlowModel>({
156
+ use: 'FlowModel',
157
+ uid: 'form-item',
158
+ });
159
+ const field = engine.createModel<RecordPickerFieldModel>({
160
+ use: 'RecordPickerFieldModel',
161
+ uid: 'company-field',
162
+ parentId: formItem.uid,
163
+ });
164
+ const setFormValue = vi.fn();
165
+ field.context.defineProperty('collectionField', {
166
+ value: {
167
+ dataSourceKey: 'main',
168
+ target: 'rp_na_companies',
169
+ },
170
+ });
171
+ field.context.defineProperty('fieldPathArray', { value: ['company'] });
172
+ field.context.defineProperty('setFormValue', { value: setFormValue });
173
+
174
+ await field.applyFlow('recordPickerSettings');
175
+ const get = vi.spyOn(field.resource, 'get').mockResolvedValue({ id: 1, name: '北京启明教育' });
176
+ const onChange = vi.fn();
177
+
178
+ renderHook(() =>
179
+ useAssociationValueHydration({
180
+ model: field,
181
+ value: { id: 1 },
182
+ isMultiple: false,
183
+ fieldNames: { label: 'name', value: 'id' },
184
+ onChange,
185
+ }),
186
+ );
187
+
188
+ await waitFor(() => {
189
+ expect(get).toHaveBeenCalledWith(1);
190
+ expect(setFormValue).toHaveBeenCalledWith(
191
+ ['company'],
192
+ { id: 1, name: '北京启明教育' },
193
+ { source: 'default', markExplicit: false, triggerEvent: false },
194
+ );
195
+ });
196
+ expect(onChange).not.toHaveBeenCalled();
197
+ });
198
+
81
199
  it('returns the empty popup select value for the current multiple mode', () => {
82
200
  expect(getRecordPickerEmptyValue(true)).toEqual([]);
83
201
  expect(getRecordPickerEmptyValue(false)).toBeUndefined();
@@ -243,6 +361,52 @@ describe('RecordPickerFieldModel item context', () => {
243
361
  expect(associationOpts.serverOnlyWhenContextParams).toBe(true);
244
362
  });
245
363
 
364
+ it('builds whole-association descriptors for current and recursive parent item values', async () => {
365
+ const people = {
366
+ name: 'people',
367
+ dataSourceKey: 'main',
368
+ filterTargetKey: 'id',
369
+ getFields: () => [],
370
+ getField: () => undefined,
371
+ } as any;
372
+ const owner = {
373
+ name: 'owner',
374
+ target: 'people',
375
+ targetCollection: people,
376
+ isAssociationField: () => true,
377
+ } as any;
378
+ const collection = {
379
+ name: 'tasks',
380
+ dataSourceKey: 'main',
381
+ getFields: () => [owner],
382
+ getField: (name: string) => (name === 'owner' ? owner : undefined),
383
+ } as any;
384
+ const options = createAssociationItemChainContextPropertyOptions({
385
+ t: (value) => value,
386
+ title: 'Current item',
387
+ collectionAccessor: () => collection,
388
+ propertiesAccessor: (ctx) => ctx.item.value,
389
+ resolverPropertiesAccessor: () => ({ owner: { id: 7 } }),
390
+ parentCollectionAccessor: () => collection,
391
+ parentAccessors: {
392
+ parentPropertiesAccessor: () => ({ owner: { id: 8 } }),
393
+ parentItemMetaAccessor: () => undefined,
394
+ parentItemResolverAccessor: () => undefined,
395
+ },
396
+ });
397
+ const meta = await options.meta();
398
+ const params = await meta.buildVariablesParams({
399
+ item: { value: { owner: { id: 7 } }, parentItem: { value: { owner: { id: 8 } } } },
400
+ });
401
+
402
+ expect(params).toEqual({
403
+ parentItem: { value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 8 } } },
404
+ value: { owner: { collection: 'people', dataSourceKey: 'main', filterByTk: 7 } },
405
+ });
406
+ expect(options.resolveOnServer('value.owner.name')).toBe(true);
407
+ expect(options.resolveOnServer('parentItem.value.owner.name')).toBe(true);
408
+ });
409
+
246
410
  it('disables current item attributes for select-record popup item context', async () => {
247
411
  const parentCtx = new FlowContext();
248
412
  const departmentsCollection = {
@@ -119,6 +119,162 @@ export interface LazySelectProps extends Omit<SelectProps<any>, 'mode' | 'option
119
119
  allowEdit?: boolean;
120
120
  }
121
121
 
122
+ type AssociationHydrationSetterOptions = {
123
+ source?: string;
124
+ markExplicit?: boolean;
125
+ triggerEvent?: boolean;
126
+ };
127
+
128
+ type AssociationHydrationSetterContext = {
129
+ setFormValue?: (
130
+ namePath: unknown,
131
+ value: unknown,
132
+ options?: AssociationHydrationSetterOptions,
133
+ ) => Promise<unknown> | void;
134
+ };
135
+
136
+ export type AssociationHydrationModel = {
137
+ resource?: {
138
+ get?: (tk: unknown) => Promise<unknown> | unknown;
139
+ };
140
+ context?: unknown;
141
+ props?: {
142
+ name?: unknown;
143
+ };
144
+ };
145
+
146
+ type AssociationHydrationStatus = 'pending' | 'done';
147
+
148
+ type AssociationHydrationCandidate = {
149
+ item: AssociationOption;
150
+ tk: unknown;
151
+ tkKey: string;
152
+ };
153
+
154
+ export function isAssociationRecord(value: unknown): value is AssociationOption {
155
+ return !!value && typeof value === 'object' && !Array.isArray(value);
156
+ }
157
+
158
+ export function collectAssociationHydrationCandidates(options: {
159
+ value: LazySelectProps['value'];
160
+ isMultiple: boolean;
161
+ valueKey: string;
162
+ labelKey: string;
163
+ statusMap: Map<string, AssociationHydrationStatus>;
164
+ }): AssociationHydrationCandidate[] {
165
+ const { value, isMultiple, valueKey, labelKey, statusMap } = options;
166
+ const list = isMultiple ? (Array.isArray(value) ? value : []) : value != null ? [value] : [];
167
+ const activeTkKeys = new Set<string>();
168
+
169
+ for (const item of list) {
170
+ if (!isAssociationRecord(item)) continue;
171
+ const tk = item[valueKey];
172
+ if (tk == null) continue;
173
+ const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
174
+ if (tkKey) activeTkKeys.add(tkKey);
175
+ }
176
+
177
+ for (const key of Array.from(statusMap.keys())) {
178
+ if (!activeTkKeys.has(key)) statusMap.delete(key);
179
+ }
180
+
181
+ const candidates: AssociationHydrationCandidate[] = [];
182
+ for (const item of list) {
183
+ if (!isAssociationRecord(item)) continue;
184
+ const tk = item[valueKey];
185
+ if (tk == null || item[labelKey] != null) continue;
186
+
187
+ const tkKey = typeof tk === 'object' ? JSON.stringify(tk) : String(tk);
188
+ if (!tkKey || statusMap.has(tkKey)) continue;
189
+
190
+ statusMap.set(tkKey, 'pending');
191
+ candidates.push({ item, tk, tkKey });
192
+ }
193
+
194
+ return candidates;
195
+ }
196
+
197
+ function hasAssociationHydrationSetter(value: unknown): value is AssociationHydrationSetterContext {
198
+ return isAssociationRecord(value) && typeof value.setFormValue === 'function';
199
+ }
200
+
201
+ export function getAssociationHydrationNamePath(model: AssociationHydrationModel) {
202
+ const context = isAssociationRecord(model.context) ? model.context : undefined;
203
+ return context?.fieldPathArray ?? context?.fieldPath ?? model.props?.name;
204
+ }
205
+
206
+ export function getAssociationHydrationSetterContext(model: AssociationHydrationModel) {
207
+ if (hasAssociationHydrationSetter(model.context)) return model.context;
208
+
209
+ const context = isAssociationRecord(model.context) ? model.context : undefined;
210
+ const blockModel = isAssociationRecord(context?.blockModel) ? context.blockModel : undefined;
211
+ return hasAssociationHydrationSetter(blockModel?.context) ? blockModel.context : undefined;
212
+ }
213
+
214
+ export function useAssociationValueHydration(options: {
215
+ model: AssociationHydrationModel;
216
+ value: LazySelectProps['value'];
217
+ isMultiple: boolean;
218
+ fieldNames: AssociationFieldNames;
219
+ onChange: LazySelectProps['onChange'];
220
+ }) {
221
+ const { model, value, isMultiple, fieldNames, onChange } = options;
222
+ const hydrateStatusRef = React.useRef<Map<string, AssociationHydrationStatus>>(new Map());
223
+
224
+ React.useEffect(() => {
225
+ const resource = model.resource;
226
+ if (typeof resource?.get !== 'function' || !fieldNames.value || !fieldNames.label) return;
227
+
228
+ const current = value;
229
+ const candidates = collectAssociationHydrationCandidates({
230
+ value: current,
231
+ isMultiple,
232
+ valueKey: fieldNames.value,
233
+ labelKey: fieldNames.label,
234
+ statusMap: hydrateStatusRef.current,
235
+ });
236
+ if (!candidates.length) return;
237
+
238
+ const namePath = getAssociationHydrationNamePath(model);
239
+ const setterContext = getAssociationHydrationSetterContext(model);
240
+
241
+ const hydrateCandidate = async ({ item, tk, tkKey }: AssociationHydrationCandidate) => {
242
+ try {
243
+ const record = await resource.get?.(tk);
244
+ if (!isAssociationRecord(record)) return;
245
+
246
+ const merged = { ...item, ...record };
247
+ if (merged[fieldNames.label] == null) return;
248
+
249
+ const nextValue = isMultiple
250
+ ? (Array.isArray(current) ? current : []).map((entry) =>
251
+ isAssociationRecord(entry) && entry[fieldNames.value] === tk ? merged : entry,
252
+ )
253
+ : merged;
254
+
255
+ if (typeof setterContext?.setFormValue === 'function' && namePath != null) {
256
+ await setterContext.setFormValue(namePath, nextValue, {
257
+ source: 'default',
258
+ markExplicit: false,
259
+ triggerEvent: false,
260
+ });
261
+ return;
262
+ }
263
+
264
+ onChange(nextValue);
265
+ } catch {
266
+ // Keep the original ID-only value when the related record cannot be loaded.
267
+ } finally {
268
+ hydrateStatusRef.current.set(tkKey, 'done');
269
+ }
270
+ };
271
+
272
+ candidates.forEach((candidate) => {
273
+ hydrateCandidate(candidate);
274
+ });
275
+ }, [fieldNames.label, fieldNames.value, isMultiple, model, onChange, value]);
276
+ }
277
+
122
278
  export interface LabelByFieldProps {
123
279
  option: AssociationOption;
124
280
  fieldNames: AssociationFieldNames;