Improve UI

This commit is contained in:
IRBorisov 2023-07-25 22:29:33 +03:00
parent e737628cea
commit e7016aab21
11 changed files with 129 additions and 75 deletions

View File

@ -17,7 +17,10 @@
"react", "simple-import-sort" "react", "simple-import-sort"
], ],
"rules": { "rules": {
"simple-import-sort/imports": "error", "simple-import-sort/imports": "warn",
"@typescript-eslint/no-unused-vars": "warn",
"no-trailing-spaces": "warn",
"no-multiple-empty-lines": "warn",
"@typescript-eslint/explicit-function-return-type": "off", "@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/semi": "off", "@typescript-eslint/semi": "off",
"@typescript-eslint/strict-boolean-expressions": "off", "@typescript-eslint/strict-boolean-expressions": "off",

View File

@ -1,17 +1,13 @@
import { type MouseEventHandler } from 'react'; interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> {
interface ButtonProps {
id?: string
text?: string text?: string
icon?: React.ReactNode icon?: React.ReactNode
tooltip?: string tooltip?: string
disabled?: boolean
dense?: boolean dense?: boolean
loading?: boolean loading?: boolean
widthClass?: string widthClass?: string
borderClass?: string borderClass?: string
colorClass?: string colorClass?: string
onClick?: MouseEventHandler<HTMLButtonElement> | undefined
} }
function Button({ function Button({
@ -26,7 +22,7 @@ function Button({
return ( return (
<button id={id} <button id={id}
type='button' type='button'
disabled={disabled} disabled={disabled ?? loading}
onClick={onClick} onClick={onClick}
title={tooltip} title={tooltip}
className={`inline-flex items-center gap-2 align-middle justify-center ${padding} ${borderClass} ${colorClass} ${widthClass} ${cursor}`} className={`inline-flex items-center gap-2 align-middle justify-center ${padding} ${borderClass} ${colorClass} ${widthClass} ${cursor}`}

View File

@ -1,6 +1,7 @@
import { useRef } from 'react'; import { useRef } from 'react';
import useClickedOutside from '../../hooks/useClickedOutside'; import useClickedOutside from '../../hooks/useClickedOutside';
import useEscapeKey from '../../hooks/useEscapeKey';
import Button from './Button'; import Button from './Button';
interface ModalProps { interface ModalProps {
@ -8,27 +9,28 @@ interface ModalProps {
submitText?: string submitText?: string
show: boolean show: boolean
canSubmit: boolean canSubmit: boolean
toggle: () => void hideWindow: () => void
onSubmit: () => void onSubmit: () => void
onCancel?: () => void onCancel?: () => void
children: React.ReactNode children: React.ReactNode
} }
function Modal({ title, show, toggle, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) { function Modal({ title, show, hideWindow, onSubmit, onCancel, canSubmit, children, submitText = 'Продолжить' }: ModalProps) {
const ref = useRef(null); const ref = useRef(null);
useClickedOutside({ ref, callback: toggle }) useClickedOutside({ ref, callback: hideWindow });
useEscapeKey(hideWindow);
if (!show) { if (!show) {
return null; return null;
} }
const handleCancel = () => { const handleCancel = () => {
toggle(); hideWindow();
if (onCancel) onCancel(); if (onCancel) onCancel();
}; };
const handleSubmit = () => { const handleSubmit = () => {
toggle(); hideWindow();
onSubmit(); onSubmit();
}; };
@ -48,6 +50,7 @@ function Modal({ title, show, toggle, onSubmit, onCancel, canSubmit, children, s
colorClass='clr-btn-primary' colorClass='clr-btn-primary'
disabled={!canSubmit} disabled={!canSubmit}
onClick={handleSubmit} onClick={handleSubmit}
autoFocus
/> />
<Button <Button
text='Отмена' text='Отмена'

View File

@ -9,7 +9,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: Submit
return ( return (
<button type='submit' <button type='submit'
className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold disabled:cursor-not-allowed rounded clr-btn-primary ${loading ? ' cursor-progress' : ''}`} className={`px-4 py-2 inline-flex items-center gap-2 align-middle justify-center font-bold disabled:cursor-not-allowed rounded clr-btn-primary ${loading ? ' cursor-progress' : ''}`}
disabled={disabled} disabled={disabled ?? loading}
> >
{icon && <span>{icon}</span>} {icon && <span>{icon}</span>}
{text && <span>{text}</span>} {text && <span>{text}</span>}

View File

@ -37,7 +37,7 @@ interface IRSFormContext {
claim: (callback?: BackendCallback) => void claim: (callback?: BackendCallback) => void
download: (callback: BackendCallback) => void download: (callback: BackendCallback) => void
cstUpdate: (data: any, callback?: BackendCallback) => void cstUpdate: (cstdID: string, data: any, callback?: BackendCallback) => void
cstCreate: (data: any, callback?: BackendCallback) => void cstCreate: (data: any, callback?: BackendCallback) => void
cstDelete: (data: any, callback?: BackendCallback) => void cstDelete: (data: any, callback?: BackendCallback) => void
cstMoveTo: (data: any, callback?: BackendCallback) => void cstMoveTo: (data: any, callback?: BackendCallback) => void
@ -102,7 +102,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error) }, onError: error => { setError(error) },
onSucccess: (response) => { onSucccess: (response) => {
reload() reload(setProcessing)
.then(() => { if (callback != null) callback(response); }) .then(() => { if (callback != null) callback(response); })
.catch(console.error); .catch(console.error);
} }
@ -131,10 +131,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error) }, onError: error => { setError(error) },
onSucccess: (response) => { onSucccess: (response) => {
schema.owner = user.id schema.owner = user.id;
schema.time_update = response.data.time_update schema.time_update = response.data.time_update;
setSchema(schema) setSchema(schema);
if (callback != null) callback(response) if (callback != null) callback(response);
} }
}).catch(console.error); }).catch(console.error);
}, [schemaID, setError, schema, user, setSchema]) }, [schemaID, setError, schema, user, setSchema])
@ -151,16 +151,20 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
}, [schemaID, setError]) }, [schemaID, setError])
const cstUpdate = useCallback( const cstUpdate = useCallback(
(data: any, callback?: BackendCallback) => { (cstID: string, data: any, callback?: BackendCallback) => {
setError(undefined) setError(undefined)
patchConstituenta(String(activeID), { patchConstituenta(cstID, {
data, data,
showError: true, showError: true,
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error) }, onError: error => { setError(error) },
onSucccess: callback onSucccess: (response) => {
reload(setProcessing)
.then(() => { if (callback != null) callback(response); })
.catch(console.error);
}
}).catch(console.error); }).catch(console.error);
}, [activeID, setError]) }, [setError])
const cstCreate = useCallback( const cstCreate = useCallback(
(data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
@ -171,11 +175,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error) }, onError: error => { setError(error) },
onSucccess: (response) => { onSucccess: (response) => {
setSchema(response.data.schema) setSchema(response.data.schema);
if (callback != null) callback(response) if (callback != null) callback(response);
} }
}).catch(console.error); }).catch(console.error);
}, [schemaID, setError, setSchema]) }, [schemaID, setError, setSchema]);
const cstDelete = useCallback( const cstDelete = useCallback(
(data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
@ -190,7 +194,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
if (callback != null) callback(response) if (callback != null) callback(response)
} }
}).catch(console.error); }).catch(console.error);
}, [schemaID, setError, setSchema]) }, [schemaID, setError, setSchema]);
const cstMoveTo = useCallback( const cstMoveTo = useCallback(
(data: any, callback?: BackendCallback) => { (data: any, callback?: BackendCallback) => {
@ -201,11 +205,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing, setLoading: setProcessing,
onError: error => { setError(error) }, onError: error => { setError(error) },
onSucccess: (response) => { onSucccess: (response) => {
setSchema(response.data) setSchema(response.data);
if (callback != null) callback(response) if (callback != null) callback(response);
} }
}).catch(console.error); }).catch(console.error);
}, [schemaID, setError, setSchema]) }, [schemaID, setError, setSchema]);
return ( return (
<RSFormContext.Provider value={{ <RSFormContext.Provider value={{

View File

@ -0,0 +1,21 @@
import { useCallback, useEffect } from 'react';
const KEY_NAME_ESC = 'Escape';
const KEY_EVENT_TYPE = 'keyup';
function useEscapeKey(handleClose: () => void) {
const handleEscKey = useCallback((event: KeyboardEvent) => {
if (event.key === KEY_NAME_ESC) {
handleClose();
}
}, [handleClose]);
useEffect(() => {
document.addEventListener(KEY_EVENT_TYPE, handleEscKey, false);
return () => {
document.removeEventListener(KEY_EVENT_TYPE, handleEscKey, false);
};
}, [handleEscKey]);
}
export default useEscapeKey;

View File

@ -12,26 +12,25 @@ export function useRSFormDetails({ target }: { target?: string }) {
function setSchema(schema?: IRSForm) { function setSchema(schema?: IRSForm) {
if (schema) CalculateStats(schema); if (schema) CalculateStats(schema);
setInnerSchema(schema); setInnerSchema(schema);
console.log(schema); console.log('Loaded schema: ', schema);
} }
const fetchData = useCallback( const fetchData = useCallback(
async () => { async (setCustomLoading?: typeof setLoading) => {
setError(undefined); setError(undefined);
setInnerSchema(undefined);
if (!target) { if (!target) {
return; return;
} }
await getRSFormDetails(target, { await getRSFormDetails(target, {
showError: true, showError: true,
setLoading, setLoading: setCustomLoading ?? setLoading,
onError: error => { setError(error); }, onError: error => { setInnerSchema(undefined); setError(error); },
onSucccess: (response) => { setSchema(response.data); } onSucccess: (response) => { setSchema(response.data); }
}); });
}, [target]); }, [target]);
async function reload() { async function reload(setCustomLoading?: typeof setLoading) {
await fetchData(); await fetchData(setCustomLoading);
} }
useEffect(() => { useEffect(() => {

View File

@ -1,5 +1,5 @@
import { type AxiosResponse } from 'axios'; import { type AxiosResponse } from 'axios';
import { useCallback, useLayoutEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useMemo, useState } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import SubmitButton from '../../components/Common/SubmitButton'; import SubmitButton from '../../components/Common/SubmitButton';
@ -19,6 +19,7 @@ function ConstituentEditor() {
} = useRSForm(); } = useRSForm();
const [showCstModal, setShowCstModal] = useState(false); const [showCstModal, setShowCstModal] = useState(false);
const [isModified, setIsModified] = useState(false);
const [editMode, setEditMode] = useState(EditMode.TEXT); const [editMode, setEditMode] = useState(EditMode.TEXT);
const [alias, setAlias] = useState(''); const [alias, setAlias] = useState('');
@ -29,13 +30,28 @@ function ConstituentEditor() {
const [convention, setConvention] = useState(''); const [convention, setConvention] = useState('');
const [typification, setTypification] = useState('N/A'); const [typification, setTypification] = useState('N/A');
const isEnabled = useMemo(() => activeCst && isEditable, [activeCst, isEditable]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (schema?.items && schema?.items.length > 0) { if (schema?.items && schema?.items.length > 0) {
// TODO: figure out why schema.items could be undef? // TODO: figure out why schema.items could be undef?
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
setActiveID((prev) => (prev ?? schema?.items![0].id ?? undefined)); setActiveID((prev) => (prev ?? schema?.items![0].id ?? undefined));
} }
}, [schema, setActiveID]) }, [schema, setActiveID]);
useLayoutEffect(() => {
if (!activeCst) {
setIsModified(false);
return;
}
setIsModified(
activeCst.term?.raw !== term ||
activeCst.definition?.text?.raw !== textDefinition ||
activeCst.convention !== convention ||
activeCst.definition?.formal !== expression
);
}, [activeCst, term, textDefinition, expression, convention]);
useLayoutEffect(() => { useLayoutEffect(() => {
if (activeCst) { if (activeCst) {
@ -57,17 +73,12 @@ function ConstituentEditor() {
alias: alias, alias: alias,
convention: convention, convention: convention,
definition_formal: expression, definition_formal: expression,
definition_text: { definition_raw: textDefinition,
raw: textDefinition, term_raw: term
resolved: ''
},
term: {
raw: term,
resolved: '',
forms: activeCst?.term?.forms ?? []
}
}; };
cstUpdate(data, () => toast.success('Изменения сохранены')); cstUpdate(String(activeID), data, () => {
toast.success('Изменения сохранены');
});
} }
}; };
@ -119,7 +130,7 @@ function ConstituentEditor() {
<div className='flex items-start w-full gap-2'> <div className='flex items-start w-full gap-2'>
<CreateCstModal <CreateCstModal
show={showCstModal} show={showCstModal}
toggle={() => { setShowCstModal(!showCstModal); }} hideWindow={() => { setShowCstModal(false); }}
onCreate={handleAddNew} onCreate={handleAddNew}
defaultType={activeCst?.cstType as CstType} defaultType={activeCst?.cstType as CstType}
/> />
@ -128,7 +139,7 @@ function ConstituentEditor() {
<button type='submit' <button type='submit'
title='Сохранить изменения' title='Сохранить изменения'
className='px-1 py-1 font-bold rounded whitespace-nowrap disabled:cursor-not-allowed clr-btn-primary' className='px-1 py-1 font-bold rounded whitespace-nowrap disabled:cursor-not-allowed clr-btn-primary'
disabled={!isEditable} disabled={!isModified || !isEnabled}
> >
<SaveIcon size={5} /> <SaveIcon size={5} />
</button> </button>
@ -158,18 +169,18 @@ function ConstituentEditor() {
<button type='button' <button type='button'
title='Создать конституенты после данной' title='Создать конституенты после данной'
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear' className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
disabled={!isEditable} disabled={!isEnabled}
onClick={() => { handleAddNew(); }} onClick={() => { handleAddNew(); }}
> >
<SmallPlusIcon size={5} color={isEditable ? 'text-green' : ''} /> <SmallPlusIcon size={5} color={isEnabled ? 'text-green' : ''} />
</button> </button>
<button type='button' <button type='button'
title='Удалить редактируемую конституенту' title='Удалить редактируемую конституенту'
className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear' className='px-1 py-1 font-bold rounded-full whitespace-nowrap disabled:cursor-not-allowed clr-btn-clear'
disabled={!isEditable} disabled={!isEnabled}
onClick={handleDelete} onClick={handleDelete}
> >
<DumpBinIcon size={5} color={isEditable ? 'text-red' : ''} /> <DumpBinIcon size={5} color={isEnabled ? 'text-red' : ''} />
</button> </button>
</div> </div>
</div> </div>
@ -177,7 +188,7 @@ function ConstituentEditor() {
placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение' placeholder='Схемный или предметный термин, обозначающий данное понятие или утверждение'
rows={2} rows={2}
value={term} value={term}
disabled={!isEditable} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setTerm(event.target.value); }} onChange={event => { setTerm(event.target.value); }}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => { setEditMode(EditMode.TEXT); }}
@ -190,7 +201,7 @@ function ConstituentEditor() {
<ExpressionEditor id='expression' label='Формальное выражение' <ExpressionEditor id='expression' label='Формальное выражение'
placeholder='Родоструктурное выражение, задающее формальное определение' placeholder='Родоструктурное выражение, задающее формальное определение'
value={expression} value={expression}
disabled={!isEditable} disabled={!isEnabled}
isActive={editMode === 'rslang'} isActive={editMode === 'rslang'}
toggleEditMode={() => { setEditMode(EditMode.RSLANG); }} toggleEditMode={() => { setEditMode(EditMode.RSLANG); }}
onChange={event => { setExpression(event.target.value); }} onChange={event => { setExpression(event.target.value); }}
@ -201,7 +212,7 @@ function ConstituentEditor() {
placeholder='Лингвистическая интерпретация формального выражения' placeholder='Лингвистическая интерпретация формального выражения'
rows={4} rows={4}
value={textDefinition} value={textDefinition}
disabled={!isEditable} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setTextDefinition(event.target.value); }} onChange={event => { setTextDefinition(event.target.value); }}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => { setEditMode(EditMode.TEXT); }}
@ -210,7 +221,7 @@ function ConstituentEditor() {
placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию' placeholder='Договоренность об интерпретации неопределяемого понятия&#x000D;&#x000A;Комментарий к производному понятию'
rows={4} rows={4}
value={convention} value={convention}
disabled={!isEditable} disabled={!isEnabled}
spellCheck spellCheck
onChange={event => { setConvention(event.target.value); }} onChange={event => { setConvention(event.target.value); }}
onFocus={() => { setEditMode(EditMode.TEXT); }} onFocus={() => { setEditMode(EditMode.TEXT); }}
@ -218,7 +229,7 @@ function ConstituentEditor() {
<div className='flex justify-center w-full mt-2'> <div className='flex justify-center w-full mt-2'>
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
disabled={!isEditable} disabled={!isModified || !isEnabled}
icon={<SaveIcon size={6} />} icon={<SaveIcon size={6} />}
/> />
</div> </div>

View File

@ -257,7 +257,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
return (<> return (<>
<CreateCstModal <CreateCstModal
show={showCstModal} show={showCstModal}
toggle={() => { setShowCstModal(!showCstModal); }} hideWindow={() => { setShowCstModal(false); }}
onCreate={handleAddNew} onCreate={handleAddNew}
/> />
<div className='w-full'> <div className='w-full'>

View File

@ -7,12 +7,12 @@ import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
interface CreateCstModalProps { interface CreateCstModalProps {
show: boolean show: boolean
toggle: () => void hideWindow: () => void
defaultType?: CstType defaultType?: CstType
onCreate: (type: CstType) => void onCreate: (type: CstType) => void
} }
function CreateCstModal({ show, toggle, defaultType, onCreate }: CreateCstModalProps) { function CreateCstModal({ show, hideWindow, defaultType, onCreate }: CreateCstModalProps) {
const [validated, setValidated] = useState(false); const [validated, setValidated] = useState(false);
const [selectedType, setSelectedType] = useState<CstType | undefined>(undefined); const [selectedType, setSelectedType] = useState<CstType | undefined>(undefined);
@ -33,7 +33,7 @@ function CreateCstModal({ show, toggle, defaultType, onCreate }: CreateCstModalP
<Modal <Modal
title='Создание конституенты' title='Создание конституенты'
show={show} show={show}
toggle={toggle} hideWindow={hideWindow}
canSubmit={validated} canSubmit={validated}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useLayoutEffect, useState } from 'react';
import { useIntl } from 'react-intl'; import { useIntl } from 'react-intl';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
@ -29,11 +29,28 @@ function RSFormCard() {
const [comment, setComment] = useState(''); const [comment, setComment] = useState('');
const [common, setCommon] = useState(false); const [common, setCommon] = useState(false);
useEffect(() => { const [isModified, setIsModified] = useState(true);
setTitle(schema?.title ?? '');
setAlias(schema?.alias ?? ''); useLayoutEffect(() => {
setComment(schema?.comment ?? ''); if (!schema) {
setCommon(schema?.is_common ?? false); setIsModified(false);
return;
}
setIsModified(
schema.title !== title ||
schema.alias !== alias ||
schema.comment !== comment ||
schema.is_common !== common
);
}, [schema, title, alias, comment, common]);
useLayoutEffect(() => {
if (schema) {
setTitle(schema.title);
setAlias(schema.alias);
setComment(schema.comment);
setCommon(schema.is_common);
}
}, [schema]); }, [schema]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => { const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@ -87,7 +104,7 @@ function RSFormCard() {
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
loading={processing} loading={processing}
disabled={!isEditable || processing} disabled={!isModified || !isEditable}
icon={<SaveIcon size={6} />} icon={<SaveIcon size={6} />}
/> />
<div className='flex justify-end gap-1'> <div className='flex justify-end gap-1'>
@ -97,7 +114,6 @@ function RSFormCard() {
onClick={shareCurrentURLProc} onClick={shareCurrentURLProc}
/> />
<Button <Button
disabled={processing}
tooltip='Скачать TRS файл' tooltip='Скачать TRS файл'
icon={<DownloadIcon color='text-primary'/>} icon={<DownloadIcon color='text-primary'/>}
loading={processing} loading={processing}
@ -105,15 +121,16 @@ function RSFormCard() {
/> />
<Button <Button
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' } tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
disabled={!isClaimable || processing || !user}
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>} icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
loading={processing}
disabled={!isClaimable || !user}
onClick={() => { claimOwnershipProc(claim); }} onClick={() => { claimOwnershipProc(claim); }}
/> />
<Button <Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'} tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
disabled={!isEditable || processing}
icon={<DumpBinIcon color={isEditable ? 'text-red' : ''} />} icon={<DumpBinIcon color={isEditable ? 'text-red' : ''} />}
loading={processing} loading={processing}
disabled={!isEditable}
onClick={handleDelete} onClick={handleDelete}
/> />
</div> </div>