mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Improve UI
This commit is contained in:
parent
e737628cea
commit
e7016aab21
|
@ -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",
|
||||
|
|
|
@ -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}`}
|
||||
|
|
|
@ -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='Отмена'
|
||||
|
|
|
@ -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>}
|
||||
|
|
|
@ -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={{
|
||||
|
|
21
rsconcept/frontend/src/hooks/useEscapeKey.ts
Normal file
21
rsconcept/frontend/src/hooks/useEscapeKey.ts
Normal 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;
|
|
@ -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(() => {
|
||||
|
|
|
@ -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='Договоренность об интерпретации неопределяемого понятия
Комментарий к производному понятию'
|
||||
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>
|
||||
|
|
|
@ -257,7 +257,7 @@ function ConstituentsTable({ onOpenEdit }: ConstituentsTableProps) {
|
|||
return (<>
|
||||
<CreateCstModal
|
||||
show={showCstModal}
|
||||
toggle={() => { setShowCstModal(!showCstModal); }}
|
||||
hideWindow={() => { setShowCstModal(false); }}
|
||||
onCreate={handleAddNew}
|
||||
/>
|
||||
<div className='w-full'>
|
||||
|
|
|
@ -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}
|
||||
>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user