@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,119 +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 '@testing-library/jest-dom';
|
|
12
|
-
import { FieldChangeItem } from '../FieldChangeItem';
|
|
13
|
-
import type { FieldChangeEntry } from '@object-ui/types';
|
|
14
|
-
|
|
15
|
-
describe('FieldChangeItem', () => {
|
|
16
|
-
it('should render field label with old and new display values', () => {
|
|
17
|
-
const change: FieldChangeEntry = {
|
|
18
|
-
field: 'status',
|
|
19
|
-
fieldLabel: 'Status',
|
|
20
|
-
oldDisplayValue: 'Open',
|
|
21
|
-
newDisplayValue: 'Closed',
|
|
22
|
-
};
|
|
23
|
-
render(<FieldChangeItem change={change} />);
|
|
24
|
-
expect(screen.getByText('Status')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByText('Open')).toBeInTheDocument();
|
|
26
|
-
expect(screen.getByText('Closed')).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should derive field label from field name when fieldLabel is not set', () => {
|
|
30
|
-
const change: FieldChangeEntry = {
|
|
31
|
-
field: 'first_name',
|
|
32
|
-
oldValue: 'John',
|
|
33
|
-
newValue: 'Jane',
|
|
34
|
-
};
|
|
35
|
-
render(<FieldChangeItem change={change} />);
|
|
36
|
-
expect(screen.getByText('First name')).toBeInTheDocument();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('should use raw values when display values are not set', () => {
|
|
40
|
-
const change: FieldChangeEntry = {
|
|
41
|
-
field: 'priority',
|
|
42
|
-
fieldLabel: 'Priority',
|
|
43
|
-
oldValue: 'low',
|
|
44
|
-
newValue: 'high',
|
|
45
|
-
};
|
|
46
|
-
render(<FieldChangeItem change={change} />);
|
|
47
|
-
expect(screen.getByText('low')).toBeInTheDocument();
|
|
48
|
-
expect(screen.getByText('high')).toBeInTheDocument();
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should show (empty) when value is null/undefined', () => {
|
|
52
|
-
const change: FieldChangeEntry = {
|
|
53
|
-
field: 'notes',
|
|
54
|
-
fieldLabel: 'Notes',
|
|
55
|
-
newValue: 'Some text',
|
|
56
|
-
};
|
|
57
|
-
render(<FieldChangeItem change={change} />);
|
|
58
|
-
expect(screen.getByText('(empty)')).toBeInTheDocument();
|
|
59
|
-
expect(screen.getByText('Some text')).toBeInTheDocument();
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('should apply custom className', () => {
|
|
63
|
-
const change: FieldChangeEntry = {
|
|
64
|
-
field: 'name',
|
|
65
|
-
fieldLabel: 'Name',
|
|
66
|
-
oldValue: 'A',
|
|
67
|
-
newValue: 'B',
|
|
68
|
-
};
|
|
69
|
-
const { container } = render(<FieldChangeItem change={change} className="custom-class" />);
|
|
70
|
-
expect(container.firstChild).toHaveClass('custom-class');
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('should render arrow icon between old and new values', () => {
|
|
74
|
-
const change: FieldChangeEntry = {
|
|
75
|
-
field: 'status',
|
|
76
|
-
fieldLabel: 'Status',
|
|
77
|
-
oldValue: 'Open',
|
|
78
|
-
newValue: 'Closed',
|
|
79
|
-
};
|
|
80
|
-
const { container } = render(<FieldChangeItem change={change} />);
|
|
81
|
-
// ArrowRight renders as an SVG with lucide classes
|
|
82
|
-
const svg = container.querySelector('svg');
|
|
83
|
-
expect(svg).toBeInTheDocument();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('should render old value with line-through style', () => {
|
|
87
|
-
const change: FieldChangeEntry = {
|
|
88
|
-
field: 'status',
|
|
89
|
-
fieldLabel: 'Status',
|
|
90
|
-
oldDisplayValue: 'Open',
|
|
91
|
-
newDisplayValue: 'Closed',
|
|
92
|
-
};
|
|
93
|
-
render(<FieldChangeItem change={change} />);
|
|
94
|
-
const oldEl = screen.getByText('Open');
|
|
95
|
-
expect(oldEl).toHaveClass('line-through');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should use fieldLabel priority over auto-generated label', () => {
|
|
99
|
-
const change: FieldChangeEntry = {
|
|
100
|
-
field: 'first_name',
|
|
101
|
-
fieldLabel: 'Custom Label',
|
|
102
|
-
oldValue: 'A',
|
|
103
|
-
newValue: 'B',
|
|
104
|
-
};
|
|
105
|
-
render(<FieldChangeItem change={change} />);
|
|
106
|
-
expect(screen.getByText('Custom Label')).toBeInTheDocument();
|
|
107
|
-
expect(screen.queryByText('First name')).not.toBeInTheDocument();
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
it('should show (empty) for both null old and new values', () => {
|
|
111
|
-
const change: FieldChangeEntry = {
|
|
112
|
-
field: 'notes',
|
|
113
|
-
fieldLabel: 'Notes',
|
|
114
|
-
};
|
|
115
|
-
render(<FieldChangeItem change={change} />);
|
|
116
|
-
const emptyTexts = screen.getAllByText('(empty)');
|
|
117
|
-
expect(emptyTexts).toHaveLength(2);
|
|
118
|
-
});
|
|
119
|
-
});
|
|
@@ -1,213 +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 { 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
|
-
|
|
69
|
-
it('should render currency fields with formatted value via CellRenderer', () => {
|
|
70
|
-
const currencyFields: HighlightField[] = [
|
|
71
|
-
{ name: 'amount', label: 'Amount', type: 'currency' },
|
|
72
|
-
];
|
|
73
|
-
render(<HeaderHighlight fields={currencyFields} data={{ amount: 250000 }} />);
|
|
74
|
-
// CurrencyCellRenderer should format — should NOT show raw "250000"
|
|
75
|
-
expect(screen.queryByText('250000')).not.toBeInTheDocument();
|
|
76
|
-
expect(screen.getByText(/250,000/)).toBeInTheDocument();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('should render select fields as badge via CellRenderer', () => {
|
|
80
|
-
const selectFields: HighlightField[] = [
|
|
81
|
-
{ name: 'stage', label: 'Stage', type: 'select' },
|
|
82
|
-
];
|
|
83
|
-
render(
|
|
84
|
-
<HeaderHighlight
|
|
85
|
-
fields={selectFields}
|
|
86
|
-
data={{ stage: 'prospecting' }}
|
|
87
|
-
objectSchema={{
|
|
88
|
-
fields: {
|
|
89
|
-
stage: {
|
|
90
|
-
type: 'select',
|
|
91
|
-
options: [
|
|
92
|
-
{ value: 'prospecting', label: 'Prospecting', color: 'blue' },
|
|
93
|
-
],
|
|
94
|
-
},
|
|
95
|
-
},
|
|
96
|
-
}}
|
|
97
|
-
/>
|
|
98
|
-
);
|
|
99
|
-
expect(screen.getByText('Prospecting')).toBeInTheDocument();
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('should enrich field type from objectSchema when field.type is not set', () => {
|
|
103
|
-
const fieldsNoType: HighlightField[] = [
|
|
104
|
-
{ name: 'amount', label: 'Amount' },
|
|
105
|
-
];
|
|
106
|
-
render(
|
|
107
|
-
<HeaderHighlight
|
|
108
|
-
fields={fieldsNoType}
|
|
109
|
-
data={{ amount: 5000 }}
|
|
110
|
-
objectSchema={{
|
|
111
|
-
fields: {
|
|
112
|
-
amount: { type: 'currency', currency: 'USD' },
|
|
113
|
-
},
|
|
114
|
-
}}
|
|
115
|
-
/>
|
|
116
|
-
);
|
|
117
|
-
// CurrencyCellRenderer should format, not raw String()
|
|
118
|
-
expect(screen.queryByText('5000')).not.toBeInTheDocument();
|
|
119
|
-
expect(screen.getByText(/5,000/)).toBeInTheDocument();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should fall back to text when no type info is available', () => {
|
|
123
|
-
const fieldsNoType: HighlightField[] = [
|
|
124
|
-
{ name: 'custom', label: 'Custom' },
|
|
125
|
-
];
|
|
126
|
-
render(<HeaderHighlight fields={fieldsNoType} data={{ custom: 'raw-value' }} />);
|
|
127
|
-
expect(screen.getByText('raw-value')).toBeInTheDocument();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('should safely render object values without crashing (React error #310 guard)', () => {
|
|
131
|
-
// Simulates MongoDB Decimal128 or expanded reference objects
|
|
132
|
-
const fieldsWithNumber: HighlightField[] = [
|
|
133
|
-
{ name: 'amount', label: 'Amount' },
|
|
134
|
-
{ name: 'account', label: 'Account' },
|
|
135
|
-
];
|
|
136
|
-
const objectData = {
|
|
137
|
-
amount: { $numberDecimal: '250000' },
|
|
138
|
-
account: { id: 'abc', name: 'Acme Corp' },
|
|
139
|
-
};
|
|
140
|
-
const objectSchema = {
|
|
141
|
-
fields: {
|
|
142
|
-
amount: { type: 'number' },
|
|
143
|
-
account: { type: 'text' },
|
|
144
|
-
},
|
|
145
|
-
};
|
|
146
|
-
// Should NOT crash — cell renderers coerce values via coerceToSafeValue
|
|
147
|
-
const { container } = render(
|
|
148
|
-
<HeaderHighlight
|
|
149
|
-
fields={fieldsWithNumber}
|
|
150
|
-
data={objectData}
|
|
151
|
-
objectSchema={objectSchema}
|
|
152
|
-
/>
|
|
153
|
-
);
|
|
154
|
-
expect(container.innerHTML).not.toBe('');
|
|
155
|
-
// NumberCellRenderer coerces $numberDecimal to number
|
|
156
|
-
expect(screen.getByText('250,000')).toBeInTheDocument();
|
|
157
|
-
// TextCellRenderer extracts name from object
|
|
158
|
-
expect(screen.getByText('Acme Corp')).toBeInTheDocument();
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should safely render lookup object values via LookupCellRenderer', () => {
|
|
162
|
-
const lookupFields: HighlightField[] = [
|
|
163
|
-
{ name: 'owner', label: 'Owner' },
|
|
164
|
-
];
|
|
165
|
-
const lookupData = {
|
|
166
|
-
owner: { id: 'u1', name: 'Jane Doe' },
|
|
167
|
-
};
|
|
168
|
-
const objectSchema = {
|
|
169
|
-
fields: {
|
|
170
|
-
owner: { type: 'lookup' },
|
|
171
|
-
},
|
|
172
|
-
};
|
|
173
|
-
// LookupCellRenderer handles object values natively
|
|
174
|
-
render(
|
|
175
|
-
<HeaderHighlight
|
|
176
|
-
fields={lookupFields}
|
|
177
|
-
data={lookupData}
|
|
178
|
-
objectSchema={objectSchema}
|
|
179
|
-
/>
|
|
180
|
-
);
|
|
181
|
-
expect(screen.getByText('Jane Doe')).toBeInTheDocument();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('should safely render array values via TextCellRenderer', () => {
|
|
185
|
-
const arrayFields: HighlightField[] = [
|
|
186
|
-
{ name: 'tags', label: 'Tags' },
|
|
187
|
-
];
|
|
188
|
-
const arrayData = {
|
|
189
|
-
tags: ['urgent', 'follow-up'],
|
|
190
|
-
};
|
|
191
|
-
const { container } = render(
|
|
192
|
-
<HeaderHighlight fields={arrayFields} data={arrayData} />
|
|
193
|
-
);
|
|
194
|
-
expect(container.innerHTML).not.toBe('');
|
|
195
|
-
// TextCellRenderer coerces arrays to comma-separated string
|
|
196
|
-
expect(screen.getByText('urgent, follow-up')).toBeInTheDocument();
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
it('should safely render array of objects via TextCellRenderer', () => {
|
|
200
|
-
const arrayFields: HighlightField[] = [
|
|
201
|
-
{ name: 'contacts', label: 'Contacts' },
|
|
202
|
-
];
|
|
203
|
-
const arrayData = {
|
|
204
|
-
contacts: [{ name: 'Alice' }, { name: 'Bob' }],
|
|
205
|
-
};
|
|
206
|
-
const { container } = render(
|
|
207
|
-
<HeaderHighlight fields={arrayFields} data={arrayData} />
|
|
208
|
-
);
|
|
209
|
-
expect(container.innerHTML).not.toBe('');
|
|
210
|
-
// TextCellRenderer coerces array of objects to "Alice, Bob"
|
|
211
|
-
expect(screen.getByText('Alice, Bob')).toBeInTheDocument();
|
|
212
|
-
});
|
|
213
|
-
});
|
|
@@ -1,97 +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, vi } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import '@testing-library/jest-dom';
|
|
12
|
-
import { MentionAutocomplete, createMentionFromSuggestion } from '../MentionAutocomplete';
|
|
13
|
-
import type { MentionSuggestionItem } from '../MentionAutocomplete';
|
|
14
|
-
|
|
15
|
-
const mockSuggestions: MentionSuggestionItem[] = [
|
|
16
|
-
{ id: 'u1', name: 'Alice Smith', type: 'user' },
|
|
17
|
-
{ id: 'u2', name: 'Bob Johnson', type: 'user', avatarUrl: 'https://example.com/bob.jpg' },
|
|
18
|
-
{ id: 't1', name: 'Engineering', type: 'team' },
|
|
19
|
-
];
|
|
20
|
-
|
|
21
|
-
describe('MentionAutocomplete', () => {
|
|
22
|
-
it('should render suggestions when visible', () => {
|
|
23
|
-
const onSelect = vi.fn();
|
|
24
|
-
render(
|
|
25
|
-
<MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
26
|
-
);
|
|
27
|
-
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
28
|
-
expect(screen.getByText('Bob Johnson')).toBeInTheDocument();
|
|
29
|
-
expect(screen.getByText('Engineering')).toBeInTheDocument();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('should not render when not visible', () => {
|
|
33
|
-
const onSelect = vi.fn();
|
|
34
|
-
const { container } = render(
|
|
35
|
-
<MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible={false} />,
|
|
36
|
-
);
|
|
37
|
-
expect(container.firstChild).toBeNull();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should filter suggestions by query', () => {
|
|
41
|
-
const onSelect = vi.fn();
|
|
42
|
-
render(
|
|
43
|
-
<MentionAutocomplete query="Ali" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
44
|
-
);
|
|
45
|
-
expect(screen.getByText('Alice Smith')).toBeInTheDocument();
|
|
46
|
-
expect(screen.queryByText('Bob Johnson')).not.toBeInTheDocument();
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('should show type label for non-user types', () => {
|
|
50
|
-
const onSelect = vi.fn();
|
|
51
|
-
render(
|
|
52
|
-
<MentionAutocomplete query="Eng" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
53
|
-
);
|
|
54
|
-
expect(screen.getByText('(team)')).toBeInTheDocument();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('should call onSelect when a suggestion is clicked', () => {
|
|
58
|
-
const onSelect = vi.fn();
|
|
59
|
-
render(
|
|
60
|
-
<MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
61
|
-
);
|
|
62
|
-
fireEvent.mouseDown(screen.getByText('Alice Smith'));
|
|
63
|
-
expect(onSelect).toHaveBeenCalledWith(mockSuggestions[0]);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should render avatar image when avatarUrl is present', () => {
|
|
67
|
-
const onSelect = vi.fn();
|
|
68
|
-
render(
|
|
69
|
-
<MentionAutocomplete query="" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
70
|
-
);
|
|
71
|
-
const img = screen.getByAltText('Bob Johnson');
|
|
72
|
-
expect(img).toBeInTheDocument();
|
|
73
|
-
expect(img).toHaveAttribute('src', 'https://example.com/bob.jpg');
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should not render when no matching suggestions', () => {
|
|
77
|
-
const onSelect = vi.fn();
|
|
78
|
-
const { container } = render(
|
|
79
|
-
<MentionAutocomplete query="xyz" suggestions={mockSuggestions} onSelect={onSelect} visible />,
|
|
80
|
-
);
|
|
81
|
-
expect(container.firstChild).toBeNull();
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
describe('createMentionFromSuggestion', () => {
|
|
86
|
-
it('should create a Mention object from a suggestion item', () => {
|
|
87
|
-
const item: MentionSuggestionItem = { id: 'u1', name: 'Alice', type: 'user' };
|
|
88
|
-
const mention = createMentionFromSuggestion(item, 5, 6);
|
|
89
|
-
expect(mention).toEqual({
|
|
90
|
-
type: 'user',
|
|
91
|
-
id: 'u1',
|
|
92
|
-
name: 'Alice',
|
|
93
|
-
offset: 5,
|
|
94
|
-
length: 6,
|
|
95
|
-
});
|
|
96
|
-
});
|
|
97
|
-
});
|
|
@@ -1,113 +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, vi } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/react';
|
|
11
|
-
import '@testing-library/jest-dom';
|
|
12
|
-
import { ReactionPicker } from '../ReactionPicker';
|
|
13
|
-
import type { Reaction } from '@object-ui/types';
|
|
14
|
-
|
|
15
|
-
const mockReactions: Reaction[] = [
|
|
16
|
-
{ emoji: '👍', count: 3, reacted: true },
|
|
17
|
-
{ emoji: '❤️', count: 1, reacted: false },
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
describe('ReactionPicker', () => {
|
|
21
|
-
it('should render existing reactions with counts', () => {
|
|
22
|
-
render(<ReactionPicker reactions={mockReactions} />);
|
|
23
|
-
expect(screen.getByText('👍')).toBeInTheDocument();
|
|
24
|
-
expect(screen.getByText('3')).toBeInTheDocument();
|
|
25
|
-
expect(screen.getByText('❤️')).toBeInTheDocument();
|
|
26
|
-
expect(screen.getByText('1')).toBeInTheDocument();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should highlight reacted emoji', () => {
|
|
30
|
-
render(<ReactionPicker reactions={mockReactions} />);
|
|
31
|
-
const thumbs = screen.getByLabelText(/👍 3/);
|
|
32
|
-
expect(thumbs).toHaveClass('bg-primary/10');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('should show add reaction button when onToggleReaction provided', () => {
|
|
36
|
-
const onToggle = vi.fn();
|
|
37
|
-
render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
|
|
38
|
-
expect(screen.getByLabelText('Add reaction')).toBeInTheDocument();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('should not show add button when no onToggleReaction', () => {
|
|
42
|
-
render(<ReactionPicker reactions={[]} />);
|
|
43
|
-
expect(screen.queryByLabelText('Add reaction')).not.toBeInTheDocument();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('should call onToggleReaction when clicking existing reaction', () => {
|
|
47
|
-
const onToggle = vi.fn();
|
|
48
|
-
render(<ReactionPicker reactions={mockReactions} onToggleReaction={onToggle} />);
|
|
49
|
-
fireEvent.click(screen.getByLabelText(/👍 3/));
|
|
50
|
-
expect(onToggle).toHaveBeenCalledWith('👍');
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should show emoji picker when add button is clicked', () => {
|
|
54
|
-
const onToggle = vi.fn();
|
|
55
|
-
render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
|
|
56
|
-
fireEvent.click(screen.getByLabelText('Add reaction'));
|
|
57
|
-
expect(screen.getByRole('listbox', { name: 'Emoji picker' })).toBeInTheDocument();
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should call onToggleReaction when emoji is selected from picker', () => {
|
|
61
|
-
const onToggle = vi.fn();
|
|
62
|
-
render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
|
|
63
|
-
fireEvent.click(screen.getByLabelText('Add reaction'));
|
|
64
|
-
// Select first emoji option (👍)
|
|
65
|
-
const options = screen.getAllByRole('option');
|
|
66
|
-
fireEvent.click(options[0]);
|
|
67
|
-
expect(onToggle).toHaveBeenCalledWith('👍');
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('should disable reaction buttons when no onToggleReaction', () => {
|
|
71
|
-
render(<ReactionPicker reactions={mockReactions} />);
|
|
72
|
-
const thumbsBtn = screen.getByLabelText(/👍 3/);
|
|
73
|
-
expect(thumbsBtn).toBeDisabled();
|
|
74
|
-
const heartBtn = screen.getByLabelText(/❤️ 1/);
|
|
75
|
-
expect(heartBtn).toBeDisabled();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('should render custom emojiOptions', () => {
|
|
79
|
-
const onToggle = vi.fn();
|
|
80
|
-
const customEmoji = ['🚀', '🔥', '✅'];
|
|
81
|
-
render(
|
|
82
|
-
<ReactionPicker reactions={[]} onToggleReaction={onToggle} emojiOptions={customEmoji} />,
|
|
83
|
-
);
|
|
84
|
-
fireEvent.click(screen.getByLabelText('Add reaction'));
|
|
85
|
-
const options = screen.getAllByRole('option');
|
|
86
|
-
expect(options).toHaveLength(3);
|
|
87
|
-
expect(options[0]).toHaveTextContent('🚀');
|
|
88
|
-
expect(options[1]).toHaveTextContent('🔥');
|
|
89
|
-
expect(options[2]).toHaveTextContent('✅');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('should include emoji and count in aria-label', () => {
|
|
93
|
-
render(<ReactionPicker reactions={mockReactions} />);
|
|
94
|
-
expect(screen.getByLabelText('👍 3 reactions')).toBeInTheDocument();
|
|
95
|
-
expect(screen.getByLabelText('❤️ 1 reaction')).toBeInTheDocument();
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('should show non-reacted emoji with bg-muted style', () => {
|
|
99
|
-
render(<ReactionPicker reactions={mockReactions} />);
|
|
100
|
-
const heart = screen.getByLabelText(/❤️ 1/);
|
|
101
|
-
expect(heart).toHaveClass('bg-muted');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('should close picker after selecting emoji', () => {
|
|
105
|
-
const onToggle = vi.fn();
|
|
106
|
-
render(<ReactionPicker reactions={[]} onToggleReaction={onToggle} />);
|
|
107
|
-
fireEvent.click(screen.getByLabelText('Add reaction'));
|
|
108
|
-
expect(screen.getByRole('listbox', { name: 'Emoji picker' })).toBeInTheDocument();
|
|
109
|
-
const options = screen.getAllByRole('option');
|
|
110
|
-
fireEvent.click(options[0]);
|
|
111
|
-
expect(screen.queryByRole('listbox', { name: 'Emoji picker' })).not.toBeInTheDocument();
|
|
112
|
-
});
|
|
113
|
-
});
|