@object-ui/plugin-kanban 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 (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +24 -0
  3. package/dist/{KanbanEnhanced-TdUe0kQH.js → KanbanEnhanced-Do9ZB1Mh.js} +35 -32
  4. package/dist/{KanbanImpl-BtlPa7GE.js → KanbanImpl-BdocXM5T.js} +1 -1
  5. package/dist/{chevron-down-B6UH8BbF.js → chevron-down-C0JUlGjk.js} +1 -1
  6. package/dist/index.js +3 -3
  7. package/dist/index.umd.cjs +2 -2
  8. package/dist/{plus-BTqoaaEC.js → plus-CHsXVJSY.js} +1 -1
  9. package/package.json +34 -11
  10. package/.turbo/turbo-build.log +0 -32
  11. package/src/CardTemplates.tsx +0 -123
  12. package/src/InlineQuickAdd.tsx +0 -189
  13. package/src/KanbanEnhanced.tsx +0 -525
  14. package/src/KanbanImpl.tsx +0 -597
  15. package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
  16. package/src/ObjectKanban.msw.test.tsx +0 -95
  17. package/src/ObjectKanban.stories.tsx +0 -152
  18. package/src/ObjectKanban.tsx +0 -276
  19. package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
  20. package/src/__tests__/KanbanGrouping.test.tsx +0 -164
  21. package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
  22. package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
  23. package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
  24. package/src/__tests__/accessibility.test.tsx +0 -296
  25. package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
  26. package/src/__tests__/performance-benchmark.test.tsx +0 -306
  27. package/src/__tests__/phase13-features.test.tsx +0 -387
  28. package/src/__tests__/view-states.test.tsx +0 -403
  29. package/src/index.test.ts +0 -112
  30. package/src/index.tsx +0 -327
  31. package/src/registration.test.tsx +0 -26
  32. package/src/types.ts +0 -185
  33. package/src/useColumnWidths.ts +0 -125
  34. package/src/useCrossSwimlaneMove.ts +0 -116
  35. package/src/useQuickAddReorder.ts +0 -107
  36. package/tsconfig.json +0 -19
  37. package/vite.config.ts +0 -62
  38. package/vitest.config.ts +0 -12
  39. package/vitest.setup.ts +0 -1
@@ -1,168 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { SchemaRenderer } from '@object-ui/react';
3
- import type { BaseSchema } from '@object-ui/types';
4
-
5
- const meta = {
6
- title: 'Plugins/ObjectKanban/Edge Cases',
7
- component: SchemaRenderer,
8
- parameters: {
9
- layout: 'padded',
10
- },
11
- tags: ['autodocs'],
12
- argTypes: {
13
- schema: { table: { disable: true } },
14
- },
15
- } satisfies Meta<any>;
16
-
17
- export default meta;
18
- type Story = StoryObj<typeof meta>;
19
-
20
- const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
-
22
- // ── Empty Board ───────────────────────────────────────────────
23
-
24
- export const EmptyBoard: Story = {
25
- name: 'Empty Board – No Columns',
26
- render: renderStory,
27
- args: {
28
- type: 'kanban',
29
- columns: [],
30
- className: 'w-full',
31
- } as any,
32
- };
33
-
34
- // ── Columns With No Cards ─────────────────────────────────────
35
-
36
- export const ColumnsWithNoCards: Story = {
37
- name: 'Columns With No Cards',
38
- render: renderStory,
39
- args: {
40
- type: 'kanban',
41
- columns: [
42
- { id: 'todo', title: 'To Do', cards: [] },
43
- { id: 'in-progress', title: 'In Progress', cards: [] },
44
- { id: 'done', title: 'Done', cards: [] },
45
- ],
46
- className: 'w-full',
47
- } as any,
48
- };
49
-
50
- // ── Column At WIP Limit ───────────────────────────────────────
51
-
52
- export const ColumnAtWipLimit: Story = {
53
- name: 'Column At WIP Limit',
54
- render: renderStory,
55
- args: {
56
- type: 'kanban',
57
- columns: [
58
- {
59
- id: 'todo',
60
- title: 'To Do',
61
- cards: [
62
- { id: 'c-1', title: 'Plan sprint', badges: [{ label: 'Planning', variant: 'default' }] },
63
- ],
64
- },
65
- {
66
- id: 'wip',
67
- title: 'In Progress (At Limit)',
68
- limit: 3,
69
- cards: [
70
- { id: 'c-2', title: 'Build auth module', description: 'JWT implementation', badges: [{ label: 'Feature', variant: 'default' }] },
71
- { id: 'c-3', title: 'Write unit tests', description: 'Coverage > 80%', badges: [{ label: 'Testing', variant: 'secondary' }] },
72
- { id: 'c-4', title: 'Deploy staging', description: 'Push to staging env', badges: [{ label: 'DevOps', variant: 'secondary' }] },
73
- ],
74
- },
75
- {
76
- id: 'done',
77
- title: 'Done',
78
- cards: [
79
- { id: 'c-5', title: 'Setup repo', badges: [{ label: 'Completed', variant: 'outline' }] },
80
- ],
81
- },
82
- ],
83
- className: 'w-full',
84
- } as any,
85
- };
86
-
87
- // ── Cards With Very Long Titles ───────────────────────────────
88
-
89
- export const CardsWithLongTitles: Story = {
90
- name: 'Cards With Very Long Titles',
91
- render: renderStory,
92
- args: {
93
- type: 'kanban',
94
- columns: [
95
- {
96
- id: 'backlog',
97
- title: 'Backlog',
98
- cards: [
99
- {
100
- id: 'long-1',
101
- title: 'Investigate the root cause of the intermittent timeout errors occurring in the payment processing pipeline during peak traffic hours on weekends',
102
- description: 'This card has an extremely long title to test text wrapping and overflow behaviour within kanban cards.',
103
- badges: [
104
- { label: 'Bug', variant: 'destructive' },
105
- { label: 'P0 – Critical Production Incident', variant: 'destructive' },
106
- ],
107
- },
108
- {
109
- id: 'long-2',
110
- title: 'Refactor the legacy monolithic authentication service into a set of microservices following domain-driven design principles and ensuring backward compatibility',
111
- badges: [{ label: 'Tech Debt', variant: 'secondary' }],
112
- },
113
- ],
114
- },
115
- {
116
- id: 'in-progress',
117
- title: 'In Progress',
118
- cards: [
119
- {
120
- id: 'long-3',
121
- title: 'A short title for contrast',
122
- description: 'Normal-length description.',
123
- },
124
- ],
125
- },
126
- ],
127
- className: 'w-full',
128
- } as any,
129
- };
130
-
131
- // ── Many Columns (10+) ───────────────────────────────────────
132
-
133
- export const ManyColumns: Story = {
134
- name: 'Many Columns (10+)',
135
- render: renderStory,
136
- args: {
137
- type: 'kanban',
138
- columns: Array.from({ length: 12 }, (_, i) => ({
139
- id: `col-${i + 1}`,
140
- title: `Stage ${i + 1}`,
141
- limit: i === 3 ? 2 : undefined,
142
- cards: i % 3 === 0
143
- ? [
144
- {
145
- id: `mc-${i}-1`,
146
- title: `Task ${i * 2 + 1}`,
147
- description: `Description for task in stage ${i + 1}`,
148
- badges: [{ label: `S${i + 1}`, variant: 'default' as const }],
149
- },
150
- {
151
- id: `mc-${i}-2`,
152
- title: `Task ${i * 2 + 2}`,
153
- badges: [{ label: 'Active', variant: 'secondary' as const }],
154
- },
155
- ]
156
- : i % 3 === 1
157
- ? [
158
- {
159
- id: `mc-${i}-1`,
160
- title: `Task ${i * 2 + 1}`,
161
- badges: [{ label: 'Review', variant: 'outline' as const }],
162
- },
163
- ]
164
- : [],
165
- })),
166
- className: 'w-full',
167
- } as any,
168
- };
@@ -1,95 +0,0 @@
1
- import { describe, it, expect, vi, beforeAll, afterAll, afterEach } from 'vitest';
2
- import { render, screen, waitFor } from '@testing-library/react';
3
- import '@testing-library/jest-dom';
4
- import { ObjectKanban } from './ObjectKanban';
5
- import { ObjectStackAdapter } from '@object-ui/data-objectstack';
6
- import { setupServer } from 'msw/node';
7
- import { http, HttpResponse } from 'msw';
8
- import React from 'react';
9
-
10
- // Register layout components (if needed by cards)
11
- // registerLayout();
12
-
13
- const BASE_URL = 'http://localhost';
14
-
15
- // --- Mock Data ---
16
-
17
- const mockTasks = {
18
- value: [
19
- { id: '1', title: 'Task 1', status: 'todo', description: 'Description 1' },
20
- { id: '2', title: 'Task 2', status: 'done', description: 'Description 2' },
21
- { id: '3', title: 'Task 3', status: 'todo', description: 'Description 3' }
22
- ]
23
- };
24
-
25
- // --- MSW Setup ---
26
-
27
- const handlers = [
28
- // OPTIONS handler for CORS preflight checks
29
- http.options('*', () => {
30
- return new HttpResponse(null, { status: 200 });
31
- }),
32
-
33
- // Health check / discovery
34
- http.get(`${BASE_URL}/api/v1`, () => {
35
- return HttpResponse.json({ status: 'ok', version: '1.0.0' });
36
- }),
37
-
38
- http.get(`${BASE_URL}/api/v1/discovery`, () => {
39
- return HttpResponse.json({ status: 'ok', version: '1.0.0' });
40
- }),
41
-
42
- // Data Query: GET /api/v1/data/tasks
43
- http.get(`${BASE_URL}/api/v1/data/tasks`, () => {
44
- return HttpResponse.json(mockTasks);
45
- })
46
- ];
47
-
48
- const server = setupServer(...handlers);
49
-
50
- // --- Test Suite ---
51
-
52
- describe('ObjectKanban with MSW', () => {
53
- if (!process.env.OBJECTSTACK_API_URL) {
54
- beforeAll(() => server.listen());
55
- afterEach(() => server.resetHandlers());
56
- afterAll(() => server.close());
57
- }
58
-
59
- const dataSource = new ObjectStackAdapter({
60
- baseUrl: BASE_URL,
61
- });
62
-
63
- it('fetches tasks and renders them in columns based on groupBy', async () => {
64
- render(
65
- <ObjectKanban
66
- schema={{
67
- type: 'kanban',
68
- objectName: 'tasks',
69
- groupBy: 'status',
70
- columns: [
71
- { id: 'todo', title: 'To Do', cards: [] },
72
- { id: 'done', title: 'Done', cards: [] }
73
- ]
74
- }}
75
- dataSource={dataSource}
76
- />
77
- );
78
-
79
- // Initial state might show Skeleton, wait for data
80
- await waitFor(() => {
81
- expect(screen.getByText('Task 1')).toBeInTheDocument();
82
- }, { timeout: 10000 });
83
-
84
- // Check classification
85
- // Task 1 (todo) and Task 3 (todo) should be in To Do column.
86
- // Task 2 (done) should be in Done column.
87
-
88
- // We can verify "Task 1" is present.
89
- expect(screen.getByText('Task 2')).toBeInTheDocument();
90
- expect(screen.getByText('Task 3')).toBeInTheDocument();
91
-
92
- // Check descriptions
93
- expect(screen.getByText('Description 1')).toBeInTheDocument();
94
- });
95
- });
@@ -1,152 +0,0 @@
1
- import type { Meta, StoryObj } from '@storybook/react';
2
- import { SchemaRenderer } from '@object-ui/react';
3
- import type { BaseSchema } from '@object-ui/types';
4
-
5
- const meta = {
6
- title: 'Plugins/ObjectKanban',
7
- component: SchemaRenderer,
8
- parameters: {
9
- layout: 'padded',
10
- },
11
- tags: ['autodocs'],
12
- argTypes: {
13
- schema: { table: { disable: true } },
14
- },
15
- } satisfies Meta<any>;
16
-
17
- export default meta;
18
- type Story = StoryObj<typeof meta>;
19
-
20
- const renderStory = (args: any) => <SchemaRenderer schema={args as unknown as BaseSchema} />;
21
-
22
- export const Default: Story = {
23
- render: renderStory,
24
- args: {
25
- type: 'kanban',
26
- columns: [
27
- {
28
- id: 'todo',
29
- title: 'To Do',
30
- cards: [
31
- {
32
- id: 'card-1',
33
- title: 'Design homepage',
34
- description: 'Create wireframes and mockups',
35
- badges: [{ label: 'Design', variant: 'default' }],
36
- },
37
- {
38
- id: 'card-2',
39
- title: 'Setup CI pipeline',
40
- description: 'Configure GitHub Actions',
41
- badges: [{ label: 'DevOps', variant: 'secondary' }],
42
- },
43
- ],
44
- },
45
- {
46
- id: 'in-progress',
47
- title: 'In Progress',
48
- limit: 3,
49
- cards: [
50
- {
51
- id: 'card-3',
52
- title: 'Implement auth',
53
- description: 'JWT-based authentication flow',
54
- badges: [
55
- { label: 'Feature', variant: 'default' },
56
- { label: 'High Priority', variant: 'destructive' },
57
- ],
58
- },
59
- ],
60
- },
61
- {
62
- id: 'done',
63
- title: 'Done',
64
- cards: [
65
- {
66
- id: 'card-4',
67
- title: 'Project scaffolding',
68
- description: 'Initial project setup completed',
69
- badges: [{ label: 'Completed', variant: 'outline' }],
70
- },
71
- ],
72
- },
73
- ],
74
- className: 'w-full',
75
- } as any,
76
- };
77
-
78
- export const SprintBoard: Story = {
79
- render: renderStory,
80
- args: {
81
- type: 'kanban',
82
- columns: [
83
- {
84
- id: 'backlog',
85
- title: 'Backlog',
86
- cards: [
87
- { id: 's-1', title: 'Refactor API layer', description: 'Improve error handling', badges: [{ label: 'Tech Debt', variant: 'secondary' }] },
88
- { id: 's-2', title: 'Add unit tests', description: 'Increase coverage to 80%', badges: [{ label: 'Testing', variant: 'default' }] },
89
- ],
90
- },
91
- {
92
- id: 'in-progress',
93
- title: 'In Progress',
94
- limit: 2,
95
- cards: [
96
- { id: 's-3', title: 'User dashboard', description: 'Build analytics dashboard', badges: [{ label: 'Feature', variant: 'default' }, { label: 'P1', variant: 'destructive' }] },
97
- ],
98
- },
99
- {
100
- id: 'review',
101
- title: 'In Review',
102
- cards: [
103
- { id: 's-4', title: 'Search functionality', description: 'Full-text search implementation', badges: [{ label: 'Feature', variant: 'default' }] },
104
- ],
105
- },
106
- {
107
- id: 'done',
108
- title: 'Done',
109
- cards: [
110
- { id: 's-5', title: 'Login page', badges: [{ label: 'Done', variant: 'outline' }] },
111
- { id: 's-6', title: 'Database schema', badges: [{ label: 'Done', variant: 'outline' }] },
112
- ],
113
- },
114
- ],
115
- className: 'w-full',
116
- } as any,
117
- };
118
-
119
- export const WithColumnLimits: Story = {
120
- render: renderStory,
121
- args: {
122
- type: 'kanban',
123
- columns: [
124
- {
125
- id: 'todo',
126
- title: 'To Do',
127
- cards: [
128
- { id: 'l-1', title: 'Task A', badges: [{ label: 'P1', variant: 'destructive' }] },
129
- { id: 'l-2', title: 'Task B', badges: [{ label: 'P2', variant: 'default' }] },
130
- ],
131
- },
132
- {
133
- id: 'wip',
134
- title: 'WIP (Over Limit)',
135
- limit: 2,
136
- cards: [
137
- { id: 'l-3', title: 'Task C', description: 'Almost done' },
138
- { id: 'l-4', title: 'Task D', description: 'In review' },
139
- { id: 'l-5', title: 'Task E', description: 'Blocked', badges: [{ label: 'Blocked', variant: 'destructive' }] },
140
- ],
141
- },
142
- {
143
- id: 'done',
144
- title: 'Done',
145
- cards: [
146
- { id: 'l-6', title: 'Task F', badges: [{ label: 'Completed', variant: 'outline' }] },
147
- ],
148
- },
149
- ],
150
- className: 'w-full',
151
- } as any,
152
- };
@@ -1,276 +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 React, { useEffect, useState, useMemo } from 'react';
10
- import type { DataSource } from '@object-ui/types';
11
- import { useDataScope, useNavigationOverlay } from '@object-ui/react';
12
- import { NavigationOverlay } from '@object-ui/components';
13
- import { extractRecords, buildExpandFields } from '@object-ui/core';
14
- import { KanbanRenderer } from './index';
15
- import { KanbanSchema } from './types';
16
-
17
- export interface ObjectKanbanProps {
18
- schema: KanbanSchema;
19
- dataSource?: DataSource;
20
- className?: string; // Allow override
21
- /** Pre-fetched records passed by a parent (e.g. ListView). When provided, skips internal data fetching. */
22
- data?: any[];
23
- /** Loading state propagated from a parent. Respected only when `data` is also provided. */
24
- loading?: boolean;
25
- onRowClick?: (record: any) => void;
26
- onCardClick?: (record: any) => void;
27
- }
28
-
29
- export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
30
- schema,
31
- dataSource,
32
- className,
33
- data: externalData,
34
- loading: externalLoading,
35
- onRowClick,
36
- onCardClick,
37
- ...props
38
- }) => {
39
- // When a parent (e.g. ListView) pre-fetches data and passes it via the `data` prop,
40
- // we must not trigger a second fetch. Detect external data by checking if externalData
41
- // is an array (undefined when not provided by parent).
42
- const hasExternalData = Array.isArray(externalData);
43
-
44
- const [fetchedData, setFetchedData] = useState<any[]>([]);
45
- const [objectDef, setObjectDef] = useState<any>(null);
46
- // loading state
47
- const [loading, setLoading] = useState(hasExternalData ? (externalLoading ?? false) : false);
48
- const [error, setError] = useState<Error | null>(null);
49
- const [refreshKey, setRefreshKey] = useState(0);
50
-
51
- // Resolve bound data if 'bind' property exists
52
- const boundData = useDataScope(schema.bind);
53
-
54
- // P2: Auto-subscribe to DataSource mutation events (standalone mode only).
55
- // When rendered as a child of ListView, data is managed externally and this is skipped.
56
- useEffect(() => {
57
- if (hasExternalData) return; // Parent handles refresh
58
- if (!dataSource?.onMutation || !schema.objectName) return;
59
- const unsub = dataSource.onMutation((event: any) => {
60
- if (event.resource === schema.objectName) {
61
- setRefreshKey(k => k + 1);
62
- }
63
- });
64
- return unsub;
65
- }, [dataSource, schema.objectName, hasExternalData]);
66
-
67
- // Sync external data changes from parent (e.g. ListView re-fetches after filter change)
68
- useEffect(() => {
69
- if (hasExternalData && externalLoading !== undefined) {
70
- setLoading(externalLoading);
71
- }
72
- }, [externalLoading, hasExternalData]);
73
-
74
- // Fetch object definition for metadata (labels, options)
75
- useEffect(() => {
76
- let isMounted = true;
77
- const fetchMeta = async () => {
78
- if (!dataSource || !schema.objectName) return;
79
- try {
80
- const def = await dataSource.getObjectSchema(schema.objectName);
81
- if (isMounted) setObjectDef(def);
82
- } catch (e) {
83
- console.warn("Failed to fetch object def", e);
84
- }
85
- };
86
- fetchMeta();
87
- return () => { isMounted = false; };
88
- }, [schema.objectName, dataSource]);
89
-
90
- useEffect(() => {
91
- // Skip internal fetch when data is managed by a parent component
92
- if (hasExternalData) return;
93
-
94
- let isMounted = true;
95
- const fetchData = async () => {
96
- if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
97
- if (isMounted) setLoading(true);
98
- try {
99
- // Auto-inject $expand for lookup/master_detail fields
100
- const expand = buildExpandFields(objectDef?.fields);
101
- const results = await dataSource.find(schema.objectName, {
102
- options: { $top: 100 },
103
- $filter: schema.filter,
104
- ...(expand.length > 0 ? { $expand: expand } : {}),
105
- });
106
-
107
- // Handle { value: [] } OData shape or { data: [] } shape or direct array
108
- const data = extractRecords(results);
109
-
110
- if (isMounted) {
111
- setFetchedData(data);
112
- }
113
- } catch (e) {
114
- console.error('[ObjectKanban] Fetch error:', e);
115
- if (isMounted) setError(e as Error);
116
- } finally {
117
- if (isMounted) setLoading(false);
118
- }
119
- };
120
-
121
- // Trigger fetch if we have an objectName AND verify no inline/bound data overrides it
122
- if (schema.objectName && !boundData && !schema.data) {
123
- fetchData();
124
- }
125
- return () => { isMounted = false; };
126
- }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, hasExternalData, objectDef, refreshKey]);
127
-
128
- // Determine which data to use: external -> bound -> inline -> fetched
129
- const rawData = (hasExternalData ? externalData : undefined) || boundData || schema.data || fetchedData;
130
-
131
- // Enhance data with title mapping and ensure IDs
132
- const effectiveData = useMemo(() => {
133
- if (!Array.isArray(rawData)) return [];
134
-
135
- // Support cardTitle property from schema (passed by ObjectView)
136
- // Fallback to legacy titleField for backwards compatibility
137
- let titleField = schema.cardTitle || (schema as any).titleField;
138
-
139
- // Fallback: Try to infer from object definition
140
- if (!titleField && objectDef) {
141
- // 1. Check for titleFormat like "{subject}" first (Higher priority for Cards)
142
- if (objectDef.titleFormat) {
143
- const match = /\{(.+?)\}/.exec(objectDef.titleFormat);
144
- if (match) titleField = match[1];
145
- }
146
- // 2. Check for standard NAME_FIELD_KEY
147
- if (!titleField && objectDef.NAME_FIELD_KEY) {
148
- titleField = objectDef.NAME_FIELD_KEY;
149
- }
150
- }
151
-
152
- // Common title field names to try as fallback
153
- const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name'];
154
-
155
- return rawData.map(item => {
156
- // If a specific title field was configured, try it first
157
- let resolvedTitle = titleField ? item[titleField] : undefined;
158
-
159
- // Fallback: try common field names
160
- if (!resolvedTitle) {
161
- for (const field of TITLE_FALLBACK_FIELDS) {
162
- if (item[field]) {
163
- resolvedTitle = item[field];
164
- break;
165
- }
166
- }
167
- }
168
-
169
- return {
170
- ...item,
171
- // Ensure id exists
172
- id: item.id || item._id,
173
- // Map title
174
- title: resolvedTitle || 'Untitled',
175
- };
176
- });
177
- }, [rawData, schema, objectDef]);
178
-
179
- // Generate columns if missing but groupBy is present
180
- const effectiveColumns = useMemo(() => {
181
- // If columns exist, returns them (normalized)
182
- if (schema.columns && schema.columns.length > 0) {
183
- // If columns is array of strings, normalize to objects
184
- if (typeof schema.columns[0] === 'string') {
185
- // If grouping is active, assume string columns are meant for data display, not lanes
186
- if (!schema.groupBy) {
187
- return (schema.columns as unknown as string[]).map(val => ({
188
- id: val,
189
- title: val
190
- }));
191
- }
192
- } else {
193
- return schema.columns;
194
- }
195
- }
196
-
197
- // Try to get options from metadata
198
- if (schema.groupBy && objectDef?.fields?.[schema.groupBy]?.options) {
199
- return objectDef.fields[schema.groupBy].options.map((opt: any) => ({
200
- id: opt.value,
201
- title: opt.label
202
- }));
203
- }
204
-
205
- // If no columns, but we have groupBy and data, generate from data
206
- if (schema.groupBy && effectiveData.length > 0) {
207
- const groups = new Set(effectiveData.map(item => item[schema.groupBy!]));
208
- return Array.from(groups).map(g => ({
209
- id: String(g),
210
- title: String(g)
211
- }));
212
- }
213
-
214
- return [];
215
- }, [schema.columns, schema.groupBy, effectiveData, objectDef]);
216
-
217
- // Clone schema to inject data and className
218
- // Use grouping.fields[0].field as swimlaneField fallback when no explicit swimlaneField
219
- const effectiveSwimlaneField = schema.swimlaneField
220
- || (schema.grouping?.fields?.[0]?.field);
221
-
222
- const effectiveSchema = {
223
- ...schema,
224
- data: effectiveData,
225
- columns: effectiveColumns,
226
- className: className || schema.className,
227
- ...(effectiveSwimlaneField ? { swimlaneField: effectiveSwimlaneField } : {}),
228
- };
229
-
230
- const navigation = useNavigationOverlay({
231
- navigation: (schema as any).navigation,
232
- objectName: schema.objectName,
233
- onRowClick: onRowClick ?? onCardClick,
234
- });
235
-
236
- if (error) {
237
- return (
238
- <div className="p-4 border border-destructive/50 rounded bg-destructive/10 text-destructive">
239
- Error loading kanban data: {error.message}
240
- </div>
241
- );
242
- }
243
-
244
- // Pass through to the renderer
245
- const detailTitle = schema.objectName
246
- ? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1).replace(/_/g, ' ')} Detail`
247
- : 'Card Details';
248
-
249
- return (
250
- <>
251
- <KanbanRenderer schema={{
252
- ...effectiveSchema,
253
- onCardClick: (card: any) => {
254
- navigation.handleClick(card);
255
- onCardClick?.(card);
256
- },
257
- }} />
258
- {navigation.isOverlay && (
259
- <NavigationOverlay {...navigation} title={detailTitle}>
260
- {(record) => (
261
- <div className="space-y-3">
262
- {Object.entries(record).map(([key, value]) => (
263
- <div key={key} className="flex flex-col">
264
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
265
- {key.replace(/_/g, ' ')}
266
- </span>
267
- <span className="text-sm">{String(value ?? '—')}</span>
268
- </div>
269
- ))}
270
- </div>
271
- )}
272
- </NavigationOverlay>
273
- )}
274
- </>
275
- );
276
- }