Refactor and optimize dialogs

This commit is contained in:
IRBorisov 2023-12-07 01:21:27 +03:00
parent 38b661f290
commit eab63814d5
20 changed files with 196 additions and 164 deletions

View File

@ -9,7 +9,7 @@ extends Omit<React.DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTML
function Label({ text, tooltip, className, ...restProps }: LabelProps) {
return (
<label
className={`text-sm font-semibold ${className} whitespace-nowrap`}
className={`text-sm font-semibold whitespace-nowrap ${className}`}
title={tooltip}
{...restProps}
>

View File

@ -16,12 +16,13 @@ export interface ModalProps {
onSubmit?: () => void
onCancel?: () => void
children: React.ReactNode
className?: string
}
function Modal({
title, hideWindow, onSubmit,
readonly, onCancel, canSubmit,
submitInvalidTooltip,
submitInvalidTooltip, className,
children,
submitText = 'Продолжить'
}: ModalProps) {
@ -42,9 +43,9 @@ function Modal({
<>
<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-6 w-fit max-w-[calc(100vw-2rem)] h-fit z-modal clr-app border shadow-md'
className='fixed -translate-x-1/2 translate-y-1/2 border shadow-md bottom-1/2 left-1/2 z-modal clr-app'
>
<Overlay position='right-[-1rem] top-2' className='text-disabled'>
<Overlay position='right-[0.3rem] top-2' className='text-disabled'>
<MiniButton
tooltip='Закрыть диалоговое окно [ESC]'
icon={<CrossIcon size={5}/>}
@ -52,25 +53,32 @@ function Modal({
/>
</Overlay>
{title ? <h1 className='my-2 text-lg select-none'>{title}</h1> : null}
{title ? <h1 className='px-12 py-2 text-lg select-none'>{title}</h1> : null}
<div className='max-h-[calc(100vh-8rem)] overflow-auto flex flex-col justify-start '>
<div
className={`w-full h-fit ${className}`}
style={{
maxHeight: 'calc(100vh - 8rem)',
maxWidth: 'calc(100vw - 2rem)',
overflow: 'auto'
}}
>
{children}
</div>
<div className='flex justify-center w-full gap-6 my-3 z-modal-controls'>
<div className='flex justify-center w-full gap-12 px-6 py-3 z-modal-controls min-w-fit'>
{!readonly ?
<Button autoFocus
text={submitText}
tooltip={!canSubmit ? submitInvalidTooltip: ''}
dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit'
dimensions='min-w-[8rem] min-h-[2.6rem]'
colors='clr-btn-primary'
disabled={!canSubmit}
onClick={handleSubmit}
/> : null}
<Button
text={readonly ? 'Закрыть' : 'Отмена'}
dimensions='min-w-[8rem] min-h-[2.6rem] w-fit h-fit'
dimensions='min-w-[8rem] min-h-[2.6rem]'
onClick={handleCancel}
/>
</div>

View File

@ -25,7 +25,8 @@ function SelectMulti<Option, Group extends GroupBase<Option> = GroupBase<Option>
control: (styles, { isDisabled }) => ({
...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer'
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
option: (styles, { isSelected }) => ({
...styles,

View File

@ -25,7 +25,8 @@ function SelectSingle<Option, Group extends GroupBase<Option> = GroupBase<Option
control: (styles, { isDisabled }) => ({
...styles,
borderRadius: '0.25rem',
cursor: isDisabled ? 'not-allowed' : 'pointer'
cursor: isDisabled ? 'not-allowed' : 'pointer',
boxShadow: 'none'
}),
menuPortal: (styles) => ({
...styles,

View File

@ -87,7 +87,7 @@ function RSInput({
{ tag: tags.literal, color: colors.fgBlue }, // literals
{ tag: tags.controlKeyword, fontWeight: '500'}, // R | I | D
{ tag: tags.unit, fontSize: '0.75rem' }, // indicies
{ tag: tags.brace, color:colors.fgPurple, fontWeight: '500' }, // braces (curly brackets)
{ tag: tags.brace, color:colors.fgPurple, fontWeight: '700' }, // braces (curly brackets)
]
}), [disabled, colors, darkMode]);
@ -108,12 +108,14 @@ function RSInput({
if (event.altKey) {
if (text.processAltKey(event.code, event.shiftKey)) {
event.preventDefault();
event.stopPropagation();
}
} else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
if (newSymbol) {
text.replaceWith(newSymbol);
event.preventDefault();
event.stopPropagation();
}
}
}, [thisRef]);

View File

@ -60,8 +60,8 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
canSubmit={canSubmit}
submitText='Создать'
onSubmit={handleSubmit}
className='flex flex-col gap-3 px-6 py-2'
>
<div className='flex flex-col gap-3'>
<TextInput
label='Полное название'
value={title}
@ -83,7 +83,6 @@ function DlgCloneLibraryItem({ hideWindow, base }: DlgCloneLibraryItemProps) {
value={common}
setValue={value => setCommon(value)}
/>
</div>
</Modal>);
}

View File

@ -114,33 +114,35 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
canSubmit={validated}
onSubmit={handleSubmit}
submitText='Создать'
className='max-w-[43rem] min-w-[43rem] min-h-[35rem] px-6'
>
<Tabs defaultFocus forceRenderTabPanel
className='flex flex-col items-center max-w-[40rem] min-w-[40rem] min-h-[35rem] mb-1'
selectedTabClassName='clr-selected'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<Overlay position='top-0 left-[12.3rem]'>
<Overlay position='top-0 right-[6rem]'>
<HelpButton topic={HelpTopic.RSTEMPLATES} dimensions='max-w-[35rem]' />
</Overlay>
<TabList className='flex mb-3 border'>
<ConceptTab
label='Шаблон'
tooltip='Выбор шаблона выражения'
className='border-r w-[8rem]'
/>
<ConceptTab
label='Аргументы'
tooltip='Подстановка аргументов шаблона'
className='border-r w-[8rem]'
/>
<ConceptTab
label='Конституента'
tooltip='Редактирование атрибутов конституенты'
className='w-[8rem]'
/>
<TabList className='flex justify-center mb-3'>
<div className='flex border w-fit'>
<ConceptTab
label='Шаблон'
tooltip='Выбор шаблона выражения'
className='border-r w-[8rem]'
/>
<ConceptTab
label='Аргументы'
tooltip='Подстановка аргументов шаблона'
className='border-r w-[8rem]'
/>
<ConceptTab
label='Конституента'
tooltip='Редактирование атрибутов конституенты'
className='w-[8rem]'
/>
</div>
</TabList>
<div className='w-full'>

View File

@ -20,7 +20,6 @@ extends Pick<ModalProps, 'hideWindow'> {
function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstProps) {
const [validated, setValidated] = useState(false);
const [cstData, updateCstData] = usePartialUpdate(
initial || {
cst_type: CstType.BASE,
@ -53,51 +52,51 @@ function DlgCreateCst({ hideWindow, initial, schema, onCreate }: DlgCreateCstPro
canSubmit={validated}
onSubmit={handleSubmit}
submitText='Создать'
className='h-fit min-w-[35rem] py-2 flex flex-col justify-stretch gap-3 px-6'
>
<div className='h-fit w-[35rem] px-2 my-2 flex flex-col justify-stretch gap-3'>
<div className='flex justify-center w-full gap-6'>
<SelectSingle
className='min-w-[15rem] self-center'
placeholder='Выберите тип'
className='min-w-[15rem] self-center'
options={SelectorCstType}
value={{ value: cstData.cst_type, label: labelCstType(cstData.cst_type) }}
onChange={data => updateCstData({ cst_type: data?.value ?? CstType.BASE})}
/>
<TextInput id='alias' label='Имя'
dense
<TextInput dense
label='Имя'
dimensions='w-[7rem]'
value={cstData.alias}
onChange={event => updateCstData({ alias: event.target.value})}
/>
</div>
<TextArea id='term' label='Термин'
<TextArea spellCheck
label='Термин'
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2}
value={cstData.term_raw}
spellCheck
onChange={event => updateCstData({ term_raw: event.target.value })}
/>
<RSInput id='expression' label='Формальное определение'
<RSInput
label='Формальное определение'
placeholder='Родоструктурное выражение, задающее формальное определение'
height='5.1rem'
value={cstData.definition_formal}
onChange={value => updateCstData({definition_formal: value})}
/>
<TextArea id='definition' label='Текстовое определение'
<TextArea spellCheck
label='Текстовое определение'
placeholder='Лингвистическая интерпретация формального выражения'
rows={2}
value={cstData.definition_raw}
spellCheck
onChange={event => updateCstData({ definition_raw: event.target.value })}
/>
<TextArea id='convention' label='Конвенция / Комментарий'
<TextArea spellCheck
label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение'
rows={2}
value={cstData.convention}
spellCheck
onChange={event => updateCstData({ convention: event.target.value })}
/>
</div>
</Modal>);
}

View File

@ -1,69 +0,0 @@
import { useMemo, useState } from 'react';
import Checkbox from '../components/Common/Checkbox';
import Modal, { ModalProps } from '../components/Common/Modal';
import { useRSForm } from '../context/RSFormContext';
import { prefixes } from '../utils/constants';
import { labelConstituenta } from '../utils/labels';
interface DlgDeleteCstProps
extends Pick<ModalProps, 'hideWindow'> {
selected: number[]
onDelete: (items: number[]) => void
}
function DlgDeleteCst({ hideWindow, selected, onDelete }: DlgDeleteCstProps) {
const { schema } = useRSForm();
const [expandOut, setExpandOut] = useState(false);
const expansion: number[] = useMemo(() => schema?.graph.expandOutputs(selected) ?? [], [selected, schema?.graph]);
function handleSubmit() {
hideWindow();
if (expandOut) {
onDelete(selected.concat(expansion));
} else {
onDelete(selected);
}
}
return (
<Modal canSubmit
title='Удаление конституент'
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
hideWindow={hideWindow}
onSubmit={handleSubmit}
>
<div className='max-w-[60vw] min-w-[30rem]'>
<p>Выбраны к удалению: <b>{selected.length}</b></p>
<div className='px-3 border h-[9rem] mt-1 overflow-y-auto whitespace-nowrap'>
{selected.map(
(id) => {
const cst = schema!.items.find(cst => cst.id === id);
return (cst ?
<p key={`${prefixes.cst_delete_list}${cst.id}`}>
{labelConstituenta(cst)}
</p> : null);
})}
</div>
<p className='mt-4'>Зависимые конституенты: <b>{expansion.length}</b></p>
<div className='mt-1 mb-3 px-3 border h-[9rem] overflow-y-auto whitespace-nowrap'>
{expansion.map(
(id) => {
const cst = schema!.items.find(cst => cst.id === id);
return (cst ?
<p key={`${prefixes.cst_dependant_list}${cst.id}`}>
{labelConstituenta(cst)}
</p> : null);
})}
</div>
<Checkbox
label='Удалить зависимые конституенты'
value={expandOut}
setValue={value => setExpandOut(value)}
/>
</div>
</Modal>);
}
export default DlgDeleteCst;

View File

@ -0,0 +1,31 @@
import { IConstituenta } from '../../models/rsform';
import { labelConstituenta } from '../../utils/labels';
interface ConstituentsListProps {
list: number[]
items: IConstituenta[]
prefix: string
title?: string
}
function ConstituentsList({ list, items, title, prefix }: ConstituentsListProps) {
return (
<div>
{title ?
<p className='pb-1'>
{title}: <b>{list.length}</b>
</p> : null}
<div className='px-3 border h-[9rem] overflow-y-auto whitespace-nowrap'>
{list.map(
(id) => {
const cst = items.find(cst => cst.id === id);
return (cst ?
<p key={`${prefix}${cst.id}`}>
{labelConstituenta(cst)}
</p> : null);
})}
</div>
</div>);
}
export default ConstituentsList;

View File

@ -0,0 +1,57 @@
import { useMemo, useState } from 'react';
import Checkbox from '../../components/Common/Checkbox';
import Modal, { ModalProps } from '../../components/Common/Modal';
import { IRSForm } from '../../models/rsform';
import { prefixes } from '../../utils/constants';
import ConstituentsList from './ConstituentsList';
interface DlgDeleteCstProps
extends Pick<ModalProps, 'hideWindow'> {
selected: number[]
onDelete: (items: number[]) => void
schema: IRSForm
}
function DlgDeleteCst({ hideWindow, selected, schema, onDelete }: DlgDeleteCstProps) {
const [expandOut, setExpandOut] = useState(false);
const expansion: number[] = useMemo(() => schema.graph.expandOutputs(selected), [selected, schema.graph]);
function handleSubmit() {
hideWindow();
if (expandOut) {
onDelete(selected.concat(expansion));
} else {
onDelete(selected);
}
}
return (
<Modal canSubmit
title='Удаление конституент'
submitText={expandOut ? 'Удалить с зависимыми' : 'Удалить'}
hideWindow={hideWindow}
onSubmit={handleSubmit}
className='max-w-[60vw] min-w-[30rem] px-6 flex flex-col gap-3'
>
<ConstituentsList
title='Выбраны к удалению'
list={selected}
items={schema.items}
prefix={prefixes.cst_delete_list}
/>
<ConstituentsList
title='Зависимые конституенты'
list={expansion}
items={schema.items}
prefix={prefixes.cst_dependant_list}
/>
<Checkbox
label='Удалить зависимые конституенты'
value={expandOut}
setValue={value => setExpandOut(value)}
/>
</Modal>);
}
export default DlgDeleteCst;

View File

@ -0,0 +1 @@
export { default } from './DlgDeleteCst';

View File

@ -47,28 +47,30 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
hideWindow={hideWindow}
canSubmit={isValid}
onSubmit={handleSubmit}
className='items-center min-w-[40rem] max-w-[40rem] px-6 min-h-[34rem]'
>
<Tabs defaultFocus
className='flex flex-col items-center min-w-[40rem] max-w-[40rem] mb-2 min-h-[34rem]'
selectedTabClassName='clr-selected'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<Overlay position='top-0 left-[12.2rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={14} />
<Overlay position='top-0 right-[4rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[35rem]' offset={14} />
</Overlay>
<TabList className='flex mb-3 border'>
<ConceptTab
label={labelReferenceType(ReferenceType.ENTITY)}
tooltip='Отсылка на термин в заданной словоформе'
className='w-[12rem] border-r-2'
/>
<ConceptTab
label={labelReferenceType(ReferenceType.SYNTACTIC)}
tooltip='Установление синтаксической связи с отсылкой на термин'
className='w-[12rem]'
/>
<TabList className='flex justify-center mb-3'>
<div className='flex border w-fit'>
<ConceptTab
label={labelReferenceType(ReferenceType.ENTITY)}
tooltip='Отсылка на термин в заданной словоформе'
className='w-[12rem] border-r-2'
/>
<ConceptTab
label={labelReferenceType(ReferenceType.SYNTACTIC)}
tooltip='Установление синтаксической связи с отсылкой на термин'
className='w-[12rem]'
/>
</div>
</TabList>
<div className='w-full'>

View File

@ -10,15 +10,14 @@ interface WordformButtonProps {
function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...restProps }: WordformButtonProps) {
return (
<button type='button' tabIndex={-1}
onClick={() => onSelectGrams(grams)}
className={`min-w-[6rem] p-1 border rounded-none cursor-pointer clr-btn-clear clr-hover ${isSelected ? 'clr-selected': ''}`}
{...restProps}
>
<p className='font-semibold'>{text}</p>
<p>{example}</p>
</button>
);
<button type='button' tabIndex={-1}
onClick={() => onSelectGrams(grams)}
className={`min-w-[6rem] p-1 border rounded-none cursor-pointer clr-btn-clear clr-hover ${isSelected ? 'clr-selected': ''}`}
{...restProps}
>
<p className='font-semibold'>{text}</p>
<p>{example}</p>
</button>);
}
export default WordformButton;

View File

@ -1,5 +1,6 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import Label from '../components/Common/Label';
import MiniButton from '../components/Common/MiniButton';
import Modal from '../components/Common/Modal';
import Overlay from '../components/Common/Overlay';
@ -210,28 +211,28 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
hideWindow={hideWindow}
submitText='Сохранить'
onSubmit={handleSubmit}
className='min-w-[40rem] max-w-[40rem] px-6'
>
<div className='min-w-[40rem] max-w-[40rem]'>
<Overlay position='top-[-0.2rem] left-[7.5rem]'>
<HelpButton topic={HelpTopic.TERM_CONTROL} dimensions='max-w-[38rem]' offset={3} />
</Overlay>
<TextArea disabled spellCheck
label='Начальная форма'
placeholder='Начальная форма'
placeholder='Термин в начальной форме'
rows={1}
value={term}
/>
<div className='mt-3 mb-2 text-sm font-semibold'>
Параметры словоформы
<div className='mt-3 mb-2'>
<Label text='Параметры словоформы' />
</div>
<div className='flex items-start justify-between w-full'>
<div className='flex items-center'>
<TextArea
placeholder='Введите текст'
dimensions='min-w-[18rem] w-full min-h-[4.2rem]'
dimensions='min-w-[18rem] w-full min-h-[5rem]'
rows={2}
value={inputText}
onChange={event => setInputText(event.target.value)}
@ -309,7 +310,6 @@ function DlgEditWordForms({ hideWindow, target, onSave }: DlgEditWordFormsProps)
onRowClicked={handleRowClicked}
/>
</div>
</div>
</Modal>);
}

View File

@ -25,10 +25,10 @@ function DlgGraphParams({ hideWindow, initial, onConfirm } : DlgGraphParamsProps
title='Настройки графа термов'
onSubmit={handleSubmit}
submitText='Применить'
className='flex gap-12 px-6 py-2'
>
<div className='flex gap-6 my-2'>
<div className='flex flex-col gap-1'>
<h1>Преобразования</h1>
<h1 className='mb-2'>Преобразования</h1>
<Checkbox
label='Скрыть текст'
tooltip='Не отображать термины'
@ -55,7 +55,7 @@ function DlgGraphParams({ hideWindow, initial, onConfirm } : DlgGraphParamsProps
/>
</div>
<div className='flex flex-col gap-1'>
<h1>Типы конституент</h1>
<h1 className='mb-2'>Типы конституент</h1>
<Checkbox
label={labelCstType(CstType.BASE)}
value={params.allowBase}
@ -97,7 +97,6 @@ function DlgGraphParams({ hideWindow, initial, onConfirm } : DlgGraphParamsProps
setValue={value => updateParams({ allowTheorem: value})}
/>
</div>
</div>
</Modal>);
}

View File

@ -47,8 +47,8 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
hideWindow={hideWindow}
canSubmit={validated}
onSubmit={handleSubmit}
className='flex justify-center items-center gap-6 w-full min-w-[24rem] py-6 px-6'
>
<div className='flex justify-center items-center gap-6 w-full min-w-[22rem] my-3'>
<SelectSingle
placeholder='Выберите тип'
className='min-w-[14rem] self-center'
@ -67,7 +67,6 @@ function DlgRenameCst({ hideWindow, initial, onRename }: DlgRenameCstProps) {
onChange={event => updateData({alias: event.target.value})}
/>
</div>
</div>
</Modal>);
}

View File

@ -53,9 +53,9 @@ function DlgShowAST({ hideWindow, syntaxTree, expression }: DlgShowASTProps) {
, []);
return (
<Modal
<Modal readonly
hideWindow={hideWindow}
readonly
className='px-6'
>
<div className='w-full my-2 text-lg text-center'>
{!hoverNode ? expression : null}

View File

@ -44,20 +44,20 @@ function DlgUploadRSForm({ hideWindow }: DlgUploadRSFormProps) {
canSubmit={!!file}
onSubmit={handleSubmit}
submitText='Загрузить'
className='w-[20rem] px-6'
>
<div className='flex flex-col items-start min-w-[20rem] max-w-[20rem]'>
<FileInput
label='Выбрать файл'
acceptType={EXTEOR_TRS_FILE}
onChange={handleFile}
/>
<Checkbox
label='Загружать название и комментарий'
dimensions='w-fit pb-2'
value={loadMetadata}
setValue={value => setLoadMetadata(value)}
/>
</div>
<FileInput
label='Выбрать файл'
dimensions='w-full flex items-center'
acceptType={EXTEOR_TRS_FILE}
onChange={handleFile}
/>
<Checkbox
label='Загружать название и комментарий'
dimensions='w-fit py-2'
value={loadMetadata}
setValue={value => setLoadMetadata(value)}
/>
</Modal>);
}

View File

@ -362,6 +362,7 @@ function RSTabs() {
/> : null}
{showDeleteCst ?
<DlgDeleteCst
schema={schema!}
hideWindow={() => setShowDeleteCst(false)}
onDelete={handleDeleteCst}
selected={toBeDeleted}