@nocobase/client-v2 3.0.0-alpha.1 → 3.0.0-alpha.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.
- package/es/APIClient.d.ts +12 -0
- package/es/acl/aclCheckReadiness.d.ts +11 -0
- package/es/authRedirect.d.ts +6 -2
- package/es/flow/actions/linkageRules.d.ts +24 -0
- package/es/flow/components/FieldAssignValueInput.d.ts +4 -30
- package/es/flow/components/field-value-variable/DateVariableEditor.d.ts +24 -0
- package/es/flow/components/field-value-variable/FieldValueVariableInput.d.ts +31 -0
- package/es/flow/components/field-value-variable/dateValue.d.ts +36 -0
- package/es/flow/components/field-value-variable/index.d.ts +11 -0
- package/es/flow/components/placeholders/BlockPlaceholder.d.ts +1 -0
- package/es/flow/internal/utils/operatorSchemaHelper.d.ts +2 -2
- package/es/flow/models/actions/UpdateRecordActionUtils.d.ts +8 -2
- package/es/flow/models/base/CollectionBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/details/DetailsBlockModel.d.ts +3 -0
- package/es/flow/models/blocks/filter-form/FilterFormGridModel.d.ts +1 -0
- package/es/flow/models/blocks/filter-form/FilterFormSubmitActionModel.d.ts +3 -1
- package/es/flow/models/blocks/form/FormBlockModel.d.ts +7 -0
- package/es/flow/models/blocks/form/FormGridModel.d.ts +6 -0
- package/es/flow/models/blocks/form/value-runtime/rules.d.ts +1 -0
- package/es/flow/models/blocks/table/JSColumnModel.d.ts +2 -0
- package/es/flow/models/blocks/table/TableBlockModel.d.ts +1 -0
- package/es/flow/models/blocks/table/TableColumnModel.d.ts +11 -0
- package/es/flow/models/blocks/table/utils.d.ts +1 -0
- package/es/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.d.ts +2 -1
- package/es/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.d.ts +0 -17
- package/es/flow/models/fields/AssociationFieldModel/recordSelectShared.d.ts +41 -0
- package/es/flow-compat/passwordUtils.d.ts +1 -1
- package/es/index.d.ts +2 -1
- package/es/index.mjs +140 -140
- package/es/nocobase-buildin-plugin/index.d.ts +1 -0
- package/es/settings-app/SettingsGroupNav.d.ts +3 -2
- package/es/settings-app/SettingsSearch.d.ts +1 -1
- package/es/ui-operation/index.d.ts +15 -0
- package/es/ui-operation/ui-operation-codec.d.ts +10 -0
- package/lib/index.js +145 -145
- package/package.json +7 -7
- package/src/APIClient.ts +92 -0
- package/src/Application.tsx +3 -1
- package/src/__tests__/APIClient.test.tsx +58 -0
- package/src/__tests__/app.test.tsx +122 -3
- package/src/__tests__/authRedirect.test.ts +17 -0
- package/src/__tests__/browserChecker.test.ts +81 -33
- package/src/__tests__/nocobase-buildin-plugin-auth.test.tsx +37 -0
- package/src/__tests__/settings-center.test.tsx +312 -11
- package/src/__tests__/settings-layout-root.test.tsx +346 -6
- package/src/__tests__/settings-runtime-paths.test.ts +1 -1
- package/src/__tests__/settings-search-shortcut.test.tsx +321 -0
- package/src/__tests__/settings-shell.test.tsx +62 -1
- package/src/acl/ACLProvider.tsx +34 -3
- package/src/acl/__tests__/ACLProvider.test.tsx +92 -0
- package/src/acl/aclCheckReadiness.ts +44 -0
- package/src/authRedirect.ts +13 -5
- package/src/components/AppComponents.tsx +16 -7
- package/src/components/form/filter/CollectionFilterItem.tsx +4 -2
- package/src/components/form/filter/__tests__/CollectionFilterItem.test.tsx +17 -0
- package/src/components/form/table/Table.tsx +81 -6
- package/src/components/form/table/__tests__/Table.columnWidth.test.tsx +433 -0
- package/src/flow/actions/__tests__/linkageRules.actionStates.test.ts +33 -0
- package/src/flow/actions/__tests__/linkageRules.subFormSetFieldProps.test.ts +72 -0
- package/src/flow/actions/linkageRules.tsx +33 -10
- package/src/flow/admin-shell/admin-layout/AdminLayoutMenuUtils.tsx +78 -59
- package/src/flow/admin-shell/admin-layout/__tests__/AdminLayoutMenuModels.test.ts +228 -0
- package/src/flow/components/FieldAssignValueInput.tsx +38 -621
- package/src/flow/components/field-value-variable/DateVariableEditor.tsx +181 -0
- package/src/flow/components/field-value-variable/FieldValueVariableInput.tsx +326 -0
- package/src/flow/components/field-value-variable/__tests__/FieldValueVariableInput.test.tsx +380 -0
- package/src/flow/components/field-value-variable/dateValue.ts +223 -0
- package/src/flow/components/field-value-variable/index.ts +12 -0
- package/src/flow/components/filter/VariableFilterItem.tsx +12 -3
- package/src/flow/components/filter/__tests__/VariableFilterItem.test.tsx +125 -0
- package/src/flow/components/placeholders/BlockPlaceholder.tsx +70 -23
- package/src/flow/components/placeholders/__tests__/BlockPlaceholder.test.tsx +57 -7
- package/src/flow/internal/utils/operatorSchemaHelper.ts +3 -2
- package/src/flow/models/actions/UpdateRecordActionModel.tsx +18 -1
- package/src/flow/models/actions/UpdateRecordActionUtils.ts +18 -6
- package/src/flow/models/actions/__tests__/UpdateRecordActionModel.test.ts +76 -4
- package/src/flow/models/base/CollectionBlockModel.tsx +32 -1
- package/src/flow/models/base/__tests__/CollectionBlockModel.initialBeforeRenderRefresh.test.ts +125 -9
- package/src/flow/models/blocks/assign-form/AssignFormItemModel.tsx +28 -53
- package/src/flow/models/blocks/details/DetailsBlockModel.tsx +8 -3
- package/src/flow/models/blocks/details/__tests__/DetailsBlockModel.initialPaginationChangeRefresh.test.ts +32 -1
- package/src/flow/models/blocks/filter-form/FilterFormGridModel.tsx +36 -5
- package/src/flow/models/blocks/filter-form/FilterFormItemModel.tsx +129 -14
- package/src/flow/models/blocks/filter-form/FilterFormSubmitActionModel.tsx +57 -4
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormGridModel.onModelCreated.test.ts +174 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.defineChildren.test.ts +359 -1
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormItemModel.getFilterValue.test.ts +72 -0
- package/src/flow/models/blocks/filter-form/__tests__/FilterFormSubmitActionModel.lifecycle.test.ts +90 -0
- package/src/flow/models/blocks/filter-manager/FilterManager.ts +5 -0
- package/src/flow/models/blocks/filter-manager/__tests__/FilterManager.test.ts +40 -0
- package/src/flow/models/blocks/form/FormBlockModel.tsx +47 -0
- package/src/flow/models/blocks/form/FormGridModel.tsx +4 -0
- package/src/flow/models/blocks/form/__tests__/FormBlockModel.test.tsx +8 -2
- package/src/flow/models/blocks/form/__tests__/runJsFormSubmit.test.ts +131 -0
- package/src/flow/models/blocks/form/value-runtime/__tests__/runtime.test.ts +435 -0
- package/src/flow/models/blocks/form/value-runtime/rules.ts +67 -14
- package/src/flow/models/blocks/form/value-runtime/runtime.ts +43 -19
- package/src/flow/models/blocks/table/JSColumnModel.tsx +10 -1
- package/src/flow/models/blocks/table/TableBlockModel.tsx +25 -1
- package/src/flow/models/blocks/table/TableColumnModel.tsx +18 -8
- package/src/flow/models/blocks/table/__tests__/JSColumnModel.test.tsx +52 -5
- package/src/flow/models/blocks/table/__tests__/TableBlockModel.filterReset.test.ts +78 -0
- package/src/flow/models/blocks/table/utils.ts +7 -0
- package/src/flow/models/fields/AssociationFieldModel/CascadeSelectFieldModel.tsx +33 -1
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/PopupSubTableFieldModel.tsx +8 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/__tests__/popupContext.test.ts +120 -0
- package/src/flow/models/fields/AssociationFieldModel/PopupSubTableFieldModel/actions/PopupSubTableEditActionModel.tsx +10 -2
- package/src/flow/models/fields/AssociationFieldModel/RecordPickerFieldModel.tsx +24 -1
- package/src/flow/models/fields/AssociationFieldModel/RecordSelectFieldModel.tsx +9 -136
- package/src/flow/models/fields/AssociationFieldModel/__tests__/RecordPickerFieldModel.itemContext.test.ts +167 -3
- package/src/flow/models/fields/AssociationFieldModel/recordSelectShared.tsx +156 -0
- package/src/flow/models/fields/DisplayNumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/NumberFieldModel.tsx +1 -1
- package/src/flow/models/fields/__tests__/DisplayNumberFieldModel.test.ts +33 -0
- package/src/flow/models/fields/__tests__/NumberFieldModel.test.tsx +47 -0
- package/src/flow/models/fields/mobile-components/MobileLazySelect.tsx +6 -0
- package/src/flow/models/fields/mobile-components/MobileSelect.tsx +27 -2
- package/src/flow/models/fields/mobile-components/__tests__/MobileSelect.test.tsx +139 -13
- package/src/index.ts +2 -0
- package/src/layout-manager/LayoutContentRoute.tsx +2 -1
- package/src/layout-manager/LayoutRoute.tsx +2 -1
- package/src/layout-manager/__tests__/LayoutRoute.test.tsx +87 -1
- package/src/nocobase-buildin-plugin/index.tsx +14 -1
- package/src/settings-app/SettingsGroupNav.tsx +11 -11
- package/src/settings-app/SettingsSearch.tsx +115 -10
- package/src/settings-app/SettingsShell.tsx +23 -6
- package/src/settings-app/runtimePaths.ts +0 -1
- package/src/settings-app/settingsTheme.ts +82 -14
- package/src/settings-center/AdminSettingsLayout.tsx +132 -107
- package/src/settings-center/__tests__/groups.test.ts +25 -0
- package/src/settings-center/groups.ts +1 -1
- package/src/ui-operation/index.ts +33 -0
- package/src/ui-operation/ui-operation-codec.ts +54 -0
|
@@ -28,9 +28,11 @@ interface AppErrorPayload {
|
|
|
28
28
|
maintainingDialog?: string;
|
|
29
29
|
};
|
|
30
30
|
};
|
|
31
|
-
[key: string]: any;
|
|
32
31
|
}
|
|
33
32
|
|
|
33
|
+
const isAppCommanding = (error?: AppErrorPayload) => error?.code === 'APP_COMMANDING';
|
|
34
|
+
const isAppUpgrading = (error?: AppErrorPayload) => isAppCommanding(error) && error?.command?.name === 'upgrade';
|
|
35
|
+
|
|
34
36
|
export const AppSpin = () => {
|
|
35
37
|
return (
|
|
36
38
|
<Spin style={{ position: 'fixed', top: '50%', left: '50%', fontSize: 72, transform: 'translate(-50%, -50%)' }} />
|
|
@@ -168,6 +170,7 @@ const getProps = (app: Application) => {
|
|
|
168
170
|
export const AppError: FC<{ error: Error & { title?: string }; app: Application }> = observer(
|
|
169
171
|
({ app, error }) => {
|
|
170
172
|
const props = getProps(app);
|
|
173
|
+
const commanding = isAppCommanding(app.error as AppErrorPayload | undefined);
|
|
171
174
|
return (
|
|
172
175
|
<div>
|
|
173
176
|
<Result
|
|
@@ -180,11 +183,13 @@ export const AppError: FC<{ error: Error & { title?: string }; app: Application
|
|
|
180
183
|
status="error"
|
|
181
184
|
title={error?.title || app.i18n.t('App error', { ns: 'client' })}
|
|
182
185
|
subTitle={app.i18n.t(error?.message)}
|
|
183
|
-
extra={
|
|
184
|
-
|
|
185
|
-
{
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
extra={
|
|
187
|
+
commanding ? null : (
|
|
188
|
+
<Button type="primary" key="try" onClick={() => window.location.reload()}>
|
|
189
|
+
{app.i18n.t('Try again')}
|
|
190
|
+
</Button>
|
|
191
|
+
)
|
|
192
|
+
}
|
|
188
193
|
{...props}
|
|
189
194
|
/>
|
|
190
195
|
</div>
|
|
@@ -223,10 +228,14 @@ export const AppMaintaining: FC<{ app: Application; error: Error }> = observer(
|
|
|
223
228
|
|
|
224
229
|
export const AppMaintainingDialog: FC<{ app: Application; error: Error }> = observer(
|
|
225
230
|
({ app, error }) => {
|
|
226
|
-
const
|
|
231
|
+
const payload = error as AppErrorPayload | undefined;
|
|
232
|
+
const component = payload?.command?.components?.maintainingDialog;
|
|
227
233
|
if (component) {
|
|
228
234
|
return app.renderComponent(component, { app, error });
|
|
229
235
|
}
|
|
236
|
+
if (isAppUpgrading(payload)) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
230
239
|
|
|
231
240
|
const { icon, status, title, subTitle } = getProps(app);
|
|
232
241
|
return (
|
|
@@ -130,8 +130,10 @@ export const CollectionFilterItem: FC<CollectionFilterItemProps> = observer(
|
|
|
130
130
|
[operatorOptions, operator],
|
|
131
131
|
);
|
|
132
132
|
|
|
133
|
-
const handleFieldChange = (next
|
|
134
|
-
|
|
133
|
+
const handleFieldChange = (next?: (string | number)[]) => {
|
|
134
|
+
// rc-cascader emits `undefined` when its clear icon is clicked in single-select mode, despite its public type
|
|
135
|
+
// declaring an array. Treat that runtime value as an empty path so clearing the field also resets the row.
|
|
136
|
+
const path = (next ?? []).map(String);
|
|
135
137
|
props.value.path = path.join('.');
|
|
136
138
|
const leaf = findOptionByPath(options, path);
|
|
137
139
|
props.value.operator = leaf?.operators?.[0]?.value || '';
|
|
@@ -104,6 +104,23 @@ describe('CollectionFilterItem', () => {
|
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
+
it('clears the filter row when the selected field is cleared', async () => {
|
|
108
|
+
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
|
+
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
110
|
+
|
|
111
|
+
const { container } = render(<CollectionFilterItem value={value} collection={collection} />);
|
|
112
|
+
|
|
113
|
+
const clear = container.querySelector('.ant-select-clear');
|
|
114
|
+
if (!clear) throw new Error('expected Cascader clear control to be rendered');
|
|
115
|
+
fireEvent.mouseDown(clear);
|
|
116
|
+
|
|
117
|
+
await waitFor(() => {
|
|
118
|
+
expect(value.path).toBe('');
|
|
119
|
+
expect(value.operator).toBe('');
|
|
120
|
+
expect(value.value).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
107
124
|
it('clears value when the operator changes', async () => {
|
|
108
125
|
const value = observable({ path: 'username', operator: '$eq', value: 'alice' });
|
|
109
126
|
const collection = buildStubCollection([{ name: 'username', title: 'Username' }]);
|
|
@@ -9,11 +9,11 @@
|
|
|
9
9
|
|
|
10
10
|
import { DragOverlay, type DragEndEvent, type DragStartEvent } from '@dnd-kit/core';
|
|
11
11
|
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
|
|
12
|
-
import { cx } from '@emotion/css';
|
|
12
|
+
import { css, cx, injectGlobal } from '@emotion/css';
|
|
13
13
|
import { DndProvider } from '@nocobase/flow-engine';
|
|
14
14
|
import { useMemoizedFn } from 'ahooks';
|
|
15
|
-
import { Table as AntdTable, type TableProps as AntdTableProps } from 'antd';
|
|
16
|
-
import type { ColumnsType, ColumnType, GetRowKey } from 'antd/es/table/interface';
|
|
15
|
+
import { Table as AntdTable, theme, type TableProps as AntdTableProps } from 'antd';
|
|
16
|
+
import type { ColumnsType, ColumnGroupType, ColumnType, GetRowKey } from 'antd/es/table/interface';
|
|
17
17
|
import type { RenderedCell } from 'rc-table/lib/interface';
|
|
18
18
|
import React, { useMemo, useState } from 'react';
|
|
19
19
|
import { SortableRow, SortHandle } from './dnd/SortableRow';
|
|
@@ -24,6 +24,20 @@ import { readRowKey, snapshotSourceRow, type RowKey, type RowSnapshot } from './
|
|
|
24
24
|
|
|
25
25
|
type RowSelectionRenderCellResult<RecordType> = React.ReactNode | RenderedCell<RecordType>;
|
|
26
26
|
|
|
27
|
+
const DEFAULT_COLUMN_CONTENT_CLASS_NAME = 'nb-table-default-column-content';
|
|
28
|
+
const DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY = '--nb-table-default-column-content-max-width';
|
|
29
|
+
|
|
30
|
+
// Zero specificity lets caller classes remain authoritative regardless of stylesheet insertion order. The max-width
|
|
31
|
+
// value itself comes from a token-derived private CSS variable set by each Table instance.
|
|
32
|
+
injectGlobal`
|
|
33
|
+
:where(.${DEFAULT_COLUMN_CONTENT_CLASS_NAME}) {
|
|
34
|
+
max-width: var(${DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY});
|
|
35
|
+
white-space: normal;
|
|
36
|
+
overflow-wrap: anywhere;
|
|
37
|
+
word-break: break-word;
|
|
38
|
+
}
|
|
39
|
+
`;
|
|
40
|
+
|
|
27
41
|
/**
|
|
28
42
|
* Default initial page size for `Table`. Exposed so consumers can seed their
|
|
29
43
|
* controlled `pageSize` state with the same value the component would use if
|
|
@@ -50,6 +64,54 @@ function isRenderedCell<RecordType>(value: unknown): value is RenderedCell<Recor
|
|
|
50
64
|
return typeof value === 'object' && value !== null && !React.isValidElement(value) && 'children' in value;
|
|
51
65
|
}
|
|
52
66
|
|
|
67
|
+
function isColumnGroup<RecordType>(
|
|
68
|
+
column: ColumnGroupType<RecordType> | ColumnType<RecordType>,
|
|
69
|
+
): column is ColumnGroupType<RecordType> {
|
|
70
|
+
return 'children' in column && Array.isArray(column.children) && column.children.length > 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function addDefaultColumnContentClassName<RecordType>(
|
|
74
|
+
columns: ColumnsType<RecordType>,
|
|
75
|
+
defaultClassName: string,
|
|
76
|
+
): ColumnsType<RecordType> {
|
|
77
|
+
return columns.map((column) => {
|
|
78
|
+
if (column === AntdTable.EXPAND_COLUMN || column === AntdTable.SELECTION_COLUMN) {
|
|
79
|
+
return column;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isColumnGroup(column)) {
|
|
83
|
+
return {
|
|
84
|
+
...column,
|
|
85
|
+
children: addDefaultColumnContentClassName(column.children, defaultClassName),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (column.width !== undefined || column.ellipsis || column.fixed) {
|
|
90
|
+
return column;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const originalOnCell = column.onCell;
|
|
94
|
+
const originalOnHeaderCell = column.onHeaderCell;
|
|
95
|
+
return {
|
|
96
|
+
...column,
|
|
97
|
+
onCell: (record, index) => {
|
|
98
|
+
const cellProps = originalOnCell?.(record, index) ?? {};
|
|
99
|
+
return {
|
|
100
|
+
...cellProps,
|
|
101
|
+
className: cx(defaultClassName, cellProps.className),
|
|
102
|
+
};
|
|
103
|
+
},
|
|
104
|
+
onHeaderCell: (headerColumn) => {
|
|
105
|
+
const cellProps = originalOnHeaderCell?.(headerColumn) ?? {};
|
|
106
|
+
return {
|
|
107
|
+
...cellProps,
|
|
108
|
+
className: cx(defaultClassName, cellProps.className),
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
53
115
|
export interface TableProps<RecordType extends object = any> extends AntdTableProps<RecordType> {
|
|
54
116
|
/**
|
|
55
117
|
* Required so drag-sort, hover-swap and row identity work. Accepts the same
|
|
@@ -108,6 +170,7 @@ export interface TableProps<RecordType extends object = any> extends AntdTablePr
|
|
|
108
170
|
* plus the index swap, so it is safe as the default table on any page.
|
|
109
171
|
*/
|
|
110
172
|
export function Table<RecordType extends object = any>(props: TableProps<RecordType>) {
|
|
173
|
+
const { token } = theme.useToken();
|
|
111
174
|
const {
|
|
112
175
|
rowKey,
|
|
113
176
|
showIndex = true,
|
|
@@ -143,6 +206,17 @@ export function Table<RecordType extends object = any>(props: TableProps<RecordT
|
|
|
143
206
|
const showHandleInSelection = isDraggable && showSortHandle && !!rowSelection;
|
|
144
207
|
const showStandaloneHandleColumn = isDraggable && showSortHandle && !rowSelection;
|
|
145
208
|
|
|
209
|
+
const defaultColumnContentClassName = useMemo(
|
|
210
|
+
() =>
|
|
211
|
+
cx(
|
|
212
|
+
DEFAULT_COLUMN_CONTENT_CLASS_NAME,
|
|
213
|
+
css`
|
|
214
|
+
${DEFAULT_COLUMN_CONTENT_MAX_WIDTH_PROPERTY}: ${token.screenXS - token.paddingXL * 3 + token.padding * 2}px;
|
|
215
|
+
`,
|
|
216
|
+
),
|
|
217
|
+
[token.padding, token.paddingXL, token.screenXS],
|
|
218
|
+
);
|
|
219
|
+
|
|
146
220
|
const itemKeys = useMemo<string[]>(() => {
|
|
147
221
|
if (!isDraggable || !dataSource) return [];
|
|
148
222
|
return dataSource
|
|
@@ -191,15 +265,16 @@ export function Table<RecordType extends object = any>(props: TableProps<RecordT
|
|
|
191
265
|
// selection cell — see `augmentedRowSelection` below.
|
|
192
266
|
const augmentedColumns = useMemo<ColumnsType<RecordType>>(() => {
|
|
193
267
|
const baseColumns: ColumnsType<RecordType> = columns ?? [];
|
|
194
|
-
|
|
268
|
+
const styledColumns = addDefaultColumnContentClassName(baseColumns, defaultColumnContentClassName);
|
|
269
|
+
if (!showStandaloneHandleColumn) return styledColumns;
|
|
195
270
|
const handleColumn: ColumnType<RecordType> = {
|
|
196
271
|
key: '__sort__',
|
|
197
272
|
width: sortHandleColumnWidth,
|
|
198
273
|
align: 'center',
|
|
199
274
|
render: () => <SortHandle />,
|
|
200
275
|
};
|
|
201
|
-
return [handleColumn, ...
|
|
202
|
-
}, [columns, showStandaloneHandleColumn, sortHandleColumnWidth]);
|
|
276
|
+
return [handleColumn, ...styledColumns];
|
|
277
|
+
}, [columns, defaultColumnContentClassName, showStandaloneHandleColumn, sortHandleColumnWidth]);
|
|
203
278
|
|
|
204
279
|
const augmentedRowSelection = useMemo(() => {
|
|
205
280
|
if (!rowSelection) return rowSelection;
|
|
@@ -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
|
+
});
|