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();
};
return (
<>
return (<>
<div className='fixed top-0 left-0 w-full h-full z-navigation clr-modal-backdrop' />
<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'>
{children}
</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 ?
<Button autoFocus
text={submitText}
@ -62,8 +61,7 @@ function Modal({
/>
</div>
</div>
</>
);
</>);
}
export default Modal;

View File

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

View File

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

View File

@ -27,7 +27,7 @@ function ConstituentaToolbar({
return (
<div className='relative w-full'>
<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
tooltip='Сохранить изменения'
disabled={!canSave}
@ -35,7 +35,7 @@ function ConstituentaToolbar({
onClick={onSubmit}
/>
<MiniButton
tooltip='Сборсить несохраненные изменения'
tooltip='Сбросить несохраненные изменения'
disabled={!canSave}
onClick={onReset}
icon={<ArrowsRotateIcon size={5} color={canSave ? 'text-primary' : ''} />}
@ -67,7 +67,10 @@ function ConstituentaToolbar({
<div id='cst-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip anchorSelect='#cst-help' offset={4}>
<ConceptTooltip
anchorSelect='#cst-help'
offset={4}
>
<HelpConstituenta />
</ConceptTooltip>
</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 DataTable, { createColumnHelper, type RowSelectionState,VisibilityState } from '../../../components/DataTable';
import ConstituentaBadge from '../../../components/Shared/ConstituentaBadge';
import { type RowSelectionState } from '../../../components/DataTable';
import { useRSForm } from '../../../context/RSFormContext';
import { useConceptTheme } from '../../../context/ThemeContext';
import useWindowSize from '../../../hooks/useWindowSize';
import { CstType, IConstituenta, ICstCreateData, ICstMovetoData } from '../../../models/rsform'
import { prefixes } from '../../../utils/constants';
import { labelCstTypification } from '../../../utils/labels';
import { CstType, ICstCreateData, ICstMovetoData } from '../../../models/rsform'
import RSListToolbar from './RSListToolbar';
// 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>();
import RSTable from './RSTable';
interface EditorRSListProps {
onOpenEdit: (cstID: number) => void
@ -26,13 +15,25 @@ interface EditorRSListProps {
}
function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: EditorRSListProps) {
const { colors, noNavigation } = useConceptTheme();
const windowSize = useWindowSize();
const { schema, editorMode: isEditable, cstMoveTo, resetAliases } = useRSForm();
const [selected, setSelected] = useState<number[]>([]);
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
function handleDelete() {
@ -192,110 +193,6 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
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 (
<div tabIndex={-1}
className='w-full outline-none'
@ -318,36 +215,13 @@ function EditorRSList({ onOpenEdit, onCreateCst, onDeleteCst, onTemplates }: Edi
/>
</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}
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()}
>
Создать новую конституенту
</p>
</span>
}
<RSTable
items={schema?.items}
selected={rowSelection}
setSelected={setRowSelection}
onEdit={onOpenEdit}
onCreateNew={() => handleCreateCst()}
/>
</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;