@object-ui/plugin-detail 3.1.1 → 3.1.2
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/.turbo/turbo-build.log +40 -40
- package/CHANGELOG.md +10 -0
- package/dist/{AddressField-B1iVr404.js → AddressField-QBIlXCFl.js} +1 -1
- package/dist/{AvatarField-Duw4xOLZ.js → AvatarField-BEZuQTAH.js} +1 -1
- package/dist/{BooleanField-CZ4axVeq.js → BooleanField-doa93aFX.js} +1 -1
- package/dist/{CodeField-BSz-mk2v.js → CodeField-jVV-hIXg.js} +1 -1
- package/dist/{ColorField-B522ad8m.js → ColorField-B53qKQGW.js} +1 -1
- package/dist/{CurrencyField-Cwr3_pow.js → CurrencyField-og0NJ2ax.js} +1 -1
- package/dist/{DateField-DCo6dxud.js → DateField-BFx64AtG.js} +1 -1
- package/dist/{DateTimeField-BWfBuANO.js → DateTimeField-Cxs2Rx2f.js} +1 -1
- package/dist/{EmailField-CpwbdVCU.js → EmailField-BfcpzRe7.js} +1 -1
- package/dist/{FileField-DVAUAJ8e.js → FileField-KarqvhYm.js} +1 -1
- package/dist/{GeolocationField-DNCKitgo.js → GeolocationField-B5SKZaqn.js} +1 -1
- package/dist/{GridField-DSblZNfp.js → GridField-DOotrUTo.js} +1 -1
- package/dist/{ImageField-DBAlnMon.js → ImageField-Ddotp4u-.js} +1 -1
- package/dist/{LocationField-DsHsXA6R.js → LocationField-tOkQaPIM.js} +1 -1
- package/dist/{LookupField-CsT0QQz2.js → LookupField-DF36GvIP.js} +1 -1
- package/dist/{MasterDetailField-Db8b7Gqs.js → MasterDetailField-CpHw3nTE.js} +1 -1
- package/dist/{NumberField-0IGp7lcA.js → NumberField-CzBb2a28.js} +1 -1
- package/dist/{ObjectField-BLApgJtS.js → ObjectField-BoL-JqE4.js} +1 -1
- package/dist/{PasswordField-pHKyNlmo.js → PasswordField-DrTzkYgj.js} +1 -1
- package/dist/{PercentField-CwgKmlIb.js → PercentField-B9ZUQ3zE.js} +1 -1
- package/dist/{PhoneField-lKtbYOdN.js → PhoneField-Bf9lhpdu.js} +1 -1
- package/dist/{QRCodeField-BTTasT3w.js → QRCodeField-PzMpdBKd.js} +1 -1
- package/dist/{RatingField-De2X-l44.js → RatingField-CeBMFe8o.js} +1 -1
- package/dist/{RichTextField-B5QnvUOr.js → RichTextField-Ch7CHSQ0.js} +1 -1
- package/dist/{SelectField-C9AZRHWu.js → SelectField-f5Nbi02x.js} +1 -1
- package/dist/{SignatureField-BgcEmYzd.js → SignatureField-CpxTX2tR.js} +1 -1
- package/dist/{SliderField-BzrttVOY.js → SliderField-BoZtzgcr.js} +1 -1
- package/dist/{TextAreaField-DSE_CaU6.js → TextAreaField-rT1DLnV2.js} +1 -1
- package/dist/{TextField-DFQ4T9PR.js → TextField-CflRxusu.js} +1 -1
- package/dist/{TimeField-F0cfmsps.js → TimeField-DeVeCpRu.js} +1 -1
- package/dist/{UrlField-DLXrFIH-.js → UrlField-UWKfhP9T.js} +1 -1
- package/dist/{UserField-PXMmxJY9.js → UserField-Cp2zQDjz.js} +1 -1
- package/dist/index-V_WBvcaA.js +100249 -0
- package/dist/index.js +1 -1
- package/dist/index.umd.cjs +117 -46
- package/dist/plugin-detail.css +1 -1
- package/dist/src/DetailSection.d.ts +9 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DetailSection.tsx +42 -24
- package/src/DetailView.tsx +15 -8
- package/src/__tests__/DetailSection.test.tsx +110 -1
- package/src/__tests__/DetailView.test.tsx +31 -0
- package/dist/index-qQ1C-yUR.js +0 -59976
- package/src/registration.test.tsx +0 -18
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DetailSection.d.ts","sourceRoot":"","sources":["../../src/DetailSection.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,iBAAiB,IAAI,qBAAqB,EAAkC,MAAM,kBAAkB,CAAC;AAKnH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACrD;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,
|
|
1
|
+
{"version":3,"file":"DetailSection.d.ts","sourceRoot":"","sources":["../../src/DetailSection.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAoB/B,OAAO,KAAK,EAAE,iBAAiB,IAAI,qBAAqB,EAAkC,MAAM,kBAAkB,CAAC;AAKnH;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAYxF;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,qBAAqB,CAAC;IAC/B,IAAI,CAAC,EAAE,GAAG,CAAC;IACX,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8DAA8D;IAC9D,YAAY,CAAC,EAAE,GAAG,CAAC;IACnB,kDAAkD;IAClD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uCAAuC;IACvC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;CACrD;AAED,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAuOtD,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DetailView.d.ts","sourceRoot":"","sources":["../../src/DetailView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA4C/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMrE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,
|
|
1
|
+
{"version":3,"file":"DetailView.d.ts","sourceRoot":"","sources":["../../src/DetailView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA4C/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMrE,MAAM,WAAW,eAAe;IAC9B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,qDAAqD;IACrD,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,kDAAkD;IAClD,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChF;AAED,eAAO,MAAM,UAAU,EAAE,KAAK,CAAC,EAAE,CAAC,eAAe,CA+uBhD,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@object-ui/plugin-detail",
|
|
3
|
-
"version": "3.1.
|
|
3
|
+
"version": "3.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "DetailView plugin for Object UI - comprehensive detail page with sections, tabs, and related lists",
|
|
@@ -25,11 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
27
|
"lucide-react": "^0.576.0",
|
|
28
|
-
"@object-ui/components": "3.1.
|
|
29
|
-
"@object-ui/
|
|
30
|
-
"@object-ui/
|
|
31
|
-
"@object-ui/
|
|
32
|
-
"@object-ui/
|
|
28
|
+
"@object-ui/components": "3.1.2",
|
|
29
|
+
"@object-ui/core": "3.1.2",
|
|
30
|
+
"@object-ui/fields": "3.1.2",
|
|
31
|
+
"@object-ui/types": "3.1.2",
|
|
32
|
+
"@object-ui/react": "3.1.2"
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/DetailSection.tsx
CHANGED
|
@@ -31,6 +31,28 @@ import { applyDetailAutoLayout } from './autoLayout';
|
|
|
31
31
|
import { useDetailTranslation } from './useDetailTranslation';
|
|
32
32
|
import { useSafeFieldLabel } from '@object-ui/react';
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Compute responsive col-span classes so that col-span never exceeds the
|
|
36
|
+
* visible column count at each Tailwind breakpoint.
|
|
37
|
+
*
|
|
38
|
+
* For columns=1: no span class (always single column)
|
|
39
|
+
* For columns=2: md:col-span-{min(span,2)}
|
|
40
|
+
* For columns>=3: md:col-span-{min(span,2)} lg:col-span-{min(span,3)}
|
|
41
|
+
*/
|
|
42
|
+
export function getResponsiveSpanClass(span: number | undefined, columns: number): string {
|
|
43
|
+
if (!span || span <= 1 || columns <= 1) return '';
|
|
44
|
+
|
|
45
|
+
if (columns === 2) {
|
|
46
|
+
return span >= 2 ? 'md:col-span-2' : '';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// columns >= 3: grid-cols-1 md:grid-cols-2 lg:grid-cols-3
|
|
50
|
+
if (span === 2) return 'md:col-span-2';
|
|
51
|
+
if (span >= 3) return 'md:col-span-2 lg:col-span-3';
|
|
52
|
+
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
|
|
34
56
|
export interface DetailSectionProps {
|
|
35
57
|
section: DetailViewSectionType;
|
|
36
58
|
data?: any;
|
|
@@ -67,6 +89,23 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
|
|
|
67
89
|
});
|
|
68
90
|
}, []);
|
|
69
91
|
|
|
92
|
+
// Filter out empty fields when hideEmpty is set
|
|
93
|
+
const visibleFields = section.hideEmpty
|
|
94
|
+
? section.fields.filter((field) => {
|
|
95
|
+
const value = data?.[field.name] ?? field.value;
|
|
96
|
+
return value !== null && value !== undefined && value !== '';
|
|
97
|
+
})
|
|
98
|
+
: section.fields;
|
|
99
|
+
|
|
100
|
+
// Hide entire section when all fields are empty
|
|
101
|
+
if (visibleFields.length === 0) return null;
|
|
102
|
+
|
|
103
|
+
// Apply auto-layout: infer columns and auto-span wide fields
|
|
104
|
+
const { fields: layoutFields, columns: effectiveColumns } = applyDetailAutoLayout(
|
|
105
|
+
visibleFields,
|
|
106
|
+
section.columns
|
|
107
|
+
);
|
|
108
|
+
|
|
70
109
|
const renderField = (field: DetailViewField) => {
|
|
71
110
|
const value = data?.[field.name] ?? field.value;
|
|
72
111
|
|
|
@@ -75,13 +114,9 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
|
|
|
75
114
|
return <SchemaRenderer schema={field.render} data={{ ...data, value }} />;
|
|
76
115
|
}
|
|
77
116
|
|
|
78
|
-
// Calculate span class
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
field.span === 3 ? 'col-span-3' :
|
|
82
|
-
field.span === 4 ? 'col-span-4' :
|
|
83
|
-
field.span === 5 ? 'col-span-5' :
|
|
84
|
-
field.span === 6 ? 'col-span-6' : '';
|
|
117
|
+
// Calculate responsive span class so col-span never exceeds the visible
|
|
118
|
+
// column count at each breakpoint, preventing implicit columns on mobile.
|
|
119
|
+
const spanClass = getResponsiveSpanClass(field.span, effectiveColumns);
|
|
85
120
|
|
|
86
121
|
const displayValue = (() => {
|
|
87
122
|
if (value === null || value === undefined) return <span className="text-muted-foreground/50 text-xs italic">—</span>;
|
|
@@ -178,23 +213,6 @@ export const DetailSection: React.FC<DetailSectionProps> = ({
|
|
|
178
213
|
);
|
|
179
214
|
};
|
|
180
215
|
|
|
181
|
-
// Filter out empty fields when hideEmpty is set
|
|
182
|
-
const visibleFields = section.hideEmpty
|
|
183
|
-
? section.fields.filter((field) => {
|
|
184
|
-
const value = data?.[field.name] ?? field.value;
|
|
185
|
-
return value !== null && value !== undefined && value !== '';
|
|
186
|
-
})
|
|
187
|
-
: section.fields;
|
|
188
|
-
|
|
189
|
-
// Hide entire section when all fields are empty
|
|
190
|
-
if (visibleFields.length === 0) return null;
|
|
191
|
-
|
|
192
|
-
// Apply auto-layout: infer columns and auto-span wide fields
|
|
193
|
-
const { fields: layoutFields, columns: effectiveColumns } = applyDetailAutoLayout(
|
|
194
|
-
visibleFields,
|
|
195
|
-
section.columns
|
|
196
|
-
);
|
|
197
|
-
|
|
198
216
|
const content = (
|
|
199
217
|
<div
|
|
200
218
|
className={cn(
|
package/src/DetailView.tsx
CHANGED
|
@@ -130,14 +130,8 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
130
130
|
? dataSource.findOne(objectName, resourceId, params)
|
|
131
131
|
: dataSource.findOne(objectName, resourceId);
|
|
132
132
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
if (result) {
|
|
136
|
-
setData(result);
|
|
137
|
-
setLoading(false);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
// Fallback: try alternate ID format for backward compatibility
|
|
133
|
+
// Helper: try alternate ID format (strip or prepend objectName prefix)
|
|
134
|
+
const tryAltId = () => {
|
|
141
135
|
const resIdStr = String(resourceId);
|
|
142
136
|
const altId = resIdStr.startsWith(prefix)
|
|
143
137
|
? resIdStr.slice(prefix.length) // strip prefix
|
|
@@ -156,6 +150,19 @@ export const DetailView: React.FC<DetailViewProps> = ({
|
|
|
156
150
|
setLoading(false);
|
|
157
151
|
}
|
|
158
152
|
});
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
return findOnePromise
|
|
156
|
+
.catch(() => null) // Convert any error to null to trigger alternate ID fallback
|
|
157
|
+
.then((result) => {
|
|
158
|
+
if (!isMounted) return;
|
|
159
|
+
if (result) {
|
|
160
|
+
setData(result);
|
|
161
|
+
setLoading(false);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
// Fallback: try alternate ID format for backward compatibility
|
|
165
|
+
return tryAltId();
|
|
159
166
|
});
|
|
160
167
|
}).catch((err) => {
|
|
161
168
|
if (isMounted) {
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect } from 'vitest';
|
|
10
10
|
import { render, screen } from '@testing-library/react';
|
|
11
|
-
import { DetailSection } from '../DetailSection';
|
|
11
|
+
import { DetailSection, getResponsiveSpanClass } from '../DetailSection';
|
|
12
12
|
|
|
13
13
|
describe('DetailSection', () => {
|
|
14
14
|
it('should render text fields as plain text', () => {
|
|
@@ -317,4 +317,113 @@ describe('DetailSection', () => {
|
|
|
317
317
|
// Should use 'text' renderer, not 'number'
|
|
318
318
|
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
319
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
|
+
|
|
396
|
+
describe('getResponsiveSpanClass', () => {
|
|
397
|
+
it('should return empty string for no span', () => {
|
|
398
|
+
expect(getResponsiveSpanClass(undefined, 2)).toBe('');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it('should return empty string for span=1', () => {
|
|
402
|
+
expect(getResponsiveSpanClass(1, 3)).toBe('');
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
it('should return empty string for 1-column layout', () => {
|
|
406
|
+
expect(getResponsiveSpanClass(3, 1)).toBe('');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should return md:col-span-2 for span=2 in 2-column layout', () => {
|
|
410
|
+
expect(getResponsiveSpanClass(2, 2)).toBe('md:col-span-2');
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it('should cap span to 2 in 2-column layout', () => {
|
|
414
|
+
expect(getResponsiveSpanClass(3, 2)).toBe('md:col-span-2');
|
|
415
|
+
expect(getResponsiveSpanClass(6, 2)).toBe('md:col-span-2');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should return md:col-span-2 for span=2 in 3-column layout', () => {
|
|
419
|
+
expect(getResponsiveSpanClass(2, 3)).toBe('md:col-span-2');
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('should return responsive classes for span=3 in 3-column layout', () => {
|
|
423
|
+
expect(getResponsiveSpanClass(3, 3)).toBe('md:col-span-2 lg:col-span-3');
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
it('should cap span to 3 in 3-column layout', () => {
|
|
427
|
+
expect(getResponsiveSpanClass(6, 3)).toBe('md:col-span-2 lg:col-span-3');
|
|
428
|
+
});
|
|
320
429
|
});
|
|
@@ -539,6 +539,37 @@ describe('DetailView', () => {
|
|
|
539
539
|
expect(onBack).toHaveBeenCalled();
|
|
540
540
|
});
|
|
541
541
|
|
|
542
|
+
it('should try fallback with alternate ID when first findOne throws an error', async () => {
|
|
543
|
+
let callCount = 0;
|
|
544
|
+
const mockDataSource = {
|
|
545
|
+
findOne: vi.fn().mockImplementation((_obj: string, id: string) => {
|
|
546
|
+
callCount++;
|
|
547
|
+
if (callCount === 1) {
|
|
548
|
+
// First call throws (simulate server error)
|
|
549
|
+
return Promise.reject(new Error('Server error'));
|
|
550
|
+
}
|
|
551
|
+
// Second call (fallback) succeeds
|
|
552
|
+
return Promise.resolve({ name: 'Alice' });
|
|
553
|
+
}),
|
|
554
|
+
} as any;
|
|
555
|
+
|
|
556
|
+
const schema: DetailViewSchema = {
|
|
557
|
+
type: 'detail-view',
|
|
558
|
+
title: 'Contact Details',
|
|
559
|
+
objectName: 'contact',
|
|
560
|
+
resourceId: 'contact-123',
|
|
561
|
+
fields: [{ name: 'name', label: 'Name' }],
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
const { findByText } = render(<DetailView schema={schema} dataSource={mockDataSource} />);
|
|
565
|
+
// The fallback should find the record using the stripped ID
|
|
566
|
+
expect(await findByText('Alice')).toBeInTheDocument();
|
|
567
|
+
// findOne should be called twice: first with original ID, then with stripped prefix
|
|
568
|
+
expect(mockDataSource.findOne).toHaveBeenCalledTimes(2);
|
|
569
|
+
expect(mockDataSource.findOne).toHaveBeenNthCalledWith(1, 'contact', 'contact-123');
|
|
570
|
+
expect(mockDataSource.findOne).toHaveBeenNthCalledWith(2, 'contact', '123');
|
|
571
|
+
});
|
|
572
|
+
|
|
542
573
|
it('should call findOne with $expand when objectSchema has lookup fields', async () => {
|
|
543
574
|
const mockDataSource = {
|
|
544
575
|
getObjectSchema: vi.fn().mockResolvedValue({
|