@nocobase/client-v2 2.2.0-beta.9 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/es/APIClient.d.ts +13 -3
  2. package/es/BaseApplication.d.ts +1 -0
  3. package/es/collection-field-interface/CollectionFieldInterface.d.ts +1 -0
  4. package/es/collection-field-interface/CollectionFieldInterfaceManager.d.ts +1 -0
  5. package/es/components/category-tabs/SortableCategoryTabs.d.ts +55 -0
  6. package/es/components/category-tabs/index.d.ts +9 -0
  7. package/es/components/form/ScanInput/useCodeScanner.d.ts +3 -2
  8. package/es/components/form/TypedVariableInput.d.ts +31 -5
  9. package/es/components/form/filter/CollectionFilter.d.ts +2 -0
  10. package/es/components/form/filter/CollectionFilterItem.d.ts +11 -1
  11. package/es/components/form/filter/CollectionFilterPanel.d.ts +2 -0
  12. package/es/components/form/filter/index.d.ts +2 -0
  13. package/es/components/form/filter/useFilterActionProps.d.ts +2 -0
  14. package/es/components/index.d.ts +1 -0
  15. package/es/flow/actions/index.d.ts +1 -1
  16. package/es/flow/actions/linkageRules.d.ts +27 -0
  17. package/es/flow/admin-shell/admin-layout/AdminLayoutMenuModels.d.ts +1 -0
  18. package/es/flow/components/FieldAssignExactDatePicker.d.ts +1 -0
  19. package/es/flow/components/FieldAssignValueInput.d.ts +7 -24
  20. package/es/flow/components/FlowRoute.d.ts +2 -2
  21. package/es/flow/components/RunJSValueEditor.d.ts +1 -0
  22. package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
  23. package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
  24. package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
  25. package/es/flow/components/field-value-variable/index.d.ts +11 -0
  26. package/es/flow/components/filter/FilterGroup.d.ts +1 -0
  27. package/es/flow/components/filter/VariableFilterItem.d.ts +6 -1
  28. package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
  29. package/es/flow/index.d.ts +1 -0
  30. package/es/flow/internal/registerDeviceTypeContext.d.ts +10 -0
  31. package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
  32. package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
  33. package/es/flow/models/base/ActionModelCore.d.ts +4 -2
  34. package/es/flow/models/base/BlockGridModel.d.ts +3 -0
  35. package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
  36. package/es/flow/models/base/PageModel/PageModel.d.ts +12 -0
  37. package/es/flow/models/base/PageModel/PageModelTabBar.d.ts +22 -0
  38. package/es/flow/models/base/PageModel/PageTabModel.d.ts +12 -0
  39. package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
  40. package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
  41. package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
  42. package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
  43. package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
  44. package/es/flow/models/blocks/form/QuickEditFormModel.d.ts +17 -2
  45. package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
  46. package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
  47. package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
  48. package/es/flow/models/blocks/table/TableColumnModel.d.ts +12 -0
  49. package/es/flow/models/blocks/table/utils.d.ts +1 -0
  50. package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
  51. package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
  52. package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
  53. package/es/flow/models/fields/DateTimeFieldModel/dateLimit.d.ts +15 -7
  54. package/es/flow-compat/fieldValidationConstants.d.ts +1 -1
  55. package/es/flow-compat/passwordUtils.d.ts +1 -1
  56. package/es/flow-compat/routeTypes.d.ts +1 -0
  57. package/es/index.d.ts +5 -0
  58. package/es/index.mjs +199 -161
  59. package/es/ui-operation/index.d.ts +15 -0
  60. package/es/ui-operation/ui-operation-codec.d.ts +10 -0
  61. package/es/utils/getRouteRuntimeVersion.d.ts +23 -0
  62. package/es/utils/index.d.ts +1 -0
  63. package/lib/index.js +215 -177
  64. package/lib/locale/languageCodes.js +3 -1
  65. package/package.json +9 -7
  66. package/src/APIClient.ts +90 -7
  67. package/src/Application.tsx +7 -1
  68. package/src/BaseApplication.tsx +11 -6
  69. package/src/__tests__/APIClient.test.tsx +58 -0
  70. package/src/__tests__/app.test.tsx +73 -0
  71. package/src/__tests__/browserChecker.test.ts +61 -0
  72. package/src/__tests__/getRouteRuntimeVersion.test.ts +68 -0
  73. package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +35 -2
  74. package/src/__tests__/settings-center.test.tsx +270 -6
  75. package/src/acl/ACLProvider.tsx +2 -0
  76. package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
  77. package/src/collection-field-interface/CollectionFieldInterface.ts +1 -0
  78. package/src/collection-field-interface/CollectionFieldInterfaceManager.ts +1 -0
  79. package/src/collection-manager/field-validation.ts +1 -1
  80. package/src/components/README.md +7 -1
  81. package/src/components/README.zh-CN.md +6 -1
  82. package/src/components/category-tabs/SortableCategoryTabs.tsx +210 -0
  83. package/src/components/category-tabs/index.ts +10 -0
  84. package/src/components/form/JsonTextArea.tsx +4 -0
  85. package/src/components/form/ScanInput/CodeScanner.tsx +79 -35
  86. package/src/components/form/ScanInput/__tests__/CodeScanner.test.tsx +101 -0
  87. package/src/components/form/ScanInput/__tests__/useCodeScanner.test.tsx +284 -11
  88. package/src/components/form/ScanInput/useCodeScanner.ts +332 -24
  89. package/src/components/form/TypedVariableInput.tsx +452 -101
  90. package/src/components/form/__tests__/TypedVariableInput.test.tsx +364 -12
  91. package/src/components/form/filter/CollectionFilter.tsx +4 -0
  92. package/src/components/form/filter/CollectionFilterItem.tsx +36 -9
  93. package/src/components/form/filter/CollectionFilterPanel.tsx +4 -0
  94. package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +55 -0
  95. package/src/components/form/filter/__tests__/useFilterActionProps.test.tsx +95 -0
  96. package/src/components/form/filter/index.ts +2 -0
  97. package/src/components/form/filter/useFilterActionProps.ts +37 -6
  98. package/src/components/form/table/Table.tsx +81 -6
  99. package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
  100. package/src/components/index.ts +1 -0
  101. package/src/flow/FlowPage.tsx +9 -1
  102. package/src/flow/__tests__/FlowPage.test.tsx +50 -3
  103. package/src/flow/__tests__/FlowRoute.test.tsx +128 -10
  104. package/src/flow/__tests__/PluginFlowEngine.test.ts +58 -0
  105. package/src/flow/actions/__tests__/actionLinkageRules.forkProps.test.ts +314 -0
  106. package/src/flow/actions/__tests__/dataScopeFormValueClear.test.ts +102 -0
  107. package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
  108. package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
  109. package/src/flow/actions/__tests__/linkageRules.tab.test.ts +171 -0
  110. package/src/flow/actions/__tests__/linkageRulesRefresh.test.ts +7 -3
  111. package/src/flow/actions/index.ts +2 -0
  112. package/src/flow/actions/linkageRules.tsx +144 -26
  113. package/src/flow/actions/linkageRulesRefresh.tsx +2 -6
  114. package/src/flow/admin-shell/admin-layout/AdminLayoutComponent.tsx +0 -1
  115. package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
  116. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutComponent.test.tsx +253 -6
  117. package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
  118. package/src/flow/common/Markdown/style.ts +4 -0
  119. package/src/flow/components/BlockItemCard.tsx +2 -2
  120. package/src/flow/components/FieldAssignExactDatePicker.tsx +25 -11
  121. package/src/flow/components/FieldAssignValueInput.tsx +72 -584
  122. package/src/flow/components/FlowRoute.tsx +78 -32
  123. package/src/flow/components/RunJSValueEditor.tsx +9 -1
  124. package/src/flow/components/__tests__/FieldAssignValueInput.context.test.tsx +216 -0
  125. package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
  126. package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
  127. package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
  128. package/src/flow/components/field-value-variable/dateValue.ts +223 -0
  129. package/src/flow/components/field-value-variable/index.ts +12 -0
  130. package/src/flow/components/filter/FilterGroup.tsx +33 -5
  131. package/src/flow/components/filter/VariableFilterItem.tsx +170 -13
  132. package/src/flow/components/filter/__tests__/FilterGroup.test.tsx +45 -0
  133. package/src/flow/components/filter/__tests__/VariableFilterItem.leftMetaTree.test.tsx +9 -0
  134. package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +195 -1
  135. package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
  136. package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
  137. package/src/flow/index.ts +3 -0
  138. package/src/flow/internal/registerDeviceTypeContext.ts +39 -0
  139. package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
  140. package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
  141. package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
  142. package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
  143. package/src/flow/models/base/ActionModelCore.tsx +11 -2
  144. package/src/flow/models/base/BlockGridModel.tsx +26 -0
  145. package/src/flow/models/base/CollectionBlockModel.tsx +38 -3
  146. package/src/flow/models/base/PageModel/PageModel.tsx +387 -53
  147. package/src/flow/models/base/PageModel/PageModelTabBar.tsx +114 -0
  148. package/src/flow/models/base/PageModel/PageTabModel.tsx +249 -18
  149. package/src/flow/models/base/PageModel/__tests__/PageModel.test.ts +790 -10
  150. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.module-isolation.test.tsx +130 -0
  151. package/src/flow/models/base/PageModel/__tests__/PageModelTabBar.test.tsx +118 -0
  152. package/src/flow/models/base/PageModel/__tests__/PageTabModel.test.ts +1075 -7
  153. package/src/flow/models/base/__tests__/ActionModelCore.render.test.tsx +49 -0
  154. package/src/flow/models/base/__tests__/BlockGridModel.selectSceneActivation.test.ts +124 -0
  155. package/src/flow/models/base/__tests__/CollectionBlockModel.addAppends.test.ts +41 -0
  156. package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
  157. package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
  158. package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
  159. package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
  160. package/src/flow/models/blocks/filter-form/FilterFormBlockModel.tsx +28 -3
  161. package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
  162. package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +155 -25
  163. package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
  164. package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
  165. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +395 -1
  166. package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
  167. package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
  168. package/src/flow/models/blocks/filter-form/__tests__/defaultValues.wiring.test.ts +66 -4
  169. package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
  170. package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
  171. package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
  172. package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
  173. package/src/flow/models/blocks/form/QuickEditFormModel.tsx +189 -36
  174. package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
  175. package/src/flow/models/blocks/form/__tests__/QuickEditFormModel.quickEdit.test.ts +350 -2
  176. package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
  177. package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
  178. package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
  179. package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
  180. package/src/flow/models/blocks/js-block/JSBlock.tsx +1 -1
  181. package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
  182. package/src/flow/models/blocks/table/TableActionsColumnModel.tsx +2 -1
  183. package/src/flow/models/blocks/table/TableBlockModel.tsx +29 -3
  184. package/src/flow/models/blocks/table/TableColumnModel.tsx +48 -9
  185. package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
  186. package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
  187. package/src/flow/models/blocks/table/__tests__/TableBlockModel.quickEditRefresh.test.ts +12 -0
  188. package/src/flow/models/blocks/table/__tests__/TableColumnModel.test.tsx +33 -0
  189. package/src/flow/models/blocks/table/utils.ts +7 -0
  190. package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
  191. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +10 -1
  192. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
  193. package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
  194. package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
  195. package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
  196. package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
  197. package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
  198. package/src/flow/models/fields/DateTimeFieldModel/__tests__/DateTimeNoTzFieldModel.dateLimit.test.tsx +149 -0
  199. package/src/flow/models/fields/DateTimeFieldModel/dateLimit.ts +149 -113
  200. package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
  201. package/src/flow/models/fields/JsonFieldModel.tsx +31 -8
  202. package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
  203. package/src/flow/models/fields/VariableFieldFormModel.tsx +3 -3
  204. package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
  205. package/src/flow/models/fields/__tests__/JsonFieldModel.test.ts +82 -0
  206. package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
  207. package/src/flow/models/fields/__tests__/VariableFieldFormModel.test.tsx +51 -0
  208. package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +26 -10
  209. package/src/flow/models/fields/mobile-components/MobileSelect.tsx +51 -14
  210. package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
  211. package/src/flow/utils/dataScopeFormValueClear.ts +4 -3
  212. package/src/flow-compat/fieldValidationConstants.ts +1 -1
  213. package/src/flow-compat/routeTypes.ts +1 -0
  214. package/src/index.ts +5 -0
  215. package/src/layout-manager/LayoutContentRoute.tsx +2 -1
  216. package/src/layout-manager/LayoutRoute.tsx +2 -1
  217. package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
  218. package/src/locale/languageCodes.ts +3 -1
  219. package/src/nocobase-buildin-plugin/index.tsx +12 -0
  220. package/src/settings-center/AdminSettingsLayout.tsx +23 -2
  221. package/src/ui-operation/index.ts +33 -0
  222. package/src/ui-operation/ui-operation-codec.ts +54 -0
  223. package/src/utils/getRouteRuntimeVersion.ts +185 -0
  224. package/src/utils/index.tsx +1 -0
