@object-ui/plugin-detail 3.3.0 → 3.3.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.
- package/CHANGELOG.md +11 -0
- package/README.md +21 -1
- package/dist/AddressField-LgHnO2Lk.js +98 -0
- package/dist/AutoNumberField-xZCrU0eW.js +14 -0
- package/dist/{AvatarField-Xuieq0ZI.js → AvatarField-Dy2XGlPz.js} +16 -15
- package/dist/{BooleanField-DwfMKknK.js → BooleanField-C0Clfka5.js} +11 -10
- package/dist/CodeField-CHUa07B6.js +23 -0
- package/dist/ColorField-vxHqEhcS.js +38 -0
- package/dist/CurrencyField-DiWjYWDo.js +49 -0
- package/dist/DateField-DGaRPM4P.js +22 -0
- package/dist/DateTimeField-8QnpsI_h.js +30 -0
- package/dist/EmailField-CkVgMbpI.js +26 -0
- package/dist/FileField-5UPV7uek.js +149 -0
- package/dist/FormulaField-BUgt6-Pi.js +17 -0
- package/dist/GeolocationField-D9T_jgG6.js +118 -0
- package/dist/GridField-DE_HwiIN.js +49 -0
- package/dist/ImageField-Dswnqtzf.js +73 -0
- package/dist/LocationField-gjqbE6na.js +36 -0
- package/dist/LookupField-BcS3LRKc.js +901 -0
- package/dist/{MasterDetailField-B0HTmmD7.js → MasterDetailField-BF6_-X3A.js} +20 -19
- package/dist/NumberField-Dj2rYmrS.js +27 -0
- package/dist/ObjectField-BymIojwd.js +50 -0
- package/dist/{PasswordField-DVTimsc3.js → PasswordField-ED_Xgqz-.js} +8 -7
- package/dist/PercentField-D-JKOxKC.js +61 -0
- package/dist/PhoneField-DSCaGYq7.js +26 -0
- package/dist/QRCodeField-CtcOUapi.js +73 -0
- package/dist/{RatingField-rRi_P0N0.js → RatingField-BDnyQFWy.js} +10 -9
- package/dist/RichTextField-CH6LVZQA.js +33 -0
- package/dist/SelectField-DE4dpkMV.js +36 -0
- package/dist/{SignatureField-2CnhcWI0.js → SignatureField-B1wh3f5A.js} +18 -17
- package/dist/{SliderField-DEpMVXko.js → SliderField-zoTCKh9n.js} +2 -1
- package/dist/SummaryField-BeBVT6VN.js +22 -0
- package/dist/TextAreaField-rfUGrRxh.js +37 -0
- package/dist/TextField-C_yM7ATQ.js +30 -0
- package/dist/TimeField-BcQmBZi9.js +22 -0
- package/dist/UrlField-BakaF6NI.js +31 -0
- package/dist/UserField-zS7y3eKb.js +76 -0
- package/dist/VectorField-CTZ4myDM.js +34 -0
- package/dist/index.js +1912 -1728
- package/dist/index.umd.cjs +38 -47
- package/dist/packages/plugin-detail/src/DetailSection.d.ts.map +1 -1
- package/dist/packages/plugin-detail/src/DetailView.d.ts +24 -0
- package/dist/packages/plugin-detail/src/DetailView.d.ts.map +1 -1
- package/dist/packages/plugin-detail/src/RelatedList.d.ts +8 -0
- package/dist/packages/plugin-detail/src/RelatedList.d.ts.map +1 -1
- package/dist/packages/plugin-detail/src/useDetailTranslation.d.ts.map +1 -1
- package/dist/plugin-detail.css +1 -2
- package/dist/rolldown-runtime-DnwLefa7.js +23 -0
- package/dist/{src-C56Ly5uG.js → src-DyUKLvMN.js} +18271 -26636
- package/dist/{useFieldTranslation-CkxqyB82.js → useFieldTranslation-BRgjC1oq.js} +1 -1
- package/package.json +33 -11
- package/.turbo/turbo-build.log +0 -64
- package/dist/AddressField-CDLSeyNx.js +0 -93
- package/dist/AutoNumberField-CtE7suf5.js +0 -14
- package/dist/CodeField-CfwgRxx2.js +0 -22
- package/dist/ColorField-YKHA7dBD.js +0 -37
- package/dist/CurrencyField-tvS3fPAF.js +0 -51
- package/dist/DateField-BKqXpkOh.js +0 -21
- package/dist/DateTimeField-CR-nJCE7.js +0 -32
- package/dist/EmailField-CgvW1Qal.js +0 -28
- package/dist/FileField-BVAme2ML.js +0 -151
- package/dist/FormulaField-DamJ2VaG.js +0 -14
- package/dist/GeolocationField-C99z7ZBM.js +0 -113
- package/dist/GridField-C9JbpTx_.js +0 -51
- package/dist/ImageField-CDANtgVV.js +0 -75
- package/dist/LocationField-ZSyZ0O-h.js +0 -35
- package/dist/LookupField-B3hQJt95.js +0 -903
- package/dist/LookupField-D00z6gn_.js +0 -2
- package/dist/NumberField-DL2QAL7X.js +0 -26
- package/dist/ObjectField-JYvUnuRO.js +0 -52
- package/dist/PercentField-DjR6BSpw.js +0 -63
- package/dist/PhoneField-CX1JL-jp.js +0 -28
- package/dist/QRCodeField-CH_1pU6R.js +0 -72
- package/dist/RichTextField-CJqLWlrb.js +0 -32
- package/dist/SelectField-DGoDoRM_.js +0 -30
- package/dist/SelectField-XBVI50AD.js +0 -2
- package/dist/SummaryField-7ch9aqAu.js +0 -19
- package/dist/TextAreaField-Cmw1oXcw.js +0 -36
- package/dist/TextField-OTLa3p51.js +0 -29
- package/dist/TimeField-DKPoNWoR.js +0 -21
- package/dist/UrlField-CxbmzP9f.js +0 -33
- package/dist/UserField-ChvwUkMK.js +0 -78
- package/dist/VectorField-BVClL8Vw.js +0 -36
- package/src/ActivityTimeline.tsx +0 -184
- package/src/CommentAttachment.tsx +0 -194
- package/src/CommentInput.tsx +0 -81
- package/src/DetailSection.tsx +0 -340
- package/src/DetailTabs.tsx +0 -73
- package/src/DetailView.stories.tsx +0 -334
- package/src/DetailView.tsx +0 -823
- package/src/DiffView.tsx +0 -233
- package/src/FieldChangeItem.tsx +0 -46
- package/src/HeaderHighlight.tsx +0 -88
- package/src/InlineCreateRelated.tsx +0 -291
- package/src/MentionAutocomplete.tsx +0 -123
- package/src/PointInTimeRestore.tsx +0 -261
- package/src/ReactionPicker.tsx +0 -106
- package/src/RecordActivityTimeline.tsx +0 -433
- package/src/RecordChatterPanel.tsx +0 -209
- package/src/RecordComments.tsx +0 -217
- package/src/RecordNavigationEnhanced.tsx +0 -213
- package/src/RelatedList.tsx +0 -413
- package/src/RelationshipGraph.tsx +0 -286
- package/src/RichTextCommentInput.tsx +0 -350
- package/src/SectionGroup.tsx +0 -101
- package/src/SubscriptionToggle.tsx +0 -62
- package/src/ThreadedReplies.tsx +0 -163
- package/src/__tests__/ActivityTimeline.test.tsx +0 -119
- package/src/__tests__/ActivityTimelineFiltering.test.tsx +0 -143
- package/src/__tests__/CommentInput.test.tsx +0 -57
- package/src/__tests__/DetailSection.test.tsx +0 -490
- package/src/__tests__/DetailView.test.tsx +0 -694
- package/src/__tests__/FieldChangeItem.test.tsx +0 -119
- package/src/__tests__/HeaderHighlight.test.tsx +0 -213
- package/src/__tests__/MentionAutocomplete.test.tsx +0 -97
- package/src/__tests__/ReactionPicker.test.tsx +0 -113
- package/src/__tests__/RecordActivityTimeline.test.tsx +0 -395
- package/src/__tests__/RecordChatterPanel.test.tsx +0 -265
- package/src/__tests__/RecordComments.test.tsx +0 -96
- package/src/__tests__/RecordCommentsPinSearch.test.tsx +0 -133
- package/src/__tests__/RelatedList.test.tsx +0 -160
- package/src/__tests__/SectionGroup.test.tsx +0 -101
- package/src/__tests__/SubscriptionToggle.test.tsx +0 -84
- package/src/__tests__/ThreadedReplies.test.tsx +0 -212
- package/src/__tests__/autoLayout.test.ts +0 -228
- package/src/__tests__/phase12-features.test.tsx +0 -583
- package/src/__tests__/roadmap-features.test.tsx +0 -478
- package/src/autoLayout.ts +0 -128
- package/src/index.tsx +0 -149
- package/src/useDetailTranslation.ts +0 -183
- package/tsconfig.json +0 -18
- package/vite.config.ts +0 -57
- package/vitest.config.ts +0 -13
- package/vitest.setup.ts +0 -1
|
@@ -1,490 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ObjectUI
|
|
3
|
-
* Copyright (c) 2024-present ObjectStack Inc.
|
|
4
|
-
*
|
|
5
|
-
* This source code is licensed under the MIT license found in the
|
|
6
|
-
* LICENSE file in the root directory of this source tree.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { describe, it, expect } from 'vitest';
|
|
10
|
-
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import { DetailSection, getResponsiveSpanClass } from '../DetailSection';
|
|
12
|
-
|
|
13
|
-
describe('DetailSection', () => {
|
|
14
|
-
it('should render text fields as plain text', () => {
|
|
15
|
-
const section = {
|
|
16
|
-
title: 'Info',
|
|
17
|
-
fields: [{ name: 'name', label: 'Name', type: 'text' }],
|
|
18
|
-
columns: 1,
|
|
19
|
-
};
|
|
20
|
-
render(<DetailSection section={section} data={{ name: 'Alice' }} />);
|
|
21
|
-
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it('should render date fields formatted (not raw ISO)', () => {
|
|
25
|
-
const section = {
|
|
26
|
-
title: 'Info',
|
|
27
|
-
fields: [{ name: 'order_date', label: 'Order Date', type: 'date' }],
|
|
28
|
-
columns: 1,
|
|
29
|
-
};
|
|
30
|
-
render(<DetailSection section={section} data={{ order_date: '2024-01-15T00:00:00.000Z' }} />);
|
|
31
|
-
// Should NOT show raw ISO string
|
|
32
|
-
expect(screen.queryByText('2024-01-15T00:00:00.000Z')).not.toBeInTheDocument();
|
|
33
|
-
// Should show formatted date (e.g. "Jan 15, 2024")
|
|
34
|
-
expect(screen.getByText(/Jan/)).toBeInTheDocument();
|
|
35
|
-
expect(screen.getByText(/2024/)).toBeInTheDocument();
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should render currency fields formatted', () => {
|
|
39
|
-
const section = {
|
|
40
|
-
title: 'Info',
|
|
41
|
-
fields: [{ name: 'total_amount', label: 'Total Amount', type: 'currency' }],
|
|
42
|
-
columns: 1,
|
|
43
|
-
};
|
|
44
|
-
render(<DetailSection section={section} data={{ total_amount: 15459.99 }} />);
|
|
45
|
-
// Should NOT show plain number
|
|
46
|
-
expect(screen.queryByText('15459.99')).not.toBeInTheDocument();
|
|
47
|
-
// Should show formatted currency (e.g. "$15,459.99")
|
|
48
|
-
expect(screen.getByText(/15,459\.99/)).toBeInTheDocument();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should render boolean fields with checkbox', () => {
|
|
52
|
-
const section = {
|
|
53
|
-
title: 'Info',
|
|
54
|
-
fields: [{ name: 'active', label: 'Active', type: 'boolean' }],
|
|
55
|
-
columns: 1,
|
|
56
|
-
};
|
|
57
|
-
render(<DetailSection section={section} data={{ active: true }} />);
|
|
58
|
-
// BooleanCellRenderer renders a checkbox
|
|
59
|
-
const checkbox = screen.getByRole('checkbox');
|
|
60
|
-
expect(checkbox).toBeInTheDocument();
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should render select fields as badge', () => {
|
|
64
|
-
const section = {
|
|
65
|
-
title: 'Info',
|
|
66
|
-
fields: [
|
|
67
|
-
{
|
|
68
|
-
name: 'status',
|
|
69
|
-
label: 'Status',
|
|
70
|
-
type: 'select',
|
|
71
|
-
options: [
|
|
72
|
-
{ value: 'Draft', label: 'Draft', color: 'yellow' },
|
|
73
|
-
{ value: 'Active', label: 'Active', color: 'green' },
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
],
|
|
77
|
-
columns: 1,
|
|
78
|
-
};
|
|
79
|
-
render(<DetailSection section={section} data={{ status: 'Draft' }} />);
|
|
80
|
-
expect(screen.getByText('Draft')).toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('should render null/undefined values as dash', () => {
|
|
84
|
-
const section = {
|
|
85
|
-
title: 'Info',
|
|
86
|
-
fields: [{ name: 'missing', label: 'Missing', type: 'text' }],
|
|
87
|
-
columns: 1,
|
|
88
|
-
};
|
|
89
|
-
render(<DetailSection section={section} data={{}} />);
|
|
90
|
-
expect(screen.getByText('—')).toBeInTheDocument();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should render section title', () => {
|
|
94
|
-
const section = {
|
|
95
|
-
title: 'Basic Information',
|
|
96
|
-
fields: [{ name: 'name', label: 'Name' }],
|
|
97
|
-
columns: 1,
|
|
98
|
-
};
|
|
99
|
-
render(<DetailSection section={section} data={{ name: 'Test' }} />);
|
|
100
|
-
expect(screen.getByText('Basic Information')).toBeInTheDocument();
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('should auto-infer 2 columns when columns is not set and 5+ fields exist', () => {
|
|
104
|
-
const section = {
|
|
105
|
-
title: 'Auto Layout',
|
|
106
|
-
fields: Array.from({ length: 6 }, (_, i) => ({
|
|
107
|
-
name: `field_${i}`,
|
|
108
|
-
label: `Field ${i}`,
|
|
109
|
-
type: 'text',
|
|
110
|
-
})),
|
|
111
|
-
};
|
|
112
|
-
const { container } = render(
|
|
113
|
-
<DetailSection section={section} data={{}} />
|
|
114
|
-
);
|
|
115
|
-
// The grid container should have the md:grid-cols-2 class
|
|
116
|
-
const grid = container.querySelector('.grid');
|
|
117
|
-
expect(grid).toBeTruthy();
|
|
118
|
-
expect(grid!.className).toContain('md:grid-cols-2');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('should auto-infer 3 columns when columns is not set and 11+ fields exist', () => {
|
|
122
|
-
const section = {
|
|
123
|
-
title: 'Many Fields',
|
|
124
|
-
fields: Array.from({ length: 12 }, (_, i) => ({
|
|
125
|
-
name: `field_${i}`,
|
|
126
|
-
label: `Field ${i}`,
|
|
127
|
-
type: 'text',
|
|
128
|
-
})),
|
|
129
|
-
};
|
|
130
|
-
const { container } = render(
|
|
131
|
-
<DetailSection section={section} data={{}} />
|
|
132
|
-
);
|
|
133
|
-
const grid = container.querySelector('.grid');
|
|
134
|
-
expect(grid).toBeTruthy();
|
|
135
|
-
expect(grid!.className).toContain('lg:grid-cols-3');
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it('should keep 1 column when columns is not set and ≤3 fields exist', () => {
|
|
139
|
-
const section = {
|
|
140
|
-
title: 'Few Fields',
|
|
141
|
-
fields: [
|
|
142
|
-
{ name: 'a', label: 'A', type: 'text' },
|
|
143
|
-
{ name: 'b', label: 'B', type: 'text' },
|
|
144
|
-
],
|
|
145
|
-
};
|
|
146
|
-
const { container } = render(
|
|
147
|
-
<DetailSection section={section} data={{}} />
|
|
148
|
-
);
|
|
149
|
-
const grid = container.querySelector('.grid');
|
|
150
|
-
expect(grid).toBeTruthy();
|
|
151
|
-
expect(grid!.className).toContain('grid-cols-1');
|
|
152
|
-
expect(grid!.className).not.toContain('sm:grid-cols-2');
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('should respect explicit columns=1 even with many fields', () => {
|
|
156
|
-
const section = {
|
|
157
|
-
title: 'Forced Single Column',
|
|
158
|
-
fields: Array.from({ length: 15 }, (_, i) => ({
|
|
159
|
-
name: `field_${i}`,
|
|
160
|
-
label: `Field ${i}`,
|
|
161
|
-
type: 'text',
|
|
162
|
-
})),
|
|
163
|
-
columns: 1,
|
|
164
|
-
};
|
|
165
|
-
const { container } = render(
|
|
166
|
-
<DetailSection section={section} data={{}} />
|
|
167
|
-
);
|
|
168
|
-
const grid = container.querySelector('.grid');
|
|
169
|
-
expect(grid).toBeTruthy();
|
|
170
|
-
expect(grid!.className).toContain('grid-cols-1');
|
|
171
|
-
expect(grid!.className).not.toContain('sm:grid-cols-2');
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('should hide empty fields when hideEmpty is true', () => {
|
|
175
|
-
const section = {
|
|
176
|
-
title: 'Info',
|
|
177
|
-
hideEmpty: true,
|
|
178
|
-
fields: [
|
|
179
|
-
{ name: 'name', label: 'Name', type: 'text' },
|
|
180
|
-
{ name: 'email', label: 'Email', type: 'text' },
|
|
181
|
-
{ name: 'phone', label: 'Phone', type: 'text' },
|
|
182
|
-
],
|
|
183
|
-
columns: 1,
|
|
184
|
-
};
|
|
185
|
-
render(<DetailSection section={section} data={{ name: 'Alice', email: null, phone: '' }} />);
|
|
186
|
-
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
187
|
-
expect(screen.queryByText('Email')).not.toBeInTheDocument();
|
|
188
|
-
expect(screen.queryByText('Phone')).not.toBeInTheDocument();
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
it('should hide entire section when all fields are empty and hideEmpty is true', () => {
|
|
192
|
-
const section = {
|
|
193
|
-
title: 'Empty Section',
|
|
194
|
-
hideEmpty: true,
|
|
195
|
-
fields: [
|
|
196
|
-
{ name: 'a', label: 'A', type: 'text' },
|
|
197
|
-
{ name: 'b', label: 'B', type: 'text' },
|
|
198
|
-
],
|
|
199
|
-
columns: 1,
|
|
200
|
-
};
|
|
201
|
-
const { container } = render(<DetailSection section={section} data={{ a: null, b: undefined }} />);
|
|
202
|
-
// Section should be hidden entirely
|
|
203
|
-
expect(container.innerHTML).toBe('');
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
it('should still show empty fields when hideEmpty is not set', () => {
|
|
207
|
-
const section = {
|
|
208
|
-
title: 'Info',
|
|
209
|
-
fields: [
|
|
210
|
-
{ name: 'name', label: 'Name', type: 'text' },
|
|
211
|
-
{ name: 'missing', label: 'Missing', type: 'text' },
|
|
212
|
-
],
|
|
213
|
-
columns: 1,
|
|
214
|
-
};
|
|
215
|
-
render(<DetailSection section={section} data={{ name: 'Alice' }} />);
|
|
216
|
-
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
217
|
-
expect(screen.getByText('—')).toBeInTheDocument();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('should use md: breakpoint for 2-column layouts', () => {
|
|
221
|
-
const section = {
|
|
222
|
-
title: 'Responsive',
|
|
223
|
-
fields: Array.from({ length: 6 }, (_, i) => ({
|
|
224
|
-
name: `field_${i}`,
|
|
225
|
-
label: `Field ${i}`,
|
|
226
|
-
type: 'text',
|
|
227
|
-
})),
|
|
228
|
-
};
|
|
229
|
-
const { container } = render(
|
|
230
|
-
<DetailSection section={section} data={{}} />
|
|
231
|
-
);
|
|
232
|
-
const grid = container.querySelector('.grid');
|
|
233
|
-
expect(grid).toBeTruthy();
|
|
234
|
-
expect(grid!.className).toContain('md:grid-cols-2');
|
|
235
|
-
expect(grid!.className).not.toContain('sm:grid-cols-2');
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
it('should use lg: breakpoint for 3-column layouts', () => {
|
|
239
|
-
const section = {
|
|
240
|
-
title: 'Responsive',
|
|
241
|
-
fields: Array.from({ length: 12 }, (_, i) => ({
|
|
242
|
-
name: `field_${i}`,
|
|
243
|
-
label: `Field ${i}`,
|
|
244
|
-
type: 'text',
|
|
245
|
-
})),
|
|
246
|
-
};
|
|
247
|
-
const { container } = render(
|
|
248
|
-
<DetailSection section={section} data={{}} />
|
|
249
|
-
);
|
|
250
|
-
const grid = container.querySelector('.grid');
|
|
251
|
-
expect(grid).toBeTruthy();
|
|
252
|
-
expect(grid!.className).toContain('lg:grid-cols-3');
|
|
253
|
-
expect(grid!.className).not.toContain('md:grid-cols-3');
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('should enrich field type from objectSchema when field.type is not set', () => {
|
|
257
|
-
const section = {
|
|
258
|
-
title: 'Info',
|
|
259
|
-
fields: [{ name: 'status', label: 'Status' }],
|
|
260
|
-
columns: 1,
|
|
261
|
-
};
|
|
262
|
-
const objectSchema = {
|
|
263
|
-
fields: {
|
|
264
|
-
status: {
|
|
265
|
-
type: 'select',
|
|
266
|
-
options: [
|
|
267
|
-
{ value: 'Draft', label: 'Draft', color: 'yellow' },
|
|
268
|
-
{ value: 'Active', label: 'Active', color: 'green' },
|
|
269
|
-
],
|
|
270
|
-
},
|
|
271
|
-
},
|
|
272
|
-
};
|
|
273
|
-
render(<DetailSection section={section} data={{ status: 'Draft' }} objectSchema={objectSchema} />);
|
|
274
|
-
// Should render via SelectCellRenderer (displays label), not plain String()
|
|
275
|
-
expect(screen.getByText('Draft')).toBeInTheDocument();
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('should render percent field from objectSchema enrichment', () => {
|
|
279
|
-
const section = {
|
|
280
|
-
title: 'Info',
|
|
281
|
-
fields: [{ name: 'discount', label: 'Discount' }],
|
|
282
|
-
columns: 1,
|
|
283
|
-
};
|
|
284
|
-
const objectSchema = {
|
|
285
|
-
fields: {
|
|
286
|
-
discount: { type: 'percent' },
|
|
287
|
-
},
|
|
288
|
-
};
|
|
289
|
-
render(<DetailSection section={section} data={{ discount: 25 }} objectSchema={objectSchema} />);
|
|
290
|
-
// PercentCellRenderer should format as "25%"
|
|
291
|
-
expect(screen.getByText(/25/)).toBeInTheDocument();
|
|
292
|
-
expect(screen.getByText(/%/)).toBeInTheDocument();
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should fall back to String(value) when neither field.type nor objectSchema provides a type', () => {
|
|
296
|
-
const section = {
|
|
297
|
-
title: 'Info',
|
|
298
|
-
fields: [{ name: 'notes', label: 'Notes' }],
|
|
299
|
-
columns: 1,
|
|
300
|
-
};
|
|
301
|
-
render(<DetailSection section={section} data={{ notes: 'Hello World' }} />);
|
|
302
|
-
expect(screen.getByText('Hello World')).toBeInTheDocument();
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('should prefer explicit field.type over objectSchema type', () => {
|
|
306
|
-
const section = {
|
|
307
|
-
title: 'Info',
|
|
308
|
-
fields: [{ name: 'name', label: 'Name', type: 'text' as const }],
|
|
309
|
-
columns: 1,
|
|
310
|
-
};
|
|
311
|
-
const objectSchema = {
|
|
312
|
-
fields: {
|
|
313
|
-
name: { type: 'number' },
|
|
314
|
-
},
|
|
315
|
-
};
|
|
316
|
-
render(<DetailSection section={section} data={{ name: 'Alice' }} objectSchema={objectSchema} />);
|
|
317
|
-
// Should use 'text' renderer, not 'number'
|
|
318
|
-
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it('should use responsive span classes for wide fields in 3-column layout', () => {
|
|
322
|
-
const section = {
|
|
323
|
-
title: 'Wide Fields',
|
|
324
|
-
fields: Array.from({ length: 12 }, (_, i) => ({
|
|
325
|
-
name: `field_${i}`,
|
|
326
|
-
label: `Field ${i}`,
|
|
327
|
-
type: i === 5 ? 'textarea' : 'text',
|
|
328
|
-
})),
|
|
329
|
-
};
|
|
330
|
-
const { container } = render(
|
|
331
|
-
<DetailSection section={section} data={{}} />
|
|
332
|
-
);
|
|
333
|
-
const grid = container.querySelector('.grid');
|
|
334
|
-
expect(grid).toBeTruthy();
|
|
335
|
-
expect(grid!.className).toContain('lg:grid-cols-3');
|
|
336
|
-
// Wide field (textarea) should have responsive span, not bare col-span-3
|
|
337
|
-
const fields = container.querySelectorAll('[class*="col-span"]');
|
|
338
|
-
fields.forEach((field) => {
|
|
339
|
-
// No bare col-span-3 at base level — must be lg: prefixed
|
|
340
|
-
const classes = field.className.split(/\s+/);
|
|
341
|
-
const hasBareSpan3 = classes.some((c: string) => c === 'col-span-3');
|
|
342
|
-
expect(hasBareSpan3).toBe(false);
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it('should use responsive span classes for wide fields in 2-column layout', () => {
|
|
347
|
-
const section = {
|
|
348
|
-
title: 'Wide Fields',
|
|
349
|
-
fields: [
|
|
350
|
-
{ name: 'a', label: 'A', type: 'text' },
|
|
351
|
-
{ name: 'b', label: 'B', type: 'text' },
|
|
352
|
-
{ name: 'c', label: 'C', type: 'text' },
|
|
353
|
-
{ name: 'd', label: 'D', type: 'text' },
|
|
354
|
-
{ name: 'notes', label: 'Notes', type: 'textarea' },
|
|
355
|
-
],
|
|
356
|
-
};
|
|
357
|
-
const { container } = render(
|
|
358
|
-
<DetailSection section={section} data={{}} />
|
|
359
|
-
);
|
|
360
|
-
const grid = container.querySelector('.grid');
|
|
361
|
-
expect(grid!.className).toContain('md:grid-cols-2');
|
|
362
|
-
// Wide field should have md:col-span-2, not bare col-span-2
|
|
363
|
-
const fields = container.querySelectorAll('[class*="col-span"]');
|
|
364
|
-
fields.forEach((field) => {
|
|
365
|
-
const classes = field.className.split(/\s+/);
|
|
366
|
-
const hasBareSpan2 = classes.some((c: string) => c === 'col-span-2');
|
|
367
|
-
expect(hasBareSpan2).toBe(false);
|
|
368
|
-
});
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should not apply col-span at base breakpoint to prevent implicit grid columns on mobile', () => {
|
|
372
|
-
const section = {
|
|
373
|
-
title: 'Mobile Safe',
|
|
374
|
-
fields: Array.from({ length: 15 }, (_, i) => ({
|
|
375
|
-
name: `field_${i}`,
|
|
376
|
-
label: `Field ${i}`,
|
|
377
|
-
type: i === 0 ? 'textarea' : 'text',
|
|
378
|
-
})),
|
|
379
|
-
};
|
|
380
|
-
const { container } = render(
|
|
381
|
-
<DetailSection section={section} data={{}} />
|
|
382
|
-
);
|
|
383
|
-
// Ensure no bare col-span-N (N>1) classes without responsive prefix
|
|
384
|
-
const allElements = container.querySelectorAll('*');
|
|
385
|
-
allElements.forEach((el) => {
|
|
386
|
-
const classes = el.className?.split?.(/\s+/) || [];
|
|
387
|
-
classes.forEach((cls: string) => {
|
|
388
|
-
if (cls.match(/^col-span-[2-9]$/)) {
|
|
389
|
-
throw new Error(`Found bare "${cls}" class without responsive prefix — would break mobile single-column layout`);
|
|
390
|
-
}
|
|
391
|
-
});
|
|
392
|
-
});
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
it('should initially render a batch when virtualScroll is enabled with many fields', () => {
|
|
396
|
-
const section = {
|
|
397
|
-
title: 'Virtual',
|
|
398
|
-
fields: Array.from({ length: 50 }, (_, i) => ({
|
|
399
|
-
name: `field_${i}`,
|
|
400
|
-
label: `Field ${i}`,
|
|
401
|
-
type: 'text',
|
|
402
|
-
})),
|
|
403
|
-
};
|
|
404
|
-
const { container } = render(
|
|
405
|
-
<DetailSection
|
|
406
|
-
section={section}
|
|
407
|
-
data={{}}
|
|
408
|
-
virtualScroll={{ enabled: true, batchSize: 10 }}
|
|
409
|
-
/>
|
|
410
|
-
);
|
|
411
|
-
const grid = container.querySelector('.grid');
|
|
412
|
-
expect(grid).toBeTruthy();
|
|
413
|
-
// Initially should render only the batch (10 fields), not all 50
|
|
414
|
-
const fieldElements = grid!.children;
|
|
415
|
-
expect(fieldElements.length).toBeLessThanOrEqual(10);
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
it('should render all fields when virtualScroll is disabled', () => {
|
|
419
|
-
const section = {
|
|
420
|
-
title: 'No Virtual',
|
|
421
|
-
fields: Array.from({ length: 50 }, (_, i) => ({
|
|
422
|
-
name: `field_${i}`,
|
|
423
|
-
label: `Field ${i}`,
|
|
424
|
-
type: 'text',
|
|
425
|
-
})),
|
|
426
|
-
};
|
|
427
|
-
const { container } = render(
|
|
428
|
-
<DetailSection section={section} data={{}} />
|
|
429
|
-
);
|
|
430
|
-
const grid = container.querySelector('.grid');
|
|
431
|
-
expect(grid).toBeTruthy();
|
|
432
|
-
expect(grid!.children.length).toBe(50);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('should render all fields when virtualScroll is enabled but field count is below batch size', () => {
|
|
436
|
-
const section = {
|
|
437
|
-
title: 'Small',
|
|
438
|
-
fields: Array.from({ length: 5 }, (_, i) => ({
|
|
439
|
-
name: `field_${i}`,
|
|
440
|
-
label: `Field ${i}`,
|
|
441
|
-
type: 'text',
|
|
442
|
-
})),
|
|
443
|
-
};
|
|
444
|
-
const { container } = render(
|
|
445
|
-
<DetailSection
|
|
446
|
-
section={section}
|
|
447
|
-
data={{}}
|
|
448
|
-
virtualScroll={{ enabled: true, batchSize: 20 }}
|
|
449
|
-
/>
|
|
450
|
-
);
|
|
451
|
-
const grid = container.querySelector('.grid');
|
|
452
|
-
expect(grid).toBeTruthy();
|
|
453
|
-
expect(grid!.children.length).toBe(5);
|
|
454
|
-
});
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
describe('getResponsiveSpanClass', () => {
|
|
458
|
-
it('should return empty string for no span', () => {
|
|
459
|
-
expect(getResponsiveSpanClass(undefined, 2)).toBe('');
|
|
460
|
-
});
|
|
461
|
-
|
|
462
|
-
it('should return empty string for span=1', () => {
|
|
463
|
-
expect(getResponsiveSpanClass(1, 3)).toBe('');
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
it('should return empty string for 1-column layout', () => {
|
|
467
|
-
expect(getResponsiveSpanClass(3, 1)).toBe('');
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
it('should return md:col-span-2 for span=2 in 2-column layout', () => {
|
|
471
|
-
expect(getResponsiveSpanClass(2, 2)).toBe('md:col-span-2');
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('should cap span to 2 in 2-column layout', () => {
|
|
475
|
-
expect(getResponsiveSpanClass(3, 2)).toBe('md:col-span-2');
|
|
476
|
-
expect(getResponsiveSpanClass(6, 2)).toBe('md:col-span-2');
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
it('should return md:col-span-2 for span=2 in 3-column layout', () => {
|
|
480
|
-
expect(getResponsiveSpanClass(2, 3)).toBe('md:col-span-2');
|
|
481
|
-
});
|
|
482
|
-
|
|
483
|
-
it('should return responsive classes for span=3 in 3-column layout', () => {
|
|
484
|
-
expect(getResponsiveSpanClass(3, 3)).toBe('md:col-span-2 lg:col-span-3');
|
|
485
|
-
});
|
|
486
|
-
|
|
487
|
-
it('should cap span to 3 in 3-column layout', () => {
|
|
488
|
-
expect(getResponsiveSpanClass(6, 3)).toBe('md:col-span-2 lg:col-span-3');
|
|
489
|
-
});
|
|
490
|
-
});
|