@object-ui/plugin-grid 3.3.0 → 3.3.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.
Files changed (49) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +21 -1
  3. package/dist/index.js +631 -599
  4. package/dist/index.umd.cjs +8 -8
  5. package/package.json +44 -12
  6. package/.turbo/turbo-build.log +0 -32
  7. package/src/FormulaBar.tsx +0 -151
  8. package/src/GroupRow.tsx +0 -69
  9. package/src/ImportWizard.tsx +0 -412
  10. package/src/InlineEditing.tsx +0 -235
  11. package/src/ListColumnExtensions.test.tsx +0 -373
  12. package/src/ListColumnSchema.test.ts +0 -88
  13. package/src/ObjectGrid.EdgeCases.stories.tsx +0 -147
  14. package/src/ObjectGrid.msw.test.tsx +0 -130
  15. package/src/ObjectGrid.stories.tsx +0 -139
  16. package/src/ObjectGrid.tsx +0 -1598
  17. package/src/SplitPaneGrid.tsx +0 -120
  18. package/src/VirtualGrid.tsx +0 -183
  19. package/src/__tests__/GroupRow.test.tsx +0 -206
  20. package/src/__tests__/ImportPreview.test.tsx +0 -171
  21. package/src/__tests__/InlineEditing.test.tsx +0 -360
  22. package/src/__tests__/VirtualGrid.test.tsx +0 -438
  23. package/src/__tests__/accessibility.test.tsx +0 -254
  24. package/src/__tests__/accessorKey-inference.test.tsx +0 -132
  25. package/src/__tests__/airtable-style.test.tsx +0 -508
  26. package/src/__tests__/column-features.test.tsx +0 -490
  27. package/src/__tests__/grid-export.test.tsx +0 -121
  28. package/src/__tests__/mobile-card-view.test.tsx +0 -355
  29. package/src/__tests__/objectdef-enrichment.test.tsx +0 -566
  30. package/src/__tests__/performance-benchmark.test.tsx +0 -182
  31. package/src/__tests__/phase11-features.test.tsx +0 -418
  32. package/src/__tests__/row-bulk-actions.test.tsx +0 -413
  33. package/src/__tests__/row-height.test.tsx +0 -160
  34. package/src/__tests__/useGroupedData.test.ts +0 -165
  35. package/src/__tests__/view-states.test.tsx +0 -203
  36. package/src/components/BulkActionBar.tsx +0 -66
  37. package/src/components/RowActionMenu.tsx +0 -91
  38. package/src/index.test.tsx +0 -29
  39. package/src/index.tsx +0 -99
  40. package/src/useCellClipboard.ts +0 -136
  41. package/src/useColumnSummary.ts +0 -128
  42. package/src/useGradientColor.ts +0 -103
  43. package/src/useGroupReorder.ts +0 -123
  44. package/src/useGroupedData.ts +0 -187
  45. package/src/useRowColor.ts +0 -74
  46. package/tsconfig.json +0 -9
  47. package/vite.config.ts +0 -58
  48. package/vitest.config.ts +0 -13
  49. package/vitest.setup.ts +0 -1