@@ -0,0 +1,433 @@
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 { fireEvent, render } from '@testing-library/react';
11
+ import { css } from '@emotion/css';
12
+ import { ConfigProvider, Table as AntdTable } from 'antd';
13
+ import type { ColumnsType } from 'antd/es/table/interface';
14
+ import type { RenderedCell } from 'rc-table/lib/interface';
15
+ import React from 'react';
16
+ import { describe, expect, it, vi } from 'vitest';
17
+ import { Table, type TableProps } from '../Table';
18
+
19
+ type Row = {
20
+ id: number;
21
+ name: string;
22
+ note: string;
23
+ };
24
+
25
+ const row: Row = {
26
+ id: 1,
27
+ name: 'Long unbroken content',
28
+ note: 'Secondary content',
29
+ };
30
+
31
+ const defaultColumnMaxWidth = 400;
32
+ const defaultColumnContentClassName = 'nb-table-default-column-content';
33
+
34
+ function getMatchingStyleValues(element: Element, property: string) {
35
+ return Array.from(document.styleSheets).flatMap((styleSheet) => {
36
+ try {
37
+ return Array.from(styleSheet.cssRules).flatMap((rule) => {
38
+ if (!(rule instanceof CSSStyleRule)) return [];
39
+ try {
40
+ return element.matches(rule.selectorText) ? [rule.style.getPropertyValue(property)] : [];
41
+ } catch {
42
+ return [];
43
+ }
44
+ });
45
+ } catch {
46
+ return [];
47
+ }
48
+ });
49
+ }
50
+
51
+ function renderTable(columns: ColumnsType<Row>, props: Partial<TableProps<Row>> = {}) {
52
+ return render(
53
+ <ConfigProvider
54
+ theme={{
55
+ token: {
56
+ screenXS: 500,
57
+ paddingXL: 40,
58
+ padding: 10,
59
+ },
60
+ }}
61
+ >
62
+ <Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} {...props} />
63
+ </ConfigProvider>,
64
+ );
65
+ }
66
+
67
+ function getResolvedMaxWidth(element: HTMLElement) {
68
+ const style = window.getComputedStyle(element);
69
+ const variableName = style.maxWidth.match(/^var\((--[^)]+)\)$/)?.[1];
70
+ return variableName ? style.getPropertyValue(variableName).trim() : style.maxWidth;
71
+ }
72
+
73
+ function expectDefaultWrappingStyle(element: Element | null) {
74
+ expect(element).not.toBeNull();
75
+ const htmlElement = element as HTMLElement;
76
+ const style = window.getComputedStyle(htmlElement);
77
+ expect(htmlElement.classList).toContain(defaultColumnContentClassName);
78
+ expect(getResolvedMaxWidth(htmlElement)).toBe(`${defaultColumnMaxWidth}px`);
79
+ expect(style.whiteSpace).toBe('normal');
80
+ expect(style.overflowWrap).toBe('break-word');
81
+ expect(style.wordBreak).toBe('break-word');
82
+ }
83
+
84
+ function expectNoDefaultWrappingStyle(element: Element | null) {
85
+ expect(element).not.toBeNull();
86
+ const htmlElement = element as HTMLElement;
87
+ expect(htmlElement.classList).not.toContain(defaultColumnContentClassName);
88
+ expect(getResolvedMaxWidth(htmlElement)).not.toBe(`${defaultColumnMaxWidth}px`);
89
+ }
90
+
91
+ describe('Table default column content width', () => {
92
+ it('adds token-derived wrapping styles to unconstrained leaf headers and cells', () => {
93
+ const { getByRole } = renderTable([{ title: 'Name', dataIndex: 'name' }]);
94
+
95
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Name' }));
96
+ expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
97
+ });
98
+
99
+ it('leaves columns with explicit width, ellipsis, or fixed positioning unchanged', () => {
100
+ const columns: ColumnsType<Row> = [
101
+ { title: 'Width', dataIndex: 'name', width: 240 },
102
+ { title: 'Ellipsis', dataIndex: 'note', ellipsis: true },
103
+ { title: 'Fixed', dataIndex: 'id', fixed: 'left' },
104
+ ];
105
+ const { getByRole } = renderTable(columns, { scroll: { x: 800 } });
106
+
107
+ for (const title of ['Width', 'Ellipsis', 'Fixed']) {
108
+ expectNoDefaultWrappingStyle(getByRole('columnheader', { name: title }));
109
+ }
110
+
111
+ const widthCell = getByRole('cell', { name: row.name });
112
+ const ellipsisCell = getByRole('cell', { name: row.note });
113
+ const fixedCell = getByRole('cell', { name: String(row.id) });
114
+ for (const cell of [widthCell, ellipsisCell, fixedCell]) {
115
+ expectNoDefaultWrappingStyle(cell);
116
+ }
117
+ expect(ellipsisCell.classList).toContain('ant-table-cell-ellipsis');
118
+ expect(window.getComputedStyle(ellipsisCell).whiteSpace).toBe('nowrap');
119
+ });
120
+
121
+ it('preserves cell callbacks and lets caller styles override the defaults', () => {
122
+ const handleClick = vi.fn();
123
+ const onCell = vi.fn(() => ({
124
+ colSpan: 2,
125
+ className: 'custom-body-cell',
126
+ onClick: handleClick,
127
+ style: {
128
+ maxWidth: 120,
129
+ whiteSpace: 'pre' as const,
130
+ },
131
+ }));
132
+ const onHeaderCell = vi.fn(() => ({
133
+ colSpan: 2,
134
+ className: 'custom-header-cell',
135
+ style: {
136
+ maxWidth: 180,
137
+ overflowWrap: 'normal' as const,
138
+ },
139
+ }));
140
+ const columns: ColumnsType<Row> = [
141
+ { title: 'Name', dataIndex: 'name', onCell, onHeaderCell },
142
+ { title: 'Note', dataIndex: 'note', width: 100 },
143
+ ];
144
+ const { container } = renderTable(columns);
145
+
146
+ const header = container.querySelector<HTMLElement>('th.custom-header-cell');
147
+ expect(header?.getAttribute('colspan')).toBe('2');
148
+ const headerStyle = window.getComputedStyle(header as HTMLElement);
149
+ expect(headerStyle.maxWidth).toBe('180px');
150
+ expect(headerStyle.whiteSpace).toBe('normal');
151
+ expect(headerStyle.overflowWrap).toBe('normal');
152
+ expect(headerStyle.wordBreak).toBe('break-word');
153
+
154
+ const cell = container.querySelector<HTMLElement>('td.custom-body-cell');
155
+ expect(cell?.getAttribute('colspan')).toBe('2');
156
+ const cellStyle = window.getComputedStyle(cell as HTMLElement);
157
+ expect(cellStyle.maxWidth).toBe('120px');
158
+ expect(cellStyle.whiteSpace).toBe('pre');
159
+ expect(cellStyle.overflowWrap).toBe('break-word');
160
+ expect(cellStyle.wordBreak).toBe('break-word');
161
+
162
+ fireEvent.click(cell as HTMLElement);
163
+ expect(handleClick).toHaveBeenCalledTimes(1);
164
+ expect(onCell).toHaveBeenCalledWith(row, 0);
165
+ expect(onHeaderCell).toHaveBeenCalledWith(expect.objectContaining({ title: 'Name' }));
166
+ });
167
+
168
+ it('keeps the default wrapping behavior when caller classNames have no styles', () => {
169
+ const columns: ColumnsType<Row> = [
170
+ {
171
+ title: 'Name',
172
+ dataIndex: 'name',
173
+ className: 'column-marker',
174
+ onCell: () => ({ className: 'body-marker' }),
175
+ onHeaderCell: () => ({ className: 'header-marker' }),
176
+ },
177
+ {
178
+ title: 'Note',
179
+ dataIndex: 'note',
180
+ render: () => ({ children: 'Rendered marker', props: { className: 'render-marker' } }),
181
+ },
182
+ ];
183
+ const { getByRole } = renderTable(columns);
184
+
185
+ const nameHeader = getByRole('columnheader', { name: 'Name' });
186
+ const nameCell = getByRole('cell', { name: row.name });
187
+ const renderedCell = getByRole('cell', { name: 'Rendered marker' });
188
+ expect(nameHeader.classList).toContain('column-marker');
189
+ expect(nameHeader.classList).toContain('header-marker');
190
+ expect(nameCell.classList).toContain('column-marker');
191
+ expect(nameCell.classList).toContain('body-marker');
192
+ expect(renderedCell.classList).toContain('render-marker');
193
+ for (const element of [nameHeader, nameCell, renderedCell]) {
194
+ expectDefaultWrappingStyle(element);
195
+ }
196
+ });
197
+
198
+ it('lets caller className styles override the defaults', () => {
199
+ const callerClassName = css`
200
+ max-width: 135px;
201
+ white-space: pre;
202
+ overflow-wrap: normal;
203
+ word-break: normal;
204
+ `;
205
+ const columns: ColumnsType<Row> = [
206
+ {
207
+ title: 'Name',
208
+ dataIndex: 'name',
209
+ onCell: () => ({ className: callerClassName }),
210
+ onHeaderCell: () => ({ className: callerClassName }),
211
+ },
212
+ { title: 'Note', dataIndex: 'note', className: callerClassName },
213
+ ];
214
+ // Use a distinct token value so this render creates its default Emotion class after the caller class. This
215
+ // catches source-order regressions that would be hidden if a previous test had already inserted the default rule.
216
+ const { getByRole } = render(
217
+ <ConfigProvider theme={{ token: { screenXS: 501, paddingXL: 40, padding: 10 } }}>
218
+ <Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} />
219
+ </ConfigProvider>,
220
+ );
221
+
222
+ for (const element of [
223
+ getByRole('columnheader', { name: 'Name' }),
224
+ getByRole('columnheader', { name: 'Note' }),
225
+ getByRole('cell', { name: row.name }),
226
+ getByRole('cell', { name: row.note }),
227
+ ]) {
228
+ const style = window.getComputedStyle(element);
229
+ expect(getResolvedMaxWidth(element)).toBe('135px');
230
+ expect(style.whiteSpace).toBe('pre');
231
+ expect(style.overflowWrap).not.toBe('anywhere');
232
+ expect(getMatchingStyleValues(element, 'overflow-wrap')).toContain('normal');
233
+ expect(style.wordBreak).toBe('normal');
234
+ }
235
+ });
236
+
237
+ it('lets className returned from render override the defaults on first mount', () => {
238
+ const callerClassName = css`
239
+ max-width: 136px;
240
+ white-space: pre;
241
+ overflow-wrap: normal;
242
+ word-break: normal;
243
+ `;
244
+ const renderedCell: RenderedCell<Row> = {
245
+ props: { className: callerClassName },
246
+ };
247
+ const { container } = render(
248
+ <ConfigProvider theme={{ token: { screenXS: 502, paddingXL: 40, padding: 10 } }}>
249
+ <Table<Row>
250
+ rowKey="id"
251
+ columns={[{ title: 'Name', dataIndex: 'name', render: () => renderedCell }]}
252
+ dataSource={[row]}
253
+ pagination={false}
254
+ />
255
+ </ConfigProvider>,
256
+ );
257
+
258
+ const cell = container.querySelector<HTMLElement>('.ant-table-tbody td');
259
+ expect(cell).not.toBeNull();
260
+ const htmlCell = cell as HTMLElement;
261
+ const style = window.getComputedStyle(htmlCell);
262
+ expect(getResolvedMaxWidth(htmlCell)).toBe('136px');
263
+ expect(style.whiteSpace).toBe('pre');
264
+ expect(style.overflowWrap).not.toBe('anywhere');
265
+ expect(getMatchingStyleValues(cell as HTMLElement, 'overflow-wrap')).toContain('normal');
266
+ expect(style.wordBreak).toBe('normal');
267
+ });
268
+
269
+ it('lets static caller className styles override defaults inserted later', () => {
270
+ const callerClassName = 'caller-static-table-column';
271
+ const callerStyle = document.createElement('style');
272
+ callerStyle.textContent = `
273
+ .${callerClassName} {
274
+ max-width: 137px;
275
+ white-space: pre;
276
+ overflow-wrap: normal;
277
+ word-break: normal;
278
+ }
279
+ `;
280
+ document.head.prepend(callerStyle);
281
+
282
+ try {
283
+ const columns: ColumnsType<Row> = [
284
+ {
285
+ title: 'Name',
286
+ dataIndex: 'name',
287
+ onCell: () => ({ className: callerClassName }),
288
+ onHeaderCell: () => ({ className: callerClassName }),
289
+ },
290
+ { title: 'Note', dataIndex: 'note', className: callerClassName },
291
+ {
292
+ title: 'Rendered',
293
+ dataIndex: 'id',
294
+ render: () => ({ props: { className: callerClassName } }),
295
+ },
296
+ ];
297
+ const { container, getByRole } = render(
298
+ <ConfigProvider theme={{ token: { screenXS: 503, paddingXL: 40, padding: 10 } }}>
299
+ <Table<Row> rowKey="id" columns={columns} dataSource={[row]} pagination={false} />
300
+ </ConfigProvider>,
301
+ );
302
+ const cells = Array.from(container.querySelectorAll<HTMLElement>('.ant-table-tbody td'));
303
+
304
+ for (const element of [
305
+ getByRole('columnheader', { name: 'Name' }),
306
+ getByRole('columnheader', { name: 'Note' }),
307
+ cells[0],
308
+ cells[1],
309
+ cells[2],
310
+ ]) {
311
+ expect(element).toBeDefined();
312
+ const htmlElement = element as HTMLElement;
313
+ const style = window.getComputedStyle(htmlElement);
314
+ expect(getResolvedMaxWidth(htmlElement)).toBe('137px');
315
+ expect(style.whiteSpace).toBe('pre');
316
+ expect(style.overflowWrap).not.toBe('anywhere');
317
+ expect(getMatchingStyleValues(htmlElement, 'overflow-wrap')).toContain('normal');
318
+ expect(style.wordBreak).toBe('normal');
319
+ }
320
+ } finally {
321
+ callerStyle.remove();
322
+ }
323
+ });
324
+
325
+ it('lets styles returned from render override the defaults', () => {
326
+ const renderedCell: RenderedCell<Row> = {
327
+ children: 'Rendered cell',
328
+ props: {
329
+ className: 'rendered-cell',
330
+ style: {
331
+ maxWidth: 123,
332
+ whiteSpace: 'pre',
333
+ overflowWrap: 'normal',
334
+ wordBreak: 'normal',
335
+ },
336
+ },
337
+ };
338
+ const { getByRole } = renderTable([{ title: 'Name', dataIndex: 'name', render: () => renderedCell }]);
339
+
340
+ const style = window.getComputedStyle(getByRole('cell', { name: 'Rendered cell' }));
341
+ expect(style.maxWidth).toBe('123px');
342
+ expect(style.whiteSpace).toBe('pre');
343
+ expect(style.overflowWrap).toBe('normal');
344
+ expect(style.wordBreak).toBe('normal');
345
+ });
346
+
347
+ it('processes only leaves in nested columns without mutating the input', () => {
348
+ const nameColumn = { title: 'Name', dataIndex: 'name' as const };
349
+ const noteColumn = { title: 'Note', dataIndex: 'note' as const };
350
+ const children = [nameColumn, noteColumn];
351
+ const groupColumn = { title: 'Details', children };
352
+ const columns: ColumnsType<Row> = [groupColumn];
353
+
354
+ const { getByRole } = renderTable(columns);
355
+
356
+ expectNoDefaultWrappingStyle(getByRole('columnheader', { name: 'Details' }));
357
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Name' }));
358
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Note' }));
359
+ expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
360
+ expectDefaultWrappingStyle(getByRole('cell', { name: row.note }));
361
+
362
+ expect(columns[0]).toBe(groupColumn);
363
+ expect(groupColumn.children).toBe(children);
364
+ expect(children[0]).toBe(nameColumn);
365
+ expect(children[1]).toBe(noteColumn);
366
+ expect(nameColumn).not.toHaveProperty('onCell');
367
+ expect(nameColumn).not.toHaveProperty('onHeaderCell');
368
+ expect(noteColumn).not.toHaveProperty('onCell');
369
+ expect(noteColumn).not.toHaveProperty('onHeaderCell');
370
+ });
371
+
372
+ it('does not apply the default boundary to selection or drag-handle columns', () => {
373
+ const columns: ColumnsType<Row> = [{ title: 'Name', dataIndex: 'name' }];
374
+ const selectionTable = renderTable(columns, { rowSelection: {} });
375
+
376
+ expectNoDefaultWrappingStyle(selectionTable.container.querySelector<HTMLElement>('th.ant-table-selection-column'));
377
+ expectNoDefaultWrappingStyle(selectionTable.container.querySelector<HTMLElement>('td.ant-table-selection-column'));
378
+ expectDefaultWrappingStyle(selectionTable.getByRole('columnheader', { name: 'Name' }));
379
+ selectionTable.unmount();
380
+
381
+ const dragTable = renderTable(columns, { isDraggable: true, onSortEnd: vi.fn() });
382
+ const dragHeader = dragTable.container.querySelector<HTMLElement>('.ant-table-thead th:first-child');
383
+ const dragCell = dragTable.container.querySelector<HTMLElement>('.ant-table-tbody td:first-child');
384
+
385
+ expectNoDefaultWrappingStyle(dragHeader);
386
+ expectNoDefaultWrappingStyle(dragCell);
387
+ expectDefaultWrappingStyle(dragTable.getByRole('columnheader', { name: 'Name' }));
388
+ });
389
+
390
+ it('preserves Ant Design sentinels used to position expand and selection columns', () => {
391
+ const columns: ColumnsType<Row> = [
392
+ { title: 'Name', dataIndex: 'name' },
393
+ AntdTable.EXPAND_COLUMN,
394
+ AntdTable.SELECTION_COLUMN,
395
+ { title: 'Note', dataIndex: 'note' },
396
+ ];
397
+ const { container } = renderTable(columns, {
398
+ expandable: { expandedRowRender: (record) => <span>{record.note}</span> },
399
+ rowSelection: {},
400
+ });
401
+
402
+ const headers = Array.from(container.querySelectorAll<HTMLElement>('.ant-table-thead th'));
403
+ expect(headers).toHaveLength(4);
404
+ expect(headers[0].textContent).toBe('Name');
405
+ expect(headers[1].classList).toContain('ant-table-row-expand-icon-cell');
406
+ expect(headers[2].classList).toContain('ant-table-selection-column');
407
+ expect(headers[3].textContent).toBe('Note');
408
+ expectNoDefaultWrappingStyle(headers[1]);
409
+ expectNoDefaultWrappingStyle(headers[2]);
410
+ });
411
+
412
+ it('treats empty, undefined, or null children as leaves like antd', () => {
413
+ const columns = [
414
+ { title: 'Empty children', dataIndex: 'name', children: [] },
415
+ { title: 'Undefined children', dataIndex: 'note', children: undefined },
416
+ { title: 'Null children', dataIndex: 'id', children: null },
417
+ ] as unknown as ColumnsType<Row>;
418
+ const { getByRole } = renderTable(columns);
419
+
420
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Empty children' }));
421
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Undefined children' }));
422
+ expectDefaultWrappingStyle(getByRole('columnheader', { name: 'Null children' }));
423
+ expectDefaultWrappingStyle(getByRole('cell', { name: row.name }));
424
+ expectDefaultWrappingStyle(getByRole('cell', { name: row.note }));
425
+ expectDefaultWrappingStyle(getByRole('cell', { name: String(row.id) }));
426
+ });
427
+
428
+ it('does not force fixed table layout', () => {
429
+ const { container } = renderTable([{ title: 'Name', dataIndex: 'name' }]);
430
+
431
+ expect(container.querySelector<HTMLTableElement>('table')?.style.tableLayout).not.toBe('fixed');
432
+ });
433
+ });
@@ -9,6 +9,7 @@
9
9
 
