1
|
import 'antd/dist/antd.css';
|
2
|
import React, { useContext, useEffect, useState } from 'react';
|
3
|
|
4
|
import { useUnauthRedirect } from '../../hooks';
|
5
|
import { useRouter } from 'next/router';
|
6
|
import { Button, Checkbox, Popconfirm, Tooltip, Typography } from 'antd';
|
7
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
8
|
import {
|
9
|
faTags,
|
10
|
faPlus,
|
11
|
faPenToSquare,
|
12
|
faTrashCan,
|
13
|
} from '@fortawesome/free-solid-svg-icons';
|
14
|
import { LoggedUserContext } from '../../contexts/LoggedUserContext';
|
15
|
import { MainLayout } from '../../layouts/MainLayout';
|
16
|
import { Container, Row, Stack } from 'react-bootstrap';
|
17
|
import { Table } from 'antd';
|
18
|
import { tagController } from '../../controllers';
|
19
|
import Search from 'antd/lib/input/Search';
|
20
|
import CategoryModal, {
|
21
|
CategoryModalValues,
|
22
|
} from '../../components/modals/CategoryModal';
|
23
|
import TagModal, { TagModalValues } from '../../components/modals/TagModal';
|
24
|
import SubTagModal, { SubTagModalValues } from '../../components/modals/SubTagModal';
|
25
|
|
26
|
/**
|
27
|
* Creates a tag management page.
|
28
|
* @returns The tag management page.
|
29
|
*/
|
30
|
function TagsPage() {
|
31
|
const redirecting = useUnauthRedirect('/login');
|
32
|
const { logout, role } = useContext(LoggedUserContext);
|
33
|
const router = useRouter();
|
34
|
|
35
|
const [tagData, setTagData] = useState<any[] | null>([]);
|
36
|
|
37
|
/**
|
38
|
* Data in a table.
|
39
|
*/
|
40
|
const [shownData, setShownData] = useState<any[] | undefined>([]);
|
41
|
|
42
|
// States that says if modal windows should be shown.
|
43
|
const [showAddCategoryModal, setShowAddCategoryModal] = useState(false);
|
44
|
const [showEditCategoryModal, setShowEditCategoryModal] = useState(false);
|
45
|
const [showAddTagModal, setShowAddTagModal] = useState(false);
|
46
|
const [showEditTagModal, setShowEditTagModal] = useState(false);
|
47
|
const [showAddSubTagModal, setShowAddSubTagModal] = useState(false);
|
48
|
const [showEditSubTagModal, setShowEditSubTagModal] = useState(false);
|
49
|
|
50
|
/**
|
51
|
* Currently selected record in a table.
|
52
|
*/
|
53
|
const [selectedRecord, setSelectedRecord] = useState({
|
54
|
key: '',
|
55
|
name: '',
|
56
|
description: '',
|
57
|
color: '',
|
58
|
disabledForAnnotators: false,
|
59
|
});
|
60
|
|
61
|
/**
|
62
|
* Loads data on start.
|
63
|
*/
|
64
|
useEffect(() => {
|
65
|
if (!redirecting && role === 'ADMINISTRATOR') {
|
66
|
loadData();
|
67
|
}
|
68
|
}, [logout, redirecting, role, router]);
|
69
|
|
70
|
/**
|
71
|
* Definition of columns of a table.
|
72
|
*/
|
73
|
const columns = [
|
74
|
{ title: 'Název', dataIndex: 'name', key: 'name' },
|
75
|
{
|
76
|
title: 'Barva',
|
77
|
dataIndex: 'color',
|
78
|
key: 'color',
|
79
|
width: 75,
|
80
|
render: (
|
81
|
columnData: string | null | undefined,
|
82
|
record: any,
|
83
|
index: number
|
84
|
) => {
|
85
|
if (columnData) return <input type="color" value={columnData} disabled />;
|
86
|
},
|
87
|
},
|
88
|
{
|
89
|
title: 'Skrýt pro anotátory',
|
90
|
dataIndex: 'disabledForAnnotators',
|
91
|
key: 'disabledForAnnotators',
|
92
|
width: 150,
|
93
|
render: (columnData: boolean | undefined, record: any, index: number) =>
|
94
|
columnData !== undefined && <Checkbox checked={columnData} disabled />,
|
95
|
},
|
96
|
{
|
97
|
title: 'Povolit sentiment',
|
98
|
dataIndex: 'sentimentEnabled',
|
99
|
key: 'sentimentEnabled',
|
100
|
width: 150,
|
101
|
render: (columnData: boolean | undefined, record: any, index: number) =>
|
102
|
columnData !== undefined && <Checkbox checked={columnData} disabled />,
|
103
|
},
|
104
|
{ title: 'Popis', dataIndex: 'description', key: 'description' },
|
105
|
{
|
106
|
title: '',
|
107
|
dataIndex: 'operations',
|
108
|
key: 'operations',
|
109
|
width: 200,
|
110
|
render: (text: any, record: any, index: number) => (
|
111
|
<Stack direction="horizontal">
|
112
|
{record.depth < 2 && (
|
113
|
<Tooltip title="Přidat potomka">
|
114
|
<Button type="primary" onClick={addChild(record)}>
|
115
|
<FontAwesomeIcon icon={faPlus} />
|
116
|
</Button>
|
117
|
</Tooltip>
|
118
|
)}
|
119
|
<Tooltip title="Upravit">
|
120
|
<Button type="text" onClick={editRecord(record)}>
|
121
|
<FontAwesomeIcon icon={faPenToSquare} />
|
122
|
</Button>
|
123
|
</Tooltip>
|
124
|
<Popconfirm
|
125
|
title="Opravdu chcete smazat?"
|
126
|
onConfirm={deleteRecord(record)}
|
127
|
>
|
128
|
<Tooltip title="Odstranit">
|
129
|
<Button type="primary" danger>
|
130
|
<FontAwesomeIcon icon={faTrashCan} />
|
131
|
</Button>
|
132
|
</Tooltip>
|
133
|
</Popconfirm>
|
134
|
</Stack>
|
135
|
),
|
136
|
},
|
137
|
];
|
138
|
|
139
|
// ------------------------------------ ADD CATEGORY MODAL -------------------------------------
|
140
|
|
141
|
const openAddCategoryModal = () => {
|
142
|
setShowAddCategoryModal(true);
|
143
|
};
|
144
|
|
145
|
const submitAddCategoryModal = (val: CategoryModalValues) => {
|
146
|
tagController
|
147
|
.categoriesPost({
|
148
|
name: val.name,
|
149
|
description: val.description,
|
150
|
color: val.color,
|
151
|
disabledForAnnotators: val.disabledForAnnotators,
|
152
|
})
|
153
|
.then(
|
154
|
() => loadData(),
|
155
|
(reason) => console.log(reason)
|
156
|
);
|
157
|
|
158
|
setShowAddCategoryModal(false);
|
159
|
};
|
160
|
|
161
|
const cancelAddCategoryModal = () => {
|
162
|
setShowAddCategoryModal(false);
|
163
|
};
|
164
|
|
165
|
// --------------------------------------------------------------------------------------------
|
166
|
|
167
|
// ------------------------------------ EDIT CATEGORY MODAL -----------------------------------
|
168
|
|
169
|
const openEditCategoryModal = () => {
|
170
|
setShowEditCategoryModal(true);
|
171
|
};
|
172
|
|
173
|
const submitEditCategoryModel = (val: CategoryModalValues) => {
|
174
|
tagController
|
175
|
.categoryCategoryIdPut(selectedRecord.key, {
|
176
|
name: val.name,
|
177
|
description: val.description,
|
178
|
color: val.color,
|
179
|
disabledForAnnotators: val.disabledForAnnotators,
|
180
|
})
|
181
|
.then(() => loadData());
|
182
|
|
183
|
setShowEditCategoryModal(false);
|
184
|
};
|
185
|
|
186
|
const cancelEditCategoryModal = () => {
|
187
|
setShowEditCategoryModal(false);
|
188
|
};
|
189
|
|
190
|
// --------------------------------------------------------------------------------------------
|
191
|
|
192
|
// ------------------------------------ ADD TAG MODAL -------------------------------------
|
193
|
|
194
|
const openAddTagModal = () => {
|
195
|
setShowAddTagModal(true);
|
196
|
};
|
197
|
|
198
|
const submitAddTagModal = (val: TagModalValues) => {
|
199
|
tagController
|
200
|
.tagsPost({
|
201
|
categoryId: selectedRecord.key,
|
202
|
name: val.name,
|
203
|
description: val.description,
|
204
|
color: val.color,
|
205
|
sentimentEnabled: val.sentimentEnabled,
|
206
|
})
|
207
|
.then(() => loadData());
|
208
|
|
209
|
setShowAddTagModal(false);
|
210
|
};
|
211
|
|
212
|
const cancelAddTagModal = () => {
|
213
|
setShowAddTagModal(false);
|
214
|
};
|
215
|
|
216
|
// --------------------------------------------------------------------------------------------
|
217
|
|
218
|
// ------------------------------------ EDIT TAG MODAL -------------------------------------
|
219
|
|
220
|
const openEditTagModal = () => {
|
221
|
setShowEditTagModal(true);
|
222
|
};
|
223
|
|
224
|
const submitEditTagModal = (val: TagModalValues) => {
|
225
|
tagController
|
226
|
.tagTagIdPut(selectedRecord.key, {
|
227
|
name: val.name,
|
228
|
description: val.description,
|
229
|
color: val.color,
|
230
|
sentimentEnabled: val.sentimentEnabled,
|
231
|
})
|
232
|
.then(() => loadData());
|
233
|
|
234
|
setShowEditTagModal(false);
|
235
|
};
|
236
|
|
237
|
const cancelEditTagModal = () => {
|
238
|
setShowEditTagModal(false);
|
239
|
};
|
240
|
|
241
|
// --------------------------------------------------------------------------------------------
|
242
|
|
243
|
// ------------------------------------ ADD SUB TAG MODAL -------------------------------------
|
244
|
|
245
|
const openAddSubTagModal = () => {
|
246
|
setShowAddSubTagModal(true);
|
247
|
};
|
248
|
|
249
|
const submitAddSubTagModal = (val: SubTagModalValues) => {
|
250
|
tagController
|
251
|
.subtagsPost({
|
252
|
tagId: selectedRecord.key,
|
253
|
name: val.name,
|
254
|
description: val.description,
|
255
|
})
|
256
|
.then(() => loadData());
|
257
|
|
258
|
setShowAddSubTagModal(false);
|
259
|
};
|
260
|
|
261
|
const cancelAddSubTagModal = () => {
|
262
|
setShowAddSubTagModal(false);
|
263
|
};
|
264
|
|
265
|
// --------------------------------------------------------------------------------------------
|
266
|
|
267
|
// ------------------------------------ EDIT SUB TAG MODAL -------------------------------------
|
268
|
|
269
|
const openEditSubTagModal = () => {
|
270
|
setShowEditSubTagModal(true);
|
271
|
};
|
272
|
|
273
|
const submitEditSubTagModal = (val: SubTagModalValues) => {
|
274
|
tagController
|
275
|
.subtagSubtagIdPut(selectedRecord.key, {
|
276
|
name: val.name,
|
277
|
description: val.description,
|
278
|
})
|
279
|
.then(() => loadData());
|
280
|
|
281
|
setShowEditSubTagModal(false);
|
282
|
};
|
283
|
|
284
|
const cancelEditSubTagModal = () => {
|
285
|
setShowEditSubTagModal(false);
|
286
|
};
|
287
|
|
288
|
// --------------------------------------------------------------------------------------------
|
289
|
|
290
|
// ------------------------------------ RECORD OPERATIONS -------------------------------------
|
291
|
|
292
|
const addChild = (record: any) => (e: any) => {
|
293
|
setSelectedRecord(record);
|
294
|
|
295
|
if (record.depth === 0) {
|
296
|
openAddTagModal();
|
297
|
} else if (record.depth === 1) {
|
298
|
openAddSubTagModal();
|
299
|
}
|
300
|
};
|
301
|
|
302
|
const editRecord = (record: any) => (e: any) => {
|
303
|
setSelectedRecord(record);
|
304
|
|
305
|
if (record.depth === 0) {
|
306
|
openEditCategoryModal();
|
307
|
} else if (record.depth === 1) {
|
308
|
openEditTagModal();
|
309
|
} else if (record.depth === 2) {
|
310
|
openEditSubTagModal();
|
311
|
}
|
312
|
};
|
313
|
|
314
|
const deleteRecord = (record: any) => (e: any) => {
|
315
|
if (record.depth === 0) {
|
316
|
tagController.categoryCategoryIdDelete(record.key).then(() => loadData());
|
317
|
} else if (record.depth === 1) {
|
318
|
tagController.tagTagIdDelete(record.key).then(() => loadData());
|
319
|
} else if (record.depth === 2) {
|
320
|
tagController.subtagSubtagIdDelete(record.key).then(() => loadData());
|
321
|
}
|
322
|
};
|
323
|
|
324
|
// --------------------------------------------------------------------------------------------
|
325
|
|
326
|
/**
|
327
|
* Loads data from a server.
|
328
|
*/
|
329
|
const loadData = () => {
|
330
|
tagController.tagsGet().then((tagTree) => {
|
331
|
if (typeof tagTree.data.tagCategories != 'undefined') {
|
332
|
setTagData(tagTree.data.tagCategories);
|
333
|
setShownData(mapData(tagTree.data.tagCategories));
|
334
|
}
|
335
|
});
|
336
|
};
|
337
|
|
338
|
const mapData = (data: any[] | null) => {
|
339
|
return data?.map((catInfo) => {
|
340
|
return {
|
341
|
key: catInfo.id,
|
342
|
name: catInfo.name,
|
343
|
description: catInfo.description,
|
344
|
color: catInfo.color,
|
345
|
depth: 0,
|
346
|
disabledForAnnotators: catInfo.disabledForAnnotators,
|
347
|
...(catInfo.tags?.length && {
|
348
|
children: catInfo.tags?.map((tagInfo: any) => {
|
349
|
return {
|
350
|
key: tagInfo.id,
|
351
|
name: tagInfo.name,
|
352
|
description: tagInfo.description,
|
353
|
color: tagInfo.color,
|
354
|
sentimentEnabled: tagInfo.sentimentEnabled,
|
355
|
depth: 1,
|
356
|
...(tagInfo.subTags?.length && {
|
357
|
children: tagInfo.subTags?.map((subInfo: any) => {
|
358
|
return {
|
359
|
key: subInfo.id,
|
360
|
name: subInfo.name,
|
361
|
description: subInfo.description,
|
362
|
depth: 2,
|
363
|
};
|
364
|
}),
|
365
|
}),
|
366
|
};
|
367
|
}),
|
368
|
}),
|
369
|
};
|
370
|
});
|
371
|
};
|
372
|
|
373
|
/**
|
374
|
* Searches given value in 'name' column in a table.
|
375
|
* If the provided value is empty the method loads all data from a server.
|
376
|
* @param value Value that is searched.
|
377
|
*/
|
378
|
const searchTag = (value: string) => {
|
379
|
let data = mapData(tagData);
|
380
|
|
381
|
if (value) {
|
382
|
data = data?.filter((category) => {
|
383
|
category.children = category.children?.filter((tag: any) => {
|
384
|
tag.children = tag.children?.filter((subTag: any) =>
|
385
|
subTag.name.toLowerCase().includes(value.toLowerCase())
|
386
|
);
|
387
|
|
388
|
return (
|
389
|
tag.children?.length > 0 ||
|
390
|
tag.name.toLowerCase().includes(value.toLowerCase())
|
391
|
);
|
392
|
});
|
393
|
|
394
|
return (
|
395
|
category.children?.length > 0 ||
|
396
|
category.name.toLowerCase().includes(value.toLowerCase())
|
397
|
);
|
398
|
});
|
399
|
}
|
400
|
|
401
|
setShownData(data);
|
402
|
};
|
403
|
|
404
|
return redirecting || role !== 'ADMINISTRATOR' ? null : (
|
405
|
<MainLayout>
|
406
|
{showAddCategoryModal && (
|
407
|
<CategoryModal
|
408
|
title="Vytvořit kategorii"
|
409
|
submitText="Přidat"
|
410
|
onCancel={cancelAddCategoryModal}
|
411
|
onSubmit={submitAddCategoryModal}
|
412
|
/>
|
413
|
)}
|
414
|
|
415
|
{showEditCategoryModal && (
|
416
|
<CategoryModal
|
417
|
title="Editovat kategorii"
|
418
|
submitText="Změnit"
|
419
|
onCancel={cancelEditCategoryModal}
|
420
|
onSubmit={submitEditCategoryModel}
|
421
|
defaultValues={selectedRecord}
|
422
|
/>
|
423
|
)}
|
424
|
|
425
|
{showAddTagModal && (
|
426
|
<TagModal
|
427
|
title="Vytvořit tag"
|
428
|
submitText="Přidat"
|
429
|
onCancel={cancelAddTagModal}
|
430
|
onSubmit={submitAddTagModal}
|
431
|
defaultValues={{ color: selectedRecord.color }}
|
432
|
/>
|
433
|
)}
|
434
|
|
435
|
{showEditTagModal && (
|
436
|
<TagModal
|
437
|
title="Editovat tag"
|
438
|
submitText="Změnit"
|
439
|
onCancel={cancelEditTagModal}
|
440
|
onSubmit={submitEditTagModal}
|
441
|
defaultValues={selectedRecord}
|
442
|
/>
|
443
|
)}
|
444
|
|
445
|
{showAddSubTagModal && (
|
446
|
<SubTagModal
|
447
|
title="Vytvořit sub tag"
|
448
|
submitText="Přidat"
|
449
|
onCancel={cancelAddSubTagModal}
|
450
|
onSubmit={submitAddSubTagModal}
|
451
|
/>
|
452
|
)}
|
453
|
|
454
|
{showEditSubTagModal && (
|
455
|
<SubTagModal
|
456
|
title="Editovat sub tag"
|
457
|
submitText="Změnit"
|
458
|
onCancel={cancelEditSubTagModal}
|
459
|
onSubmit={submitEditSubTagModal}
|
460
|
defaultValues={selectedRecord}
|
461
|
/>
|
462
|
)}
|
463
|
|
464
|
<Container>
|
465
|
<Row>
|
466
|
<Typography.Title level={2}>
|
467
|
<FontAwesomeIcon icon={faTags} /> Značky
|
468
|
</Typography.Title>
|
469
|
</Row>
|
470
|
<Row>
|
471
|
<Table
|
472
|
columns={columns}
|
473
|
dataSource={shownData}
|
474
|
scroll={{ y: 'calc(100vh - 300px)' }}
|
475
|
size="small"
|
476
|
title={() => (
|
477
|
<Stack direction="horizontal" gap={3}>
|
478
|
<Button type="primary" onClick={openAddCategoryModal}>
|
479
|
Přidat kategorii
|
480
|
</Button>
|
481
|
<Search
|
482
|
placeholder="Vyhledání tagu"
|
483
|
onSearch={searchTag}
|
484
|
style={{ width: 200 }}
|
485
|
className="ms-auto"
|
486
|
allowClear
|
487
|
/>
|
488
|
</Stack>
|
489
|
)}
|
490
|
/>
|
491
|
</Row>
|
492
|
</Container>
|
493
|
</MainLayout>
|
494
|
);
|
495
|
}
|
496
|
|
497
|
export default TagsPage;
|