@object-ui/plugin-grid 0.5.0 → 3.0.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 (35) hide show
  1. package/.turbo/turbo-build.log +51 -6
  2. package/CHANGELOG.md +37 -0
  3. package/README.md +97 -0
  4. package/dist/index.js +994 -584
  5. package/dist/index.umd.cjs +3 -3
  6. package/dist/packages/plugin-grid/src/InlineEditing.d.ts +28 -0
  7. package/dist/packages/plugin-grid/src/ListColumnExtensions.test.d.ts +0 -0
  8. package/dist/packages/plugin-grid/src/ListColumnSchema.test.d.ts +1 -0
  9. package/dist/packages/plugin-grid/src/ObjectGrid.EdgeCases.stories.d.ts +25 -0
  10. package/dist/packages/plugin-grid/src/ObjectGrid.d.ts +7 -1
  11. package/dist/packages/plugin-grid/src/ObjectGrid.stories.d.ts +33 -0
  12. package/dist/packages/plugin-grid/src/__tests__/InlineEditing.test.d.ts +0 -0
  13. package/dist/packages/plugin-grid/src/__tests__/VirtualGrid.test.d.ts +0 -0
  14. package/dist/packages/plugin-grid/src/__tests__/accessibility.test.d.ts +0 -0
  15. package/dist/packages/plugin-grid/src/__tests__/performance-benchmark.test.d.ts +0 -0
  16. package/dist/packages/plugin-grid/src/__tests__/view-states.test.d.ts +0 -0
  17. package/dist/packages/plugin-grid/src/index.d.ts +5 -0
  18. package/dist/packages/plugin-grid/src/useGroupedData.d.ts +30 -0
  19. package/dist/packages/plugin-grid/src/useRowColor.d.ts +8 -0
  20. package/package.json +11 -10
  21. package/src/InlineEditing.tsx +235 -0
  22. package/src/ListColumnExtensions.test.tsx +374 -0
  23. package/src/ListColumnSchema.test.ts +88 -0
  24. package/src/ObjectGrid.EdgeCases.stories.tsx +147 -0
  25. package/src/ObjectGrid.msw.test.tsx +24 -1
  26. package/src/ObjectGrid.stories.tsx +139 -0
  27. package/src/ObjectGrid.tsx +409 -113
  28. package/src/__tests__/InlineEditing.test.tsx +360 -0
  29. package/src/__tests__/VirtualGrid.test.tsx +438 -0
  30. package/src/__tests__/accessibility.test.tsx +254 -0
  31. package/src/__tests__/performance-benchmark.test.tsx +182 -0
  32. package/src/__tests__/view-states.test.tsx +203 -0
  33. package/src/index.tsx +17 -2
  34. package/src/useGroupedData.ts +122 -0
  35. package/src/useRowColor.ts +74 -0
@@ -0,0 +1,147 @@
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
+ };
@@ -30,6 +30,26 @@ const mockData = {
30
30
  // --- MSW Setup ---
31
31
 
32
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
+
33
53
  // OPTIONS handler for CORS preflight
34
54
  http.options(`${BASE_URL}/*`, () => {
35
55
  return new HttpResponse(null, {
@@ -47,7 +67,10 @@ const handlers = [
47
67
  return HttpResponse.json({ status: 'ok', version: '1.0.0' });
48
68
  }),
49
69
 
50
- // Schema: /api/v1/meta/object/:name
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
+ }),
51
74
  http.get(`${BASE_URL}/api/v1/meta/object/contact`, () => {
52
75
  return HttpResponse.json(mockSchema);
53
76
  }),
@@ -0,0 +1,139 @@
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
+ };