@object-ui/plugin-kanban 0.3.1 → 2.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.
@@ -0,0 +1,188 @@
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 } from '@object-ui/react';
12
+ import { KanbanRenderer } from './index';
13
+ import { KanbanSchema } from './types';
14
+
15
+ export interface ObjectKanbanProps {
16
+ schema: KanbanSchema;
17
+ dataSource?: DataSource;
18
+ className?: string; // Allow override
19
+ }
20
+
21
+ export const ObjectKanban: React.FC<ObjectKanbanProps> = ({
22
+ schema,
23
+ dataSource,
24
+ className,
25
+ ...props
26
+ }) => {
27
+ const [fetchedData, setFetchedData] = useState<any[]>([]);
28
+ const [objectDef, setObjectDef] = useState<any>(null);
29
+ // loading state
30
+ const [loading, setLoading] = useState(false);
31
+ const [error, setError] = useState<Error | null>(null);
32
+
33
+ // Resolve bound data if 'bind' property exists
34
+ const boundData = useDataScope(schema.bind);
35
+
36
+ // Fetch object definition for metadata (labels, options)
37
+ useEffect(() => {
38
+ let isMounted = true;
39
+ const fetchMeta = async () => {
40
+ if (!dataSource || !schema.objectName) return;
41
+ try {
42
+ const def = await dataSource.getObjectSchema(schema.objectName);
43
+ if (isMounted) setObjectDef(def);
44
+ } catch (e) {
45
+ console.warn("Failed to fetch object def", e);
46
+ }
47
+ };
48
+ fetchMeta();
49
+ return () => { isMounted = false; };
50
+ }, [schema.objectName, dataSource]);
51
+
52
+ useEffect(() => {
53
+ let isMounted = true;
54
+ const fetchData = async () => {
55
+ if (!dataSource || !schema.objectName) return;
56
+ if (isMounted) setLoading(true);
57
+ try {
58
+ const results = await dataSource.find(schema.objectName, {
59
+ options: { $top: 100 },
60
+ $filter: schema.filter
61
+ });
62
+
63
+ // Handle { value: [] } OData shape or { data: [] } shape or direct array
64
+ let data: any[] = [];
65
+ if (Array.isArray(results)) {
66
+ data = results;
67
+ } else if (results && typeof results === 'object') {
68
+ if (Array.isArray((results as any).value)) {
69
+ data = (results as any).value;
70
+ } else if (Array.isArray((results as any).data)) {
71
+ data = (results as any).data;
72
+ }
73
+ }
74
+
75
+ console.log(`[ObjectKanban] Extracted data (length: ${data.length})`);
76
+
77
+ if (isMounted) {
78
+ setFetchedData(data);
79
+ }
80
+ } catch (e) {
81
+ console.error('[ObjectKanban] Fetch error:', e);
82
+ if (isMounted) setError(e as Error);
83
+ } finally {
84
+ if (isMounted) setLoading(false);
85
+ }
86
+ };
87
+
88
+ // Trigger fetch if we have an objectName AND verify no inline/bound data overrides it
89
+ // And NO props.data passed from ListView
90
+ if (schema.objectName && !boundData && !schema.data && !(props as any).data) {
91
+ fetchData();
92
+ }
93
+ return () => { isMounted = false; };
94
+ }, [schema.objectName, dataSource, boundData, schema.data, schema.filter, (props as any).data]);
95
+
96
+ // Determine which data to use: props.data -> bound -> inline -> fetched
97
+ const rawData = (props as any).data || boundData || schema.data || fetchedData;
98
+
99
+ // Enhance data with title mapping and ensure IDs
100
+ const effectiveData = useMemo(() => {
101
+ if (!Array.isArray(rawData)) return [];
102
+
103
+ // Support cardTitle property from schema (passed by ObjectView)
104
+ // Fallback to legacy titleField for backwards compatibility
105
+ let titleField = schema.cardTitle || (schema as any).titleField;
106
+
107
+ // Fallback: Try to infer from object definition
108
+ if (!titleField && objectDef) {
109
+ // 1. Check for titleFormat like "{subject}" first (Higher priority for Cards)
110
+ if (objectDef.titleFormat) {
111
+ const match = /\{(.+?)\}/.exec(objectDef.titleFormat);
112
+ if (match) titleField = match[1];
113
+ }
114
+ // 2. Check for standard NAME_FIELD_KEY
115
+ if (!titleField && objectDef.NAME_FIELD_KEY) {
116
+ titleField = objectDef.NAME_FIELD_KEY;
117
+ }
118
+ }
119
+
120
+ // Default to 'name'
121
+ const finalTitleField = titleField || 'name';
122
+
123
+ return rawData.map(item => ({
124
+ ...item,
125
+ // Ensure id exists
126
+ id: item.id || item._id,
127
+ // Map title
128
+ title: item[finalTitleField] || item.title || 'Untitled',
129
+ }));
130
+ }, [rawData, schema, objectDef]);
131
+
132
+ // Generate columns if missing but groupBy is present
133
+ const effectiveColumns = useMemo(() => {
134
+ // If columns exist, returns them (normalized)
135
+ if (schema.columns && schema.columns.length > 0) {
136
+ // If columns is array of strings, normalize to objects
137
+ if (typeof schema.columns[0] === 'string') {
138
+ // If grouping is active, assume string columns are meant for data display, not lanes
139
+ if (!schema.groupBy) {
140
+ return (schema.columns as unknown as string[]).map(val => ({
141
+ id: val,
142
+ title: val
143
+ }));
144
+ }
145
+ } else {
146
+ return schema.columns;
147
+ }
148
+ }
149
+
150
+ // Try to get options from metadata
151
+ if (schema.groupBy && objectDef?.fields?.[schema.groupBy]?.options) {
152
+ return objectDef.fields[schema.groupBy].options.map((opt: any) => ({
153
+ id: opt.value,
154
+ title: opt.label
155
+ }));
156
+ }
157
+
158
+ // If no columns, but we have groupBy and data, generate from data
159
+ if (schema.groupBy && effectiveData.length > 0) {
160
+ const groups = new Set(effectiveData.map(item => item[schema.groupBy!]));
161
+ return Array.from(groups).map(g => ({
162
+ id: String(g),
163
+ title: String(g)
164
+ }));
165
+ }
166
+
167
+ return [];
168
+ }, [schema.columns, schema.groupBy, effectiveData, objectDef]);
169
+
170
+ // Clone schema to inject data and className
171
+ const effectiveSchema = {
172
+ ...schema,
173
+ data: effectiveData,
174
+ columns: effectiveColumns,
175
+ className: className || schema.className
176
+ };
177
+
178
+ if (error) {
179
+ return (
180
+ <div className="p-4 border border-destructive/50 rounded bg-destructive/10 text-destructive">
181
+ Error loading kanban data: {error.message}
182
+ </div>
183
+ );
184
+ }
185
+
186
+ // Pass through to the renderer
187
+ return <KanbanRenderer schema={effectiveSchema} />;
188
+ }
@@ -0,0 +1,259 @@
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 { describe, it, expect, vi } from 'vitest';
10
+ import { render, screen, fireEvent } from '@testing-library/react';
11
+ import { KanbanEnhanced, type KanbanColumn } from '../KanbanEnhanced';
12
+
13
+ // Mock @tanstack/react-virtual
14
+ vi.mock('@tanstack/react-virtual', () => ({
15
+ useVirtualizer: () => ({
16
+ getTotalSize: () => 1000,
17
+ getVirtualItems: () => [],
18
+ measureElement: vi.fn(),
19
+ }),
20
+ }));
21
+
22
+ // Mock @dnd-kit/core and utilities
23
+ vi.mock('@dnd-kit/core', () => ({
24
+ DndContext: ({ children }: any) => <div data-testid="dnd-context">{children}</div>,
25
+ DragOverlay: ({ children }: any) => <div data-testid="drag-overlay">{children}</div>,
26
+ PointerSensor: vi.fn(),
27
+ useSensor: vi.fn(),
28
+ useSensors: () => [],
29
+ closestCorners: vi.fn(),
30
+ }));
31
+
32
+ vi.mock('@dnd-kit/sortable', () => ({
33
+ SortableContext: ({ children }: any) => <div data-testid="sortable-context">{children}</div>,
34
+ useSortable: () => ({
35
+ attributes: {},
36
+ listeners: {},
37
+ setNodeRef: vi.fn(),
38
+ transform: null,
39
+ transition: null,
40
+ isDragging: false,
41
+ }),
42
+ arrayMove: (array: any[], from: number, to: number) => {
43
+ const newArray = [...array];
44
+ newArray.splice(to, 0, newArray.splice(from, 1)[0]);
45
+ return newArray;
46
+ },
47
+ verticalListSortingStrategy: vi.fn(),
48
+ }));
49
+
50
+ vi.mock('@dnd-kit/utilities', () => ({
51
+ CSS: {
52
+ Transform: {
53
+ toString: () => '',
54
+ },
55
+ },
56
+ }));
57
+
58
+ describe('KanbanEnhanced', () => {
59
+ const mockColumns: KanbanColumn[] = [
60
+ {
61
+ id: 'todo',
62
+ title: 'To Do',
63
+ cards: [
64
+ {
65
+ id: 'card-1',
66
+ title: 'Task 1',
67
+ description: 'Description 1',
68
+ badges: [{ label: 'High', variant: 'destructive' }],
69
+ },
70
+ {
71
+ id: 'card-2',
72
+ title: 'Task 2',
73
+ description: 'Description 2',
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ id: 'in-progress',
79
+ title: 'In Progress',
80
+ limit: 3,
81
+ cards: [
82
+ {
83
+ id: 'card-3',
84
+ title: 'Task 3',
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ id: 'done',
90
+ title: 'Done',
91
+ cards: [],
92
+ },
93
+ ];
94
+
95
+ it('should render without crashing', () => {
96
+ const { container } = render(<KanbanEnhanced columns={mockColumns} />);
97
+ expect(container).toBeTruthy();
98
+ });
99
+
100
+ it('should render all columns', () => {
101
+ render(<KanbanEnhanced columns={mockColumns} />);
102
+
103
+ expect(screen.getByText('To Do')).toBeInTheDocument();
104
+ expect(screen.getByText('In Progress')).toBeInTheDocument();
105
+ expect(screen.getByText('Done')).toBeInTheDocument();
106
+ });
107
+
108
+ it('should render all cards', () => {
109
+ render(<KanbanEnhanced columns={mockColumns} />);
110
+
111
+ expect(screen.getByText('Task 1')).toBeInTheDocument();
112
+ expect(screen.getByText('Task 2')).toBeInTheDocument();
113
+ expect(screen.getByText('Task 3')).toBeInTheDocument();
114
+ });
115
+
116
+ it('should display card count for each column', () => {
117
+ render(<KanbanEnhanced columns={mockColumns} />);
118
+
119
+ // Columns should show card counts in the format "count" or "count / limit"
120
+ expect(screen.getByText('To Do')).toBeInTheDocument();
121
+ expect(screen.getByText('In Progress')).toBeInTheDocument();
122
+ expect(screen.getByText('1 / 3')).toBeInTheDocument(); // In Progress has 1 card with limit 3
123
+ });
124
+
125
+ it('should display column limit warning when at 80% capacity', () => {
126
+ const columnsNearLimit: KanbanColumn[] = [
127
+ {
128
+ id: 'limited',
129
+ title: 'Limited Column',
130
+ limit: 5,
131
+ cards: Array(4).fill(null).map((_, i) => ({
132
+ id: `card-${i}`,
133
+ title: `Task ${i}`,
134
+ })),
135
+ },
136
+ ];
137
+
138
+ const { container } = render(<KanbanEnhanced columns={columnsNearLimit} />);
139
+
140
+ // Should show warning indicator (80% of 5 = 4) - AlertTriangle icon with yellow color
141
+ expect(container.querySelector('.text-yellow-500')).toBeTruthy();
142
+ });
143
+
144
+ it('should collapse/expand columns when toggle button is clicked', () => {
145
+ render(<KanbanEnhanced columns={mockColumns} enableCollapse={true} />);
146
+
147
+ // Find collapse toggle button (chevron icons)
148
+ const toggleButtons = screen.getAllByRole('button');
149
+ const collapseButton = toggleButtons.find(btn =>
150
+ btn.querySelector('svg') !== null
151
+ );
152
+
153
+ if (collapseButton) {
154
+ fireEvent.click(collapseButton);
155
+ // After clicking, the column state would change
156
+ // In a real test with proper DOM, we would verify:
157
+ // - Column content is hidden
158
+ // - Icon changes from ChevronDown to ChevronRight
159
+ expect(collapseButton).toBeTruthy();
160
+ }
161
+ });
162
+
163
+ it('should call onCardMove when a card is moved', () => {
164
+ const onCardMove = vi.fn();
165
+ render(<KanbanEnhanced columns={mockColumns} onCardMove={onCardMove} />);
166
+
167
+ // In our mocked environment with mocked dnd-kit,
168
+ // we can't easily simulate the full drag and drop interaction.
169
+ // In a real integration test, this would verify:
170
+ // - Dragging a card from one column to another
171
+ // - onCardMove is called with correct parameters (cardId, fromColumn, toColumn)
172
+ expect(onCardMove).toBeDefined();
173
+
174
+ // Example of what the callback would receive:
175
+ // expect(onCardMove).toHaveBeenCalledWith('card-1', 'todo', 'in-progress');
176
+ });
177
+
178
+ it('should call onColumnToggle when a column is collapsed', () => {
179
+ const onColumnToggle = vi.fn();
180
+ render(
181
+ <KanbanEnhanced
182
+ columns={mockColumns}
183
+ enableCollapse={true}
184
+ onColumnToggle={onColumnToggle}
185
+ />
186
+ );
187
+
188
+ expect(onColumnToggle).toBeDefined();
189
+ });
190
+
191
+ it('should render card badges', () => {
192
+ render(<KanbanEnhanced columns={mockColumns} />);
193
+
194
+ expect(screen.getByText('High')).toBeInTheDocument();
195
+ });
196
+
197
+ it('should render card descriptions', () => {
198
+ render(<KanbanEnhanced columns={mockColumns} />);
199
+
200
+ expect(screen.getByText('Description 1')).toBeInTheDocument();
201
+ expect(screen.getByText('Description 2')).toBeInTheDocument();
202
+ });
203
+
204
+ it('should apply custom className', () => {
205
+ const { container } = render(
206
+ <KanbanEnhanced columns={mockColumns} className="custom-class" />
207
+ );
208
+
209
+ const kanbanContainer = container.querySelector('.custom-class');
210
+ expect(kanbanContainer).toBeTruthy();
211
+ });
212
+
213
+ it('should render empty columns', () => {
214
+ render(<KanbanEnhanced columns={mockColumns} />);
215
+
216
+ // "Done" column has 0 cards
217
+ expect(screen.getByText('Done')).toBeInTheDocument();
218
+ });
219
+
220
+ it('should enable virtual scrolling when specified', () => {
221
+ render(<KanbanEnhanced columns={mockColumns} enableVirtualScrolling={true} />);
222
+
223
+ // Virtual scrolling should be active
224
+ const { container } = render(<KanbanEnhanced columns={mockColumns} enableVirtualScrolling={true} />);
225
+ expect(container).toBeTruthy();
226
+ });
227
+
228
+ it('should render drag overlay when dragging', () => {
229
+ const { container } = render(<KanbanEnhanced columns={mockColumns} />);
230
+
231
+ const dragOverlay = container.querySelector('[data-testid="drag-overlay"]');
232
+ expect(dragOverlay).toBeTruthy();
233
+ });
234
+
235
+ it('should show error state when column is over limit', () => {
236
+ const columnsOverLimit: KanbanColumn[] = [
237
+ {
238
+ id: 'limited',
239
+ title: 'Over Limit',
240
+ limit: 3,
241
+ cards: Array(4).fill(null).map((_, i) => ({
242
+ id: `card-${i}`,
243
+ title: `Task ${i}`,
244
+ })),
245
+ },
246
+ ];
247
+
248
+ render(<KanbanEnhanced columns={columnsOverLimit} />);
249
+
250
+ // Should show error indicator (over 100% of limit)
251
+ const { container } = render(<KanbanEnhanced columns={columnsOverLimit} />);
252
+ expect(container.querySelector('[class*="destructive"]')).toBeTruthy();
253
+ });
254
+
255
+ it('should handle empty columns array', () => {
256
+ const { container } = render(<KanbanEnhanced columns={[]} />);
257
+ expect(container).toBeTruthy();
258
+ });
259
+ });
package/src/index.test.ts CHANGED
@@ -13,16 +13,16 @@ describe('Plugin Kanban', () => {
13
13
  // Import all renderers to register them
14
14
  beforeAll(async () => {
15
15
  await import('./index');
16
- });
16
+ }, 15000); // Increase timeout to 15 seconds for async import
17
17
 
18
18
  describe('kanban component', () => {
19
19
  it('should be registered in ComponentRegistry', () => {
20
- const kanbanRenderer = ComponentRegistry.get('kanban');
20
+ const kanbanRenderer = ComponentRegistry.get('kanban-ui');
21
21
  expect(kanbanRenderer).toBeDefined();
22
22
  });
23
23
 
24
24
  it('should have proper metadata', () => {
25
- const config = ComponentRegistry.getConfig('kanban');
25
+ const config = ComponentRegistry.getConfig('kanban-ui');
26
26
  expect(config).toBeDefined();
27
27
  expect(config?.label).toBe('Kanban Board');
28
28
  expect(config?.icon).toBe('LayoutDashboard');
@@ -32,7 +32,7 @@ describe('Plugin Kanban', () => {
32
32
  });
33
33
 
34
34
  it('should have expected inputs', () => {
35
- const config = ComponentRegistry.getConfig('kanban');
35
+ const config = ComponentRegistry.getConfig('kanban-ui');
36
36
  const inputNames = config?.inputs?.map((input: any) => input.name) || [];
37
37
 
38
38
  expect(inputNames).toContain('columns');
@@ -41,7 +41,7 @@ describe('Plugin Kanban', () => {
41
41
  });
42
42
 
43
43
  it('should have columns as required input', () => {
44
- const config = ComponentRegistry.getConfig('kanban');
44
+ const config = ComponentRegistry.getConfig('kanban-ui');
45
45
  const columnsInput = config?.inputs?.find((input: any) => input.name === 'columns');
46
46
 
47
47
  expect(columnsInput).toBeDefined();
@@ -51,7 +51,7 @@ describe('Plugin Kanban', () => {
51
51
  });
52
52
 
53
53
  it('should have onCardMove as code input', () => {
54
- const config = ComponentRegistry.getConfig('kanban');
54
+ const config = ComponentRegistry.getConfig('kanban-ui');
55
55
  const onCardMoveInput = config?.inputs?.find((input: any) => input.name === 'onCardMove');
56
56
 
57
57
  expect(onCardMoveInput).toBeDefined();
@@ -61,7 +61,7 @@ describe('Plugin Kanban', () => {
61
61
  });
62
62
 
63
63
  it('should have sensible default props', () => {
64
- const config = ComponentRegistry.getConfig('kanban');
64
+ const config = ComponentRegistry.getConfig('kanban-ui');
65
65
  const defaults = config?.defaultProps;
66
66
 
67
67
  expect(defaults).toBeDefined();
@@ -72,7 +72,7 @@ describe('Plugin Kanban', () => {
72
72
  });
73
73
 
74
74
  it('should have default columns with proper structure', () => {
75
- const config = ComponentRegistry.getConfig('kanban');
75
+ const config = ComponentRegistry.getConfig('kanban-ui');
76
76
  const defaults = config?.defaultProps;
77
77
  const columns = defaults?.columns || [];
78
78
 
@@ -93,7 +93,7 @@ describe('Plugin Kanban', () => {
93
93
  });
94
94
 
95
95
  it('should have cards with proper structure', () => {
96
- const config = ComponentRegistry.getConfig('kanban');
96
+ const config = ComponentRegistry.getConfig('kanban-ui');
97
97
  const defaults = config?.defaultProps;
98
98
  const columns = defaults?.columns || [];
99
99
 
package/src/index.tsx CHANGED
@@ -8,14 +8,18 @@
8
8
 
9
9
  import React, { Suspense } from 'react';
10
10
  import { ComponentRegistry } from '@object-ui/core';
11
+ import { useSchemaContext } from '@object-ui/react';
11
12
  import { Skeleton } from '@object-ui/components';
13
+ import { ObjectKanban } from './ObjectKanban';
12
14
 
13
15
  // Export types for external use
14
16
  export type { KanbanSchema, KanbanCard, KanbanColumn } from './types';
17
+ export { ObjectKanban };
18
+ export type { ObjectKanbanProps } from './ObjectKanban';
15
19
 
16
- // 🚀 Lazy load the implementation file
17
- // This ensures @dnd-kit is only loaded when the component is actually rendered
20
+ // 🚀 Lazy load the implementation files
18
21
  const LazyKanban = React.lazy(() => import('./KanbanImpl'));
22
+ const LazyKanbanEnhanced = React.lazy(() => import('./KanbanEnhanced'));
19
23
 
20
24
  export interface KanbanRendererProps {
21
25
  schema: {
@@ -40,9 +44,18 @@ export const KanbanRenderer: React.FC<KanbanRendererProps> = ({ schema }) => {
40
44
 
41
45
  // If we have flat data and a grouping key, distribute items into columns
42
46
  if (data && groupBy && Array.isArray(data)) {
43
- // 1. Group data by key
47
+ // Build label→id mapping so data values (labels like "In Progress")
48
+ // match column IDs (option values like "in_progress")
49
+ const labelToColumnId: Record<string, string> = {};
50
+ columns.forEach((col: any) => {
51
+ if (col.id) labelToColumnId[String(col.id).toLowerCase()] = col.id;
52
+ if (col.title) labelToColumnId[String(col.title).toLowerCase()] = col.id;
53
+ });
54
+
55
+ // 1. Group data by key, normalizing via label→id mapping
44
56
  const groups = data.reduce((acc, item) => {
45
- const key = item[groupBy];
57
+ const rawKey = String(item[groupBy] ?? '');
58
+ const key = labelToColumnId[rawKey.toLowerCase()] ?? rawKey;
46
59
  if (!acc[key]) acc[key] = [];
47
60
  acc[key].push(item);
48
61
  return acc;
@@ -75,9 +88,10 @@ export const KanbanRenderer: React.FC<KanbanRendererProps> = ({ schema }) => {
75
88
 
76
89
  // Register the component with the ComponentRegistry
77
90
  ComponentRegistry.register(
78
- 'kanban',
91
+ 'kanban-ui',
79
92
  KanbanRenderer,
80
93
  {
94
+ namespace: 'plugin-kanban',
81
95
  label: 'Kanban Board',
82
96
  icon: 'LayoutDashboard',
83
97
  category: 'plugin',
@@ -173,4 +187,96 @@ ComponentRegistry.register(
173
187
  // Standard Export Protocol - for manual integration
174
188
  export const kanbanComponents = {
175
189
  'kanban': KanbanRenderer,
190
+ 'kanban-enhanced': LazyKanbanEnhanced,
191
+ 'object-kanban': ObjectKanban,
176
192
  };
193
+
194
+ // Register enhanced Kanban
195
+ ComponentRegistry.register(
196
+ 'kanban-enhanced',
197
+ ({ schema }: { schema: any }) => {
198
+ const processedColumns = React.useMemo(() => {
199
+ const { columns = [], data, groupBy } = schema;
200
+ if (data && groupBy && Array.isArray(data)) {
201
+ const groups = data.reduce((acc, item) => {
202
+ const key = item[groupBy];
203
+ if (!acc[key]) acc[key] = [];
204
+ acc[key].push(item);
205
+ return acc;
206
+ }, {} as Record<string, any[]>);
207
+ return columns.map((col: any) => ({
208
+ ...col,
209
+ cards: [...(col.cards || []), ...(groups[col.id] || [])]
210
+ }));
211
+ }
212
+ return columns;
213
+ }, [schema]);
214
+
215
+ return (
216
+ <Suspense fallback={<Skeleton className="w-full h-[600px]" />}>
217
+ <LazyKanbanEnhanced
218
+ columns={processedColumns}
219
+ onCardMove={schema.onCardMove}
220
+ onColumnToggle={schema.onColumnToggle}
221
+ enableVirtualScrolling={schema.enableVirtualScrolling}
222
+ virtualScrollThreshold={schema.virtualScrollThreshold}
223
+ className={schema.className}
224
+ />
225
+ </Suspense>
226
+ );
227
+ },
228
+ {
229
+ namespace: 'plugin-kanban',
230
+ label: 'Kanban Board (Enhanced)',
231
+ icon: 'LayoutGrid',
232
+ category: 'plugin',
233
+ inputs: [
234
+ { name: 'columns', type: 'array', label: 'Columns', required: true },
235
+ { name: 'enableVirtualScrolling', type: 'boolean', label: 'Virtual Scrolling', defaultValue: false },
236
+ { name: 'virtualScrollThreshold', type: 'number', label: 'Virtual Scroll Threshold', defaultValue: 50 },
237
+ { name: 'onCardMove', type: 'code', label: 'On Card Move', advanced: true },
238
+ { name: 'onColumnToggle', type: 'code', label: 'On Column Toggle', advanced: true },
239
+ { name: 'className', type: 'string', label: 'CSS Class' }
240
+ ],
241
+ defaultProps: {
242
+ columns: [],
243
+ enableVirtualScrolling: false,
244
+ virtualScrollThreshold: 50,
245
+ className: 'w-full'
246
+ }
247
+ }
248
+ );
249
+
250
+ // Register object-kanban for ListView integration
251
+ export const ObjectKanbanRenderer: React.FC<{ schema: any; [key: string]: any }> = ({ schema, ...props }) => {
252
+ const { dataSource } = useSchemaContext() || {};
253
+ return <ObjectKanban schema={schema} dataSource={dataSource} {...props} />;
254
+ };
255
+
256
+ ComponentRegistry.register(
257
+ 'object-kanban',
258
+ ObjectKanbanRenderer,
259
+ {
260
+ namespace: 'plugin-kanban',
261
+ label: 'Object Kanban',
262
+ category: 'view',
263
+ inputs: [
264
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
265
+ { name: 'columns', type: 'array', label: 'Columns' }
266
+ ]
267
+ }
268
+ );
269
+
270
+ ComponentRegistry.register(
271
+ 'kanban',
272
+ ObjectKanbanRenderer,
273
+ {
274
+ namespace: 'view',
275
+ label: 'Kanban Board',
276
+ category: 'view',
277
+ inputs: [
278
+ { name: 'objectName', type: 'string', label: 'Object Name', required: true },
279
+ { name: 'columns', type: 'array', label: 'Columns' }
280
+ ]
281
+ }
282
+ );
@@ -0,0 +1,26 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { render, screen } from '@testing-library/react';
3
+ import React from 'react';
4
+ import { ObjectKanbanRenderer } from './index';
5
+
6
+ // Mock dependencies
7
+ vi.mock('@object-ui/react', () => ({
8
+ useSchemaContext: vi.fn(() => ({ dataSource: { type: 'mock-datasource' } })),
9
+ }));
10
+
11
+ // Mock the implementation
12
+ vi.mock('./ObjectKanban', () => ({
13
+ ObjectKanban: ({ dataSource }: any) => (
14
+ <div data-testid="kanban-mock">
15
+ {dataSource ? `DataSource: ${dataSource.type}` : 'No DataSource'}
16
+ </div>
17
+ )
18
+ }));
19
+
20
+ describe('Plugin Kanban Registration', () => {
21
+ it('renderer passes dataSource from context', () => {
22
+
23
+ render(<ObjectKanbanRenderer schema={{ type: 'object-kanban' }} />);
24
+ expect(screen.getByTestId('kanban-mock')).toHaveTextContent('DataSource: mock-datasource');
25
+ });
26
+ });