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

395 lines
12 KiB
TypeScript
Raw Normal View History

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';
2023-07-20 17:11:03 +03:00
import Divider from '../../components/Common/Divider';
2023-09-10 20:17:18 +03:00
import DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../components/DataTable';
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';
2023-09-10 20:17:18 +03:00
import useWindowSize from '../../hooks/useWindowSize';
2023-09-11 20:31:54 +03:00
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../models/rsform'
2023-09-21 14:58:01 +03:00
import { colorfgCstStatus } from '../../utils/color';
import { prefixes } from '../../utils/constants';
2023-09-21 14:58:01 +03:00
import { describeExpressionStatus, labelCstTypification } from '../../utils/labels';
import { getCstTypePrefix, getCstTypeShortcut } from '../../utils/misc';
2023-09-10 20:17:18 +03:00
// Window width cutoff for columns
const COLUMN_DEFINITION_HIDE_THRESHOLD = 1000;
const COLUMN_TYPE_HIDE_THRESHOLD = 1200;
const COLUMN_CONVENTION_HIDE_THRESHOLD = 1800;
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-09-15 23:48:04 +03:00
const { colors, mainHeight } = useConceptTheme();
2023-09-10 20:17:18 +03:00
const windowSize = useWindowSize();
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-09-10 20:17:18 +03:00
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
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,
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,
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;
2023-09-10 20:17:18 +03:00
case 'й':
case 'q': handleCreateCst(CstType.FUNCTION); return true;
2023-09-10 20:17:18 +03:00
case 'ц':
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]);
2023-09-10 20:17:18 +03:00
useLayoutEffect(
() => {
setColumnVisibility({
'type': (windowSize.width ?? 0) >= COLUMN_TYPE_HIDE_THRESHOLD,
'convention': (windowSize.width ?? 0) >= COLUMN_CONVENTION_HIDE_THRESHOLD,
'definition': (windowSize.width ?? 0) >= COLUMN_DEFINITION_HIDE_THRESHOLD
});
}, [windowSize]);
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,
2023-09-10 20:17:18 +03:00
maxSize: 65,
cell: props => {
const cst = props.row.original;
2023-08-16 00:39:16 +03:00
return (<>
<div
id={`${prefixes.cst_list}${cst.alias}`}
className='w-full min-w-[3.1rem] max-w-[3.1rem] px-1 text-center rounded-md whitespace-nowrap'
style={{
borderWidth: "1px",
2023-09-21 14:58:01 +03:00
borderColor: colorfgCstStatus(cst.status, colors),
color: colorfgCstStatus(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'
>
2023-09-21 14:58:01 +03:00
<p><span className='font-semibold'>Статус</span>: {describeExpressionStatus(cst.status)}</p>
2023-08-16 00:39:16 +03:00
</ConceptTooltip>
</>);
}
}),
2023-09-21 14:58:01 +03:00
columnHelper.accessor(cst => labelCstTypification(cst), {
2023-08-16 00:39:16 +03:00
id: 'type',
header: 'Типизация',
2023-09-10 20:17:18 +03:00
size: 150,
minSize: 150,
maxSize: 150,
enableHiding: true,
cell: props => <div className='text-sm min-w-[9.3rem] max-w-[9.3rem] break-words'>{props.getValue()}</div>
}),
columnHelper.accessor(cst => cst.term_resolved || cst.term_raw || '', {
2023-08-16 00:39:16 +03:00
id: 'term',
header: 'Термин',
2023-09-10 20:17:18 +03:00
size: 500,
minSize: 150,
2023-09-10 20:17:18 +03:00
maxSize: 500
}),
columnHelper.accessor('definition_formal', {
2023-08-16 00:39:16 +03:00
id: 'expression',
header: 'Формальное определение',
2023-09-10 20:17:18 +03:00
size: 1000,
minSize: 300,
maxSize: 1000,
cell: props => <div className='break-words'>{props.getValue()}</div>
}),
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
2023-08-16 00:39:16 +03:00
id: 'definition',
header: 'Текстовое определение',
2023-09-10 20:17:18 +03:00
size: 1000,
minSize: 200,
2023-09-10 20:17:18 +03:00
maxSize: 1000,
cell: props => <div className='text-xs'>{props.getValue()}</div>
}),
columnHelper.accessor('convention', {
2023-08-16 00:39:16 +03:00
id: 'convention',
header: 'Конвенция / Комментарий',
2023-09-10 20:17:18 +03:00
size: 500,
minSize: 100,
2023-09-10 20:17:18 +03:00
maxSize: 500,
enableHiding: true,
cell: props => <div className='text-xs'>{props.getValue()}</div>
})
2023-08-27 00:19:19 +03:00
], [colors]);
2023-07-15 17:46:19 +03:00
2023-07-29 15:37:49 +03:00
return (
2023-09-15 23:48:04 +03:00
<div
className='w-full outline-none'
style={{minHeight: mainHeight}}
tabIndex={0}
onKeyDown={handleTableKey}
>
<div className='sticky top-0 flex justify-start w-full gap-1 px-2 py-1 border-b items-center h-[2.2rem] select-none clr-app'>
<div className='mr-3 min-w-[9rem] whitespace-nowrap'>
Выбор {selected.length} из {schema?.stats?.count_all ?? 0}
2023-07-20 17:11:03 +03:00
</div>
2023-09-10 20:17:18 +03:00
<div className='flex items-center justify-start w-full gap-1'>
<Button
tooltip='Переместить вверх'
icon={<ArrowUpIcon size={6}/>}
disabled={!isEditable || nothingSelected}
dense
onClick={handleMoveUp}
/>
<Button
tooltip='Переместить вниз'
icon={<ArrowDownIcon size={6}/>}
disabled={!isEditable || nothingSelected}
dense
onClick={handleMoveDown}
/>
<Button
tooltip='Удалить выбранные'
icon={<DumpBinIcon color={isEditable && !nothingSelected ? 'text-warning' : ''} size={6}/>}
disabled={!isEditable || nothingSelected}
dense
onClick={handleDelete}
/>
<Divider vertical margins='my-1' />
<Button
tooltip='Сбросить имена'
icon={<MeshIcon color={isEditable ? 'text-primary': ''} size={6}/>}
dense
disabled={!isEditable}
onClick={handleReindex}
/>
<Button
tooltip='Новая конституента'
icon={<SmallPlusIcon color={isEditable ? 'text-success': ''} size={6}/>}
dense
disabled={!isEditable}
onClick={() => handleCreateCst()}
2023-07-30 16:48:25 +03:00
/>
2023-09-10 20:17:18 +03:00
{(Object.values(CstType)).map(
(typeStr) => {
const type = typeStr as CstType;
return (
<Button key={type}
text={getCstTypePrefix(type)}
tooltip={getCstTypeShortcut(type)}
dense
dimensions='w-[1.4rem]'
2023-09-10 20:17:18 +03:00
disabled={!isEditable}
tabIndex={-1}
onClick={() => handleCreateCst(type)}
/>);
})}
<div id='items-table-help'>
<HelpIcon color='text-primary' size={6} />
</div>
<ConceptTooltip anchorSelect='#items-table-help' offset={30}>
<HelpRSFormItems />
</ConceptTooltip>
</div>
2023-07-20 17:11:03 +03:00
</div>
2023-09-15 23:48:04 +03:00
<div className='w-full h-full text-sm'>
2023-09-10 20:17:18 +03:00
<DataTable
data={schema?.items ?? []}
columns={columns}
dense
onRowDoubleClicked={handleRowDoubleClicked}
onRowClicked={handleRowClicked}
enableHiding
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
enableRowSelection
rowSelection={rowSelection}
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()}
>
2023-09-10 20:17:18 +03:00
Создать новую конституенту
</p>
</span>
}
/>
</div>
</div>);
2023-07-15 17:46:19 +03:00
}
2023-07-28 00:03:37 +03:00
export default EditorItems;