@object-ui/plugin-detail 3.0.3 → 3.1.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/.turbo/turbo-build.log +45 -8
- package/CHANGELOG.md +11 -0
- package/dist/AddressField-B1iVr404.js +96 -0
- package/dist/AutoNumberField-BxnFqllo.js +8 -0
- package/dist/AvatarField-Duw4xOLZ.js +82 -0
- package/dist/BooleanField-CZ4axVeq.js +37 -0
- package/dist/CodeField-BSz-mk2v.js +21 -0
- package/dist/ColorField-B522ad8m.js +42 -0
- package/dist/CurrencyField-Cwr3_pow.js +43 -0
- package/dist/DateField-DCo6dxud.js +21 -0
- package/dist/DateTimeField-BWfBuANO.js +28 -0
- package/dist/EmailField-CpwbdVCU.js +31 -0
- package/dist/FileField-DVAUAJ8e.js +133 -0
- package/dist/FormulaField-CJkkwIK8.js +9 -0
- package/dist/GeolocationField-DNCKitgo.js +123 -0
- package/dist/GridField-DSblZNfp.js +30 -0
- package/dist/ImageField-DBAlnMon.js +90 -0
- package/dist/LocationField-DsHsXA6R.js +31 -0
- package/dist/LookupField-CsT0QQz2.js +96 -0
- package/dist/MasterDetailField-Db8b7Gqs.js +108 -0
- package/dist/NumberField-0IGp7lcA.js +26 -0
- package/dist/ObjectField-BLApgJtS.js +48 -0
- package/dist/PasswordField-pHKyNlmo.js +38 -0
- package/dist/PercentField-CwgKmlIb.js +63 -0
- package/dist/PhoneField-lKtbYOdN.js +31 -0
- package/dist/QRCodeField-BTTasT3w.js +77 -0
- package/dist/RatingField-De2X-l44.js +47 -0
- package/dist/RichTextField-B5QnvUOr.js +38 -0
- package/dist/SelectField-C9AZRHWu.js +26 -0
- package/dist/SignatureField-BgcEmYzd.js +85 -0
- package/dist/SliderField-BzrttVOY.js +30 -0
- package/dist/SummaryField-ugYPYxjP.js +9 -0
- package/dist/TextAreaField-DSE_CaU6.js +39 -0
- package/dist/TextField-DFQ4T9PR.js +32 -0
- package/dist/TimeField-F0cfmsps.js +21 -0
- package/dist/UrlField-DLXrFIH-.js +33 -0
- package/dist/UserField-PXMmxJY9.js +49 -0
- package/dist/VectorField-CKg9jdGa.js +25 -0
- package/dist/index-qQ1C-yUR.js +59976 -0
- package/dist/index.js +32 -55026
- package/dist/index.umd.cjs +41 -30
- package/dist/plugin-detail.css +1 -1
- package/dist/src/ActivityTimeline.d.ts +20 -0
- package/dist/src/ActivityTimeline.d.ts.map +1 -0
- package/dist/src/CommentAttachment.d.ts +25 -0
- package/dist/src/CommentAttachment.d.ts.map +1 -0
- package/dist/src/CommentInput.d.ts +24 -0
- package/dist/src/CommentInput.d.ts.map +1 -0
- package/dist/src/DetailSection.d.ts +8 -0
- package/dist/src/DetailSection.d.ts.map +1 -1
- package/dist/src/DetailView.d.ts +4 -0
- package/dist/src/DetailView.d.ts.map +1 -1
- package/dist/src/DetailView.stories.d.ts +8 -0
- package/dist/src/DetailView.stories.d.ts.map +1 -1
- package/dist/src/DiffView.d.ts +24 -0
- package/dist/src/DiffView.d.ts.map +1 -0
- package/dist/src/FieldChangeItem.d.ts +21 -0
- package/dist/src/FieldChangeItem.d.ts.map +1 -0
- package/dist/src/HeaderHighlight.d.ts +18 -0
- package/dist/src/HeaderHighlight.d.ts.map +1 -0
- package/dist/src/InlineCreateRelated.d.ts +32 -0
- package/dist/src/InlineCreateRelated.d.ts.map +1 -0
- package/dist/src/MentionAutocomplete.d.ts +43 -0
- package/dist/src/MentionAutocomplete.d.ts.map +1 -0
- package/dist/src/PointInTimeRestore.d.ts +28 -0
- package/dist/src/PointInTimeRestore.d.ts.map +1 -0
- package/dist/src/ReactionPicker.d.ts +25 -0
- package/dist/src/ReactionPicker.d.ts.map +1 -0
- package/dist/src/RecordActivityTimeline.d.ts +49 -0
- package/dist/src/RecordActivityTimeline.d.ts.map +1 -0
- package/dist/src/RecordChatterPanel.d.ts +48 -0
- package/dist/src/RecordChatterPanel.d.ts.map +1 -0
- package/dist/src/RecordComments.d.ts +20 -0
- package/dist/src/RecordComments.d.ts.map +1 -0
- package/dist/src/RecordNavigationEnhanced.d.ts +18 -0
- package/dist/src/RecordNavigationEnhanced.d.ts.map +1 -0
- package/dist/src/RelatedList.d.ts +20 -0
- package/dist/src/RelatedList.d.ts.map +1 -1
- package/dist/src/RelationshipGraph.d.ts +23 -0
- package/dist/src/RelationshipGraph.d.ts.map +1 -0
- package/dist/src/RichTextCommentInput.d.ts +24 -0
- package/dist/src/RichTextCommentInput.d.ts.map +1 -0
- package/dist/src/SectionGroup.d.ts +21 -0
- package/dist/src/SectionGroup.d.ts.map +1 -0
- package/dist/src/SubscriptionToggle.d.ts +22 -0
- package/dist/src/SubscriptionToggle.d.ts.map +1 -0
- package/dist/src/ThreadedReplies.d.ts +26 -0
- package/dist/src/ThreadedReplies.d.ts.map +1 -0
- package/dist/src/autoLayout.d.ts +34 -0
- package/dist/src/autoLayout.d.ts.map +1 -0
- package/dist/src/index.d.ts +40 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/useDetailTranslation.d.ts +34 -0
- package/dist/src/useDetailTranslation.d.ts.map +1 -0
- package/package.json +8 -7
- package/src/ActivityTimeline.tsx +184 -0
- package/src/CommentAttachment.tsx +192 -0
- package/src/CommentInput.tsx +81 -0
- package/src/DetailSection.tsx +81 -10
- package/src/DetailView.stories.tsx +76 -0
- package/src/DetailView.tsx +519 -66
- package/src/DiffView.tsx +231 -0
- package/src/FieldChangeItem.tsx +46 -0
- package/src/HeaderHighlight.tsx +67 -0
- package/src/InlineCreateRelated.tsx +291 -0
- package/src/MentionAutocomplete.tsx +123 -0
- package/src/PointInTimeRestore.tsx +261 -0
- package/src/ReactionPicker.tsx +106 -0
- package/src/RecordActivityTimeline.tsx +429 -0
- package/src/RecordChatterPanel.tsx +202 -0
- package/src/RecordComments.tsx +215 -0
- package/src/RecordNavigationEnhanced.tsx +211 -0
- package/src/RelatedList.tsx +314 -19
- package/src/RelationshipGraph.tsx +286 -0
- package/src/RichTextCommentInput.tsx +348 -0
- package/src/SectionGroup.tsx +101 -0
- package/src/SubscriptionToggle.tsx +60 -0
- package/src/ThreadedReplies.tsx +161 -0
- package/src/__tests__/ActivityTimeline.test.tsx +119 -0
- package/src/__tests__/ActivityTimelineFiltering.test.tsx +143 -0
- package/src/__tests__/CommentInput.test.tsx +57 -0
- package/src/__tests__/DetailSection.test.tsx +320 -0
- package/src/__tests__/DetailView.test.tsx +415 -1
- package/src/__tests__/FieldChangeItem.test.tsx +119 -0
- package/src/__tests__/HeaderHighlight.test.tsx +68 -0
- package/src/__tests__/MentionAutocomplete.test.tsx +97 -0
- package/src/__tests__/ReactionPicker.test.tsx +113 -0
- package/src/__tests__/RecordActivityTimeline.test.tsx +395 -0
- package/src/__tests__/RecordChatterPanel.test.tsx +227 -0
- package/src/__tests__/RecordComments.test.tsx +96 -0
- package/src/__tests__/RecordCommentsPinSearch.test.tsx +133 -0
- package/src/__tests__/RelatedList.test.tsx +160 -0
- package/src/__tests__/SectionGroup.test.tsx +101 -0
- package/src/__tests__/SubscriptionToggle.test.tsx +84 -0
- package/src/__tests__/ThreadedReplies.test.tsx +212 -0
- package/src/__tests__/autoLayout.test.ts +184 -0
- package/src/__tests__/phase12-features.test.tsx +583 -0
- package/src/__tests__/roadmap-features.test.tsx +478 -0
- package/src/autoLayout.ts +111 -0
- package/src/index.tsx +50 -0
- package/src/useDetailTranslation.ts +114 -0
|
@@ -0,0 +1,119 @@
|
|
|
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 '@testing-library/jest-dom';
|
|
12
|
+
import { ActivityTimeline } from '../ActivityTimeline';
|
|
13
|
+
import type { ActivityEntry } from '@object-ui/types';
|
|
14
|
+
|
|
15
|
+
const mockActivities: ActivityEntry[] = [
|
|
16
|
+
{
|
|
17
|
+
id: '1',
|
|
18
|
+
type: 'create',
|
|
19
|
+
user: 'Alice',
|
|
20
|
+
timestamp: '2026-02-15T10:00:00Z',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: '2',
|
|
24
|
+
type: 'field_change',
|
|
25
|
+
field: 'status',
|
|
26
|
+
oldValue: 'open',
|
|
27
|
+
newValue: 'in_progress',
|
|
28
|
+
user: 'Bob',
|
|
29
|
+
timestamp: '2026-02-16T08:30:00Z',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: '3',
|
|
33
|
+
type: 'status_change',
|
|
34
|
+
field: 'stage',
|
|
35
|
+
newValue: 'Completed',
|
|
36
|
+
user: 'Charlie',
|
|
37
|
+
timestamp: '2026-02-16T09:00:00Z',
|
|
38
|
+
description: 'Moved to Completed stage',
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: '4',
|
|
42
|
+
type: 'comment',
|
|
43
|
+
user: 'Alice',
|
|
44
|
+
timestamp: '2026-02-16T09:30:00Z',
|
|
45
|
+
description: 'Added a comment',
|
|
46
|
+
},
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
describe('ActivityTimeline', () => {
|
|
50
|
+
it('should render activity heading with count', () => {
|
|
51
|
+
render(<ActivityTimeline activities={mockActivities} />);
|
|
52
|
+
expect(screen.getByText('Activity')).toBeInTheDocument();
|
|
53
|
+
expect(screen.getByText('(4)')).toBeInTheDocument();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('should render user names', () => {
|
|
57
|
+
render(<ActivityTimeline activities={mockActivities} />);
|
|
58
|
+
expect(screen.getAllByText('Alice')).toHaveLength(2);
|
|
59
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
60
|
+
expect(screen.getByText('Charlie')).toBeInTheDocument();
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('should show "No activity recorded" when empty', () => {
|
|
64
|
+
render(<ActivityTimeline activities={[]} />);
|
|
65
|
+
expect(screen.getByText('No activity recorded')).toBeInTheDocument();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should render field change description for field_change type', () => {
|
|
69
|
+
render(
|
|
70
|
+
<ActivityTimeline
|
|
71
|
+
activities={[
|
|
72
|
+
{
|
|
73
|
+
id: '1',
|
|
74
|
+
type: 'field_change',
|
|
75
|
+
field: 'priority',
|
|
76
|
+
oldValue: 'low',
|
|
77
|
+
newValue: 'high',
|
|
78
|
+
user: 'Eve',
|
|
79
|
+
timestamp: '2026-02-16T10:00:00Z',
|
|
80
|
+
},
|
|
81
|
+
]}
|
|
82
|
+
/>,
|
|
83
|
+
);
|
|
84
|
+
expect(screen.getByText(/Changed Priority from "low" to "high"/)).toBeInTheDocument();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should use description if provided', () => {
|
|
88
|
+
render(
|
|
89
|
+
<ActivityTimeline
|
|
90
|
+
activities={[
|
|
91
|
+
{
|
|
92
|
+
id: '1',
|
|
93
|
+
type: 'comment',
|
|
94
|
+
user: 'Alice',
|
|
95
|
+
timestamp: '2026-02-16T09:30:00Z',
|
|
96
|
+
description: 'Added a comment',
|
|
97
|
+
},
|
|
98
|
+
]}
|
|
99
|
+
/>,
|
|
100
|
+
);
|
|
101
|
+
expect(screen.getByText('Added a comment')).toBeInTheDocument();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('should render create type with default description', () => {
|
|
105
|
+
render(
|
|
106
|
+
<ActivityTimeline
|
|
107
|
+
activities={[
|
|
108
|
+
{
|
|
109
|
+
id: '1',
|
|
110
|
+
type: 'create',
|
|
111
|
+
user: 'Eve',
|
|
112
|
+
timestamp: '2026-02-16T10:00:00Z',
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
/>,
|
|
116
|
+
);
|
|
117
|
+
expect(screen.getByText('Created this record')).toBeInTheDocument();
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,143 @@
|
|
|
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, fireEvent } from '@testing-library/react';
|
|
11
|
+
import '@testing-library/jest-dom';
|
|
12
|
+
import { ActivityTimeline } from '../ActivityTimeline';
|
|
13
|
+
import type { ActivityEntry } from '@object-ui/types';
|
|
14
|
+
|
|
15
|
+
const mockActivities: ActivityEntry[] = [
|
|
16
|
+
{
|
|
17
|
+
id: '1',
|
|
18
|
+
type: 'create',
|
|
19
|
+
user: 'Alice',
|
|
20
|
+
timestamp: '2026-02-15T10:00:00Z',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: '2',
|
|
24
|
+
type: 'field_change',
|
|
25
|
+
field: 'status',
|
|
26
|
+
oldValue: 'open',
|
|
27
|
+
newValue: 'in_progress',
|
|
28
|
+
user: 'Bob',
|
|
29
|
+
timestamp: '2026-02-16T08:30:00Z',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: '3',
|
|
33
|
+
type: 'comment',
|
|
34
|
+
user: 'Charlie',
|
|
35
|
+
timestamp: '2026-02-16T09:00:00Z',
|
|
36
|
+
description: 'Added a comment on the record',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: '4',
|
|
40
|
+
type: 'delete',
|
|
41
|
+
user: 'Diana',
|
|
42
|
+
timestamp: '2026-02-16T09:30:00Z',
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
id: '5',
|
|
46
|
+
type: 'comment',
|
|
47
|
+
user: 'Eve',
|
|
48
|
+
timestamp: '2026-02-16T10:00:00Z',
|
|
49
|
+
description: 'Another comment here',
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
describe('ActivityTimeline - Filtering', () => {
|
|
54
|
+
it('does not render filter controls when filterable is false/not provided', () => {
|
|
55
|
+
render(<ActivityTimeline activities={mockActivities} />);
|
|
56
|
+
expect(screen.queryByRole('group', { name: 'Activity type filter' })).not.toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('renders filter controls when filterable is true', () => {
|
|
60
|
+
render(<ActivityTimeline activities={mockActivities} filterable />);
|
|
61
|
+
const filterGroup = screen.getByRole('group', { name: 'Activity type filter' });
|
|
62
|
+
expect(filterGroup).toBeInTheDocument();
|
|
63
|
+
|
|
64
|
+
// Should have "All", "Field Changes", "Creates", "Deletes", "Comments", "Status Changes"
|
|
65
|
+
expect(screen.getByText('All')).toBeInTheDocument();
|
|
66
|
+
expect(screen.getByText('Comments')).toBeInTheDocument();
|
|
67
|
+
expect(screen.getByText('Field Changes')).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByText('Creates')).toBeInTheDocument();
|
|
69
|
+
expect(screen.getByText('Deletes')).toBeInTheDocument();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows all activities by default', () => {
|
|
73
|
+
render(<ActivityTimeline activities={mockActivities} filterable />);
|
|
74
|
+
// Count should be (5)
|
|
75
|
+
expect(screen.getByText('(5)')).toBeInTheDocument();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('filters to only comments when Comments filter is selected', () => {
|
|
79
|
+
render(<ActivityTimeline activities={mockActivities} filterable />);
|
|
80
|
+
|
|
81
|
+
const commentsFilter = screen.getByText('Comments');
|
|
82
|
+
fireEvent.click(commentsFilter);
|
|
83
|
+
|
|
84
|
+
// Only 2 comment activities
|
|
85
|
+
expect(screen.getByText('(2)')).toBeInTheDocument();
|
|
86
|
+
expect(screen.getByText('Added a comment on the record')).toBeInTheDocument();
|
|
87
|
+
expect(screen.getByText('Another comment here')).toBeInTheDocument();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('filters to only field changes when Field Changes filter is selected', () => {
|
|
91
|
+
render(<ActivityTimeline activities={mockActivities} filterable />);
|
|
92
|
+
|
|
93
|
+
const fieldChangesFilter = screen.getByText('Field Changes');
|
|
94
|
+
fireEvent.click(fieldChangesFilter);
|
|
95
|
+
|
|
96
|
+
// Only 1 field_change activity
|
|
97
|
+
expect(screen.getByText('(1)')).toBeInTheDocument();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('shows all activities when All filter is re-selected', () => {
|
|
101
|
+
render(<ActivityTimeline activities={mockActivities} filterable />);
|
|
102
|
+
|
|
103
|
+
// First filter to comments only
|
|
104
|
+
fireEvent.click(screen.getByText('Comments'));
|
|
105
|
+
expect(screen.getByText('(2)')).toBeInTheDocument();
|
|
106
|
+
|
|
107
|
+
// Then go back to All
|
|
108
|
+
fireEvent.click(screen.getByText('All'));
|
|
109
|
+
expect(screen.getByText('(5)')).toBeInTheDocument();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('respects defaultFilter prop', () => {
|
|
113
|
+
render(
|
|
114
|
+
<ActivityTimeline
|
|
115
|
+
activities={mockActivities}
|
|
116
|
+
filterable
|
|
117
|
+
defaultFilter="comment"
|
|
118
|
+
/>,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
// Should start filtered to comments
|
|
122
|
+
expect(screen.getByText('(2)')).toBeInTheDocument();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('shows "No activity recorded" when filter has no results', () => {
|
|
126
|
+
render(
|
|
127
|
+
<ActivityTimeline
|
|
128
|
+
activities={mockActivities}
|
|
129
|
+
filterable
|
|
130
|
+
defaultFilter="status_change"
|
|
131
|
+
/>,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(screen.getByText('No activity recorded')).toBeInTheDocument();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('renders all activities without filter when filterable is not set', () => {
|
|
138
|
+
render(<ActivityTimeline activities={mockActivities} />);
|
|
139
|
+
expect(screen.getByText('(5)')).toBeInTheDocument();
|
|
140
|
+
expect(screen.getByText('Alice')).toBeInTheDocument();
|
|
141
|
+
expect(screen.getByText('Bob')).toBeInTheDocument();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
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 '@testing-library/jest-dom';
|
|
12
|
+
import { CommentInput } from '../CommentInput';
|
|
13
|
+
|
|
14
|
+
describe('CommentInput', () => {
|
|
15
|
+
it('should render placeholder text', () => {
|
|
16
|
+
const onSubmit = vi.fn();
|
|
17
|
+
render(<CommentInput onSubmit={onSubmit} />);
|
|
18
|
+
expect(screen.getByPlaceholderText('Leave a comment…')).toBeInTheDocument();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('should render custom placeholder', () => {
|
|
22
|
+
const onSubmit = vi.fn();
|
|
23
|
+
render(<CommentInput onSubmit={onSubmit} placeholder="Type here…" />);
|
|
24
|
+
expect(screen.getByPlaceholderText('Type here…')).toBeInTheDocument();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should disable submit when textarea is empty', () => {
|
|
28
|
+
const onSubmit = vi.fn();
|
|
29
|
+
render(<CommentInput onSubmit={onSubmit} />);
|
|
30
|
+
expect(screen.getByLabelText('Submit comment')).toBeDisabled();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('should enable submit when text is entered', () => {
|
|
34
|
+
const onSubmit = vi.fn();
|
|
35
|
+
render(<CommentInput onSubmit={onSubmit} />);
|
|
36
|
+
fireEvent.change(screen.getByPlaceholderText('Leave a comment…'), {
|
|
37
|
+
target: { value: 'Hello' },
|
|
38
|
+
});
|
|
39
|
+
expect(screen.getByLabelText('Submit comment')).not.toBeDisabled();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should call onSubmit with text when clicked', () => {
|
|
43
|
+
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
44
|
+
render(<CommentInput onSubmit={onSubmit} />);
|
|
45
|
+
fireEvent.change(screen.getByPlaceholderText('Leave a comment…'), {
|
|
46
|
+
target: { value: 'Hello world' },
|
|
47
|
+
});
|
|
48
|
+
fireEvent.click(screen.getByLabelText('Submit comment'));
|
|
49
|
+
expect(onSubmit).toHaveBeenCalledWith('Hello world');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should disable input when disabled prop is true', () => {
|
|
53
|
+
const onSubmit = vi.fn();
|
|
54
|
+
render(<CommentInput onSubmit={onSubmit} disabled />);
|
|
55
|
+
expect(screen.getByPlaceholderText('Leave a comment…')).toBeDisabled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,320 @@
|
|
|
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 } 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
|
+
});
|