@object-ui/plugin-grid 3.0.3 → 3.1.0

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.
Files changed (44) hide show
  1. package/.turbo/turbo-build.log +12 -6
  2. package/dist/index.js +2169 -922
  3. package/dist/index.umd.cjs +9 -3
  4. package/dist/plugin-grid/src/FormulaBar.d.ts +29 -0
  5. package/dist/plugin-grid/src/GroupRow.d.ts +23 -0
  6. package/dist/plugin-grid/src/ImportWizard.d.ts +29 -0
  7. package/dist/plugin-grid/src/ObjectGrid.d.ts +1 -0
  8. package/dist/plugin-grid/src/SplitPaneGrid.d.ts +22 -0
  9. package/dist/plugin-grid/src/components/BulkActionBar.d.ts +12 -0
  10. package/dist/plugin-grid/src/components/RowActionMenu.d.ts +23 -0
  11. package/dist/plugin-grid/src/index.d.ts +22 -2
  12. package/dist/plugin-grid/src/useCellClipboard.d.ts +47 -0
  13. package/dist/plugin-grid/src/useColumnSummary.d.ts +25 -0
  14. package/dist/plugin-grid/src/useGradientColor.d.ts +37 -0
  15. package/dist/plugin-grid/src/useGroupReorder.d.ts +34 -0
  16. package/dist/plugin-grid/src/useGroupedData.d.ts +24 -3
  17. package/package.json +10 -10
  18. package/src/FormulaBar.tsx +151 -0
  19. package/src/GroupRow.tsx +69 -0
  20. package/src/ImportWizard.tsx +412 -0
  21. package/src/ListColumnExtensions.test.tsx +4 -5
  22. package/src/ObjectGrid.tsx +994 -139
  23. package/src/SplitPaneGrid.tsx +120 -0
  24. package/src/VirtualGrid.tsx +2 -2
  25. package/src/__tests__/GroupRow.test.tsx +206 -0
  26. package/src/__tests__/ImportPreview.test.tsx +171 -0
  27. package/src/__tests__/accessorKey-inference.test.tsx +132 -0
  28. package/src/__tests__/airtable-style.test.tsx +508 -0
  29. package/src/__tests__/column-features.test.tsx +490 -0
  30. package/src/__tests__/grid-export.test.tsx +121 -0
  31. package/src/__tests__/mobile-card-view.test.tsx +355 -0
  32. package/src/__tests__/objectdef-enrichment.test.tsx +566 -0
  33. package/src/__tests__/phase11-features.test.tsx +418 -0
  34. package/src/__tests__/row-bulk-actions.test.tsx +413 -0
  35. package/src/__tests__/row-height.test.tsx +160 -0
  36. package/src/__tests__/useGroupedData.test.ts +165 -0
  37. package/src/components/BulkActionBar.tsx +66 -0
  38. package/src/components/RowActionMenu.tsx +91 -0
  39. package/src/index.tsx +46 -2
  40. package/src/useCellClipboard.ts +136 -0
  41. package/src/useColumnSummary.ts +128 -0
  42. package/src/useGradientColor.ts +103 -0
  43. package/src/useGroupReorder.ts +123 -0
  44. package/src/useGroupedData.ts +69 -4
