@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.
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/admin/custom.d.ts +2 -0
- package/admin/src/components/Initializer.tsx +19 -0
- package/admin/src/components/KanbanInput/index.tsx +605 -0
- package/admin/src/components/PluginIcon.tsx +5 -0
- package/admin/src/index.ts +136 -0
- package/admin/src/pages/App.tsx +15 -0
- package/admin/src/pages/HomePage.tsx +398 -0
- package/admin/src/pluginId.ts +1 -0
- package/admin/src/translations/en.json +5 -0
- package/admin/src/translations/ru.json +5 -0
- package/admin/src/utils/getTranslation.ts +5 -0
- package/admin/tsconfig.build.json +10 -0
- package/admin/tsconfig.json +8 -0
- package/dist/_chunks/App-BEiW65up.js +343 -0
- package/dist/_chunks/App-DXTlN9Fm.mjs +341 -0
- package/dist/_chunks/en-C0sbENwZ.js +8 -0
- package/dist/_chunks/en-CHHvJuav.mjs +8 -0
- package/dist/_chunks/index-9nQMm6ez.js +465 -0
- package/dist/_chunks/index-DI_QN_uF.mjs +463 -0
- package/dist/_chunks/ru-B7uE6tx_.mjs +8 -0
- package/dist/_chunks/ru-Bl2jLOwG.js +8 -0
- package/dist/admin/index.js +156 -0
- package/dist/admin/index.mjs +157 -0
- package/dist/admin/src/components/Initializer.d.ts +5 -0
- package/dist/admin/src/components/KanbanInput/index.d.ts +33 -0
- package/dist/admin/src/components/PluginIcon.d.ts +2 -0
- package/dist/admin/src/index.d.ts +10 -0
- package/dist/admin/src/pages/App.d.ts +2 -0
- package/dist/admin/src/pages/HomePage.d.ts +2 -0
- package/dist/admin/src/pluginId.d.ts +1 -0
- package/dist/admin/src/utils/getTranslation.d.ts +2 -0
- package/dist/server/index.js +73 -0
- package/dist/server/index.mjs +74 -0
- package/dist/server/src/bootstrap.d.ts +5 -0
- package/dist/server/src/config/index.d.ts +5 -0
- package/dist/server/src/content-types/index.d.ts +2 -0
- package/dist/server/src/controllers/controller.d.ts +7 -0
- package/dist/server/src/controllers/index.d.ts +2 -0
- package/dist/server/src/destroy.d.ts +5 -0
- package/dist/server/src/index.d.ts +2 -0
- package/dist/server/src/middlewares/index.d.ts +2 -0
- package/dist/server/src/policies/index.d.ts +2 -0
- package/dist/server/src/register.d.ts +5 -0
- package/dist/server/src/routes/admin/index.d.ts +5 -0
- package/dist/server/src/routes/content-api/index.d.ts +12 -0
- package/dist/server/src/routes/index.d.ts +18 -0
- package/dist/server/src/services/index.d.ts +2 -0
- package/dist/server/src/services/service.d.ts +7 -0
- package/package.json +101 -0
- package/server/src/bootstrap.ts +7 -0
- package/server/src/config/index.ts +4 -0
- package/server/src/content-types/index.ts +1 -0
- package/server/src/controllers/controller.ts +13 -0
- package/server/src/controllers/index.ts +5 -0
- package/server/src/destroy.ts +7 -0
- package/server/src/index.ts +30 -0
- package/server/src/middlewares/index.ts +1 -0
- package/server/src/policies/index.ts +1 -0
- package/server/src/register.ts +13 -0
- package/server/src/routes/admin/index.ts +4 -0
- package/server/src/routes/content-api/index.ts +14 -0
- package/server/src/routes/index.ts +9 -0
- package/server/src/services/index.ts +5 -0
- package/server/src/services/service.ts +9 -0
- package/server/tsconfig.build.json +10 -0
- 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
|
+
};
|