10
10
  export * from './AppComponents';
11
11
  export * from './BlankComponent';
12
+ export * from './category-tabs';
12
13
  export * from './form/table/dnd';
13
14
  export * from './form';
14
15
  export * from './Icon';
@@ -60,7 +60,9 @@ type FlowPageProps = {
60
60
  type FlowPageViewContext = FlowEngineContext & {
61
61
  view?: {
62
62
  inputArgs?: {
63
+ filterByTk?: unknown;
63
64
  isMobileLayout?: unknown;
65
+ sourceId?: unknown;
64
66
  };
65
67
  };
66
68
  };
@@ -102,6 +104,10 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
102
104
  subType: 'object',
103
105
  use: pageModelClass,
104
106
  };
107
+ const isRuntimeRecordScopedPage =
108
+ !flowEngine.context.flowSettingsEnabled &&
109
+ (typeof ctx?.view?.inputArgs?.filterByTk !== 'undefined' ||
110
+ typeof ctx?.view?.inputArgs?.sourceId !== 'undefined');
105
111
  if (shouldInjectDefaultChildTab) {
106
112
  const tabTitle = defaultTabTitle || flowEngine.translate?.('Details');
107
113
  options['subModels'] = {
@@ -128,7 +134,9 @@ export const FlowPage = React.memo((props: FlowPageProps & Record<string, unknow
128
134
  },
129
135
  };
130
136
  }
131
- const data = await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled });
137
+ const data =
138
+ (isRuntimeRecordScopedPage && (await flowEngine.loadModel({ ...options, refresh: true }))) ||
139
+ (await flowEngine.loadOrCreateModel(options, { skipSave: !flowEngine.context.flowSettingsEnabled }));
132
140
  if (data?.uid && onModelLoaded) {
133
141
  data.context.addDelegate(ctx);
134
142
  bindViewLayoutState(data, ctx);
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { render, waitFor } from '@testing-library/react';
11
11
  import React from 'react';
12
- import { afterEach, describe, expect, it, vi } from 'vitest';
12
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
13
13
  import { FlowPage } from '../FlowPage';
14
14
 
15
15
  type TestContextProperty = {
@@ -71,10 +71,15 @@ const createPageModel = (): TestPageModel => {
71
71
  };
72
72
 
73
73
  const mocks = vi.hoisted(() => ({
74
+ flowSettingsEnabled: false,
75
+ loadModel: vi.fn(),
76
+ loadOrCreateModel: vi.fn(),
74
77
  rendererMobileStates: [] as boolean[],
75
78
  rendererProps: undefined as RendererProps | undefined,
76
79
  model: undefined as TestPageModel | undefined,
80
+ viewFilterByTk: undefined as unknown,
77
81
  viewInputMobileLayout: true as boolean | undefined,
82
+ viewSourceId: undefined as unknown,
78
83
  contextMobileLayout: undefined as boolean | undefined,
79
84
  }));
80
85
 
@@ -94,15 +99,20 @@ vi.mock('@nocobase/flow-engine', async () => {
94
99
  }),
95
100
  useFlowEngine: vi.fn(() => ({
96
101
  getModelClassAsync: vi.fn(async () => TestPageModelClass),
97
- loadOrCreateModel: vi.fn(async () => mocks.model),
98
- context: {},
102
+ loadModel: mocks.loadModel,
103
+ loadOrCreateModel: mocks.loadOrCreateModel,
104
+ context: {
105
+ flowSettingsEnabled: mocks.flowSettingsEnabled,
106
+ },
99
107
  translate: vi.fn((value: string) => value),
100
108
  })),
101
109
  useFlowModelById: vi.fn(() => mocks.model),
102
110
  useFlowViewContext: vi.fn(() => ({
103
111
  view: {
104
112
  inputArgs: {
113
+ ...(typeof mocks.viewFilterByTk !== 'undefined' ? { filterByTk: mocks.viewFilterByTk } : {}),
105
114
  ...(typeof mocks.viewInputMobileLayout === 'boolean' ? { isMobileLayout: mocks.viewInputMobileLayout } : {}),
115
+ ...(typeof mocks.viewSourceId !== 'undefined' ? { sourceId: mocks.viewSourceId } : {}),
106
116
  },
107
117
  },
108
118
  ...(typeof mocks.contextMobileLayout === 'boolean' ? { isMobileLayout: mocks.contextMobileLayout } : {}),
@@ -154,11 +164,21 @@ vi.mock('ahooks', async () => {
154
164
  });
155
165
 
156
166
  describe('FlowPage', () => {
167
+ beforeEach(() => {
168
+ mocks.loadModel.mockImplementation(async () => null);
169
+ mocks.loadOrCreateModel.mockImplementation(async () => mocks.model);
170
+ });
171
+
157
172
  afterEach(() => {
173
+ mocks.flowSettingsEnabled = false;
174
+ mocks.loadModel.mockReset();
175
+ mocks.loadOrCreateModel.mockReset();
158
176
  mocks.rendererMobileStates = [];
159
177
  mocks.rendererProps = undefined;
160
178
  mocks.model = undefined;
179
+ mocks.viewFilterByTk = undefined;
161
180
  mocks.viewInputMobileLayout = true;
181
+ mocks.viewSourceId = undefined;
162
182
  mocks.contextMobileLayout = undefined;
163
183
  });
164
184
 
@@ -203,4 +223,31 @@ describe('FlowPage', () => {
203
223
  expect(mocks.rendererMobileStates[0]).toBe(expected);
204
224
  },
205
225
  );
226
+
227
+ it('reloads runtime page models for record scoped views', async () => {
228
+ mocks.model = createPageModel();
229
+ mocks.viewFilterByTk = 1;
230
+ mocks.loadModel.mockImplementation(async () => mocks.model);
231
+
232
+ render(<FlowPage onModelLoaded={vi.fn()} />);
233
+
234
+ await waitFor(() => {
235
+ expect(mocks.loadModel).toHaveBeenCalled();
236
+ });
237
+ expect(mocks.loadModel.mock.calls[0]?.[0]).toMatchObject({ refresh: true });
238
+ expect(mocks.loadOrCreateModel).not.toHaveBeenCalled();
239
+ });
240
+
241
+ it('keeps configured page model cache behavior in flow settings mode', async () => {
242
+ mocks.flowSettingsEnabled = true;
243
+ mocks.model = createPageModel();
244
+ mocks.viewFilterByTk = 1;
245
+
246
+ render(<FlowPage onModelLoaded={vi.fn()} />);
247
+
248
+ await waitFor(() => {
249
+ expect(mocks.loadOrCreateModel).toHaveBeenCalled();
250
+ });
251
+ expect(mocks.loadModel).not.toHaveBeenCalled();
252
+ });
206
253
  });