@@ -1,147 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
3
- import type { BaseSchema } from '@object-ui/types';
4
- import { createStorybookDataSource } from '@storybook-config/datasource';
5
-
6
- const meta = {
7
- title: 'Plugins/ObjectGrid/Edge Cases',
8
- component: SchemaRenderer,
9
- parameters: {
10
- layout: 'padded',
11
- },
12
- tags: ['autodocs'],
13
- argTypes: {
14
- schema: { table: { disable: true } },
15
- },
16
- } satisfies Meta<any>;
17
-
18
- export default meta;
19
- type Story = StoryObj<typeof meta>;
20
-
21
- const dataSource = createStorybookDataSource();
22
-
23
- const renderStory = (args: any) => (
24
- <SchemaRendererProvider dataSource={dataSource}>
25
- <SchemaRenderer schema={args as unknown as BaseSchema} />
26
- </SchemaRendererProvider>
27
- );
28
-
29
- // ── Empty Data ────────────────────────────────────────────────
30
-
31
- export const EmptyData: Story = {
32
- name: 'Empty – No Rows',
33
- render: renderStory,
34
- args: {
35
- type: 'object-grid',
36
- objectName: 'Employee',
37
- columns: [
38
- { field: 'id', header: 'ID', width: 80 },
39
- { field: 'name', header: 'Name' },
40
- { field: 'email', header: 'Email' },
41
- { field: 'department', header: 'Department' },
42
- ],
43
- data: [],
44
- pagination: false,
45
- className: 'w-full',
46
- } as any,
47
- };
48
-
49
- // ── Single Row ────────────────────────────────────────────────
50
-
51
- export const SingleRow: Story = {
52
- name: 'Single Row',
53
- render: renderStory,
54
- args: {
55
- type: 'object-grid',
56
- objectName: 'Employee',
57
- columns: [
58
- { field: 'id', header: 'ID', width: 80 },
59
- { field: 'name', header: 'Name', sortable: true },
60
- { field: 'email', header: 'Email' },
61
- { field: 'department', header: 'Department' },
62
- ],
63
- data: [
64
- { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering' },
65
- ],
66
- pagination: false,
67
- className: 'w-full',
68
- } as any,
69
- };
70
-
71
- // ── Many Columns ──────────────────────────────────────────────
72
-
73
- export const ManyColumns: Story = {
74
- name: 'Many Columns (15+)',
75
- render: renderStory,
76
- args: {
77
- type: 'object-grid',
78
- objectName: 'WideTable',
79
- columns: Array.from({ length: 18 }, (_, i) => ({
80
- field: `col${i + 1}`,
81
- header: `Column ${i + 1}`,
82
- sortable: i < 5,
83
- })),
84
- data: Array.from({ length: 5 }, (_, row) => {
85
- const record: Record<string, any> = {};
86
- for (let c = 1; c <= 18; c++) {
87
- record[`col${c}`] = `R${row + 1}-C${c}`;
88
- }
89
- return record;
90
- }),
91
- pagination: false,
92
- className: 'w-full',
93
- } as any,
94
- };
95
-
96
- // ── Very Long Cell Values ─────────────────────────────────────
97
-
98
- const LONG_VALUE =
99
- 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.';
100
-
101
- export const LongCellValues: Story = {
102
- name: 'Very Long Cell Values',
103
- render: renderStory,
104
- args: {
105
- type: 'object-grid',
106
- objectName: 'Article',
107
- columns: [
108
- { field: 'id', header: 'ID', width: 60 },
109
- { field: 'title', header: 'Title' },
110
- { field: 'abstract', header: 'Abstract' },
111
- { field: 'author', header: 'Author' },
112
- ],
113
- data: [
114
- { id: 1, title: LONG_VALUE, abstract: LONG_VALUE + ' ' + LONG_VALUE, author: 'Dr. Extremely Long Author Name The Third Junior' },
115
- { id: 2, title: 'Short', abstract: 'Brief.', author: 'Bob' },
116
- { id: 3, title: LONG_VALUE, abstract: LONG_VALUE, author: LONG_VALUE },
117
- ],
118
- pagination: false,
119
- className: 'w-full',
120
- } as any,
121
- };
122
-
123
- // ── Null / Undefined Values ───────────────────────────────────
124
-
125
- export const NullAndUndefinedValues: Story = {
126
- name: 'Null / Undefined Cell Values',
127
- render: renderStory,
128
- args: {
129
- type: 'object-grid',
130
- objectName: 'Sparse',
131
- columns: [
132
- { field: 'id', header: 'ID', width: 60 },
133
- { field: 'name', header: 'Name' },
134
- { field: 'email', header: 'Email' },
135
- { field: 'phone', header: 'Phone' },
136
- { field: 'notes', header: 'Notes' },
137
- ],
138
- data: [
139
- { id: 1, name: 'Alice', email: null, phone: undefined, notes: '' },
140
- { id: 2, name: null, email: 'bob@example.com', phone: null, notes: undefined },
141
- { id: 3, name: undefined, email: undefined, phone: undefined, notes: null },
142
- { id: 4, name: 'Dave', email: 'dave@example.com', phone: '+1-555-0100', notes: 'Complete record' },
143
- ],
144
- pagination: false,
145
- className: 'w-full',
146
- } as any,
147
- };
@@ -1,130 +0,0 @@
1
- import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
2
- import { render, screen, waitFor, within } from '@testing-library/react';
3
- import '@testing-library/jest-dom';
4
- import { ObjectGrid } from './ObjectGrid';
5
- import { ObjectStackAdapter } from '@object-ui/data-objectstack';
6
- import { setupServer } from 'msw/node';
7
- import { http, HttpResponse } from 'msw';
8
- import { registerAllFields } from '@object-ui/fields';
9
- import React from 'react';
10
- import { ContactObject } from '../../../examples/crm/src/objects/contact.object';
11
-
12
- registerAllFields();
13
-
14
- const BASE_URL = process.env.OBJECTSTACK_API_URL || 'http://localhost';
15
-
16
- // --- Mock Data ---
17
-
18
- const mockSchema = ContactObject;
19
-
20
- const mockData = {
21
- value: [
22
- { id: '1', name: 'John Doe', email: 'john@example.com', company: 'Acme Inc' },
23
- { id: '2', name: 'Jane Smith', email: 'jane@test.com', company: 'Globex' },
24
- { id: '3', name: 'Bob Wilson', email: 'bob@test.com', company: 'Acme Inc' }
25
- ],
26
- count: 3,
27
- '@odata.count': 3
28
- };
29
-
30
- // --- MSW Setup ---
31
-
32
- const handlers = [
33
- // .well-known discovery endpoint (used by client.connect())
34
- http.get(`${BASE_URL}/.well-known/objectstack`, () => {
35
- return HttpResponse.json({
36
- name: 'ObjectStack API',
37
- version: '1.0',
38
- endpoints: {
39
- data: '/api/v1/data',
40
- metadata: '/api/v1/meta'
41
- },
42
- capabilities: {
43
- graphql: false,
44
- search: false,
45
- websockets: false,
46
- files: true,
47
- analytics: false,
48
- hub: false
49
- }
50
- });
51
- }),
52
-
53
- // OPTIONS handler for CORS preflight
54
- http.options(`${BASE_URL}/*`, () => {
55
- return new HttpResponse(null, {
56
- status: 200,
57
- headers: {
58
- 'Access-Control-Allow-Origin': '*',
59
- 'Access-Control-Allow-Methods': 'GET,HEAD,POST,PUT,DELETE,CONNECT,OPTIONS,TRACE,PATCH',
60
- 'Access-Control-Allow-Headers': 'Content-Type, Authorization',
61
- },
62
- });
63
- }),
64
-
65
- // Health check / Connection check
66
- http.get(`${BASE_URL}/api/v1`, () => {
67
- return HttpResponse.json({ status: 'ok', version: '1.0.0' });
68
- }),
69
-
70
- // Schema: /api/v1/metadata/object/:name and /api/v1/meta/object/:name (client uses /meta)
71
- http.get(`${BASE_URL}/api/v1/metadata/object/contact`, () => {
72
- return HttpResponse.json(mockSchema);
73
- }),
74
- http.get(`${BASE_URL}/api/v1/meta/object/contact`, () => {
75
- return HttpResponse.json(mockSchema);
76
- }),
77
-
78
- // Data Query: /api/v1/data/contact
79
- http.get(`${BASE_URL}/api/v1/data/contact`, () => {
80
- return HttpResponse.json(mockData);
81
- })
82
- ];
83
-
84
- const server = setupServer(...handlers);
85
-
86
- // --- Test Suite ---
87
-
88
- describe('ObjectGrid with ObjectStack/MSW', () => {
89
- // Only start MSW if we are NOT using a real server
90
- if (!process.env.OBJECTSTACK_API_URL) {
91
- beforeAll(() => server.listen());
92
- afterEach(() => server.resetHandlers());
93
- afterAll(() => server.close());
94
- }
95
-
96
- const dataSource = new ObjectStackAdapter({
97
- baseUrl: BASE_URL,
98
- });
99
-
100
- it('loads schema and data rows', async () => {
101
- render(
102
- <ObjectGrid
103
- schema={{
104
- type: 'object-grid',
105
- objectName: 'contact',
106
- columns: ['name', 'email', 'company'] // Explicit columns or auto-derived
107
- }}
108
- dataSource={dataSource}
109
- />
110
- );
111
-
112
- // Verify Column Headers (from schema or props)
113
- await waitFor(() => {
114
- // Changed from 'Full Name' to 'Name' to match CRM example schema
115
- expect(screen.getByText('Name')).toBeInTheDocument();
116
- });
117
- expect(screen.getByText('Email')).toBeInTheDocument();
118
- expect(screen.getByText('Company')).toBeInTheDocument();
119
-
120
- // Verify Data Rows
121
- await waitFor(() => {
122
- expect(screen.getByText('John Doe')).toBeInTheDocument();
123
- });
124
- expect(screen.getByText('jane@test.com')).toBeInTheDocument();
125
- expect(screen.getByText('Globex')).toBeInTheDocument();
126
-
127
- // Check Row Count (if grid displays it)
128
- // expect(screen.getByText(/3 records/i)).toBeInTheDocument();
129
- });
130
- });
@@ -1,139 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { SchemaRenderer, SchemaRendererProvider } from '@object-ui/react';
3
- import type { BaseSchema } from '@object-ui/types';
4
- import { createStorybookDataSource } from '@storybook-config/datasource';
5
-
6
- const meta = {
7
- title: 'Plugins/ObjectGrid',
8
- component: SchemaRenderer,
9
- parameters: {
10
- layout: 'padded',
11
- },
12
- tags: ['autodocs'],
13
- argTypes: {
14
- schema: { table: { disable: true } },
15
- },
16
- } satisfies Meta<any>;
17
-
18
- export default meta;
19
- type Story = StoryObj<typeof meta>;
20
-
21
- const dataSource = createStorybookDataSource();
22
-
23
- const renderStory = (args: any) => (
24
- <SchemaRendererProvider dataSource={dataSource}>
25
- <SchemaRenderer schema={args as unknown as BaseSchema} />
26
- </SchemaRendererProvider>
27
- );
28
-
29
- export const Default: Story = {
30
- render: renderStory,
31
- args: {
32
- type: 'object-grid',
33
- objectName: 'Employee',
34
- columns: [
35
- { field: 'id', header: 'ID', width: 80 },
36
- { field: 'name', header: 'Name', sortable: true, filterable: true },
37
- { field: 'email', header: 'Email', sortable: true, filterable: true },
38
- { field: 'department', header: 'Department', sortable: true },
39
- { field: 'status', header: 'Status', sortable: true },
40
- ],
41
- data: [
42
- { id: 1, name: 'Alice Johnson', email: 'alice@example.com', department: 'Engineering', status: 'Active' },
43
- { id: 2, name: 'Bob Smith', email: 'bob@example.com', department: 'Marketing', status: 'Active' },
44
- { id: 3, name: 'Carol White', email: 'carol@example.com', department: 'Sales', status: 'Inactive' },
45
- { id: 4, name: 'Dave Brown', email: 'dave@example.com', department: 'Engineering', status: 'Active' },
46
- { id: 5, name: 'Eve Davis', email: 'eve@example.com', department: 'HR', status: 'Active' },
47
- ],
48
- pagination: true,
49
- pageSize: 10,
50
- className: 'w-full',
51
- } as any,
52
- };
53
-
54
- export const WithRowActions: Story = {
55
- render: renderStory,
56
- args: {
57
- type: 'object-grid',
58
- objectName: 'Task',
59
- columns: [
60
- { field: 'id', header: 'ID', width: 80 },
61
- { field: 'title', header: 'Title', sortable: true, filterable: true },
62
- { field: 'assignee', header: 'Assignee', sortable: true },
63
- { field: 'priority', header: 'Priority', sortable: true },
64
- { field: 'status', header: 'Status', sortable: true },
65
- ],
66
- actions: [
67
- { label: 'View', action: 'view' },
68
- { label: 'Edit', action: 'edit' },
69
- { label: 'Delete', action: 'delete', variant: 'destructive' },
70
- ],
71
- data: [
72
- { id: 1, title: 'Fix login bug', assignee: 'Alice', priority: 'High', status: 'In Progress' },
73
- { id: 2, title: 'Add dark mode', assignee: 'Bob', priority: 'Medium', status: 'To Do' },
74
- { id: 3, title: 'Update docs', assignee: 'Carol', priority: 'Low', status: 'Done' },
75
- ],
76
- pagination: true,
77
- pageSize: 10,
78
- className: 'w-full',
79
- } as any,
80
- };
81
-
82
- /**
83
- * CRM Deals Pipeline — demonstrates professional data formatting:
84
- * - Currency with thousand separators (Amount column, right-aligned)
85
- * - Percentage with progress bar (Probability column, right-aligned)
86
- * - Formatted dates (Close Date column)
87
- * - Colored badges for stage/status (Stage column)
88
- * - Bold clickable name as primary link (Name column)
89
- * - Empty value placeholder (Account column)
90
- */
91
- export const CRMDeals: Story = {
92
- name: 'CRM Deals Pipeline',
93
- render: renderStory,
94
- args: {
95
- type: 'object-grid',
96
- objectName: 'Deal',
97
- columns: [
98
- { field: 'name', label: 'Name', link: true, sortable: true },
99
- { field: 'account', label: 'Account' },
100
- { field: 'stage', label: 'Stage', type: 'select', sortable: true },
101
- { field: 'amount', label: 'Amount', type: 'currency', sortable: true },
102
- { field: 'probability', label: 'Probability', type: 'percent', sortable: true },
103
- { field: 'close_date', label: 'Close Date', type: 'date', sortable: true },
104
- ],
105
- data: [
106
- { id: '1', name: 'ObjectStack Enterprise License', account: '', stage: 'Closed Won', amount: 150000, probability: 100, close_date: '2024-01-15T00:00:00.000Z' },
107
- { id: '2', name: 'Cloud Migration Project', account: 'Acme Corp', stage: 'Negotiation', amount: 85000, probability: 60, close_date: '2024-03-20T00:00:00.000Z' },
108
- { id: '3', name: 'Annual Support Renewal', account: '', stage: 'Proposal', amount: 42000, probability: 80, close_date: '2024-02-28T00:00:00.000Z' },
109
- { id: '4', name: 'Custom Integration Development', account: 'TechFlow Inc', stage: 'Qualification', amount: 230000, probability: 30, close_date: '2024-06-15T00:00:00.000Z' },
110
- { id: '5', name: 'Data Analytics Platform', account: '', stage: 'Closed Lost', amount: 95000, probability: 0, close_date: '2024-01-10T00:00:00.000Z' },
111
- { id: '6', name: 'Security Audit Contract', account: 'SecureNet', stage: 'Closed Won', amount: 67500, probability: 100, close_date: '2024-02-01T00:00:00.000Z' },
112
- { id: '7', name: 'Mobile App Development', account: '', stage: 'Discovery', amount: 180000, probability: 15, close_date: '2024-08-30T00:00:00.000Z' },
113
- ],
114
- pagination: false,
115
- className: 'w-full',
116
- } as any,
117
- };
118
-
119
- export const EditableGrid: Story = {
120
- render: renderStory,
121
- args: {
122
- type: 'object-grid',
123
- objectName: 'Product',
124
- columns: [
125
- { field: 'sku', header: 'SKU', width: 100, editable: false },
126
- { field: 'name', header: 'Name', sortable: true },
127
- { field: 'price', header: 'Price', sortable: true },
128
- { field: 'stock', header: 'Stock', sortable: true },
129
- ],
130
- data: [
131
- { sku: 'SKU-001', name: 'Widget A', price: '$19.99', stock: 50 },
132
- { sku: 'SKU-002', name: 'Widget B', price: '$29.99', stock: 30 },
133
- { sku: 'SKU-003', name: 'Widget C', price: '$9.99', stock: 120 },
134
- ],
135
- editable: true,
136
- pagination: false,
137
- className: 'w-full',
138
- } as any,
139
- };