@object-ui/plugin-kanban 3.1.5 → 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.
Files changed (77) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +24 -0
  3. package/dist/{KanbanEnhanced-CvxO2soF.js → KanbanEnhanced-Do9ZB1Mh.js} +36 -33
  4. package/dist/{KanbanImpl-ii52_k8g.js → KanbanImpl-BdocXM5T.js} +2 -2
  5. package/dist/{chevron-down-DpXJN6OX.js → chevron-down-C0JUlGjk.js} +1 -1
  6. package/dist/index.d.ts +1 -1
  7. package/dist/index.js +35 -26
  8. package/dist/index.umd.cjs +4 -4
  9. package/dist/packages/plugin-kanban/src/CardTemplates.d.ts.map +1 -0
  10. package/dist/packages/plugin-kanban/src/InlineQuickAdd.d.ts.map +1 -0
  11. package/dist/packages/plugin-kanban/src/KanbanEnhanced.d.ts.map +1 -0
  12. package/dist/packages/plugin-kanban/src/KanbanImpl.d.ts.map +1 -0
  13. package/dist/packages/plugin-kanban/src/ObjectKanban.EdgeCases.stories.d.ts.map +1 -0
  14. package/dist/packages/plugin-kanban/src/ObjectKanban.d.ts.map +1 -0
  15. package/dist/packages/plugin-kanban/src/ObjectKanban.stories.d.ts.map +1 -0
  16. package/dist/packages/plugin-kanban/src/index.d.ts.map +1 -0
  17. package/dist/packages/plugin-kanban/src/types.d.ts.map +1 -0
  18. package/dist/packages/plugin-kanban/src/useColumnWidths.d.ts.map +1 -0
  19. package/dist/packages/plugin-kanban/src/useCrossSwimlaneMove.d.ts.map +1 -0
  20. package/dist/packages/plugin-kanban/src/useQuickAddReorder.d.ts.map +1 -0
  21. package/dist/{plus-CAtTu4zt.js → plus-CHsXVJSY.js} +39 -36
  22. package/dist/{sortable.esm-DzUCoMzQ.js → sortable.esm-LJG1TjKd.js} +4 -4
  23. package/package.json +35 -12
  24. package/.turbo/turbo-build.log +0 -32
  25. package/dist/src/CardTemplates.d.ts.map +0 -1
  26. package/dist/src/InlineQuickAdd.d.ts.map +0 -1
  27. package/dist/src/KanbanEnhanced.d.ts.map +0 -1
  28. package/dist/src/KanbanImpl.d.ts.map +0 -1
  29. package/dist/src/ObjectKanban.EdgeCases.stories.d.ts.map +0 -1
  30. package/dist/src/ObjectKanban.d.ts.map +0 -1
  31. package/dist/src/ObjectKanban.stories.d.ts.map +0 -1
  32. package/dist/src/index.d.ts.map +0 -1
  33. package/dist/src/types.d.ts.map +0 -1
  34. package/dist/src/useColumnWidths.d.ts.map +0 -1
  35. package/dist/src/useCrossSwimlaneMove.d.ts.map +0 -1
  36. package/dist/src/useQuickAddReorder.d.ts.map +0 -1
  37. package/src/CardTemplates.tsx +0 -123
  38. package/src/InlineQuickAdd.tsx +0 -189
  39. package/src/KanbanEnhanced.tsx +0 -525
  40. package/src/KanbanImpl.tsx +0 -597
  41. package/src/ObjectKanban.EdgeCases.stories.tsx +0 -168
  42. package/src/ObjectKanban.msw.test.tsx +0 -91
  43. package/src/ObjectKanban.stories.tsx +0 -152
  44. package/src/ObjectKanban.tsx +0 -262
  45. package/src/__tests__/KanbanEnhanced.test.tsx +0 -260
  46. package/src/__tests__/KanbanGrouping.test.tsx +0 -164
  47. package/src/__tests__/KanbanSwimlanes.test.tsx +0 -194
  48. package/src/__tests__/ObjectKanbanTitle.test.tsx +0 -93
  49. package/src/__tests__/SwimlanePersistence.test.tsx +0 -159
  50. package/src/__tests__/accessibility.test.tsx +0 -296
  51. package/src/__tests__/dnd-undo-integration.test.tsx +0 -525
  52. package/src/__tests__/performance-benchmark.test.tsx +0 -306
  53. package/src/__tests__/phase13-features.test.tsx +0 -387
  54. package/src/__tests__/view-states.test.tsx +0 -403
  55. package/src/index.test.ts +0 -112
  56. package/src/index.tsx +0 -327
  57. package/src/registration.test.tsx +0 -26
  58. package/src/types.ts +0 -185
  59. package/src/useColumnWidths.ts +0 -125
  60. package/src/useCrossSwimlaneMove.ts +0 -116
  61. package/src/useQuickAddReorder.ts +0 -107
  62. package/tsconfig.json +0 -19
  63. package/vite.config.ts +0 -61
  64. package/vitest.config.ts +0 -12
  65. package/vitest.setup.ts +0 -1
  66. /package/dist/{src → packages/plugin-kanban/src}/CardTemplates.d.ts +0 -0
  67. /package/dist/{src → packages/plugin-kanban/src}/InlineQuickAdd.d.ts +0 -0
  68. /package/dist/{src → packages/plugin-kanban/src}/KanbanEnhanced.d.ts +0 -0
  69. /package/dist/{src → packages/plugin-kanban/src}/KanbanImpl.d.ts +0 -0
  70. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.EdgeCases.stories.d.ts +0 -0
  71. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.d.ts +0 -0
  72. /package/dist/{src → packages/plugin-kanban/src}/ObjectKanban.stories.d.ts +0 -0
  73. /package/dist/{src → packages/plugin-kanban/src}/index.d.ts +0 -0
  74. /package/dist/{src → packages/plugin-kanban/src}/types.d.ts +0 -0
  75. /package/dist/{src → packages/plugin-kanban/src}/useColumnWidths.d.ts +0 -0
  76. /package/dist/{src → packages/plugin-kanban/src}/useCrossSwimlaneMove.d.ts +0 -0
  77. /package/dist/{src → packages/plugin-kanban/src}/useQuickAddReorder.d.ts +0 -0
