Minor UI improvements

This commit is contained in:
IRBorisov 2023-12-02 00:15:56 +03:00
parent 6999e086d5
commit b026a57fad
7 changed files with 252 additions and 197 deletions

View File

@ -35,17 +35,16 @@ function Modal({
if (onSubmit) onSubmit(); if (onSubmit) onSubmit();
}; };
return ( return (<>
<>
<div className='fixed top-0 left-0 w-full h-full z-navigation clr-modal-backdrop' /> <div className='fixed top-0 left-0 w-full h-full z-navigation clr-modal-backdrop' />
<div ref={ref} <div ref={ref}
className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-4 py-3 flex flex-col justify-start w-fit max-w-[calc(100vw-2rem)] overflow-x-auto h-fit z-modal clr-app border shadow-md' className='fixed bottom-1/2 left-1/2 translate-y-1/2 -translate-x-1/2 px-4 flex flex-col justify-start w-fit max-w-[calc(100vw-2rem)] overflow-x-auto h-fit z-modal clr-app border shadow-md'
> >
{title ? <h1 className='pb-3 text-xl select-none'>{title}</h1> : null} {title ? <h1 className='py-2 text-lg select-none'>{title}</h1> : null}
<div className='max-h-[calc(100vh-8rem)] overflow-auto px-2'> <div className='max-h-[calc(100vh-8rem)] overflow-auto px-2'>
{children} {children}
</div> </div>
<div className='flex justify-center w-full gap-4 pt-3 mt-2 border-t-2 z-modal-controls'> <div className='flex justify-center w-full gap-6 py-3 z-modal-controls'>
{!readonly ? {!readonly ?
<Button autoFocus <Button autoFocus
text={submitText} text={submitText}
@ -62,8 +61,7 @@ function Modal({
/> />
</div> </div>
</div> </div>
</> </>);
);
} }
export default Modal; export default Modal;

View File

@ -1,7 +1,7 @@
function HelpTerminologyControl() { function HelpTerminologyControl() {
return ( return (
<div> <div className='flex flex-col gap-1'>
<h1>Терминологизация: Контроль терминологии</h1> <h1>Терминологизация: Контроль терминологии</h1>
<p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p> <p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p>
<p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>связывание слов.</i></p> <p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>связывание слов.</i></p>

View File

@ -3,7 +3,7 @@ import { Dispatch, useCallback, useEffect, useMemo, useState } from 'react';
import MiniButton from '../../components/Common/MiniButton'; import MiniButton from '../../components/Common/MiniButton';
import DataTable, { IConditionalStyle } from '../../components/DataTable'; import DataTable, { IConditionalStyle } from '../../components/DataTable';
import { CheckIcon, CrossIcon } from '../../components/Icons'; import { ArrowsRotateIcon, CheckIcon, CrossIcon } from '../../components/Icons';
import RSInput from '../../components/RSInput'; import RSInput from '../../components/RSInput';
import ConstituentaPicker from '../../components/Shared/ConstituentaPicker'; import ConstituentaPicker from '../../components/Shared/ConstituentaPicker';
import { useConceptTheme } from '../../context/ThemeContext'; import { useConceptTheme } from '../../context/ThemeContext';
@ -32,6 +32,15 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
const [argumentValue, setArgumentValue] = useState(''); const [argumentValue, setArgumentValue] = useState('');
const selectedClearable = useMemo(
() => {
return argumentValue && !!selectedArgument && !!selectedArgument.value;
}, [argumentValue, selectedArgument]);
const isModified = useMemo(
() => (selectedArgument && argumentValue !== selectedArgument.value),
[selectedArgument, argumentValue]);
useEffect( useEffect(
() => { () => {
if (!selectedArgument && state.arguments.length > 0) { if (!selectedArgument && state.arguments.length > 0) {
@ -55,24 +64,25 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
const handleClearArgument = useCallback( const handleClearArgument = useCallback(
(target: IArgumentValue) => { (target: IArgumentValue) => {
target.value = ''; const newArg = { ...target, value: '' }
partialUpdate({ partialUpdate({
arguments: [ arguments: state.arguments.map((arg) => (arg.alias !== target.alias ? arg : newArg))
target,
...state.arguments.filter(arg => arg.alias !== target.alias)
]
}); });
setSelectedArgument(newArg);
}, [partialUpdate, state.arguments]); }, [partialUpdate, state.arguments]);
const handleReset = useCallback(
() => {
setArgumentValue(selectedArgument?.value ?? '');
}, [selectedArgument]);
const handleAssignArgument = useCallback( const handleAssignArgument = useCallback(
(target: IArgumentValue, value: string) => { (target: IArgumentValue, value: string) => {
target.value = value; const newArg = { ...target, value: value }
partialUpdate({ partialUpdate({
arguments: [ arguments: state.arguments.map((arg) => (arg.alias !== target.alias ? arg : newArg))
target,
...state.arguments.filter(arg => arg.alias !== target.alias)
]
}); });
setSelectedArgument(newArg);
}, [partialUpdate, state.arguments]); }, [partialUpdate, state.arguments]);
const columns = useMemo( const columns = useMemo(
@ -148,21 +158,31 @@ function ArgumentsTab({ state, schema, partialUpdate }: ArgumentsTabProps) {
{selectedArgument?.alias || 'ARG'} {selectedArgument?.alias || 'ARG'}
</span> </span>
<span>=</span> <span>=</span>
<RSInput <RSInput noTooltip
dimensions='max-w-[12rem] w-full' dimensions='max-w-[12rem] w-full'
value={argumentValue} value={argumentValue}
noTooltip
onChange={newValue => setArgumentValue(newValue)} onChange={newValue => setArgumentValue(newValue)}
/> />
<MiniButton <div className='flex'>
tooltip='Подставить значение аргумента' <MiniButton
icon={<CheckIcon tooltip='Подставить значение аргумента'
size={5} icon={<CheckIcon size={5} color={!argumentValue || !selectedArgument ? 'text-disabled' : 'text-success'} />}
color={!argumentValue || !selectedArgument ? 'text-disabled' : 'text-success'} disabled={!argumentValue || !selectedArgument}
/>} onClick={() => handleAssignArgument(selectedArgument!, argumentValue)}
disabled={!argumentValue || !selectedArgument} />
onClick={() => handleAssignArgument(selectedArgument!, argumentValue)} <MiniButton
/> tooltip='Откатить значение'
disabled={!isModified}
onClick={handleReset}
icon={<ArrowsRotateIcon size={5} color={isModified ? 'text-primary' : ''} />}
/>
<MiniButton
tooltip='Очистить значение аргумента'
disabled={!selectedClearable}
icon={<CrossIcon size={5} color={!selectedClearable ? 'text-disabled' : 'text-warning'}/>}
onClick={() => selectedArgument ? handleClearArgument(selectedArgument) : undefined}
/>
</div>
</div> </div>
<ConstituentaPicker <ConstituentaPicker

View File

@ -218,8 +218,9 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
</div> </div>
<ConceptTooltip <ConceptTooltip
anchorSelect='#terminology-help' anchorSelect='#terminology-help'
className='max-w-[30rem] z-modal-tooltip' className='max-w-[40rem]'
offset={4} layer='z-modal-tooltip'
offset={1}
> >
<HelpTerminologyControl /> <HelpTerminologyControl />
</ConceptTooltip> </ConceptTooltip>

View File

@ -27,7 +27,7 @@ function ConstituentaToolbar({
return ( return (
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute right-0 flex items-start justify-center w-full top-1'> <div className='absolute right-0 flex items-start justify-center w-full top-1'>
<div className=' flex justify-start w-fit select-auto z-pop'> <div className='flex justify-start select-auto w-fit z-tooltip'>
<MiniButton <MiniButton
tooltip='Сохранить изменения' tooltip='Сохранить изменения'
disabled={!canSave} disabled={!canSave}
@ -35,7 +35,7 @@ function ConstituentaToolbar({
onClick={onSubmit} onClick={onSubmit}
/> />
<MiniButton <MiniButton
tooltip='Сборсить несохраненные изменения' tooltip='Сбросить несохраненные изменения'
disabled={!canSave} disabled={!canSave}
onClick={onReset} onClick={onReset}
icon={<ArrowsRotateIcon size={5} color={canSave ? 'text-primary' : ''} />} icon={<ArrowsRotateIcon size={5} color={canSave ? 'text-primary' : ''} />}
@ -67,7 +67,10 @@ function ConstituentaToolbar({
<div id='cst-help' className='px-1 py-1'> <div id='cst-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} /> <HelpIcon color='text-primary' size={5} />
</div> </div>
<ConceptTooltip anchorSelect='#cst-help' offset={4}> <ConceptTooltip
anchorSelect='#cst-help'
offset={4}
>
<HelpConstituenta /> <HelpConstituenta />
</ConceptTooltip> </ConceptTooltip>
</div> </div>

View File

@ -1,22 +1,11 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react'; import { useLayoutEffect, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../../components/DataTable'; import { type RowSelectionState } from '../../../components/DataTable';
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
import { useRSForm } from '../../../context/RSFormContext'; import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext'; import { CstType, ICstCreateData, ICstMovetoData } from '../../../models/rsform'
import useWindowSize from '../../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../../models/rsform'
import { prefixes } from '../../../utils/constants';
import { labelCstTypification } from '../../../utils/labels';
import RSListToolbar from './RSListToolbar'; import RSListToolbar from './RSListToolbar';
import RSTable from './RSTable';
// 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>();
interface EditorRSListProps { interface EditorRSListProps {
onOpenEdit: (cstID: number) => void onOpenEdit: (cstID: number) => void
@ -26,13 +15,25 @@ interface EditorRSListProps {
} }
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) { function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
const { colors, noNavigation } = useConceptTheme();
const windowSize = useWindowSize();
const { schema, editorMode: isEditable, cstMoveTo, resetAliases } = useRSForm(); const { schema, editorMode: isEditable, cstMoveTo, resetAliases } = useRSForm();
const [selected, setSelected] = useState<number[]>([]); const [selected, setSelected] = useState<number[]>([]);
const [rowSelection, setRowSelection] = useState<RowSelectionState>({}); const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
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]);
// Delete selected constituents // Delete selected constituents
function handleDelete() { function handleDelete() {
@ -179,123 +180,19 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
} }
} }
switch (code) { switch (code) {
case 'Backquote': handleCreateCst(); return true; case 'Backquote': handleCreateCst(); return true;
case 'Digit1': handleCreateCst(CstType.BASE); return true; case 'Digit1': handleCreateCst(CstType.BASE); return true;
case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true; case 'Digit2': handleCreateCst(CstType.STRUCTURED); return true;
case 'Digit3': handleCreateCst(CstType.TERM); return true; case 'Digit3': handleCreateCst(CstType.TERM); return true;
case 'Digit4': handleCreateCst(CstType.AXIOM); return true; case 'Digit4': handleCreateCst(CstType.AXIOM); return true;
case 'KeyQ': handleCreateCst(CstType.FUNCTION); return true; case 'KeyQ': handleCreateCst(CstType.FUNCTION); return true;
case 'KeyW': handleCreateCst(CstType.PREDICATE); return true; case 'KeyW': handleCreateCst(CstType.PREDICATE); return true;
case 'Digit5': handleCreateCst(CstType.CONSTANT); return true; case 'Digit5': handleCreateCst(CstType.CONSTANT); return true;
case 'Digit6': handleCreateCst(CstType.THEOREM); return true; case 'Digit6': handleCreateCst(CstType.THEOREM); return true;
} }
return false; return false;
} }
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(
() => {
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]);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
maxSize: 65,
cell: props =>
<ConstituentaBadge
theme={colors}
value={props.row.original}
prefixID={prefixes.cst_list}
shortTooltip
/>
}),
columnHelper.accessor(cst => labelCstTypification(cst), {
id: 'type',
header: 'Типизация',
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 || '', {
id: 'term',
header: 'Термин',
size: 500,
minSize: 150,
maxSize: 500
}),
columnHelper.accessor('definition_formal', {
id: 'expression',
header: 'Формальное определение',
size: 1000,
minSize: 300,
maxSize: 1000,
cell: props => <div className='break-words'>{props.getValue()}</div>
}),
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
id: 'definition',
header: 'Текстовое определение',
size: 1000,
minSize: 200,
maxSize: 1000,
cell: props => <div className='text-xs'>{props.getValue()}</div>
}),
columnHelper.accessor('convention', {
id: 'convention',
header: 'Конвенция / Комментарий',
size: 500,
minSize: 100,
maxSize: 500,
enableHiding: true,
cell: props => <div className='text-xs'>{props.getValue()}</div>
})
], [colors]);
const tableHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 7.2rem - 4px)'
: 'calc(100vh - 4.4rem - 4px)';
}, [noNavigation]);
return ( return (
<div tabIndex={-1} <div tabIndex={-1}
className='w-full outline-none' className='w-full outline-none'
@ -317,37 +214,14 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
onReindex={handleReindex} onReindex={handleReindex}
/> />
</div> </div>
<div className='w-full h-full overflow-auto text-sm' style={{maxHeight: tableHeight}}>
<DataTable dense noFooter
data={schema?.items ?? []}
columns={columns}
headPosition='0rem'
onRowDoubleClicked={handleRowDoubleClicked} <RSTable
onRowClicked={handleRowClicked} items={schema?.items}
selected={rowSelection}
enableHiding setSelected={setRowSelection}
columnVisibility={columnVisibility} onEdit={onOpenEdit}
onColumnVisibilityChange={setColumnVisibility} onCreateNew={() => handleCreateCst()}
/>
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()}
>
Создать новую конституенту
</p>
</span>
}
/>
</div>
</div>); </div>);
} }

View File

@ -0,0 +1,159 @@
import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import DataTable, { createColumnHelper,RowSelectionState,VisibilityState } from '../../../components/DataTable';
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
import { useConceptTheme } from '../../../context/ThemeContext';
import useWindowSize from '../../../hooks/useWindowSize';
import { IConstituenta } from '../../../models/rsform';
import { prefixes } from '../../../utils/constants';
import { labelCstTypification } from '../../../utils/labels';
interface RSTableProps {
items?: IConstituenta[]
selected: RowSelectionState
setSelected: React.Dispatch<React.SetStateAction<RowSelectionState>>
onEdit: (cstID: number) => void
onCreateNew: () => void
}
// 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>();
function RSTable({
items, selected, setSelected,
onEdit, onCreateNew
}: RSTableProps) {
const { colors, noNavigation } = useConceptTheme();
const windowSize = useWindowSize();
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
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]);
const handleRowClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
if (event.altKey) {
event.preventDefault();
onEdit(cst.id);
}
}, [onEdit]);
const handleRowDoubleClicked = useCallback(
(cst: IConstituenta, event: React.MouseEvent<Element, MouseEvent>) => {
event.preventDefault();
onEdit(cst.id);
}, [onEdit]);
const columns = useMemo(
() => [
columnHelper.accessor('alias', {
id: 'alias',
header: 'Имя',
size: 65,
minSize: 65,
maxSize: 65,
cell: props =>
<ConstituentaBadge
theme={colors}
value={props.row.original}
prefixID={prefixes.cst_list}
shortTooltip
/>
}),
columnHelper.accessor(cst => labelCstTypification(cst), {
id: 'type',
header: 'Типизация',
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 || '', {
id: 'term',
header: 'Термин',
size: 500,
minSize: 150,
maxSize: 500
}),
columnHelper.accessor('definition_formal', {
id: 'expression',
header: 'Формальное определение',
size: 1000,
minSize: 300,
maxSize: 1000,
cell: props => <div className='break-words'>{props.getValue()}</div>
}),
columnHelper.accessor(cst => cst.definition_resolved || cst.definition_raw || '', {
id: 'definition',
header: 'Текстовое определение',
size: 1000,
minSize: 200,
maxSize: 1000,
cell: props => <div className='text-xs'>{props.getValue()}</div>
}),
columnHelper.accessor('convention', {
id: 'convention',
header: 'Конвенция / Комментарий',
size: 500,
minSize: 100,
maxSize: 500,
enableHiding: true,
cell: props => <div className='text-xs'>{props.getValue()}</div>
})
], [colors]);
const tableHeight = useMemo(
() => {
return !noNavigation ?
'calc(100vh - 7.2rem - 4px)'
: 'calc(100vh - 4.4rem - 4px)';
}, [noNavigation]);
return (
<div className='w-full h-full overflow-auto text-sm min-h-[20rem]' style={{maxHeight: tableHeight}}>
<DataTable dense noFooter
data={items ?? []}
columns={columns}
headPosition='0rem'
onRowDoubleClicked={handleRowDoubleClicked}
onRowClicked={handleRowClicked}
enableHiding
columnVisibility={columnVisibility}
onColumnVisibilityChange={setColumnVisibility}
enableRowSelection
rowSelection={selected}
onRowSelectionChange={setSelected}
noDataComponent={
<span className='flex flex-col justify-center p-2 text-center'>
<p>Список пуст</p>
<p
className='cursor-pointer text-primary hover:underline'
onClick={() => onCreateNew()}
>
Создать новую конституенту
</p>
</span>
}
/>
</div>);
}
export default RSTable;