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

382 lines
12 KiB
TypeScript
Raw Normal View History

import { createColumnHelper,RowSelectionState } from '@tanstack/react-table';
import { useCallback, useLayoutEffect, 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';
2023-07-30 16:48:25 +03:00
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import DataTable from '../../components/Common/DataTable';
2023-07-20 17:11:03 +03:00
import Divider from '../../components/Common/Divider';
2023-08-23 18:11:42 +03:00
import HelpRSFormItems from '../../components/Help/HelpRSFormItems';
import { ArrowDownIcon, ArrowUpIcon, DumpBinIcon, HelpIcon, MeshIcon, SmallPlusIcon } from '../../components/Icons';
2023-07-25 20:27:29 +03:00
import { useRSForm } from '../../context/RSFormContext';
2023-08-27 00:19:19 +03:00
import { useConceptTheme } from '../../context/ThemeContext';
import { prefixes } from '../../utils/constants';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../utils/models'
import { getCstStatusFgColor, getCstTypePrefix, getCstTypeShortcut, getCstTypificationLabel, mapStatusInfo } from '../../utils/staticUI';
const columnHelper = createColumnHelper<IConstituenta>();
2023-07-15 17:46:19 +03:00
2023-07-28 00:03:37 +03:00
interface EditorItemsProps {
onOpenEdit: (cstID: number) => void
onCreateCst: (initial: ICstCreateData, skipDialog?: boolean) => void
onDeleteCst: (selected: number[], callback: (items: number[]) => void) => void
2023-07-15 17:46:19 +03:00
}
function EditorItems({ onOpenEdit, onCreateCst, onDeleteCst }: EditorItemsProps) {
2023-08-27 00:19:19 +03:00
const { colors } = useConceptTheme();
const { schema, isEditable, cstMoveTo, resetAliases } = useRSForm();
const [selected, setSelected] = useState<number[]>([]);
const nothingSelected = useMemo(() => selected.length === 0, [selected]);
2023-07-15 17:46:19 +03:00
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
2023-07-15 17:46:19 +03:00
// Delete selected constituents
function handleDelete() {
if (!schema) {
return;
}
onDeleteCst(selected, () => {
setRowSelection({});
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 = {
items: selected.map(id => ({ id: id })),
move_to: target
}
cstMoveTo(data, () => {
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
})
setRowSelection(newSelection);
});
}
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 = {
items: selected.map(id => ({ id: id })),
move_to: target
}
cstMoveTo(data, () => {
const newSelection: RowSelectionState = {};
selected.forEach((_, index) => {
newSelection[String(target + index - 1)] = true;
})
setRowSelection(newSelection);
});
}
2023-07-15 17:46:19 +03:00
// Generate new names for all constituents
function handleReindex() {
resetAliases(() => toast.success('Имена конституент обновлены'));
}
2023-07-25 20:27:29 +03:00
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) {
event.preventDefault();
onOpenEdit(cst.id);
}
}, [onOpenEdit]);
const handleRowDoubleClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
event.preventDefault();
onOpenEdit(cst.id);
}, [onOpenEdit]);
useLayoutEffect(
() => {
if (!schema || Object.keys(rowSelection).length === 0) {
setSelected([]);
} else {
const selected: number[] = [];
schema.items.forEach((cst, index) => {
if (rowSelection[String(index)] === true) {
selected.push(cst.id);
}
});
setSelected(selected);
}
}, [rowSelection, schema]);
2023-08-16 00:39:16 +03:00
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
2023-08-16 00:39:16 +03:00
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
cell: props => {
const cst = props.row.original;
const info = mapStatusInfo.get(cst.status);
2023-08-16 00:39:16 +03:00
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className='w-full px-1 text-center rounded-md whitespace-nowrap'
style={{
borderWidth: "1px",
borderColor: getCstStatusFgColor(cst.status, colors),
color: getCstStatusFgColor(cst.status, colors),
2023-09-09 17:51:34 +03:00
fontWeight: 600,
backgroundColor: colors.bgInput
}}
2023-08-16 00:39:16 +03:00
>
{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><span className='font-semibold'>Статус</span>: {info!.tooltip}</p>
2023-08-16 00:39:16 +03:00
</ConceptTooltip>
</>);
}
}),
columnHelper.accessor(cst => getCstTypificationLabel(cst), {
2023-08-16 00:39:16 +03:00
id: 'type',
header: 'Типизация',
size: 175,
maxSize: 175,
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
}),
columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
2023-08-16 00:39:16 +03:00
id: 'term',
header: 'Термин',
size: 350,
minSize: 150,
maxSize: 350
}),
columnHelper.accessor('definition_formal', {
2023-08-16 00:39:16 +03:00
id: 'expression',
header: 'Формальное определение',
size: 300,
minSize: 300,
maxSize: 500
}),
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
2023-08-16 00:39:16 +03:00
id: 'definition',
header: 'Текстовое определение',
size: 200,
minSize: 200,
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
}),
columnHelper.accessor('convention', {
2023-08-16 00:39:16 +03:00
id: 'convention',
header: 'Конвенция / Комментарий',
minSize: 100,
maxSize: undefined,
cell: props => <div style={{ fontSize: 12 }}>{props.getValue()}</div>
}),
2023-08-27 00:19:19 +03:00
], [colors]);
2023-07-15 17:46:19 +03:00
// name: 'Типизация',
// hide: 1600
// },
// {
// name: 'Формальное определение',
// grow: 2,
// },
// {
// name: 'Текстовое определение',
// grow: 2,
// },
// {
// name: 'Конвенция / Комментарий',
// id: 'convention',
// hide: 1800
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
2023-09-05 23:18:21 +03:00
className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem] select-none clr-app'
>
2023-07-25 20:27:29 +03:00
<div className='mr-3 whitespace-nowrap'>
Выбраны
<span className='ml-2'>
2023-09-05 00:23:53 +03:00
{selected.length} из {schema?.stats?.count_all ?? 0}
2023-07-25 20:27:29 +03:00
</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='Удалить выбранные'
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={6}/>}
2023-08-04 13:26:51 +03:00
disabled={!isEditable || nothingSelected}
dense
onClick={handleDelete}
/>
<Divider vertical margins='my-1' />
<Button
tooltip='Сбросить имена'
icon={<MeshIcon color={isEditable ? 'text-primary': ''} size={6}/>}
dense
2023-08-04 13:26:51 +03:00
disabled={!isEditable}
onClick={handleReindex}
/>
<Button
tooltip='Новая конституента'
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} 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-08-22 23:45:59 +03:00
return (
<Button key={type}
2023-09-04 19:12:27 +03:00
text={getCstTypePrefix(type)}
tooltip={getCstTypeShortcut(type)}
dense
2023-08-22 23:45:59 +03:00
widthClass='w-[1.4rem]'
2023-08-04 13:26:51 +03:00
disabled={!isEditable}
2023-09-04 19:12:27 +03:00
tabIndex={-1}
onClick={() => handleCreateCst(type)}
2023-08-22 23:45:59 +03:00
/>);
})}
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' offset={30}>
2023-08-23 18:11:42 +03:00
<HelpRSFormItems />
2023-07-30 16:48:25 +03:00
</ConceptTooltip>
2023-08-04 13:26:51 +03:00
</div>
2023-07-20 17:11:03 +03:00
</div>
<div className='w-full h-full text-sm' onKeyDown={handleTableKey}>
<DataTable
2023-07-25 20:27:29 +03:00
data={schema?.items ?? []}
2023-07-20 17:11:03 +03:00
columns={columns}
state={{
rowSelection: rowSelection
}}
enableMultiRowSelection
onRowDoubleClicked={handleRowDoubleClicked}
onRowClicked={handleRowClicked}
onRowSelectionChange={setRowSelection}
noDataComponent={
<span className='flex flex-col justify-center p-2 text-center'>
<p>Список пуст</p>
<p
className='cursor-pointer text-primary hover:underline'
onClick={() => handleCreateCst()}>
Создать новую конституенту
</p>
</span>
}
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;