@@ -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,91 +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
34
- http.get(`${BASE_URL}/api/v1`, () => {
35
- return HttpResponse.json({ status: 'ok', version: '1.0.0' });
36
- }),
37
-
38
- // Data Query: GET /api/v1/data/tasks
39
- http.get(`${BASE_URL}/api/v1/data/tasks`, () => {
40
- return HttpResponse.json(mockTasks);
41
- })
42
- ];
43
-
44
- const server = setupServer(...handlers);
45
-
46
- // --- Test Suite ---
47
-
48
- describe('ObjectKanban with MSW', () => {
49
- if (!process.env.OBJECTSTACK_API_URL) {
50
- beforeAll(() => server.listen());
51
- afterEach(() => server.resetHandlers());
52
- afterAll(() => server.close());
53
- }
54
-
55
- const dataSource = new ObjectStackAdapter({
56
- baseUrl: BASE_URL,
57
- });
58
-
59
- it('fetches tasks and renders them in columns based on groupBy', async () => {
60
- render(
61
- <ObjectKanban
62
- schema={{
63
- type: 'kanban',
64
- objectName: 'tasks',
65
- groupBy: 'status',
66
- columns: [
67
- { id: 'todo', title: 'To Do', cards: [] },
68
- { id: 'done', title: 'Done', cards: [] }
69
- ]
70
- }}
71
- dataSource={dataSource}
72
- />
73
- );
74
-
75
- // Initial state might show Skeleton, wait for data
76
- await waitFor(() => {
77
- expect(screen.getByText('Task 1')).toBeInTheDocument();
78
- }, { timeout: 10000 });
79
-
80
- // Check classification
81
- // Task 1 (todo) and Task 3 (todo) should be in To Do column.
82
- // Task 2 (done) should be in Done column.
83
-
84
- // We can verify "Task 1" is present.
85
- expect(screen.getByText('Task 2')).toBeInTheDocument();
86
- expect(screen.getByText('Task 3')).toBeInTheDocument();
87
-
88
- // Check descriptions
89
- expect(screen.getByText('Description 1')).toBeInTheDocument();
90
- });
91
- });
@@ -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,262 +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
-
50
- // Resolve bound data if 'bind' property exists
51
- const boundData = useDataScope(schema.bind);
52
-
53
- // Sync external data changes from parent (e.g. ListView re-fetches after filter change)
54
- useEffect(() => {
55
- if (hasExternalData && externalLoading !== undefined) {
56
- setLoading(externalLoading);
57
- }
58
- }, [externalLoading, hasExternalData]);
59
-
60
- // Fetch object definition for metadata (labels, options)
61
- useEffect(() => {
62
- let isMounted = true;
63
- const fetchMeta = async () => {
64
- if (!dataSource || !schema.objectName) return;
65
- try {
66
- const def = await dataSource.getObjectSchema(schema.objectName);
67
- if (isMounted) setObjectDef(def);
68
- } catch (e) {
69
- console.warn("Failed to fetch object def", e);
70
- }
71
- };
72
- fetchMeta();
73
- return () => { isMounted = false; };
74
- }, [schema.objectName, dataSource]);
75
-
76
- useEffect(() => {
77
- // Skip internal fetch when data is managed by a parent component
78
- if (hasExternalData) return;
79
-
80
- let isMounted = true;
81
- const fetchData = async () => {
82
- if (!dataSource || typeof dataSource.find !== 'function' || !schema.objectName) return;
83
- if (isMounted) setLoading(true);
84
- try {
85
- // Auto-inject $expand for lookup/master_detail fields
86
- const expand = buildExpandFields(objectDef?.fields);
87
- const results = await dataSource.find(schema.objectName, {
88
- options: { $top: 100 },
89
- $filter: schema.filter,
90
- ...(expand.length > 0 ? { $expand: expand } : {}),
91
- });
92
-
93
- // Handle { value: [] } OData shape or { data: [] } shape or direct array
94
- const data = extractRecords(results);
95
-
96
- if (isMounted) {
97
- setFetchedData(data);
98
- }
99
- } catch (e) {
100
- console.error('[ObjectKanban] Fetch error:', e);
101
- if (isMounted) setError(e as Error);
102
- } finally {
103
- if (isMounted) setLoading(false);
104
- }
105
- };
106
-
107
- // Trigger fetch if we have an objectName AND verify no inline/bound data overrides it
108
- if (schema.objectName && !boundData && !schema.data) {
109
- fetchData();
110
- }
111
- return () => { isMounted = false; };
112
- }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, hasExternalData, objectDef]);
113
-
114
- // Determine which data to use: external -> bound -> inline -> fetched
115
- const rawData = (hasExternalData ? externalData : undefined) || boundData || schema.data || fetchedData;
116
-
117
- // Enhance data with title mapping and ensure IDs
118
- const effectiveData = useMemo(() => {
119
- if (!Array.isArray(rawData)) return [];
120
-
121
- // Support cardTitle property from schema (passed by ObjectView)
122
- // Fallback to legacy titleField for backwards compatibility
123
- let titleField = schema.cardTitle || (schema as any).titleField;
124
-
125
- // Fallback: Try to infer from object definition
126
- if (!titleField && objectDef) {
127
- // 1. Check for titleFormat like "{subject}" first (Higher priority for Cards)
128
- if (objectDef.titleFormat) {
129
- const match = /\{(.+?)\}/.exec(objectDef.titleFormat);
130
- if (match) titleField = match[1];
131
- }
132
- // 2. Check for standard NAME_FIELD_KEY
133
- if (!titleField && objectDef.NAME_FIELD_KEY) {
134
- titleField = objectDef.NAME_FIELD_KEY;
135
- }
136
- }
137
-
138
- // Common title field names to try as fallback
139
- const TITLE_FALLBACK_FIELDS = ['name', 'title', 'subject', 'label', 'display_name'];
140
-
141
- return rawData.map(item => {
142
- // If a specific title field was configured, try it first
143
- let resolvedTitle = titleField ? item[titleField] : undefined;
144
-
145
- // Fallback: try common field names
146
- if (!resolvedTitle) {
147
- for (const field of TITLE_FALLBACK_FIELDS) {
148
- if (item[field]) {
149
- resolvedTitle = item[field];
150
- break;
151
- }
152
- }
153
- }
154
-
155
- return {
156
- ...item,
157
- // Ensure id exists
158
- id: item.id || item._id,
159
- // Map title
160
- title: resolvedTitle || 'Untitled',
161
- };
162
- });
163
- }, [rawData, schema, objectDef]);
164
-
165
- // Generate columns if missing but groupBy is present
166
- const effectiveColumns = useMemo(() => {
167
- // If columns exist, returns them (normalized)
168
- if (schema.columns && schema.columns.length > 0) {
169
- // If columns is array of strings, normalize to objects
170
- if (typeof schema.columns[0] === 'string') {
171
- // If grouping is active, assume string columns are meant for data display, not lanes
172
- if (!schema.groupBy) {
173
- return (schema.columns as unknown as string[]).map(val => ({
174
- id: val,
175
- title: val
176
- }));
177
- }
178
- } else {
179
- return schema.columns;
180
- }
181
- }
182
-
183
- // Try to get options from metadata
184
- if (schema.groupBy && objectDef?.fields?.[schema.groupBy]?.options) {
185
- return objectDef.fields[schema.groupBy].options.map((opt: any) => ({
186
- id: opt.value,
187
- title: opt.label
188
- }));
189
- }
190
-
191
- // If no columns, but we have groupBy and data, generate from data
192
- if (schema.groupBy && effectiveData.length > 0) {
193
- const groups = new Set(effectiveData.map(item => item[schema.groupBy!]));
194
- return Array.from(groups).map(g => ({
195
- id: String(g),
196
- title: String(g)
197
- }));
198
- }
199
-
200
- return [];
201
- }, [schema.columns, schema.groupBy, effectiveData, objectDef]);
202
-
203
- // Clone schema to inject data and className
204
- // Use grouping.fields[0].field as swimlaneField fallback when no explicit swimlaneField
205
- const effectiveSwimlaneField = schema.swimlaneField
206
- || (schema.grouping?.fields?.[0]?.field);
207
-
208
- const effectiveSchema = {
209
- ...schema,
210
- data: effectiveData,
211
- columns: effectiveColumns,
212
- className: className || schema.className,
213
- ...(effectiveSwimlaneField ? { swimlaneField: effectiveSwimlaneField } : {}),
214
- };
215
-
216
- const navigation = useNavigationOverlay({
217
- navigation: (schema as any).navigation,
218
- objectName: schema.objectName,
219
- onRowClick: onRowClick ?? onCardClick,
220
- });
221
-
222
- if (error) {
223
- return (
224
- <div className="p-4 border border-destructive/50 rounded bg-destructive/10 text-destructive">
225
- Error loading kanban data: {error.message}
226
- </div>
227
- );
228
- }
229
-
230
- // Pass through to the renderer
231
- const detailTitle = schema.objectName
232
- ? `${schema.objectName.charAt(0).toUpperCase() + schema.objectName.slice(1).replace(/_/g, ' ')} Detail`
233
- : 'Card Details';
234
-
235
- return (
236
- <>
237
- <KanbanRenderer schema={{
238
- ...effectiveSchema,
239
- onCardClick: (card: any) => {
240
- navigation.handleClick(card);
241
- onCardClick?.(card);
242
- },
243
- }} />
244
- {navigation.isOverlay && (
245
- <NavigationOverlay {...navigation} title={detailTitle}>
246
- {(record) => (
247
- <div className="space-y-3">
248
- {Object.entries(record).map(([key, value]) => (
249
- <div key={key} className="flex flex-col">
250
- <span className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
251
- {key.replace(/_/g, ' ')}
252
- </span>
253
- <span className="text-sm">{String(value ?? '—')}</span>
254
- </div>
255
- ))}
256
- </div>
257
- )}
258
- </NavigationOverlay>
259
- )}
260
- </>
261
- );
262
- }