@object-ui/plugin-kanban 0.3.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.
- package/CHANGELOG.md +48 -0
- package/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/KanbanImpl-mGLdSHcd.js +3335 -0
- package/dist/KanbanImpl.d.ts +24 -0
- package/dist/KanbanImpl.d.ts.map +1 -0
- package/dist/index-i_5clVsp.js +746 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.umd.cjs +18 -0
- package/dist/types.d.ts +45 -0
- package/dist/types.d.ts.map +1 -0
- package/package.json +44 -0
- package/src/KanbanImpl.tsx +284 -0
- package/src/index.test.ts +104 -0
- package/src/index.tsx +168 -0
- package/src/types.ts +47 -0
- package/tsconfig.json +11 -0
- package/vite.config.ts +38 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
2
|
+
import { ComponentRegistry } from '@object-ui/core';
|
|
3
|
+
|
|
4
|
+
describe('Plugin Kanban', () => {
|
|
5
|
+
// Import all renderers to register them
|
|
6
|
+
beforeAll(async () => {
|
|
7
|
+
await import('./index');
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
describe('kanban component', () => {
|
|
11
|
+
it('should be registered in ComponentRegistry', () => {
|
|
12
|
+
const kanbanRenderer = ComponentRegistry.get('kanban');
|
|
13
|
+
expect(kanbanRenderer).toBeDefined();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should have proper metadata', () => {
|
|
17
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
18
|
+
expect(config).toBeDefined();
|
|
19
|
+
expect(config?.label).toBe('Kanban Board');
|
|
20
|
+
expect(config?.icon).toBe('LayoutDashboard');
|
|
21
|
+
expect(config?.category).toBe('plugin');
|
|
22
|
+
expect(config?.inputs).toBeDefined();
|
|
23
|
+
expect(config?.defaultProps).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have expected inputs', () => {
|
|
27
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
28
|
+
const inputNames = config?.inputs?.map((input: any) => input.name) || [];
|
|
29
|
+
|
|
30
|
+
expect(inputNames).toContain('columns');
|
|
31
|
+
expect(inputNames).toContain('onCardMove');
|
|
32
|
+
expect(inputNames).toContain('className');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should have columns as required input', () => {
|
|
36
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
37
|
+
const columnsInput = config?.inputs?.find((input: any) => input.name === 'columns');
|
|
38
|
+
|
|
39
|
+
expect(columnsInput).toBeDefined();
|
|
40
|
+
expect(columnsInput?.required).toBe(true);
|
|
41
|
+
expect(columnsInput?.type).toBe('array');
|
|
42
|
+
expect(columnsInput?.description).toBeDefined();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should have onCardMove as code input', () => {
|
|
46
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
47
|
+
const onCardMoveInput = config?.inputs?.find((input: any) => input.name === 'onCardMove');
|
|
48
|
+
|
|
49
|
+
expect(onCardMoveInput).toBeDefined();
|
|
50
|
+
expect(onCardMoveInput?.type).toBe('code');
|
|
51
|
+
expect(onCardMoveInput?.advanced).toBe(true);
|
|
52
|
+
expect(onCardMoveInput?.description).toBeDefined();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should have sensible default props', () => {
|
|
56
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
57
|
+
const defaults = config?.defaultProps;
|
|
58
|
+
|
|
59
|
+
expect(defaults).toBeDefined();
|
|
60
|
+
expect(defaults?.columns).toBeDefined();
|
|
61
|
+
expect(Array.isArray(defaults?.columns)).toBe(true);
|
|
62
|
+
expect(defaults?.columns.length).toBeGreaterThan(0);
|
|
63
|
+
expect(defaults?.className).toBe('w-full');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should have default columns with proper structure', () => {
|
|
67
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
68
|
+
const defaults = config?.defaultProps;
|
|
69
|
+
const columns = defaults?.columns || [];
|
|
70
|
+
|
|
71
|
+
// Verify at least 3 columns exist (todo, in-progress, done)
|
|
72
|
+
expect(columns.length).toBeGreaterThanOrEqual(3);
|
|
73
|
+
|
|
74
|
+
// Verify each column has required properties
|
|
75
|
+
columns.forEach((column: any) => {
|
|
76
|
+
expect(column.id).toBeDefined();
|
|
77
|
+
expect(column.title).toBeDefined();
|
|
78
|
+
expect(column.cards).toBeDefined();
|
|
79
|
+
expect(Array.isArray(column.cards)).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Verify at least one column has cards
|
|
83
|
+
const hasCards = columns.some((column: any) => column.cards.length > 0);
|
|
84
|
+
expect(hasCards).toBe(true);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should have cards with proper structure', () => {
|
|
88
|
+
const config = ComponentRegistry.getConfig('kanban');
|
|
89
|
+
const defaults = config?.defaultProps;
|
|
90
|
+
const columns = defaults?.columns || [];
|
|
91
|
+
|
|
92
|
+
// Find a column with cards
|
|
93
|
+
const columnWithCards = columns.find((column: any) => column.cards.length > 0);
|
|
94
|
+
expect(columnWithCards).toBeDefined();
|
|
95
|
+
|
|
96
|
+
const card = columnWithCards.cards[0];
|
|
97
|
+
expect(card.id).toBeDefined();
|
|
98
|
+
expect(card.title).toBeDefined();
|
|
99
|
+
expect(card.description).toBeDefined();
|
|
100
|
+
expect(card.badges).toBeDefined();
|
|
101
|
+
expect(Array.isArray(card.badges)).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
});
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import React, { Suspense } from 'react';
|
|
2
|
+
import { ComponentRegistry } from '@object-ui/core';
|
|
3
|
+
import { Skeleton } from '@object-ui/components';
|
|
4
|
+
|
|
5
|
+
// Export types for external use
|
|
6
|
+
export type { KanbanSchema, KanbanCard, KanbanColumn } from './types';
|
|
7
|
+
|
|
8
|
+
// 🚀 Lazy load the implementation file
|
|
9
|
+
// This ensures @dnd-kit is only loaded when the component is actually rendered
|
|
10
|
+
const LazyKanban = React.lazy(() => import('./KanbanImpl'));
|
|
11
|
+
|
|
12
|
+
export interface KanbanRendererProps {
|
|
13
|
+
schema: {
|
|
14
|
+
type: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
className?: string;
|
|
17
|
+
columns?: Array<any>;
|
|
18
|
+
data?: Array<any>;
|
|
19
|
+
groupBy?: string;
|
|
20
|
+
onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* KanbanRenderer - The public API for the kanban board component
|
|
26
|
+
* This wrapper handles lazy loading internally using React.Suspense
|
|
27
|
+
*/
|
|
28
|
+
export const KanbanRenderer: React.FC<KanbanRendererProps> = ({ schema }) => {
|
|
29
|
+
// ⚡️ Adapter: Map flat 'data' + 'groupBy' to nested 'cards' structure
|
|
30
|
+
const processedColumns = React.useMemo(() => {
|
|
31
|
+
const { columns = [], data, groupBy } = schema;
|
|
32
|
+
|
|
33
|
+
// If we have flat data and a grouping key, distribute items into columns
|
|
34
|
+
if (data && groupBy && Array.isArray(data)) {
|
|
35
|
+
// 1. Group data by key
|
|
36
|
+
const groups = data.reduce((acc, item) => {
|
|
37
|
+
const key = item[groupBy];
|
|
38
|
+
if (!acc[key]) acc[key] = [];
|
|
39
|
+
acc[key].push(item);
|
|
40
|
+
return acc;
|
|
41
|
+
}, {} as Record<string, any[]>);
|
|
42
|
+
|
|
43
|
+
// 2. Inject into columns
|
|
44
|
+
return columns.map((col: any) => ({
|
|
45
|
+
...col,
|
|
46
|
+
cards: [
|
|
47
|
+
...(col.cards || []), // Preserve static cards
|
|
48
|
+
...(groups[col.id] || []) // Add dynamic cards
|
|
49
|
+
]
|
|
50
|
+
}));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Default: Return columns as-is (assuming they have 'cards' inside)
|
|
54
|
+
return columns;
|
|
55
|
+
}, [schema]);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<Suspense fallback={<Skeleton className="w-full h-[600px]" />}>
|
|
59
|
+
<LazyKanban
|
|
60
|
+
columns={processedColumns}
|
|
61
|
+
onCardMove={schema.onCardMove}
|
|
62
|
+
className={schema.className}
|
|
63
|
+
/>
|
|
64
|
+
</Suspense>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// Register the component with the ComponentRegistry
|
|
69
|
+
ComponentRegistry.register(
|
|
70
|
+
'kanban',
|
|
71
|
+
KanbanRenderer,
|
|
72
|
+
{
|
|
73
|
+
label: 'Kanban Board',
|
|
74
|
+
icon: 'LayoutDashboard',
|
|
75
|
+
category: 'plugin',
|
|
76
|
+
inputs: [
|
|
77
|
+
{
|
|
78
|
+
name: 'columns',
|
|
79
|
+
type: 'array',
|
|
80
|
+
label: 'Columns',
|
|
81
|
+
description: 'Array of { id, title, cards, limit, className }',
|
|
82
|
+
required: true
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
name: 'onCardMove',
|
|
86
|
+
type: 'code',
|
|
87
|
+
label: 'On Card Move',
|
|
88
|
+
description: 'Callback when a card is moved',
|
|
89
|
+
advanced: true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'className',
|
|
93
|
+
type: 'string',
|
|
94
|
+
label: 'CSS Class'
|
|
95
|
+
}
|
|
96
|
+
],
|
|
97
|
+
defaultProps: {
|
|
98
|
+
columns: [
|
|
99
|
+
{
|
|
100
|
+
id: 'todo',
|
|
101
|
+
title: 'To Do',
|
|
102
|
+
cards: [
|
|
103
|
+
{
|
|
104
|
+
id: 'card-1',
|
|
105
|
+
title: 'Task 1',
|
|
106
|
+
description: 'This is the first task',
|
|
107
|
+
badges: [
|
|
108
|
+
{ label: 'High Priority', variant: 'destructive' },
|
|
109
|
+
{ label: 'Feature', variant: 'default' }
|
|
110
|
+
]
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
id: 'card-2',
|
|
114
|
+
title: 'Task 2',
|
|
115
|
+
description: 'This is the second task',
|
|
116
|
+
badges: [
|
|
117
|
+
{ label: 'Bug', variant: 'destructive' }
|
|
118
|
+
]
|
|
119
|
+
}
|
|
120
|
+
]
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
id: 'in-progress',
|
|
124
|
+
title: 'In Progress',
|
|
125
|
+
limit: 3,
|
|
126
|
+
cards: [
|
|
127
|
+
{
|
|
128
|
+
id: 'card-3',
|
|
129
|
+
title: 'Task 3',
|
|
130
|
+
description: 'Currently working on this',
|
|
131
|
+
badges: [
|
|
132
|
+
{ label: 'In Progress', variant: 'default' }
|
|
133
|
+
]
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
id: 'done',
|
|
139
|
+
title: 'Done',
|
|
140
|
+
cards: [
|
|
141
|
+
{
|
|
142
|
+
id: 'card-4',
|
|
143
|
+
title: 'Task 4',
|
|
144
|
+
description: 'This task is completed',
|
|
145
|
+
badges: [
|
|
146
|
+
{ label: 'Completed', variant: 'outline' }
|
|
147
|
+
]
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'card-5',
|
|
151
|
+
title: 'Task 5',
|
|
152
|
+
description: 'Another completed task',
|
|
153
|
+
badges: [
|
|
154
|
+
{ label: 'Completed', variant: 'outline' }
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
],
|
|
160
|
+
className: 'w-full'
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// Standard Export Protocol - for manual integration
|
|
166
|
+
export const kanbanComponents = {
|
|
167
|
+
'kanban': KanbanRenderer,
|
|
168
|
+
};
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { BaseSchema } from '@object-ui/types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Kanban card interface.
|
|
5
|
+
*/
|
|
6
|
+
export interface KanbanCard {
|
|
7
|
+
id: string;
|
|
8
|
+
title: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
badges?: Array<{ label: string; variant?: "default" | "secondary" | "destructive" | "outline" }>;
|
|
11
|
+
[key: string]: any;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Kanban column interface.
|
|
16
|
+
*/
|
|
17
|
+
export interface KanbanColumn {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
cards: KanbanCard[];
|
|
21
|
+
limit?: number;
|
|
22
|
+
className?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Kanban Board component schema.
|
|
27
|
+
* Renders a drag-and-drop kanban board for task management.
|
|
28
|
+
*/
|
|
29
|
+
export interface KanbanSchema extends BaseSchema {
|
|
30
|
+
type: 'kanban';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Array of columns to display in the kanban board.
|
|
34
|
+
* Each column contains an array of cards.
|
|
35
|
+
*/
|
|
36
|
+
columns?: KanbanColumn[];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Callback function when a card is moved between columns or reordered.
|
|
40
|
+
*/
|
|
41
|
+
onCardMove?: (cardId: string, fromColumnId: string, toColumnId: string, newIndex: number) => void;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Optional CSS class name to apply custom styling.
|
|
45
|
+
*/
|
|
46
|
+
className?: string;
|
|
47
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"composite": true,
|
|
7
|
+
"declarationMap": true
|
|
8
|
+
},
|
|
9
|
+
"include": ["src/**/*"],
|
|
10
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
|
|
11
|
+
}
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import dts from 'vite-plugin-dts';
|
|
4
|
+
import { resolve } from 'path';
|
|
5
|
+
|
|
6
|
+
export default defineConfig({
|
|
7
|
+
plugins: [
|
|
8
|
+
react(),
|
|
9
|
+
dts({
|
|
10
|
+
insertTypesEntry: true,
|
|
11
|
+
include: ['src'],
|
|
12
|
+
}),
|
|
13
|
+
],
|
|
14
|
+
resolve: {
|
|
15
|
+
alias: {
|
|
16
|
+
'@': resolve(__dirname, './src'),
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
build: {
|
|
20
|
+
lib: {
|
|
21
|
+
entry: resolve(__dirname, 'src/index.tsx'),
|
|
22
|
+
name: 'ObjectUIPluginKanban',
|
|
23
|
+
fileName: 'index',
|
|
24
|
+
},
|
|
25
|
+
rollupOptions: {
|
|
26
|
+
external: ['react', 'react-dom', '@object-ui/components', '@object-ui/core', '@object-ui/react'],
|
|
27
|
+
output: {
|
|
28
|
+
globals: {
|
|
29
|
+
react: 'React',
|
|
30
|
+
'react-dom': 'ReactDOM',
|
|
31
|
+
'@object-ui/components': 'ObjectUIComponents',
|
|
32
|
+
'@object-ui/core': 'ObjectUICore',
|
|
33
|
+
'@object-ui/react': 'ObjectUIReact',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|