@ittinc/strapi-plugin-kanban-board 0.0.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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/admin/custom.d.ts +2 -0
  4. package/admin/src/components/Initializer.tsx +19 -0
  5. package/admin/src/components/KanbanInput/index.tsx +605 -0
  6. package/admin/src/components/PluginIcon.tsx +5 -0
  7. package/admin/src/index.ts +136 -0
  8. package/admin/src/pages/App.tsx +15 -0
  9. package/admin/src/pages/HomePage.tsx +398 -0
  10. package/admin/src/pluginId.ts +1 -0
  11. package/admin/src/translations/en.json +5 -0
  12. package/admin/src/translations/ru.json +5 -0
  13. package/admin/src/utils/getTranslation.ts +5 -0
  14. package/admin/tsconfig.build.json +10 -0
  15. package/admin/tsconfig.json +8 -0
  16. package/dist/_chunks/App-BEiW65up.js +343 -0
  17. package/dist/_chunks/App-DXTlN9Fm.mjs +341 -0
  18. package/dist/_chunks/en-C0sbENwZ.js +8 -0
  19. package/dist/_chunks/en-CHHvJuav.mjs +8 -0
  20. package/dist/_chunks/index-9nQMm6ez.js +465 -0
  21. package/dist/_chunks/index-DI_QN_uF.mjs +463 -0
  22. package/dist/_chunks/ru-B7uE6tx_.mjs +8 -0
  23. package/dist/_chunks/ru-Bl2jLOwG.js +8 -0
  24. package/dist/admin/index.js +156 -0
  25. package/dist/admin/index.mjs +157 -0
  26. package/dist/admin/src/components/Initializer.d.ts +5 -0
  27. package/dist/admin/src/components/KanbanInput/index.d.ts +33 -0
  28. package/dist/admin/src/components/PluginIcon.d.ts +2 -0
  29. package/dist/admin/src/index.d.ts +10 -0
  30. package/dist/admin/src/pages/App.d.ts +2 -0
  31. package/dist/admin/src/pages/HomePage.d.ts +2 -0
  32. package/dist/admin/src/pluginId.d.ts +1 -0
  33. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  34. package/dist/server/index.js +73 -0
  35. package/dist/server/index.mjs +74 -0
  36. package/dist/server/src/bootstrap.d.ts +5 -0
  37. package/dist/server/src/config/index.d.ts +5 -0
  38. package/dist/server/src/content-types/index.d.ts +2 -0
  39. package/dist/server/src/controllers/controller.d.ts +7 -0
  40. package/dist/server/src/controllers/index.d.ts +2 -0
  41. package/dist/server/src/destroy.d.ts +5 -0
  42. package/dist/server/src/index.d.ts +2 -0
  43. package/dist/server/src/middlewares/index.d.ts +2 -0
  44. package/dist/server/src/policies/index.d.ts +2 -0
  45. package/dist/server/src/register.d.ts +5 -0
  46. package/dist/server/src/routes/admin/index.d.ts +5 -0
  47. package/dist/server/src/routes/content-api/index.d.ts +12 -0
  48. package/dist/server/src/routes/index.d.ts +18 -0
  49. package/dist/server/src/services/index.d.ts +2 -0
  50. package/dist/server/src/services/service.d.ts +7 -0
  51. package/package.json +101 -0
  52. package/server/src/bootstrap.ts +7 -0
  53. package/server/src/config/index.ts +4 -0
  54. package/server/src/content-types/index.ts +1 -0
  55. package/server/src/controllers/controller.ts +13 -0
  56. package/server/src/controllers/index.ts +5 -0
  57. package/server/src/destroy.ts +7 -0
  58. package/server/src/index.ts +30 -0
  59. package/server/src/middlewares/index.ts +1 -0
  60. package/server/src/policies/index.ts +1 -0
  61. package/server/src/register.ts +13 -0
  62. package/server/src/routes/admin/index.ts +4 -0
  63. package/server/src/routes/content-api/index.ts +14 -0
  64. package/server/src/routes/index.ts +9 -0
  65. package/server/src/services/index.ts +5 -0
  66. package/server/src/services/service.ts +9 -0
  67. package/server/tsconfig.build.json +10 -0
  68. package/server/tsconfig.json +8 -0
