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"
],
"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/semi": "off",
"@typescript-eslint/strict-boolean-expressions": "off",

View File

@ -1,17 +1,13 @@
import { type MouseEventHandler } from 'react';
interface ButtonProps {
id?: string
interface ButtonProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'className' | 'children'> {
text?: string
icon?: React.ReactNode
tooltip?: string
disabled?: boolean
dense?: boolean
loading?: boolean
widthClass?: string
borderClass?: string
colorClass?: string
onClick?: MouseEventHandler<HTMLButtonElement> | undefined
}
function Button({
@ -26,7 +22,7 @@ function Button({
return (
<button id={id}
type='button'
disabled={disabled}
disabled={disabled ?? loading}
onClick={onClick}
title={tooltip}
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 useClickedOutside from '../../hooks/useClickedOutside';
import useEscapeKey from '../../hooks/useEscapeKey';
import Button from './Button';
interface ModalProps {
@ -8,27 +9,28 @@ interface ModalProps {
submitText?: string
show: boolean
canSubmit: boolean
toggle: () => void
hideWindow: () => void
onSubmit: () => void
onCancel?: () => void
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);
useClickedOutside({ ref, callback: toggle })
useClickedOutside({ ref, callback: hideWindow });
useEscapeKey(hideWindow);
if (!show) {
return null;
}
const handleCancel = () => {
toggle();
hideWindow();
if (onCancel) onCancel();
};
const handleSubmit = () => {
toggle();
hideWindow();
onSubmit();
};
@ -48,6 +50,7 @@ function Modal({ title, show, toggle, onSubmit, onCancel, canSubmit, children, s
colorClass='clr-btn-primary'
disabled={!canSubmit}
onClick={handleSubmit}
autoFocus
/>
<Button
text='Отмена'

View File

@ -9,7 +9,7 @@ function SubmitButton({ text = 'ОК', icon, disabled, loading = false }: Submit
return (
<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' : ''}`}
disabled={disabled}
disabled={disabled ?? loading}
>
{icon && <span>{icon}</span>}
{text && <span>{text}</span>}

View File

@ -37,7 +37,7 @@ interface IRSFormContext {
claim: (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
cstDelete: (data: any, callback?: BackendCallback) => void
cstMoveTo: (data: any, callback?: BackendCallback) => void
@ -102,7 +102,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing,
onError: error => { setError(error) },
onSucccess: (response) => {
reload()
reload(setProcessing)
.then(() => { if (callback != null) callback(response); })
.catch(console.error);
}
@ -131,10 +131,10 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing,
onError: error => { setError(error) },
onSucccess: (response) => {
schema.owner = user.id
schema.time_update = response.data.time_update
setSchema(schema)
if (callback != null) callback(response)
schema.owner = user.id;
schema.time_update = response.data.time_update;
setSchema(schema);
if (callback != null) callback(response);
}
}).catch(console.error);
}, [schemaID, setError, schema, user, setSchema])
@ -151,16 +151,20 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
}, [schemaID, setError])
const cstUpdate = useCallback(
(data: any, callback?: BackendCallback) => {
(cstID: string, data: any, callback?: BackendCallback) => {
setError(undefined)
patchConstituenta(String(activeID), {
patchConstituenta(cstID, {
data,
showError: true,
setLoading: setProcessing,
onError: error => { setError(error) },
onSucccess: callback
onSucccess: (response) => {
reload(setProcessing)
.then(() => { if (callback != null) callback(response); })
.catch(console.error);
}
}).catch(console.error);
}, [activeID, setError])
}, [setError])
const cstCreate = useCallback(
(data: any, callback?: BackendCallback) => {
@ -171,11 +175,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing,
onError: error => { setError(error) },
onSucccess: (response) => {
setSchema(response.data.schema)
if (callback != null) callback(response)
setSchema(response.data.schema);
if (callback != null) callback(response);
}
}).catch(console.error);
}, [schemaID, setError, setSchema])
}, [schemaID, setError, setSchema]);
const cstDelete = useCallback(
(data: any, callback?: BackendCallback) => {
@ -190,7 +194,7 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
if (callback != null) callback(response)
}
}).catch(console.error);
}, [schemaID, setError, setSchema])
}, [schemaID, setError, setSchema]);
const cstMoveTo = useCallback(
(data: any, callback?: BackendCallback) => {
@ -201,11 +205,11 @@ export const RSFormState = ({ schemaID, children }: RSFormStateProps) => {
setLoading: setProcessing,
onError: error => { setError(error) },
onSucccess: (response) => {
setSchema(response.data)
if (callback != null) callback(response)
setSchema(response.data);
if (callback != null) callback(response);
}
}).catch(console.error);
}, [schemaID, setError, setSchema])
}, [schemaID, setError, setSchema]);
return (
<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) {
if (schema) CalculateStats(schema);
setInnerSchema(schema);
console.log(schema);
console.log('Loaded schema: ', schema);
}
const fetchData = useCallback(
async () => {
async (setCustomLoading?: typeof setLoading) => {
setError(undefined);
setInnerSchema(undefined);
if (!target) {
return;
}
await getRSFormDetails(target, {
showError: true,
setLoading,
onError: error => { setError(error); },
setLoading: setCustomLoading ?? setLoading,
onError: error => { setInnerSchema(undefined); setError(error); },
onSucccess: (response) => { setSchema(response.data); }
});
}, [target]);
async function reload() {
await fetchData();
async function reload(setCustomLoading?: typeof setLoading) {
await fetchData(setCustomLoading);
}
useEffect(() => {

View File

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

View File

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

View File

@ -7,12 +7,12 @@ import { CstTypeSelector, getCstTypeLabel } from '../../utils/staticUI';
interface CreateCstModalProps {
show: boolean
toggle: () => void
hideWindow: () => void
defaultType?: CstType
onCreate: (type: CstType) => void
}
function CreateCstModal({ show, toggle, defaultType, onCreate }: CreateCstModalProps) {
function CreateCstModal({ show, hideWindow, defaultType, onCreate }: CreateCstModalProps) {
const [validated, setValidated] = useState(false);
const [selectedType, setSelectedType] = useState<CstType | undefined>(undefined);
@ -33,7 +33,7 @@ function CreateCstModal({ show, toggle, defaultType, onCreate }: CreateCstModalP
<Modal
title='Создание конституенты'
show={show}
toggle={toggle}
hideWindow={hideWindow}
canSubmit={validated}
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 { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
@ -29,11 +29,28 @@ function RSFormCard() {
const [comment, setComment] = useState('');
const [common, setCommon] = useState(false);
useEffect(() => {
setTitle(schema?.title ?? '');
setAlias(schema?.alias ?? '');
setComment(schema?.comment ?? '');
setCommon(schema?.is_common ?? false);
const [isModified, setIsModified] = useState(true);
useLayoutEffect(() => {
if (!schema) {
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]);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
@ -87,7 +104,7 @@ function RSFormCard() {
<SubmitButton
text='Сохранить изменения'
loading={processing}
disabled={!isEditable || processing}
disabled={!isModified || !isEditable}
icon={<SaveIcon size={6} />}
/>
<div className='flex justify-end gap-1'>
@ -97,7 +114,6 @@ function RSFormCard() {
onClick={shareCurrentURLProc}
/>
<Button
disabled={processing}
tooltip='Скачать TRS файл'
icon={<DownloadIcon color='text-primary'/>}
loading={processing}
@ -105,15 +121,16 @@ function RSFormCard() {
/>
<Button
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
disabled={!isClaimable || processing || !user}
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
loading={processing}
disabled={!isClaimable || !user}
onClick={() => { claimOwnershipProc(claim); }}
/>
<Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
disabled={!isEditable || processing}
icon={<DumpBinIcon color={isEditable ? 'text-red' : ''} />}
loading={processing}
disabled={!isEditable}
onClick={handleDelete}
/>
</div>