UI: Improve editing experience

This commit is contained in:
IRBorisov 2023-11-05 18:41:28 +03:00
parent bdbf77faa2
commit 4cd8b31b59
13 changed files with 88 additions and 47 deletions

View File

@ -1,5 +1,5 @@
interface FormProps { interface FormProps {
title: string title?: string
dimensions?: string dimensions?: string
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void onSubmit: (event: React.FormEvent<HTMLFormElement>) => void
children: React.ReactNode children: React.ReactNode

View File

@ -3,20 +3,20 @@ import InfoCstStatus from './InfoCstStatus';
function HelpConstituenta() { function HelpConstituenta() {
return ( return (
<div> <div className='leading-tight'>
<h1>Подсказки</h1> <h1>Подсказки</h1>
<p><b className='text-warning'>Изменения сохраняются ПОСЛЕ нажатия на соответствующую кнопку снизу или по центру</b></p> <p><b>Сохранить изменения</b>: Ctrl + S или клик по кнопке Сохранить</p>
<p><b>Формальное определение</b> - обратите внимание на кнопки снизу<br/>Горячие клавиши указаны в подсказках при наведении</p> <p className='mt-1'><b>Формальное определение</b>: обратите внимание на кнопки снизу<br/>Горячие клавиши указаны в подсказках при наведении</p>
<p><b>Поля Термин и Определение</b> - Ctrl + Пробел открывает диалог редактирования отсылок<br/>Перед открытием диалога переместите текстовый курсор на заменяемое слово или ссылку</p> <p className='mt-1'><b>Поля Термин и Определение</b>: Ctrl + Пробел открывает диалог редактирования отсылок<br/>Перед открытием диалога переместите текстовый курсор на заменяемое слово или ссылку</p>
<p><b>Список конституент справа</b> - обратите внимание на настройки фильтрации</p> <p className='mt-1'><b>Список конституент справа</b>: обратите внимание на настройки фильтрации</p>
<p>- слева от ввода текста настраивается набор атрибутов конституенты</p> <p>- первая настройка - атрибуты конституенты</p>
<p>- справа от ввода текста настраивается список конституент, которые фильтруются</p> <p>- вторая настройка - принцип отбора конституент по графу термов</p>
<p>- текущая конституента выделена цветом строки</p> <p>- текущая конституента выделена цветом строки</p>
<p>- двойной клик / Alt + клик - выбор редактируемой конституенты</p> <p>- двойной клик / Alt + клик - выбор редактируемой конституенты</p>
<p>- при наведении на ID конституенты отображаются ее атрибуты</p> <p>- при наведении на имя конституенты отображаются ее атрибуты</p>
<p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p> <p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p>
<Divider margins='mt-2' /> <Divider margins='mt-4' />
<InfoCstStatus title='Статусы' /> <InfoCstStatus title='Статусы' />
</div>); </div>);

View File

@ -2,6 +2,7 @@ function HelpRSFormMeta() {
return ( return (
<div> <div>
<h1>Паспорт схемы</h1> <h1>Паспорт схемы</h1>
<p><b>Сохранить изменения</b>: Ctrl + S или клик по кнопке Сохранить</p>
<p><b>Владелец</b> - пользователь, обладающий правом редактирования</p> <p><b>Владелец</b> - пользователь, обладающий правом редактирования</p>
<p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p> <p>Для <b>общедоступных</b> схем владельцем может стать любой пользователь</p>
<p>Для <b>неизменных</b> схем правом редактирования обладают только администраторы</p> <p>Для <b>неизменных</b> схем правом редактирования обладают только администраторы</p>

View File

@ -19,7 +19,7 @@ function InfoCstClass({ title }: InfoCstClassProps) {
return ( return (
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span <span
className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm' className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm small-caps'
style={{backgroundColor: colorbgCstClass(cclass, colors)}} style={{backgroundColor: colorbgCstClass(cclass, colors)}}
> >
{labelCstClass(cclass)} {labelCstClass(cclass)}

View File

@ -21,7 +21,7 @@ function InfoCstStatus({ title }: InfoCstStatusProps) {
return ( return (
<p key={`${prefixes.cst_status_list}${index}`}> <p key={`${prefixes.cst_status_list}${index}`}>
<span <span
className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm' className='px-1 inline-block font-semibold min-w-[7rem] text-center border text-sm small-caps'
style={{backgroundColor: colorbgCstStatus(status, colors)}} style={{backgroundColor: colorbgCstStatus(status, colors)}}
> >
{labelExpressionStatus(status)} {labelExpressionStatus(status)}

View File

@ -104,29 +104,24 @@ function RSInput({
if (event.key === '*') { if (event.key === '*') {
text.insertToken(TokenID.DECART); text.insertToken(TokenID.DECART);
event.preventDefault(); event.preventDefault();
return; } else if (event.code === 'KeyB') {
}
if (event.code === 'KeyB') {
text.insertChar(''); text.insertChar('');
event.preventDefault(); event.preventDefault();
return; } else if (event.code === 'KeyZ') {
text.insertChar('Z');
event.preventDefault();
} }
} } else if (event.altKey) {
if (text.processAltKey(event.code, event.shiftKey)) {
if (event.altKey) { event.preventDefault();
if (!text.processAltKey(event.code, event.shiftKey)) {
return;
} }
} else if (!event.ctrlKey) { } else if (!event.ctrlKey) {
const newSymbol = getSymbolSubstitute(event.code, event.shiftKey); const newSymbol = getSymbolSubstitute(event.code, event.shiftKey);
if (!newSymbol) { if (newSymbol) {
return;
}
text.replaceWith(newSymbol); text.replaceWith(newSymbol);
} else {
return;
}
event.preventDefault(); event.preventDefault();
}
}
}, [thisRef]); }, [thisRef]);
return ( return (

View File

@ -66,7 +66,7 @@ function LoginPage() {
} }
return ( return (
<div className='flex items-start justify-center w-full pt-4 select-none' style={{minHeight: mainHeight}}> <div className='flex items-start justify-center w-full pt-8 select-none' style={{minHeight: mainHeight}}>
{ user && { user &&
<div className='flex flex-col items-center gap-2'> <div className='flex flex-col items-center gap-2'>
<p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p> <p className='font-semibold'>{`Вы вошли в систему как ${user.username}`}</p>
@ -87,10 +87,13 @@ function LoginPage() {
</div>} </div>}
{ !user && { !user &&
<Form <Form
title='Вход в Портал'
onSubmit={handleSubmit} onSubmit={handleSubmit}
dimensions='w-[24rem]' dimensions='w-[24rem]'
> >
<img alt='Концепт Портал'
src='/logo_full.svg'
className='max-h-[2.5rem] min-w-[2.5rem] mt-2 mb-4'
/>
<TextInput id='username' type='text' <TextInput id='username' type='text'
label='Имя пользователя' label='Имя пользователя'
required required
@ -107,7 +110,7 @@ function LoginPage() {
onChange={event => setPassword(event.target.value)} onChange={event => setPassword(event.target.value)}
/> />
<div className='flex justify-center w-full gap-2 py-2'> <div className='flex justify-center w-full py-2'>
<SubmitButton <SubmitButton
text='Войти' text='Войти'
dimensions='w-[12rem]' dimensions='w-[12rem]'

View File

@ -6,7 +6,7 @@ import MiniButton from '../../components/Common/MiniButton';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
import TextArea from '../../components/Common/TextArea'; import TextArea from '../../components/Common/TextArea';
import HelpConstituenta from '../../components/Help/HelpConstituenta'; import HelpConstituenta from '../../components/Help/HelpConstituenta';
import { CloneIcon, DumpBinIcon, EditIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons'; import { ArrowsRotateIcon, CloneIcon, DumpBinIcon, EditIcon, HelpIcon, SaveIcon, SmallPlusIcon } from '../../components/Icons';
import RefsInput from '../../components/RefsInput'; import RefsInput from '../../components/RefsInput';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useWindowSize from '../../hooks/useWindowSize'; import useWindowSize from '../../hooks/useWindowSize';
@ -47,6 +47,7 @@ function EditorConstituenta({
const [expression, setExpression] = useState(''); const [expression, setExpression] = useState('');
const [convention, setConvention] = useState(''); const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const [toggleReset, setToggleReset] = useState(false);
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]); const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
@ -77,7 +78,7 @@ function EditorConstituenta({
setExpression(activeCst.definition_formal || ''); setExpression(activeCst.definition_formal || '');
setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A'); setTypification(activeCst ? labelCstTypification(activeCst) : 'N/A');
} }
}, [activeCst, onOpenEdit, schema]); }, [activeCst, onOpenEdit, schema, toggleReset]);
function handleSubmit(event?: React.FormEvent<HTMLFormElement>) { function handleSubmit(event?: React.FormEvent<HTMLFormElement>) {
if (event) { if (event) {
@ -150,8 +151,17 @@ function EditorConstituenta({
onRenameCst(data); onRenameCst(data);
} }
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.ctrlKey && event.code === 'KeyS') {
if (isModified) {
handleSubmit();
}
event.preventDefault();
}
}
return ( return (
<div className='flex max-w-[1500px] gap-2'> <div className='flex max-w-[1500px] gap-2' tabIndex={0} onKeyDown={handleInput}>
<form onSubmit={handleSubmit} className='min-w-[50rem] max-w-[50rem] px-4 py-1'> <form onSubmit={handleSubmit} className='min-w-[50rem] max-w-[50rem] px-4 py-1'>
<div className='relative w-full'> <div className='relative w-full'>
<div className='absolute top-0 right-0 flex items-start justify-between w-full'> <div className='absolute top-0 right-0 flex items-start justify-between w-full'>
@ -184,6 +194,12 @@ function EditorConstituenta({
icon={<SaveIcon size={5} color={isModified && isEnabled ? 'text-primary' : ''}/>} icon={<SaveIcon size={5} color={isModified && isEnabled ? 'text-primary' : ''}/>}
onClick={() => handleSubmit()} onClick={() => handleSubmit()}
/> />
<MiniButton
tooltip='Сборсить несохраненные изменения'
disabled={!isEnabled || !isModified}
onClick={() => setToggleReset(prev => !prev)}
icon={<ArrowsRotateIcon size={5} color={isEnabled && isModified ? 'text-primary' : ''} />}
/>
<MiniButton <MiniButton
tooltip='Создать конституенту после данной' tooltip='Создать конституенту после данной'
disabled={!isEnabled} disabled={!isEnabled}
@ -234,6 +250,7 @@ function EditorConstituenta({
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression} value={expression}
disabled={!isEnabled} disabled={!isEnabled}
toggleReset={toggleReset}
onShowAST={onShowAST} onShowAST={onShowAST}
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
setTypification={setTypification} setTypification={setTypification}

View File

@ -24,6 +24,7 @@ interface EditorRSExpressionProps {
activeCst?: IConstituenta activeCst?: IConstituenta
label: string label: string
disabled?: boolean disabled?: boolean
toggleReset?: boolean
placeholder?: string placeholder?: string
onShowAST: (expression: string, ast: SyntaxTree) => void onShowAST: (expression: string, ast: SyntaxTree) => void
setTypification: (typificaiton: string) => void setTypification: (typificaiton: string) => void
@ -32,7 +33,7 @@ interface EditorRSExpressionProps {
} }
function EditorRSExpression({ function EditorRSExpression({
activeCst, disabled, value, onShowAST, activeCst, disabled, value, onShowAST, toggleReset,
setTypification, onChange, ...props setTypification, onChange, ...props
}: EditorRSExpressionProps) { }: EditorRSExpressionProps) {
const { schema } = useRSForm(); const { schema } = useRSForm();
@ -44,7 +45,7 @@ function EditorRSExpression({
useLayoutEffect(() => { useLayoutEffect(() => {
setIsModified(false); setIsModified(false);
resetParse(); resetParse();
}, [activeCst, resetParse]); }, [activeCst, resetParse, toggleReset]);
function handleChange(newvalue: string) { function handleChange(newvalue: string) {
onChange(newvalue); onChange(newvalue);

View File

@ -69,8 +69,10 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
} }
}, [schema]); }, [schema]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event?: React.FormEvent<HTMLFormElement>) => {
if (event) {
event.preventDefault(); event.preventDefault();
}
const data: IRSFormCreateData = { const data: IRSFormCreateData = {
item_type: LibraryItemType.RSFORM, item_type: LibraryItemType.RSFORM,
title: title, title: title,
@ -82,8 +84,17 @@ function EditorRSForm({ onDestroy, onClaim, onShare, isModified, setIsModified,
update(data, () => toast.success('Изменения сохранены')); update(data, () => toast.success('Изменения сохранены'));
}; };
function handleInput(event: React.KeyboardEvent<HTMLDivElement>) {
if (event.ctrlKey && event.code === 'KeyS') {
if (isModified) {
handleSubmit();
}
event.preventDefault();
}
}
return ( return (
<div> <div tabIndex={0} onKeyDown={handleInput}>
<div className='relative flex items-start justify-center w-full'> <div className='relative flex items-start justify-center w-full'>
<div className='absolute flex mt-1'> <div className='absolute flex mt-1'>
<MiniButton <MiniButton

View File

@ -66,12 +66,14 @@ function EditorPassword() {
<TextInput id='old_password' <TextInput id='old_password'
type='password' type='password'
label='Старый пароль' label='Старый пароль'
allowEnter
value={oldPassword} value={oldPassword}
onChange={event => setOldPassword(event.target.value)} onChange={event => setOldPassword(event.target.value)}
/> />
<TextInput id='new_password' type='password' <TextInput id='new_password' type='password'
colors={passwordColor} colors={passwordColor}
label='Новый пароль' label='Новый пароль'
allowEnter
value={newPassword} value={newPassword}
onChange={event => { onChange={event => {
setNewPassword(event.target.value); setNewPassword(event.target.value);
@ -80,6 +82,7 @@ function EditorPassword() {
<TextInput id='new_password_repeat' type='password' <TextInput id='new_password_repeat' type='password'
colors={passwordColor} colors={passwordColor}
label='Повторите новый' label='Повторите новый'
allowEnter
value={newPasswordRepeat} value={newPasswordRepeat}
onChange={event => { onChange={event => {
setNewPasswordRepeat(event.target.value); setNewPasswordRepeat(event.target.value);

View File

@ -55,17 +55,27 @@ function EditorProfile() {
<TextInput id='username' <TextInput id='username'
label='Логин' label='Логин'
tooltip='Логин изменить нельзя' tooltip='Логин изменить нельзя'
disabled={true} disabled
value={username} value={username}
onChange={event => setUsername(event.target.value)}
/> />
<TextInput id='first_name' <TextInput id='first_name'
label='Имя' label='Имя'
value={first_name} value={first_name}
allowEnter
onChange={event => setFirstName(event.target.value)} onChange={event => setFirstName(event.target.value)}
/> />
<TextInput id='last_name' label='Фамилия' value={last_name} onChange={event => setLastName(event.target.value)}/> <TextInput id='last_name'
<TextInput id='email' label='Электронная почта' value={email} onChange={event => setEmail(event.target.value)}/> label='Фамилия'
value={last_name}
allowEnter
onChange={event => setLastName(event.target.value)}
/>
<TextInput id='email'
label='Электронная почта'
allowEnter
value={email}
onChange={event => setEmail(event.target.value)}
/>
</div> </div>
<div className='flex justify-center w-full'> <div className='flex justify-center w-full'>
<SubmitButton <SubmitButton

View File

@ -152,8 +152,8 @@ export function labelCstSource(mode: DependencyMode): string {
case DependencyMode.EXPRESSION: return 'выражение'; case DependencyMode.EXPRESSION: return 'выражение';
case DependencyMode.OUTPUTS: return 'потребители'; case DependencyMode.OUTPUTS: return 'потребители';
case DependencyMode.INPUTS: return 'поставщики'; case DependencyMode.INPUTS: return 'поставщики';
case DependencyMode.EXPAND_INPUTS: return 'влияющие';
case DependencyMode.EXPAND_OUTPUTS: return 'зависимые'; case DependencyMode.EXPAND_OUTPUTS: return 'зависимые';
case DependencyMode.EXPAND_INPUTS: return 'влияющие';
} }
} }
@ -161,10 +161,10 @@ export function describeCstSource(mode: DependencyMode): string {
switch (mode) { switch (mode) {
case DependencyMode.ALL: return 'все конституенты'; case DependencyMode.ALL: return 'все конституенты';
case DependencyMode.EXPRESSION: return 'идентификаторы из выражения'; case DependencyMode.EXPRESSION: return 'идентификаторы из выражения';
case DependencyMode.OUTPUTS: return 'конституенты, ссылающиеся на данную'; case DependencyMode.OUTPUTS: return 'прямые ссылки на текущую';
case DependencyMode.INPUTS: return 'конституенты, на которые ссылается данная'; case DependencyMode.INPUTS: return 'пярмые ссылки из текущей';
case DependencyMode.EXPAND_INPUTS: return 'конституенты, зависящие по цепочке'; case DependencyMode.EXPAND_OUTPUTS: return 'опосредованные ссылки на текущую';
case DependencyMode.EXPAND_OUTPUTS: return 'конституенты, влияющие на данную по цепочке'; case DependencyMode.EXPAND_INPUTS: return 'опосредованные ссылки из текущей';
} }
} }