@object-ui/plugin-detail 3.1.0 → 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 +41 -41
- package/CHANGELOG.md +21 -0
- package/dist/{AddressField-C07oUOY6.js → AddressField-QBIlXCFl.js} +1 -1
- package/dist/{AvatarField-VThNABzo.js → AvatarField-BEZuQTAH.js} +1 -1
- package/dist/{BooleanField-CGHKBzAi.js → BooleanField-doa93aFX.js} +1 -1
- package/dist/{CodeField-Co_muhRR.js → CodeField-jVV-hIXg.js} +1 -1
- package/dist/{ColorField-DLid_tFz.js → ColorField-B53qKQGW.js} +1 -1
- package/dist/{CurrencyField-Bw-LqANM.js → CurrencyField-og0NJ2ax.js} +1 -1
- package/dist/{DateField-BNHAzMB2.js → DateField-BFx64AtG.js} +1 -1
- package/dist/{DateTimeField-DjAyn_DQ.js → DateTimeField-Cxs2Rx2f.js} +1 -1
- package/dist/{EmailField-xoNcSppb.js → EmailField-BfcpzRe7.js} +1 -1
- package/dist/{FileField-DbNJwjU2.js → FileField-KarqvhYm.js} +1 -1
- package/dist/{GeolocationField-C1AnS6VV.js → GeolocationField-B5SKZaqn.js} +1 -1
- package/dist/{GridField-DATAHIKf.js → GridField-DOotrUTo.js} +1 -1
- package/dist/{ImageField-CEKJpyJp.js → ImageField-Ddotp4u-.js} +1 -1
- package/dist/{LocationField-jDWXjlpx.js → LocationField-tOkQaPIM.js} +1 -1
- package/dist/{LookupField-DQ08L9UQ.js → LookupField-DF36GvIP.js} +1 -1
- package/dist/{MasterDetailField-Dbk529Ea.js → MasterDetailField-CpHw3nTE.js} +1 -1
- package/dist/{NumberField-BVroN9aV.js → NumberField-CzBb2a28.js} +1 -1
- package/dist/{ObjectField-CT3l_IHW.js → ObjectField-BoL-JqE4.js} +1 -1
- package/dist/{PasswordField-DweVLEE0.js → PasswordField-DrTzkYgj.js} +1 -1
- package/dist/{PercentField-ZpWUK97K.js → PercentField-B9ZUQ3zE.js} +1 -1
- package/dist/{PhoneField-mw-9fqZ_.js → PhoneField-Bf9lhpdu.js} +1 -1
- package/dist/{QRCodeField-Cbb9ck59.js → QRCodeField-PzMpdBKd.js} +1 -1
- package/dist/{RatingField-CSqgLS6t.js → RatingField-CeBMFe8o.js} +1 -1
- package/dist/{RichTextField-BpfBOd99.js → RichTextField-Ch7CHSQ0.js} +1 -1
- package/dist/{SelectField-B9Ei-5jl.js → SelectField-f5Nbi02x.js} +1 -1
- package/dist/{SignatureField-DgGpHnQ8.js → SignatureField-CpxTX2tR.js} +1 -1
- package/dist/{SliderField-C6HvOHd8.js → SliderField-BoZtzgcr.js} +1 -1
- package/dist/{TextAreaField-BK3RgzY3.js → TextAreaField-rT1DLnV2.js} +1 -1
- package/dist/{TextField-Bvzx3atT.js → TextField-CflRxusu.js} +1 -1
- package/dist/{TimeField-Cuz9-Uai.js → TimeField-DeVeCpRu.js} +1 -1
- package/dist/{UrlField-B6XHTV73.js → UrlField-UWKfhP9T.js} +1 -1
- package/dist/{UserField-ooTul2d6.js → UserField-Cp2zQDjz.js} +1 -1
- package/dist/index-V_WBvcaA.js +100249 -0
- package/dist/index.js +20 -18
- package/dist/index.umd.cjs +117 -46
- package/dist/plugin-detail.css +1 -1
- package/dist/src/DetailSection.d.ts +11 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/HeaderHighlight.d.ts +18 -0
- package/dist/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +16 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/dist/src/SectionGroup.d.ts +21 -0
- package/dist/src/SectionGroup.d.ts.map +1 -0
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/useDetailTranslation.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DetailSection.tsx +50 -26
- package/src/DetailView.tsx +286 -69
- package/src/HeaderHighlight.tsx +67 -0
- package/src/RelatedList.tsx +287 -21
- package/src/SectionGroup.tsx +101 -0
- package/src/__tests__/DetailSection.test.tsx +111 -2
- package/src/__tests__/DetailView.test.tsx +31 -0
- package/src/__tests__/HeaderHighlight.test.tsx +68 -0
- package/src/__tests__/RelatedList.test.tsx +101 -7
- package/src/__tests__/SectionGroup.test.tsx +101 -0
- package/src/__tests__/roadmap-features.test.tsx +478 -0
- package/src/index.tsx +4 -0
- package/src/useDetailTranslation.ts +11 -0
- package/dist/index-CnlyRfY_.js +0 -59461
- package/src/registration.test.tsx +0 -18
|
@@ -0,0 +1,68 @@
|
|
|
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 { HeaderHighlight } from '../HeaderHighlight';
|
|
12
|
+
import type { HighlightField } from '@object-ui/types';
|
|
13
|
+
|
|
14
|
+
describe('HeaderHighlight', () => {
|
|
15
|
+
const fields: HighlightField[] = [
|
|
16
|
+
{ name: 'revenue', label: 'Annual Revenue' },
|
|
17
|
+
{ name: 'employees', label: 'Employees' },
|
|
18
|
+
{ name: 'industry', label: 'Industry' },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const data = {
|
|
22
|
+
revenue: '$5M',
|
|
23
|
+
employees: 150,
|
|
24
|
+
industry: 'Technology',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
it('should render highlight fields with labels and values', () => {
|
|
28
|
+
render(<HeaderHighlight fields={fields} data={data} />);
|
|
29
|
+
expect(screen.getByText('Annual Revenue')).toBeInTheDocument();
|
|
30
|
+
expect(screen.getByText('$5M')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText('Employees')).toBeInTheDocument();
|
|
32
|
+
expect(screen.getByText('150')).toBeInTheDocument();
|
|
33
|
+
expect(screen.getByText('Industry')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('Technology')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should not render when no data is provided', () => {
|
|
38
|
+
const { container } = render(<HeaderHighlight fields={fields} />);
|
|
39
|
+
expect(container.innerHTML).toBe('');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should not render when fields array is empty', () => {
|
|
43
|
+
const { container } = render(<HeaderHighlight fields={[]} data={data} />);
|
|
44
|
+
expect(container.innerHTML).toBe('');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should hide fields with null or empty values', () => {
|
|
48
|
+
const sparseData = { revenue: '$5M', employees: null, industry: '' };
|
|
49
|
+
render(<HeaderHighlight fields={fields} data={sparseData} />);
|
|
50
|
+
expect(screen.getByText('$5M')).toBeInTheDocument();
|
|
51
|
+
expect(screen.queryByText('Employees')).not.toBeInTheDocument();
|
|
52
|
+
expect(screen.queryByText('Industry')).not.toBeInTheDocument();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should not render when all field values are empty', () => {
|
|
56
|
+
const emptyData = { revenue: null, employees: undefined, industry: '' };
|
|
57
|
+
const { container } = render(<HeaderHighlight fields={fields} data={emptyData} />);
|
|
58
|
+
expect(container.innerHTML).toBe('');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should render icon when provided', () => {
|
|
62
|
+
const fieldsWithIcon: HighlightField[] = [
|
|
63
|
+
{ name: 'revenue', label: 'Revenue', icon: '💰' },
|
|
64
|
+
];
|
|
65
|
+
render(<HeaderHighlight fields={fieldsWithIcon} data={{ revenue: '$5M' }} />);
|
|
66
|
+
expect(screen.getByText('💰')).toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
});
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { describe, it, expect, vi } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
10
|
+
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
|
11
11
|
import { RelatedList } from '../RelatedList';
|
|
12
12
|
|
|
13
13
|
describe('RelatedList', () => {
|
|
@@ -16,23 +16,23 @@ describe('RelatedList', () => {
|
|
|
16
16
|
expect(screen.getByText('Contacts')).toBeInTheDocument();
|
|
17
17
|
});
|
|
18
18
|
|
|
19
|
-
it('should show record count for empty list', () => {
|
|
19
|
+
it('should show record count badge for empty list', () => {
|
|
20
20
|
render(<RelatedList title="Contacts" type="table" data={[]} />);
|
|
21
|
-
expect(screen.getByText('0
|
|
21
|
+
expect(screen.getByText('0')).toBeInTheDocument();
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('should show
|
|
24
|
+
it('should show record count badge for one item', () => {
|
|
25
25
|
render(<RelatedList title="Contacts" type="table" data={[{ id: 1, name: 'Alice' }]} />);
|
|
26
|
-
expect(screen.getByText('1
|
|
26
|
+
expect(screen.getByText('1')).toBeInTheDocument();
|
|
27
27
|
});
|
|
28
28
|
|
|
29
|
-
it('should show
|
|
29
|
+
it('should show record count badge for multiple items', () => {
|
|
30
30
|
const data = [
|
|
31
31
|
{ id: 1, name: 'Alice' },
|
|
32
32
|
{ id: 2, name: 'Bob' },
|
|
33
33
|
];
|
|
34
34
|
render(<RelatedList title="Orders" type="table" data={data} />);
|
|
35
|
-
expect(screen.getByText('2
|
|
35
|
+
expect(screen.getByText('2')).toBeInTheDocument();
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it('should show "No related records found" for empty data', () => {
|
|
@@ -63,4 +63,98 @@ describe('RelatedList', () => {
|
|
|
63
63
|
expect(screen.queryByText('New')).not.toBeInTheDocument();
|
|
64
64
|
expect(screen.queryByText('View All')).not.toBeInTheDocument();
|
|
65
65
|
});
|
|
66
|
+
|
|
67
|
+
it('should auto-generate columns from object schema when api and dataSource provided but no columns', async () => {
|
|
68
|
+
const mockDataSource = {
|
|
69
|
+
getObjectSchema: vi.fn().mockResolvedValue({
|
|
70
|
+
name: 'order_item',
|
|
71
|
+
fields: {
|
|
72
|
+
product: { type: 'string', label: 'Product' },
|
|
73
|
+
quantity: { type: 'number', label: 'Quantity' },
|
|
74
|
+
_id: { type: 'string', label: 'ID' },
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
find: vi.fn(),
|
|
78
|
+
} as any;
|
|
79
|
+
|
|
80
|
+
const data = [{ product: 'Widget', quantity: 5 }];
|
|
81
|
+
render(
|
|
82
|
+
<RelatedList
|
|
83
|
+
title="Order Items"
|
|
84
|
+
type="table"
|
|
85
|
+
api="order_item"
|
|
86
|
+
data={data}
|
|
87
|
+
dataSource={mockDataSource}
|
|
88
|
+
/>,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
await waitFor(() => {
|
|
92
|
+
expect(mockDataSource.getObjectSchema).toHaveBeenCalledWith('order_item');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Verify columns are generated from schema (excluding _id)
|
|
96
|
+
await waitFor(() => {
|
|
97
|
+
expect(screen.getByText('Product')).toBeInTheDocument();
|
|
98
|
+
expect(screen.getByText('Quantity')).toBeInTheDocument();
|
|
99
|
+
});
|
|
100
|
+
// _id should be filtered out
|
|
101
|
+
expect(screen.queryByText('ID')).not.toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should not fetch object schema when explicit columns are provided', () => {
|
|
105
|
+
const mockDataSource = {
|
|
106
|
+
getObjectSchema: vi.fn(),
|
|
107
|
+
find: vi.fn(),
|
|
108
|
+
} as any;
|
|
109
|
+
|
|
110
|
+
const columns = [{ accessorKey: 'name', header: 'Name' }];
|
|
111
|
+
render(
|
|
112
|
+
<RelatedList
|
|
113
|
+
title="Contacts"
|
|
114
|
+
type="table"
|
|
115
|
+
api="contact"
|
|
116
|
+
data={[{ name: 'Alice' }]}
|
|
117
|
+
columns={columns}
|
|
118
|
+
dataSource={mockDataSource}
|
|
119
|
+
/>,
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
expect(mockDataSource.getObjectSchema).not.toHaveBeenCalled();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should render collapsed state when collapsible and defaultCollapsed are true', () => {
|
|
126
|
+
const data = [{ id: 1, name: 'Alice' }];
|
|
127
|
+
render(
|
|
128
|
+
<RelatedList title="Contacts" type="table" data={data} collapsible defaultCollapsed />,
|
|
129
|
+
);
|
|
130
|
+
expect(screen.getByText('Contacts')).toBeInTheDocument();
|
|
131
|
+
// Content should be hidden when collapsed
|
|
132
|
+
expect(screen.queryByText('Alice')).not.toBeInTheDocument();
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should expand collapsed card when header is clicked', () => {
|
|
136
|
+
render(
|
|
137
|
+
<RelatedList title="Contacts" type="table" data={[]} collapsible defaultCollapsed />,
|
|
138
|
+
);
|
|
139
|
+
// Initially collapsed - content should be hidden
|
|
140
|
+
expect(screen.queryByText('No related records found')).not.toBeInTheDocument();
|
|
141
|
+
// Click the header to expand
|
|
142
|
+
fireEvent.click(screen.getByText('Contacts'));
|
|
143
|
+
// Content should now be visible
|
|
144
|
+
expect(screen.getByText('No related records found')).toBeInTheDocument();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should show content by default when collapsible is true but defaultCollapsed is false', () => {
|
|
148
|
+
render(
|
|
149
|
+
<RelatedList title="Contacts" type="table" data={[]} collapsible />,
|
|
150
|
+
);
|
|
151
|
+
expect(screen.getByText('No related records found')).toBeInTheDocument();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should show content when collapsible is false (default)', () => {
|
|
155
|
+
render(
|
|
156
|
+
<RelatedList title="Contacts" type="table" data={[]} />,
|
|
157
|
+
);
|
|
158
|
+
expect(screen.getByText('No related records found')).toBeInTheDocument();
|
|
159
|
+
});
|
|
66
160
|
});
|
|
@@ -0,0 +1,101 @@
|
|
|
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, vi } from 'vitest';
|
|
10
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
+
import { SectionGroup } from '../SectionGroup';
|
|
12
|
+
import type { SectionGroup as SectionGroupType } from '@object-ui/types';
|
|
13
|
+
|
|
14
|
+
describe('SectionGroup', () => {
|
|
15
|
+
const baseGroup: SectionGroupType = {
|
|
16
|
+
title: 'Address Information',
|
|
17
|
+
sections: [
|
|
18
|
+
{
|
|
19
|
+
title: 'Billing',
|
|
20
|
+
fields: [
|
|
21
|
+
{ name: 'billingStreet', label: 'Street' },
|
|
22
|
+
{ name: 'billingCity', label: 'City' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
title: 'Shipping',
|
|
27
|
+
fields: [
|
|
28
|
+
{ name: 'shippingStreet', label: 'Street' },
|
|
29
|
+
{ name: 'shippingCity', label: 'City' },
|
|
30
|
+
],
|
|
31
|
+
},
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const data = {
|
|
36
|
+
billingStreet: '123 Main St',
|
|
37
|
+
billingCity: 'Springfield',
|
|
38
|
+
shippingStreet: '456 Oak Ave',
|
|
39
|
+
shippingCity: 'Shelbyville',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
it('should render group title', () => {
|
|
43
|
+
render(<SectionGroup group={baseGroup} data={data} />);
|
|
44
|
+
expect(screen.getByText('Address Information')).toBeInTheDocument();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should render child section titles', () => {
|
|
48
|
+
render(<SectionGroup group={baseGroup} data={data} />);
|
|
49
|
+
expect(screen.getByText('Billing')).toBeInTheDocument();
|
|
50
|
+
expect(screen.getByText('Shipping')).toBeInTheDocument();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should render field values in child sections', () => {
|
|
54
|
+
render(<SectionGroup group={baseGroup} data={data} />);
|
|
55
|
+
expect(screen.getByText('123 Main St')).toBeInTheDocument();
|
|
56
|
+
expect(screen.getByText('Springfield')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should be collapsible by default', () => {
|
|
60
|
+
render(<SectionGroup group={baseGroup} data={data} />);
|
|
61
|
+
// The group should render a collapsible trigger
|
|
62
|
+
const trigger = screen.getByText('Address Information');
|
|
63
|
+
expect(trigger.closest('[data-state]') || trigger.closest('div')).toBeTruthy();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should start collapsed when defaultCollapsed is true', () => {
|
|
67
|
+
const collapsedGroup = { ...baseGroup, defaultCollapsed: true };
|
|
68
|
+
render(<SectionGroup group={collapsedGroup} data={data} />);
|
|
69
|
+
// Title should still be visible
|
|
70
|
+
expect(screen.getByText('Address Information')).toBeInTheDocument();
|
|
71
|
+
// Child section content should be hidden (in collapsed state)
|
|
72
|
+
expect(screen.queryByText('123 Main St')).not.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should expand when clicked while collapsed', () => {
|
|
76
|
+
const collapsedGroup = { ...baseGroup, defaultCollapsed: true };
|
|
77
|
+
render(<SectionGroup group={collapsedGroup} data={data} />);
|
|
78
|
+
|
|
79
|
+
// Click the trigger to expand
|
|
80
|
+
fireEvent.click(screen.getByText('Address Information'));
|
|
81
|
+
|
|
82
|
+
// Content should now be visible
|
|
83
|
+
expect(screen.getByText('123 Main St')).toBeInTheDocument();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should render description when provided', () => {
|
|
87
|
+
const groupWithDesc = { ...baseGroup, description: 'Billing and shipping addresses', collapsible: false };
|
|
88
|
+
render(<SectionGroup group={groupWithDesc} data={data} />);
|
|
89
|
+
expect(screen.getByText('Billing and shipping addresses')).toBeInTheDocument();
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not be collapsible when collapsible is false', () => {
|
|
93
|
+
const nonCollapsible = { ...baseGroup, collapsible: false };
|
|
94
|
+
const { container } = render(<SectionGroup group={nonCollapsible} data={data} />);
|
|
95
|
+
// The top-level group heading should not have a cursor-pointer collapsible trigger
|
|
96
|
+
const heading = screen.getByText('Address Information');
|
|
97
|
+
const parentDiv = heading.closest('div');
|
|
98
|
+
// Non-collapsible group renders a static border-b div, not a CollapsibleTrigger
|
|
99
|
+
expect(parentDiv?.className).not.toContain('cursor-pointer');
|
|
100
|
+
});
|
|
101
|
+
});
|