ConceptPortal-public/rsconcept/frontend/src/pages/RSFormPage/ConstituentsTable.tsx

350 lines
11 KiB
TypeScript
Raw Normal View History

2023-07-25 20:27:29 +03:00
import { type AxiosResponse } from 'axios';
2023-07-15 17:46:19 +03:00
import { useCallback, useMemo, useState } from 'react';
2023-07-20 17:11:03 +03:00
import { toast } from 'react-toastify';
2023-07-25 20:27:29 +03:00
import Button from '../../components/Common/Button';
import DataTableThemed from '../../components/Common/DataTableThemed';
2023-07-20 17:11:03 +03:00
import Divider from '../../components/Common/Divider';
2023-07-25 20:27:29 +03:00
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, SmallPlusIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { CstType, type IConstituenta, type INewCstData, inferStatus, ParsingStatus, ValueClass } from '../../utils/models'
import { createAliasFor, getCstTypeLabel, getCstTypePrefix, getStatusInfo, getTypeLabel } from '../../utils/staticUI';
2023-07-22 12:24:14 +03:00
import CreateCstModal from './CreateCstModal';
2023-07-15 17:46:19 +03:00
interface ConstituentsTableProps {
onOpenEdit: (cst: IConstituenta) => void
}
2023-07-25 20:27:29 +03:00
function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
const {
schema, isEditable,
cstCreate, cstDelete, cstMoveTo
} = useRSForm();
const { noNavigation } = useConceptTheme();
const [selected, setSelected] = useState<number[]>([]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
2023-07-15 17:46:19 +03:00
const [showCstModal, setShowCstModal] = useState(false);
2023-07-15 17:46:19 +03:00
2023-07-20 17:11:03 +03:00
const handleRowClicked = useCallback(
2023-07-25 20:27:29 +03:00
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey) {
onOpenEdit(cst);
2023-07-20 17:11:03 +03:00
}
2023-07-25 20:27:29 +03:00
}, [onOpenEdit]);
2023-07-20 17:11:03 +03:00
const handleSelectionChange = useCallback(
2023-07-25 20:27:29 +03:00
({ selectedRows }: {
allSelected: boolean
selectedCount: number
selectedRows: IConstituenta[]
}) => {
setSelected(selectedRows.map((cst) => cst.id));
}, [setSelected]);
2023-07-25 20:27:29 +03:00
// Delete selected constituents
2023-07-20 17:11:03 +03:00
const handleDelete = useCallback(() => {
if (!schema?.items || !window.confirm('Вы уверены, что хотите удалить выбранные конституенты?')) {
return;
}
2023-07-25 20:27:29 +03:00
const data = {
items: selected.map(id => { return { id }; })
}
2023-07-25 20:27:29 +03:00
const deletedNames = selected.map(id => schema.items?.find((cst) => cst.id === id)?.alias);
cstDelete(data, () => toast.success(`Конституенты удалены: ${deletedNames.toString()}`));
}, [selected, schema?.items, cstDelete]);
2023-07-20 17:11:03 +03:00
// Move selected cst up
const handleMoveUp = useCallback(
() => {
if (!schema?.items || selected.length === 0) {
return;
}
const currentIndex = schema.items.reduce((prev, cst, index) => {
2023-07-25 20:27:29 +03:00
if (!selected.includes(cst.id)) {
return prev;
} else if (prev === -1) {
return index;
}
return Math.min(prev, index);
}, -1);
const insertIndex = Math.max(0, currentIndex - 1) + 1
2023-07-25 20:27:29 +03:00
const data = {
items: selected.map(id => { return { id }; }),
move_to: insertIndex
}
2023-07-25 20:27:29 +03:00
cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]);
2023-07-15 17:46:19 +03:00
// Move selected cst down
const handleMoveDown = useCallback(
2023-07-25 20:27:29 +03:00
() => {
if (!schema?.items || selected.length === 0) {
return;
}
let count = 0;
const currentIndex = schema.items.reduce((prev, cst, index) => {
2023-07-25 20:27:29 +03:00
if (!selected.includes(cst.id)) {
return prev;
} else {
count += 1;
if (prev === -1) {
return index;
}
return Math.max(prev, index);
}
}, -1);
const insertIndex = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
2023-07-25 20:27:29 +03:00
const data = {
items: selected.map(id => { return { id }; }),
move_to: insertIndex
}
2023-07-25 20:27:29 +03:00
cstMoveTo(data);
}, [selected, schema?.items, cstMoveTo]);
2023-07-15 17:46:19 +03:00
// Generate new names for all constituents
2023-07-20 17:11:03 +03:00
const handleReindex = useCallback(() => {
toast.info('Переиндексация');
}, []);
2023-07-25 20:27:29 +03:00
// Add new constituent
const handleAddNew = useCallback((csttype?: CstType) => {
2023-07-25 20:27:29 +03:00
if (!schema) {
return;
}
if (!csttype) {
2023-07-22 12:24:14 +03:00
setShowCstModal(true);
} else {
2023-07-25 20:27:29 +03:00
const data: INewCstData = {
csttype,
alias: createAliasFor(csttype, schema)
}
if (selected.length > 0) {
2023-07-25 20:27:29 +03:00
data.insert_after = selected[selected.length - 1]
}
cstCreate(data, (response: AxiosResponse) =>
2023-07-25 20:27:29 +03:00
toast.success(`Добавлена конституента ${response.data.new_cst.alias as string}`));
}
}, [schema, selected, cstCreate]);
// Implement hotkeys for working with constituents table
const handleTableKey = useCallback((event: React.KeyboardEvent<HTMLDivElement>) => {
if (!event.altKey) {
return;
}
if (!isEditable || selected.length === 0) {
return;
2023-07-22 12:24:14 +03:00
}
2023-07-25 20:27:29 +03:00
switch (event.key) {
case 'ArrowUp': handleMoveUp(); return;
2023-07-25 20:27:29 +03:00
case 'ArrowDown': handleMoveDown();
}
}, [isEditable, selected, handleMoveUp, handleMoveDown]);
2023-07-25 20:27:29 +03:00
const columns = useMemo(() =>
2023-07-15 17:46:19 +03:00
[
{
name: 'ID',
id: 'id',
selector: (cst: IConstituenta) => cst.id,
2023-07-25 20:27:29 +03:00
omit: true
2023-07-15 17:46:19 +03:00
},
{
name: 'Статус',
id: 'status',
2023-07-25 20:27:29 +03:00
cell: (cst: IConstituenta) =>
<div style={{ fontSize: 12 }}>
2023-07-20 17:11:03 +03:00
{getStatusInfo(inferStatus(cst.parse?.status, cst.parse?.valueClass)).text}
</div>,
2023-07-15 17:46:19 +03:00
width: '80px',
maxWidth: '80px',
reorder: true,
hide: 1280,
conditionalCellStyles: [
{
when: (cst: IConstituenta) => cst.parse?.status !== ParsingStatus.VERIFIED,
classNames: ['bg-[#ffc9c9]', 'dark:bg-[#592b2b]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID,
classNames: ['bg-[#beeefa]', 'dark:bg-[#286675]']
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
2023-07-25 20:27:29 +03:00
}
]
2023-07-15 17:46:19 +03:00
},
{
name: 'Имя',
id: 'alias',
selector: (cst: IConstituenta) => cst.alias,
width: '65px',
maxWidth: '65px',
reorder: true,
conditionalCellStyles: [
{
when: (cst: IConstituenta) => cst.parse?.status !== ParsingStatus.VERIFIED,
2023-07-20 17:11:03 +03:00
classNames: ['bg-[#ff8080]', 'dark:bg-[#800000]']
2023-07-15 17:46:19 +03:00
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.INVALID,
2023-07-20 17:11:03 +03:00
classNames: ['bg-[#ffbb80]', 'dark:bg-[#964600]']
2023-07-15 17:46:19 +03:00
},
{
when: (cst: IConstituenta) => cst.parse?.status === ParsingStatus.VERIFIED && cst.parse?.valueClass === ValueClass.PROPERTY,
classNames: ['bg-[#a5e9fa]', 'dark:bg-[#36899e]']
2023-07-25 20:27:29 +03:00
}
]
2023-07-15 17:46:19 +03:00
},
{
name: 'Тип',
id: 'type',
2023-07-25 20:27:29 +03:00
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getTypeLabel(cst)}</div>,
2023-07-15 17:46:19 +03:00
width: '140px',
minWidth: '100px',
maxWidth: '140px',
wrap: true,
reorder: true,
2023-07-25 20:27:29 +03:00
hide: 1600
2023-07-15 17:46:19 +03:00
},
{
name: 'Термин',
id: 'term',
2023-07-25 20:27:29 +03:00
selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '',
2023-07-15 17:46:19 +03:00
width: '350px',
minWidth: '150px',
maxWidth: '350px',
wrap: true,
2023-07-25 20:27:29 +03:00
reorder: true
2023-07-15 17:46:19 +03:00
},
{
name: 'Формальное определение',
id: 'expression',
2023-07-25 20:27:29 +03:00
selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
2023-07-20 17:11:03 +03:00
minWidth: '300px',
2023-07-15 17:46:19 +03:00
maxWidth: '500px',
2023-07-20 17:11:03 +03:00
grow: 2,
2023-07-15 17:46:19 +03:00
wrap: true,
2023-07-25 20:27:29 +03:00
reorder: true
2023-07-15 17:46:19 +03:00
},
{
name: 'Текстовое определение',
id: 'definition',
cell: (cst: IConstituenta) => (
2023-07-25 20:27:29 +03:00
<div style={{ fontSize: 12 }}>
{cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
2023-07-15 17:46:19 +03:00
</div>
),
minWidth: '200px',
2023-07-20 17:11:03 +03:00
grow: 2,
2023-07-15 17:46:19 +03:00
wrap: true,
2023-07-25 20:27:29 +03:00
reorder: true
2023-07-15 17:46:19 +03:00
},
{
name: 'Конвенция / Комментарий',
id: 'convention',
2023-07-25 20:27:29 +03:00
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{cst.convention ?? ''}</div>,
2023-07-20 17:11:03 +03:00
minWidth: '100px',
2023-07-15 17:46:19 +03:00
wrap: true,
reorder: true,
2023-07-25 20:27:29 +03:00
hide: 1800
}
2023-07-15 17:46:19 +03:00
], []
);
2023-07-22 12:24:14 +03:00
return (<>
<CreateCstModal
show={showCstModal}
2023-07-25 22:29:33 +03:00
hideWindow={() => { setShowCstModal(false); }}
2023-07-22 12:24:14 +03:00
onCreate={handleAddNew}
/>
2023-07-20 17:11:03 +03:00
<div className='w-full'>
2023-07-25 20:27:29 +03:00
<div
className={'flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] clr-app' +
(!noNavigation ? ' sticky z-10 top-[4rem]' : ' sticky z-10 top-[0rem]')}
>
2023-07-25 20:27:29 +03:00
<div className='mr-3 whitespace-nowrap'>
Выбраны
<span className='ml-2'>
<b>{selected.length}</b> из {schema?.stats?.count_all ?? 0}
</span>
</div>
{isEditable && <div className='flex justify-start w-full gap-1'>
<Button
tooltip='Переместить вверх'
icon={<ArrowUpIcon size={6}/>}
disabled={nothingSelected}
dense
onClick={handleMoveUp}
/>
<Button
tooltip='Переместить вниз'
icon={<ArrowDownIcon size={6}/>}
disabled={nothingSelected}
dense
onClick={handleMoveDown}
/>
<Button
tooltip='Удалить выбранные'
2023-07-25 20:27:29 +03:00
icon={<DumpBinIcon color={!nothingSelected ? 'text-red' : ''} size={6}/>}
disabled={nothingSelected}
dense
onClick={handleDelete}
/>
<Divider vertical margins='1' />
<Button
tooltip='Переиндексировать имена'
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
dense
onClick={handleReindex}
/>
<Button
tooltip='Новая конституента'
icon={<SmallPlusIcon color='text-green' size={6}/>}
dense
2023-07-25 20:27:29 +03:00
onClick={() => { handleAddNew(); }}
/>
{(Object.values(CstType)).map(
(typeStr) => {
const type = typeStr as CstType;
2023-07-25 20:27:29 +03:00
return <Button key={type}
text={`${getCstTypePrefix(type)}`}
tooltip={getCstTypeLabel(type)}
dense
2023-07-25 20:27:29 +03:00
onClick={() => { handleAddNew(type); }}
/>;
})}
</div>}
2023-07-20 17:11:03 +03:00
</div>
<div className='w-full h-full' onKeyDown={handleTableKey} tabIndex={0}>
2023-07-20 17:11:03 +03:00
<DataTableThemed
2023-07-25 20:27:29 +03:00
data={schema?.items ?? []}
2023-07-20 17:11:03 +03:00
columns={columns}
keyField='id'
noDataComponent={
<span className='flex flex-col justify-center p-2 text-center'>
<p>Список пуст</p>
<p>Создайте новую конституенту</p>
</span>
}
2023-07-15 17:46:19 +03:00
2023-07-20 17:11:03 +03:00
striped
highlightOnHover
pointerOnHover
2023-07-15 17:46:19 +03:00
2023-07-20 17:11:03 +03:00
selectableRows
selectableRowsHighlight
onSelectedRowsChange={handleSelectionChange}
2023-07-20 17:11:03 +03:00
onRowDoubleClicked={onOpenEdit}
onRowClicked={handleRowClicked}
dense
/>
</div>
2023-07-20 17:11:03 +03:00
</div>
2023-07-22 12:24:14 +03:00
</>);
2023-07-15 17:46:19 +03:00
}
export default ConstituentsTable;