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

363 lines
12 KiB
TypeScript
Raw Normal View History

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 ConceptDataTable from '../../components/Common/ConceptDataTable';
2023-07-30 16:48:25 +03:00
import ConceptTooltip from '../../components/Common/ConceptTooltip';
2023-07-20 17:11:03 +03:00
import Divider from '../../components/Common/Divider';
2023-08-16 10:11:22 +03:00
import CstStatusInfo from '../../components/Help/InfoCstStatus';
2023-07-30 16:48:25 +03:00
import { ArrowDownIcon, ArrowsRotateIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, SmallPlusIcon } from '../../components/Icons';
2023-07-25 20:27:29 +03:00
import { useRSForm } from '../../context/RSFormContext';
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
import { getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
2023-07-15 17:46:19 +03:00
2023-07-28 00:03:37 +03:00
interface EditorItemsProps {
onOpenEdit: (cstID: string) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: string[], callback: (items: string[]) => void) => void
2023-07-15 17:46:19 +03:00
}
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
const { noNavigation } = useConceptTheme();
const [selected, setSelected] = useState<string[]>([]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
2023-07-15 17:46:19 +03:00
2023-07-27 22:04:25 +03:00
const [toggledClearRows, setToggledClearRows] = useState(false);
2023-07-15 17:46:19 +03:00
// Delete selected constituents
function handleDelete() {
if (!schema) {
return;
}
onDeleteCst(selected, () => {
2023-07-27 22:04:25 +03:00
setToggledClearRows(prev => !prev);
setSelected([]);
2023-07-27 22:04:25 +03:00
});
}
2023-07-20 17:11:03 +03:00
// Move selected cst up
function handleMoveUp() {
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 target = Math.max(0, currentIndex - 1) + 1
2023-07-25 20:27:29 +03:00
const data = {
2023-07-27 22:04:25 +03:00
items: selected.map(id => { return { id: id }; }),
move_to: target
}
2023-07-25 20:27:29 +03:00
cstMoveTo(data);
}
2023-07-15 17:46:19 +03:00
// Move selected cst down
function handleMoveDown() {
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 target = Math.min(schema.items.length - 1, currentIndex - count + 2) + 1
const data: ICstMovetoData = {
2023-07-27 22:04:25 +03:00
items: selected.map(id => { return { id: id }; }),
move_to: target
}
2023-07-25 20:27:29 +03:00
cstMoveTo(data);
}
2023-07-15 17:46:19 +03:00
// Generate new names for all constituents
function handleReindex() {
2023-07-27 22:04:25 +03:00
resetAliases(() => toast.success('Переиндексация конституент успешна'));
}
2023-07-25 20:27:29 +03:00
// Add new constituenta
function handleCreateCst(type?: CstType) {
if (!schema) {
return;
}
const selectedPosition = selected.reduce((prev, cstID) => {
const position = schema.items.findIndex(cst => cst.id === cstID);
return Math.max(position, prev);
}, -1);
const insert_where = selectedPosition >= 0 ? schema.items[selectedPosition].id : undefined;
const data: ICstCreateData = {
insert_after: insert_where ?? null,
cst_type: type ?? CstType.BASE,
alias: '',
term_raw: '',
definition_formal: '',
definition_raw: '',
convention: '',
};
onCreateCst(data, type !== undefined);
}
// Implement hotkeys for working with constituents table
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
2023-07-27 22:04:25 +03:00
if (!isEditable) {
return;
}
if (event.key === 'Delete' && selected.length > 0) {
event.preventDefault();
handleDelete();
return;
}
if (!event.altKey || event.shiftKey) {
return;
}
if (processAltKey(event.key)) {
event.preventDefault();
return;
2023-07-22 12:24:14 +03:00
}
}
function processAltKey(key: string): boolean {
if (selected.length > 0) {
switch (key) {
case 'ArrowUp': handleMoveUp(); return true;
case 'ArrowDown': handleMoveDown(); return true;
}
}
switch (key) {
case '1': handleCreateCst(CstType.BASE); return true;
case '2': handleCreateCst(CstType.STRUCTURED); return true;
case '3': handleCreateCst(CstType.TERM); return true;
case '4': handleCreateCst(CstType.AXIOM); return true;
case 'q': handleCreateCst(CstType.FUNCTION); return true;
case 'w': handleCreateCst(CstType.PREDICATE); return true;
case '5': handleCreateCst(CstType.CONSTANT); return true;
case '6': handleCreateCst(CstType.THEOREM); return true;
}
return false;
}
2023-07-25 20:27:29 +03:00
const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey) {
onOpenEdit(cst.id);
}
}, [onOpenEdit]);
const handleSelectionChange = useCallback(
({ selectedRows }: {
allSelected: boolean
selectedCount: number
selectedRows: IConstituenta[]
}) => {
setSelected(selectedRows.map(cst => cst.id));
}, [setSelected]);
2023-08-16 00:39:16 +03:00
const columns = useMemo(
() => [
{
name: 'ID',
id: 'id',
selector: (cst: IConstituenta) => cst.id,
omit: true
},
{
name: 'Имя',
id: 'alias',
selector: (cst: IConstituenta) => cst.alias,
cell: (cst: IConstituenta) => {
const info = mapStatusInfo.get(cst.status)!;
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className={`w-full rounded-md text-center ${info.color}`}
>
{cst.alias}
2023-07-15 17:46:19 +03:00
</div>
2023-08-16 00:39:16 +03:00
<ConceptTooltip
anchorSelect={`#${prefixes.cst_list}${cst.alias}`}
place='right'
>
<p><b>Статус: </b> {info.tooltip}</p>
</ConceptTooltip>
</>);
2023-07-15 17:46:19 +03:00
},
2023-08-16 00:39:16 +03:00
width: '65px',
maxWidth: '65px',
reorder: true,
},
{
name: 'Тип',
id: 'type',
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{getCstTypificationLabel(cst)}</div>,
width: '175px',
maxWidth: '175px',
wrap: true,
reorder: true,
hide: 1600
},
{
name: 'Термин',
id: 'term',
selector: (cst: IConstituenta) => cst.term?.resolved ?? cst.term?.raw ?? '',
width: '350px',
minWidth: '150px',
maxWidth: '350px',
wrap: true,
reorder: true
},
{
name: 'Формальное определение',
id: 'expression',
selector: (cst: IConstituenta) => cst.definition?.formal ?? '',
minWidth: '300px',
maxWidth: '500px',
grow: 2,
wrap: true,
reorder: true
},
{
name: 'Текстовое определение',
id: 'definition',
cell: (cst: IConstituenta) => (
<div style={{ fontSize: 12 }}>
{cst.definition?.text.resolved ?? cst.definition?.text.raw ?? ''}
</div>
),
minWidth: '200px',
grow: 2,
wrap: true,
reorder: true
},
{
name: 'Конвенция / Комментарий',
id: 'convention',
cell: (cst: IConstituenta) => <div style={{ fontSize: 12 }}>{cst.convention ?? ''}</div>,
minWidth: '100px',
wrap: true,
reorder: true,
hide: 1800
}
], []);
2023-07-15 17:46:19 +03:00
2023-07-29 15:37:49 +03:00
return (
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] select-none clr-app' +
2023-08-03 17:34:55 +03:00
(!noNavigation ? ' sticky z-10 top-[0rem]' : ' 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>
2023-08-04 13:26:51 +03:00
<div className='flex items-center justify-start w-full gap-1'>
<Button
tooltip='Переместить вверх'
icon={<ArrowUpIcon size={6}/>}
2023-08-04 13:26:51 +03:00
disabled={!isEditable || nothingSelected}
dense
onClick={handleMoveUp}
/>
<Button
tooltip='Переместить вниз'
icon={<ArrowDownIcon size={6}/>}
2023-08-04 13:26:51 +03:00
disabled={!isEditable || nothingSelected}
dense
onClick={handleMoveDown}
/>
<Button
tooltip='Удалить выбранные'
2023-07-25 20:27:29 +03:00
icon={<DumpBinIcon color={!nothingSelected ? 'text-red' : ''} size={6}/>}
2023-08-04 13:26:51 +03:00
disabled={!isEditable || nothingSelected}
dense
onClick={handleDelete}
/>
<Divider vertical margins='my-1' />
<Button
tooltip='Переиндексировать имена'
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
dense
2023-08-04 13:26:51 +03:00
disabled={!isEditable}
onClick={handleReindex}
/>
<Button
tooltip='Новая конституента'
icon={<SmallPlusIcon color='text-green' size={6}/>}
dense
2023-08-04 13:26:51 +03:00
disabled={!isEditable}
onClick={() => handleCreateCst()}
/>
{(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={getCstTypeShortcut(type)}
dense
2023-08-04 13:26:51 +03:00
disabled={!isEditable}
onClick={() => handleCreateCst(type)}
/>;
})}
2023-07-30 16:48:25 +03:00
<div id='items-table-help'>
<HelpIcon color='text-primary' size={6} />
</div>
<ConceptTooltip anchorSelect='#items-table-help'>
<div>
<h1>Горячие клавиши</h1>
<p><b>Двойной клик / Alt + клик</b> - редактирование конституенты</p>
<p><b>Клик на квадрат слева</b> - выделение конституенты</p>
2023-07-30 16:48:25 +03:00
<p><b>Alt + вверх/вниз</b> - движение конституент</p>
<p><b>Delete</b> - удаление конституент</p>
2023-08-15 21:22:21 +03:00
<p><b>Alt + 1-6,Q,W</b> - добавление конституент</p>
<Divider margins='mt-2' />
2023-08-15 21:43:15 +03:00
<CstStatusInfo title='Статусы' />
2023-07-30 16:48:25 +03:00
</div>
</ConceptTooltip>
2023-08-04 13:26:51 +03:00
</div>
2023-07-20 17:11:03 +03:00
</div>
2023-07-30 16:48:25 +03:00
<div className='w-full h-full' onKeyDown={handleTableKey}>
<ConceptDataTable
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-30 16:48:25 +03:00
2023-07-20 17:11:03 +03:00
striped
highlightOnHover
pointerOnHover
2023-07-30 16:48:25 +03:00
2023-07-20 17:11:03 +03:00
selectableRows
selectableRowsHighlight
onSelectedRowsChange={handleSelectionChange}
onRowDoubleClicked={cst => onOpenEdit(cst.id)}
2023-07-20 17:11:03 +03:00
onRowClicked={handleRowClicked}
2023-07-27 22:04:25 +03:00
clearSelectedRows={toggledClearRows}
2023-07-20 17:11:03 +03:00
dense
2023-07-30 16:48:25 +03:00
/>
</div>
2023-07-20 17:11:03 +03:00
</div>
2023-07-29 15:37:49 +03:00
);
2023-07-15 17:46:19 +03:00
}
2023-07-28 00:03:37 +03:00
export default EditorItems;