@@ -0,0 +1,355 @@
1
+ /**
2
+ * Mobile Card View Tests
3
+ *
4
+ * Tests for the optimized mobile card-view fallback in ObjectGrid:
5
+ * - Visual hierarchy (Name as bold title, Amount+Stage row, date formatting)
6
+ * - Currency formatting in card view
7
+ * - Stage colored badge rendering
8
+ * - Skeleton loading cards
9
+ * - Card structure and layout
10
+ */
11
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
12
+ import { render, screen, waitFor } from '@testing-library/react';
13
+ import '@testing-library/jest-dom';
14
+ import React from 'react';
15
+ import { ObjectGrid } from '../ObjectGrid';
16
+ import { registerAllFields } from '@object-ui/fields';
17
+ import { ActionProvider } from '@object-ui/react';
18
+ import type { ListColumn } from '@object-ui/types';
19
+
20
+ registerAllFields();
21
+
22
+ // --- Mock CRM Opportunity Data ---
23
+ const opportunityData = [
24
+ {
25
+ _id: '101',
26
+ name: 'ObjectStack Enterprise License',
27
+ amount: 150000,
28
+ stage: 'Closed Won',
29
+ close_date: '2024-01-15T00:00:00.000Z',
30
+ probability: 100,
31
+ },
32
+ {
33
+ _id: '102',
34
+ name: 'Global Fin Q1 Upsell',
35
+ amount: 45000,
36
+ stage: 'Negotiation',
37
+ close_date: '2024-03-30T00:00:00.000Z',
38
+ probability: 80,
39
+ },
40
+ {
41
+ _id: '103',
42
+ name: 'London Annual Renewal',
43
+ amount: 85000,
44
+ stage: 'Proposal',
45
+ close_date: '2024-05-15T00:00:00.000Z',
46
+ probability: 60,
47
+ },
48
+ ];
49
+
50
+ const opportunityColumns: ListColumn[] = [
51
+ { field: 'name', label: 'Name' },
52
+ { field: 'amount', label: 'Amount' },
53
+ { field: 'stage', label: 'Stage' },
54
+ { field: 'close_date', label: 'Close Date' },
55
+ { field: 'probability', label: 'Probability' },
56
+ ];
57
+
58
+ let originalInnerWidth: number;
59
+
60
+ beforeEach(() => {
61
+ originalInnerWidth = window.innerWidth;
62
+ });
63
+
64
+ afterEach(() => {
65
+ Object.defineProperty(window, 'innerWidth', {
66
+ writable: true,
67
+ configurable: true,
68
+ value: originalInnerWidth,
69
+ });
70
+ window.dispatchEvent(new Event('resize'));
71
+ });
72
+
73
+ function setMobileViewport() {
74
+ Object.defineProperty(window, 'innerWidth', {
75
+ writable: true,
76
+ configurable: true,
77
+ value: 375,
78
+ });
79
+ window.dispatchEvent(new Event('resize'));
80
+ }
81
+
82
+ function renderGrid(data: any[], columns: ListColumn[], opts?: Record<string, any>) {
83
+ setMobileViewport();
84
+ const schema: any = {
85
+ type: 'object-grid' as const,
86
+ objectName: 'opportunity',
87
+ columns,
88
+ data: { provider: 'value', items: data },
89
+ ...opts,
90
+ };
91
+
92
+ return render(
93
+ <ActionProvider>
94
+ <ObjectGrid schema={schema} />
95
+ </ActionProvider>
96
+ );
97
+ }
98
+
99
+ // =========================================================================
100
+ // 1. Card View Title Hierarchy
101
+ // =========================================================================
102
+ describe('Mobile Card View: Title hierarchy', () => {
103
+ it('should display the first column (Name) as a bold title in each card', async () => {
104
+ renderGrid(opportunityData, opportunityColumns);
105
+
106
+ await waitFor(() => {
107
+ expect(screen.getByText('ObjectStack Enterprise License')).toBeInTheDocument();
108
+ expect(screen.getByText('Global Fin Q1 Upsell')).toBeInTheDocument();
109
+ expect(screen.getByText('London Annual Renewal')).toBeInTheDocument();
110
+ });
111
+
112
+ // Verify the name is rendered with font-semibold (bold title)
113
+ const titleEl = screen.getByText('ObjectStack Enterprise License');
114
+ expect(titleEl.className).toContain('font-semibold');
115
+ });
116
+ });
117
+
118
+ // =========================================================================
119
+ // 2. Currency Formatting
120
+ // =========================================================================
121
+ describe('Mobile Card View: Currency formatting', () => {
122
+ it('should format Amount as compact currency ($150K)', async () => {
123
+ renderGrid(opportunityData, opportunityColumns);
124
+
125
+ await waitFor(() => {
126
+ expect(screen.getByText('$150K')).toBeInTheDocument();
127
+ expect(screen.getByText('$45K')).toBeInTheDocument();
128
+ expect(screen.getByText('$85K')).toBeInTheDocument();
129
+ });
130
+ });
131
+ });
132
+
133
+ // =========================================================================
134
+ // 3. Stage Badge Rendering
135
+ // =========================================================================
136
+ describe('Mobile Card View: Stage colored badge', () => {
137
+ it('should render stage values as Badge components', async () => {
138
+ renderGrid(opportunityData, opportunityColumns);
139
+
140
+ await waitFor(() => {
141
+ const closedWonBadges = screen.getAllByText('Closed Won');
142
+ expect(closedWonBadges.length).toBeGreaterThanOrEqual(1);
143
+
144
+ const negotiationBadge = screen.getByText('Negotiation');
145
+ expect(negotiationBadge).toBeInTheDocument();
146
+
147
+ const proposalBadge = screen.getByText('Proposal');
148
+ expect(proposalBadge).toBeInTheDocument();
149
+ });
150
+ });
151
+
152
+ it('should apply green color to Closed Won stage badge', async () => {
153
+ renderGrid(opportunityData, opportunityColumns);
154
+
155
+ await waitFor(() => {
156
+ const badges = screen.getAllByText('Closed Won');
157
+ const badge = badges[0];
158
+ expect(badge.className).toContain('bg-green-100');
159
+ expect(badge.className).toContain('text-green-800');
160
+ });
161
+ });
162
+
163
+ it('should apply yellow color to Negotiation stage badge', async () => {
164
+ renderGrid(opportunityData, opportunityColumns);
165
+
166
+ await waitFor(() => {
167
+ const badge = screen.getByText('Negotiation');
168
+ expect(badge.className).toContain('bg-yellow-100');
169
+ expect(badge.className).toContain('text-yellow-800');
170
+ });
171
+ });
172
+
173
+ it('should apply blue color to Proposal stage badge', async () => {
174
+ renderGrid(opportunityData, opportunityColumns);
175
+
176
+ await waitFor(() => {
177
+ const badge = screen.getByText('Proposal');
178
+ expect(badge.className).toContain('bg-blue-100');
179
+ expect(badge.className).toContain('text-blue-800');
180
+ });
181
+ });
182
+ });
183
+
184
+ // =========================================================================
185
+ // 4. Date Formatting
186
+ // =========================================================================
187
+ describe('Mobile Card View: Date formatting', () => {
188
+ it('should format close_date as compact short date (not ISO string)', async () => {
189
+ renderGrid(opportunityData, opportunityColumns);
190
+
191
+ await waitFor(() => {
192
+ // Should NOT show raw ISO string
193
+ expect(screen.queryByText('2024-01-15T00:00:00.000Z')).not.toBeInTheDocument();
194
+ expect(screen.queryByText('2024-03-30T00:00:00.000Z')).not.toBeInTheDocument();
195
+
196
+ // Should show compact date format (e.g. "Jan 15, '24")
197
+ expect(screen.getByText("Jan 15, '24")).toBeInTheDocument();
198
+ });
199
+ });
200
+ });
201
+
202
+ // =========================================================================
203
+ // 5. Skeleton Loading Cards
204
+ // =========================================================================
205
+ describe('Mobile Card View: Skeleton loading', () => {
206
+ it('should show loading spinner when no data is available', async () => {
207
+ setMobileViewport();
208
+ const schema: any = {
209
+ type: 'object-grid' as const,
210
+ objectName: 'opportunity',
211
+ columns: opportunityColumns,
212
+ data: { provider: 'value', items: [] },
213
+ };
214
+
215
+ render(
216
+ <ActionProvider>
217
+ <ObjectGrid schema={schema} />
218
+ </ActionProvider>
219
+ );
220
+
221
+ // With empty inline data, grid shows spinner or empty state (not skeleton cards)
222
+ // The skeleton cards only appear during async loading on mobile
223
+ await waitFor(() => {
224
+ expect(screen.queryByText('ObjectStack Enterprise License')).not.toBeInTheDocument();
225
+ });
226
+ });
227
+ });
228
+
229
+ // =========================================================================
230
+ // 6. Card Structure
231
+ // =========================================================================
232
+ describe('Mobile Card View: Card structure', () => {
233
+ it('should render all data rows as individual cards', async () => {
234
+ const { container } = renderGrid(opportunityData, opportunityColumns);
235
+
236
+ await waitFor(() => {
237
+ const cards = container.querySelectorAll('.border.rounded-lg');
238
+ expect(cards.length).toBe(3);
239
+ });
240
+ });
241
+
242
+ it('should render compact date and percent in combined row', async () => {
243
+ renderGrid(opportunityData, opportunityColumns);
244
+
245
+ await waitFor(() => {
246
+ // Compact date visible
247
+ const dateEls = screen.getAllByText("Jan 15, '24");
248
+ expect(dateEls.length).toBeGreaterThanOrEqual(1);
249
+ // Probability shown as percent
250
+ expect(screen.getByText('100%')).toBeInTheDocument();
251
+ });
252
+ });
253
+ });
254
+
255
+ // =========================================================================
256
+ // 7. Percent/Probability Display
257
+ // =========================================================================
258
+ describe('Mobile Card View: Percent field display', () => {
259
+ it('should render probability values with % suffix', async () => {
260
+ renderGrid(opportunityData, opportunityColumns);
261
+
262
+ await waitFor(() => {
263
+ expect(screen.getByText('100%')).toBeInTheDocument();
264
+ expect(screen.getByText('80%')).toBeInTheDocument();
265
+ expect(screen.getByText('60%')).toBeInTheDocument();
266
+ });
267
+ });
268
+
269
+ it('should hide empty/null percent fields', async () => {
270
+ const dataWithNull = [
271
+ { _id: '201', name: 'Test Deal', amount: 50000, stage: 'Proposal', close_date: '2024-06-01', probability: null },
272
+ ];
273
+ renderGrid(dataWithNull, opportunityColumns);
274
+
275
+ await waitFor(() => {
276
+ expect(screen.getByText('Test Deal')).toBeInTheDocument();
277
+ // "Probability" label should NOT appear since value is null
278
+ expect(screen.queryByText('Probability')).not.toBeInTheDocument();
279
+ });
280
+ });
281
+ });
282
+
283
+ // =========================================================================
284
+ // 8. Left Border Stage Color
285
+ // =========================================================================
286
+ describe('Mobile Card View: Left border stage accent', () => {
287
+ it('should add green left border for Closed Won stage', async () => {
288
+ const { container } = renderGrid(opportunityData, opportunityColumns);
289
+
290
+ await waitFor(() => {
291
+ const cards = container.querySelectorAll('.border.rounded-lg');
292
+ expect(cards[0].className).toContain('border-l-green-500');
293
+ });
294
+ });
295
+
296
+ it('should add yellow left border for Negotiation stage', async () => {
297
+ const { container } = renderGrid(opportunityData, opportunityColumns);
298
+
299
+ await waitFor(() => {
300
+ const cards = container.querySelectorAll('.border.rounded-lg');
301
+ expect(cards[1].className).toContain('border-l-yellow-500');
302
+ });
303
+ });
304
+
305
+ it('should add blue left border for Proposal stage', async () => {
306
+ const { container } = renderGrid(opportunityData, opportunityColumns);
307
+
308
+ await waitFor(() => {
309
+ const cards = container.querySelectorAll('.border.rounded-lg');
310
+ expect(cards[2].className).toContain('border-l-blue-500');
311
+ });
312
+ });
313
+ });
314
+
315
+ // =========================================================================
316
+ // 9. Badge Truncation Fix
317
+ // =========================================================================
318
+ describe('Mobile Card View: Badge truncation handling', () => {
319
+ it('should have shrink-0, max-w, and truncate classes on stage badges', async () => {
320
+ renderGrid(opportunityData, opportunityColumns);
321
+
322
+ await waitFor(() => {
323
+ const badge = screen.getByText('Closed Won');
324
+ expect(badge.className).toContain('shrink-0');
325
+ expect(badge.className).toContain('max-w-[140px]');
326
+ expect(badge.className).toContain('truncate');
327
+ });
328
+ });
329
+ });
330
+
331
+ // =========================================================================
332
+ // 10. Empty field hiding
333
+ // =========================================================================
334
+ describe('Mobile Card View: Empty field hiding', () => {
335
+ it('should not render "other" fields with null/empty values', async () => {
336
+ const columnsWithExtra: ListColumn[] = [
337
+ { field: 'name', label: 'Name' },
338
+ { field: 'amount', label: 'Amount' },
339
+ { field: 'stage', label: 'Stage' },
340
+ { field: 'description', label: 'Description' },
341
+ ];
342
+ const dataWithEmpty = [
343
+ { _id: '301', name: 'Deal A', amount: 10000, stage: 'Proposal', description: null },
344
+ { _id: '302', name: 'Deal B', amount: 20000, stage: 'Proposal', description: 'Has value' },
345
+ ];
346
+ renderGrid(dataWithEmpty, columnsWithExtra);
347
+
348
+ await waitFor(() => {
349
+ // "Description" label should appear only for Deal B (has value), not Deal A (null)
350
+ const descLabels = screen.getAllByText('Description');
351
+ expect(descLabels.length).toBe(1);
352
+ expect(screen.getByText('Has value')).toBeInTheDocument();
353
+ });
354
+ });
355
+ });