@@ -0,0 +1,605 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { Box, Typography, Flex, Button, TextInput, Modal } from '@strapi/design-system';
3
+ import { Plus, Pencil, Check, Trash } from '@strapi/icons';
4
+ import styled from 'styled-components';
5
+ import { useIntl } from 'react-intl';
6
+
7
+ // --- Types ---
8
+ interface Item extends Record<string, any> {
9
+ id: string;
10
+ }
11
+
12
+ interface ColumnData {
13
+ id: string;
14
+ title: string;
15
+ code?: string;
16
+ items: Item[];
17
+ }
18
+
19
+ interface KanbanInputProps {
20
+ name: string;
21
+ value?: string | ColumnData[];
22
+ onChange: (event: { target: { name: string; value: string | ColumnData[]; type?: string } }) => void;
23
+ attribute: {
24
+ options?: {
25
+ defaultColumns?: string | string[];
26
+ itemSchema?: string | any[];
27
+ canAddColumns?: boolean;
28
+ canDeleteColumns?: boolean;
29
+ canRenameColumns?: boolean;
30
+ };
31
+ [key: string]: any;
32
+ };
33
+ intlLabel: any;
34
+ }
35
+
36
+ // --- Default Schemas ---
37
+ const DEFAULT_ITEM_SCHEMA = [
38
+ { name: 'title', label: 'Title', type: 'text', required: true },
39
+ { name: 'subtitle', label: 'Subtitle', type: 'text' }
40
+ ];
41
+
42
+ const DEFAULT_COLUMNS = ["To Do", "In Progress", "Done"];
43
+
44
+ // --- Styled Components ---
45
+
46
+ const BoardContainer = styled(Flex)`
47
+ gap: 16px;
48
+ padding: 16px;
49
+ align-items: flex-start;
50
+ overflow-x: auto;
51
+ border: 1px solid ${({ theme }) => theme.colors.neutral200};
52
+ border-radius: ${({ theme }) => theme.borderRadius};
53
+ background: ${({ theme }) => theme.colors.neutral100};
54
+ `;
55
+
56
+ const ColumnContainer = styled(Box)`
57
+ background: ${({ theme }) => theme.colors.neutral0};
58
+ border: 1px solid ${({ theme }) => theme.colors.neutral200};
59
+ border-radius: ${({ theme }) => theme.borderRadius};
60
+ min-width: 280px;
61
+ width: 280px;
62
+ display: flex;
63
+ flex-direction: column;
64
+ flex-shrink: 0;
65
+ `;
66
+
67
+ const ColumnHeader = styled(Box)`
68
+ padding: 12px;
69
+ border-bottom: 1px solid ${({ theme }) => theme.colors.neutral200};
70
+ background: ${({ theme }) => theme.colors.neutral100};
71
+ border-top-left-radius: ${({ theme }) => theme.borderRadius};
72
+ border-top-right-radius: ${({ theme }) => theme.borderRadius};
73
+ `;
74
+
75
+ const ItemList = styled(Box)<{ $isDraggingOver: boolean }>`
76
+ padding: 12px;
77
+ flex-grow: 1;
78
+ min-height: 80px;
79
+ background: ${({ theme, $isDraggingOver }) =>
80
+ $isDraggingOver ? theme.colors.primary100 : 'transparent'};
81
+ transition: background 0.2s;
82
+ `;
83
+
84
+ const CardItem = styled(Box)<{ $isDragging: boolean }>`
85
+ background: ${({ theme }) => theme.colors.neutral0};
86
+ border: 1px solid ${({ theme }) => theme.colors.neutral200};
87
+ border-radius: ${({ theme }) => theme.borderRadius};
88
+ padding: 8px;
89
+ margin-bottom: 8px;
90
+ box-shadow: ${({ theme }) => theme.shadows.filterShadow};
91
+ cursor: grab;
92
+ opacity: ${({ $isDragging }) => ($isDragging ? 0.5 : 1)};
93
+
94
+ &:hover {
95
+ border-color: ${({ theme }) => theme.colors.primary500};
96
+ }
97
+ `;
98
+
99
+ const AddItemButton = styled.button`
100
+ background: none;
101
+ border: none;
102
+ color: ${({ theme }) => theme.colors.primary600};
103
+ cursor: pointer;
104
+ padding: 8px;
105
+ width: 100%;
106
+ text-align: left;
107
+ border-top: 1px solid ${({ theme }) => theme.colors.neutral150};
108
+
109
+ &:hover {
110
+ text-decoration: underline;
111
+ background: ${({ theme }) => theme.colors.neutral100};
112
+ }
113
+ `;
114
+
115
+ // --- Component ---
116
+
117
+ export const KanbanInput = ({ name, value, onChange, intlLabel, attribute }: KanbanInputProps) => {
118
+ const { formatMessage } = useIntl();
119
+ const [columns, setColumns] = useState<ColumnData[]>([]);
120
+ const [draggedItem, setDraggedItem] = useState<{ itemId: string; sourceColId: string } | null>(null);
121
+ const [dragOverColId, setDragOverColId] = useState<string | null>(null);
122
+
123
+ // --- Configuration ---
124
+ const options = attribute?.options || {};
125
+
126
+ const canAddColumns = options.canAddColumns !== false;
127
+ const canDeleteColumns = options.canDeleteColumns !== false;
128
+ const canRenameColumns = options.canRenameColumns !== false;
129
+
130
+ const itemSchema = useMemo(() => {
131
+ let schema = options.itemSchema;
132
+ if (!schema) return DEFAULT_ITEM_SCHEMA;
133
+
134
+ if (typeof schema === 'string') {
135
+ try {
136
+ schema = JSON.parse(schema);
137
+ } catch (e) {
138
+ console.error('KanbanBoard: Invalid Item Schema JSON string', e);
139
+ return DEFAULT_ITEM_SCHEMA;
140
+ }
141
+ }
142
+
143
+ return Array.isArray(schema) ? schema : DEFAULT_ITEM_SCHEMA;
144
+ }, [options.itemSchema]);
145
+
146
+ const defaultColumnsList = useMemo(() => {
147
+ let cols = options.defaultColumns;
148
+ if (!cols) return DEFAULT_COLUMNS;
149
+
150
+ if (typeof cols === 'string') {
151
+ try {
152
+ cols = JSON.parse(cols);
153
+ } catch (e) {
154
+ console.error('KanbanBoard: Invalid Default Columns JSON string', e);
155
+ return DEFAULT_COLUMNS;
156
+ }
157
+ }
158
+
159
+ return Array.isArray(cols) ? cols : DEFAULT_COLUMNS;
160
+ }, [options.defaultColumns]);
161
+
162
+ // State for Adding/Editing Items (Modal)
163
+ const [isModalOpen, setIsModalOpen] = useState(false);
164
+ const [activeColId, setActiveColId] = useState<string | null>(null);
165
+ const [newItemData, setNewItemData] = useState<Record<string, any>>({});
166
+ const [editingItemId, setEditingItemId] = useState<string | null>(null);
167
+
168
+ // State for Adding/Editing Columns (Modal)
169
+ const [isColumnModalOpen, setIsColumnModalOpen] = useState(false);
170
+ const [columnFormData, setColumnFormData] = useState({ id: '', title: '', code: '' });
171
+ const [editingColumnId, setEditingColumnId] = useState<string | null>(null);
172
+
173
+ // Parse initial value or set defaults
174
+ useEffect(() => {
175
+ if (value) {
176
+ try {
177
+ const parsed = typeof value === 'string' ? JSON.parse(value) : value;
178
+ if (Array.isArray(parsed) && parsed.length > 0) {
179
+ setColumns(parsed);
180
+ return;
181
+ }
182
+ } catch (e) {
183
+ console.error('Failed to parse Kanban data', e);
184
+ }
185
+ }
186
+
187
+ // Initialize defaults if value is empty/invalid
188
+ if (Array.isArray(defaultColumnsList)) {
189
+ const initCols = defaultColumnsList.map((col: string | { id?: string; title: string; code?: string }, index: number) => {
190
+ if (typeof col === 'object' && col !== null && col.title) {
191
+ return {
192
+ id: col.id || `col-${index}-${Date.now()}`,
193
+ title: col.title,
194
+ code: col.code || '',
195
+ items: []
196
+ };
197
+ }
198
+ return {
199
+ id: `col-${index}-${Date.now()}`,
200
+ title: String(col),
201
+ items: []
202
+ };
203
+ });
204
+ setColumns(initCols);
205
+ }
206
+ }, [value, defaultColumnsList]);
207
+
208
+ // Update parent on change
209
+ const updateState = (newCols: ColumnData[]) => {
210
+ setColumns(newCols);
211
+ onChange({
212
+ target: {
213
+ name,
214
+ value: newCols,
215
+ type: 'json',
216
+ },
217
+ });
218
+ };
219
+
220
+ const handleDragStart = (e: React.DragEvent, itemId: string, sourceColId: string) => {
221
+ e.stopPropagation();
222
+ setDraggedItem({ itemId, sourceColId });
223
+ e.dataTransfer.effectAllowed = 'move';
224
+ };
225
+
226
+ const handleDragOver = (e: React.DragEvent, colId: string) => {
227
+ e.preventDefault();
228
+ e.stopPropagation();
229
+ e.dataTransfer.dropEffect = 'move';
230
+ setDragOverColId(colId);
231
+ };
232
+
233
+ const handleDrop = (e: React.DragEvent, targetColId: string, targetItemId?: string) => {
234
+ e.preventDefault();
235
+ e.stopPropagation();
236
+ setDragOverColId(null);
237
+
238
+ if (!draggedItem) return;
239
+ const { itemId: sourceItemId, sourceColId } = draggedItem;
240
+
241
+ const newColumns: ColumnData[] = JSON.parse(JSON.stringify(columns));
242
+ const sourceCol = newColumns.find(c => c.id === sourceColId);
243
+ const targetCol = newColumns.find(c => c.id === targetColId);
244
+
245
+ if (!sourceCol || !targetCol) return;
246
+
247
+ const itemIndex = sourceCol.items.findIndex(i => i.id === sourceItemId);
248
+ if (itemIndex === -1) return;
249
+
250
+ const [itemToMove] = sourceCol.items.splice(itemIndex, 1);
251
+
252
+ if (!targetItemId) {
253
+ targetCol.items.push(itemToMove);
254
+ } else {
255
+ const targetItemIndex = targetCol.items.findIndex(i => i.id === targetItemId);
256
+ if (targetItemIndex !== -1) {
257
+ targetCol.items.splice(targetItemIndex, 0, itemToMove);
258
+ } else {
259
+ targetCol.items.push(itemToMove);
260
+ }
261
+ }
262
+
263
+ updateState(newColumns);
264
+ setDraggedItem(null);
265
+ };
266
+
267
+ const handleOpenAddItem = (colId: string) => {
268
+ setActiveColId(colId);
269
+ setEditingItemId(null);
270
+ setNewItemData({}); // Reset form
271
+ setIsModalOpen(true);
272
+ };
273
+
274
+ const handleOpenEditItem = (colId: string, item: Item) => {
275
+ setActiveColId(colId);
276
+ setEditingItemId(item.id);
277
+ setNewItemData({ ...item });
278
+ setIsModalOpen(true);
279
+ };
280
+
281
+ const handleCloseModal = () => {
282
+ setIsModalOpen(false);
283
+ setActiveColId(null);
284
+ setEditingItemId(null);
285
+ };
286
+
287
+ const handleConfirmItemForm = () => {
288
+ if (!activeColId) return;
289
+
290
+ // Basic Validation (Check required fields)
291
+ for (const field of itemSchema) {
292
+ if (field.required && !newItemData[field.name]) {
293
+ alert(`${field.label || field.name} is required`);
294
+ return;
295
+ }
296
+ }
297
+
298
+ if (editingItemId) {
299
+ // Edit existing item
300
+ const newColumns = columns.map((col) => {
301
+ if (col.id === activeColId) {
302
+ return {
303
+ ...col,
304
+ items: col.items.map(item =>
305
+ item.id === editingItemId ? { ...newItemData, id: editingItemId } : item
306
+ )
307
+ };
308
+ }
309
+ return col;
310
+ });
311
+ updateState(newColumns);
312
+ } else {
313
+ // Add new item
314
+ const newItem: Item = {
315
+ id: Date.now().toString(),
316
+ ...newItemData,
317
+ };
318
+
319
+ const newColumns = columns.map((col) => {
320
+ if (col.id === activeColId) {
321
+ return { ...col, items: [...col.items, newItem] };
322
+ }
323
+ return col;
324
+ });
325
+
326
+ updateState(newColumns);
327
+ }
328
+
329
+ handleCloseModal();
330
+ };
331
+
332
+ const handleDeleteItem = (colId: string, itemId: string) => {
333
+ // eslint-disable-next-line no-restricted-globals
334
+ if (confirm('Are you sure you want to delete this item?')) {
335
+ const newColumns = columns.map(col => {
336
+ if (col.id === colId) {
337
+ return {
338
+ ...col,
339
+ items: col.items.filter(item => item.id !== itemId)
340
+ };
341
+ }
342
+ return col;
343
+ });
344
+ updateState(newColumns);
345
+ }
346
+ };
347
+
348
+
349
+ // --- Column Management ---
350
+
351
+ const handleOpenAddColumn = () => {
352
+ if (!canAddColumns) return;
353
+ setEditingColumnId(null);
354
+ setColumnFormData({ id: '', title: '', code: '' });
355
+ setIsColumnModalOpen(true);
356
+ };
357
+
358
+ const handleOpenEditColumn = (col: ColumnData) => {
359
+ if (!canRenameColumns) return;
360
+ setEditingColumnId(col.id);
361
+ setColumnFormData({ id: col.id, title: col.title, code: col.code || '' });
362
+ setIsColumnModalOpen(true);
363
+ };
364
+
365
+ const handleCloseColumnModal = () => {
366
+ setIsColumnModalOpen(false);
367
+ setColumnFormData({ id: '', title: '', code: '' });
368
+ setEditingColumnId(null);
369
+ };
370
+
371
+ const handleConfirmColumnForm = () => {
372
+ if (!columnFormData.title) {
373
+ alert("Column Title is required");
374
+ return;
375
+ }
376
+
377
+ if (editingColumnId) {
378
+ // Edit existing
379
+ const newColumns = columns.map(col =>
380
+ col.id === editingColumnId ? { ...col, title: columnFormData.title, code: columnFormData.code } : col
381
+ );
382
+ updateState(newColumns);
383
+ } else {
384
+ // Add new
385
+ const newCol: ColumnData = {
386
+ id: `col-${Date.now()}`,
387
+ title: columnFormData.title,
388
+ code: columnFormData.code,
389
+ items: [],
390
+ };
391
+ updateState([...columns, newCol]);
392
+ }
393
+
394
+ handleCloseColumnModal();
395
+ };
396
+
397
+ const handleDeleteColumn = (colId: string) => {
398
+ if (!canDeleteColumns) return;
399
+ // eslint-disable-next-line no-restricted-globals
400
+ if (confirm('Are you sure you want to delete this column? All items in it will be lost.')) {
401
+ const newColumns = columns.filter(col => col.id !== colId);
402
+ updateState(newColumns);
403
+ }
404
+ };
405
+
406
+ // Label resolving
407
+ let labelText = name;
408
+ if (intlLabel) {
409
+ if (typeof intlLabel === 'string') labelText = intlLabel;
410
+ else if (intlLabel.id) {
411
+ try {
412
+ labelText = formatMessage(intlLabel);
413
+ } catch (e) {
414
+ labelText = intlLabel.defaultMessage || name;
415
+ }
416
+ }
417
+ }
418
+
419
+ return (
420
+ <>
421
+ <Box>
422
+ <Flex justifyContent="space-between" alignItems="center" paddingBottom={2}>
423
+ <Typography variant="pi" fontWeight="bold">
424
+ {labelText}
425
+ </Typography>
426
+ {canAddColumns && (
427
+ <Button startIcon={<Plus />} onClick={handleOpenAddColumn} size="S" variant="secondary">
428
+ Add Column
429
+ </Button>
430
+ )}
431
+ </Flex>
432
+
433
+ <BoardContainer>
434
+ {columns.map((col) => (
435
+ <ColumnContainer key={col.id}>
436
+ <ColumnHeader>
437
+ <Flex justifyContent="space-between" alignItems="center">
438
+ <Flex direction="column" alignItems="flex-start">
439
+ <Typography variant="pi" fontWeight="bold">
440
+ {col.title} ({col.items.length})
441
+ </Typography>
442
+ {col.code && (
443
+ <Typography variant="sigma" textColor="neutral600">
444
+ Code: {col.code}
445
+ </Typography>
446
+ )}
447
+ </Flex>
448
+ <Flex gap={1}>
449
+ {canRenameColumns && (
450
+ <Button
451
+ onClick={() => handleOpenEditColumn(col)}
452
+ variant="ghost"
453
+ size="S"
454
+ startIcon={<Pencil />}
455
+ />
456
+ )}
457
+ {canDeleteColumns && (
458
+ <Button
459
+ onClick={() => handleDeleteColumn(col.id)}
460
+ variant="ghost"
461
+ size="S"
462
+ startIcon={<Trash />}
463
+ />
464
+ )}
465
+ </Flex>
466
+ </Flex>
467
+ </ColumnHeader>
468
+
469
+ <ItemList
470
+ $isDraggingOver={dragOverColId === col.id}
471
+ onDragOver={(e: React.DragEvent) => handleDragOver(e, col.id)}
472
+ onDragLeave={() => setDragOverColId(null)}
473
+ onDrop={(e: React.DragEvent) => handleDrop(e, col.id)}
474
+ >
475
+ {col.items.map((item) => (
476
+ <CardItem
477
+ key={item.id}
478
+ draggable
479
+ onDragStart={(e: React.DragEvent) => handleDragStart(e, item.id, col.id)}
480
+ onDragOver={(e: React.DragEvent) => {
481
+ e.preventDefault();
482
+ e.stopPropagation();
483
+ e.dataTransfer.dropEffect = 'move';
484
+ }}
485
+ onDrop={(e: React.DragEvent) => handleDrop(e, col.id, item.id)}
486
+ $isDragging={draggedItem?.itemId === item.id}
487
+ >
488
+ <Flex justifyContent="space-between" alignItems="start">
489
+ <Box flexGrow={1}>
490
+ {/* Render Fields based on Schema */}
491
+ {itemSchema.map((field: any, idx: number) => {
492
+ const val = item[field.name];
493
+ if (!val) return null;
494
+
495
+ if (idx === 0) {
496
+ return (
497
+ <Typography key={field.name} variant="omega" fontWeight="bold" as="div">
498
+ {val}
499
+ </Typography>
500
+ );
501
+ }
502
+ return (
503
+ <Typography key={field.name} variant="pi" textColor="neutral600" as="div">
504
+ {val}
505
+ </Typography>
506
+ );
507
+ })}
508
+ </Box>
509
+ <Flex gap={1} style={{ opacity: 0.5 }}>
510
+ <Button
511
+ onClick={() => handleOpenEditItem(col.id, item)}
512
+ variant="ghost"
513
+ size="S"
514
+ startIcon={<Pencil />}
515
+ />
516
+ <Button
517
+ onClick={() => handleDeleteItem(col.id, item.id)}
518
+ variant="ghost"
519
+ size="S"
520
+ startIcon={<Trash />}
521
+ />
522
+ </Flex>
523
+ </Flex>
524
+ </CardItem>
525
+ ))}
526
+ </ItemList>
527
+ <AddItemButton type="button" onClick={() => handleOpenAddItem(col.id)}>
528
+ + Add Item
529
+ </AddItemButton>
530
+ </ColumnContainer>
531
+ ))}
532
+ </BoardContainer>
533
+
534
+ {/* Add Item Modal */}
535
+ <Modal.Root open={isModalOpen} onOpenChange={setIsModalOpen}>
536
+ <Modal.Content>
537
+ <Modal.Header>
538
+ <Modal.Title>{editingItemId ? 'Edit Item' : 'Add New Item'}</Modal.Title>
539
+ </Modal.Header>
540
+ <Modal.Body>
541
+ <Flex direction="column" gap={4}>
542
+ {itemSchema.map((field: any) => (
543
+ <Box key={field.name} width="100%">
544
+ <TextInput
545
+ label={field.label || field.name}
546
+ placeholder={`Enter ${field.label || field.name}`}
547
+ value={newItemData[field.name] || ''}
548
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
549
+ setNewItemData({...newItemData, [field.name]: e.target.value})
550
+ }
551
+ required={field.required}
552
+ />
553
+ </Box>
554
+ ))}
555
+ </Flex>
556
+ </Modal.Body>
557
+ <Modal.Footer>
558
+ <Modal.Close>
559
+ <Button variant="tertiary" onClick={handleCloseModal}>Cancel</Button>
560
+ </Modal.Close>
561
+ <Button onClick={handleConfirmItemForm}>{editingItemId ? 'Save' : 'Add'}</Button>
562
+ </Modal.Footer>
563
+ </Modal.Content>
564
+ </Modal.Root>
565
+
566
+ {/* Add/Edit Column Modal */}
567
+ <Modal.Root open={isColumnModalOpen} onOpenChange={setIsColumnModalOpen}>
568
+ <Modal.Content>
569
+ <Modal.Header>
570
+ <Modal.Title>{editingColumnId ? 'Edit Column' : 'Add New Column'}</Modal.Title>
571
+ </Modal.Header>
572
+ <Modal.Body>
573
+ <Flex direction="column" gap={4}>
574
+ <TextInput
575
+ label="Column Title"
576
+ placeholder="Enter column title"
577
+ value={columnFormData.title}
578
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
579
+ setColumnFormData({...columnFormData, title: e.target.value})
580
+ }
581
+ required
582
+ />
583
+ <TextInput
584
+ label="Column Code"
585
+ placeholder="Enter column code (optional)"
586
+ hint="A unique code for frontend logic (e.g. 'todo', 'done')"
587
+ value={columnFormData.code}
588
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
589
+ setColumnFormData({...columnFormData, code: e.target.value})
590
+ }
591
+ />
592
+ </Flex>
593
+ </Modal.Body>
594
+ <Modal.Footer>
595
+ <Modal.Close>
596
+ <Button variant="tertiary" onClick={handleCloseColumnModal}>Cancel</Button>
597
+ </Modal.Close>
598
+ <Button onClick={handleConfirmColumnForm}>{editingColumnId ? 'Save' : 'Add'}</Button>
599
+ </Modal.Footer>
600
+ </Modal.Content>
601
+ </Modal.Root>
602
+ </Box>
603
+ </>
604
+ );
605
+ };
@@ -0,0 +1,5 @@
1
+ import { PuzzlePiece } from '@strapi/icons';
2
+
3
+ const PluginIcon = () => <PuzzlePiece />;
4
+
5
+ export { PluginIcon };