Refactor UI state flags

This commit is contained in:
IRBorisov 2024-03-28 18:44:34 +03:00
parent 102f8c2baf
commit 40696fa553
12 changed files with 85 additions and 101 deletions

View File

@ -1,6 +1,3 @@
'use client';
import { useMemo } from 'react';
import { BiDownvote, BiDuplicate, BiPlusCircle, BiReset, BiTrash, BiUpvote } from 'react-icons/bi'; import { BiDownvote, BiDuplicate, BiPlusCircle, BiReset, BiTrash, BiUpvote } from 'react-icons/bi';
import { FiSave } from 'react-icons/fi'; import { FiSave } from 'react-icons/fi';
@ -9,8 +6,8 @@ import Overlay from '@/components/ui/Overlay';
import { messages, prepareTooltip } from '@/utils/labels'; import { messages, prepareTooltip } from '@/utils/labels';
interface ConstituentaToolbarProps { interface ConstituentaToolbarProps {
isMutable: boolean; disabled: boolean;
isModified: boolean; modified: boolean;
onSubmit: () => void; onSubmit: () => void;
onReset: () => void; onReset: () => void;
@ -23,8 +20,8 @@ interface ConstituentaToolbarProps {
} }
function ConstituentaToolbar({ function ConstituentaToolbar({
isMutable, disabled,
isModified, modified,
onSubmit, onSubmit,
onReset, onReset,
onMoveUp, onMoveUp,
@ -33,49 +30,48 @@ function ConstituentaToolbar({
onClone, onClone,
onCreate onCreate
}: ConstituentaToolbarProps) { }: ConstituentaToolbarProps) {
const canSave = useMemo(() => isModified && isMutable, [isModified, isMutable]);
return ( return (
<Overlay position='top-1 right-4 sm:right-1/2 sm:translate-x-1/2' className='flex'> <Overlay position='top-1 right-4 sm:right-1/2 sm:translate-x-1/2' className='flex'>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave}
icon={<FiSave size='1.25rem' className='icon-primary' />} icon={<FiSave size='1.25rem' className='icon-primary' />}
disabled={disabled || !modified}
onClick={onSubmit} onClick={onSubmit}
/> />
<MiniButton <MiniButton
title='Сбросить несохраненные изменения' title='Сбросить несохраненные изменения'
disabled={!canSave}
onClick={onReset}
icon={<BiReset size='1.25rem' className='icon-primary' />} icon={<BiReset size='1.25rem' className='icon-primary' />}
disabled={disabled || !modified}
onClick={onReset}
/> />
<MiniButton <MiniButton
title='Создать конституенту после данной' title='Создать конституенту после данной'
disabled={!isMutable}
onClick={onCreate}
icon={<BiPlusCircle size={'1.25rem'} className='icon-green' />} icon={<BiPlusCircle size={'1.25rem'} className='icon-green' />}
disabled={disabled}
onClick={onCreate}
/> />
<MiniButton <MiniButton
titleHtml={isModified ? messages.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={modified ? messages.unsaved : prepareTooltip('Клонировать конституенту', 'Alt + V')}
disabled={!isMutable || isModified}
onClick={onClone}
icon={<BiDuplicate size='1.25rem' className='icon-green' />} icon={<BiDuplicate size='1.25rem' className='icon-green' />}
disabled={disabled || modified}
onClick={onClone}
/> />
<MiniButton <MiniButton
title='Удалить редактируемую конституенту' title='Удалить редактируемую конституенту'
disabled={!isMutable} disabled={disabled}
onClick={onDelete} onClick={onDelete}
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<BiUpvote size='1.25rem' className='icon-primary' />} icon={<BiUpvote size='1.25rem' className='icon-primary' />}
disabled={!isMutable} disabled={disabled || modified}
onClick={onMoveUp} onClick={onMoveUp}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')} titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<BiDownvote size='1.25rem' className='icon-primary' />} icon={<BiDownvote size='1.25rem' className='icon-primary' />}
disabled={!isMutable} disabled={disabled || modified}
onClick={onMoveDown} onClick={onMoveDown}
/> />
</Overlay> </Overlay>

View File

@ -8,33 +8,26 @@ import { messages } from '@/utils/labels';
interface ControlsOverlayProps { interface ControlsOverlayProps {
constituenta?: IConstituenta; constituenta?: IConstituenta;
isMutable: boolean; disabled: boolean;
isModified: boolean; modified: boolean;
processing: boolean; processing: boolean;
onRename: () => void; onRename: () => void;
onEditTerm: () => void; onEditTerm: () => void;
} }
function ControlsOverlay({ function ControlsOverlay({ constituenta, disabled, modified, processing, onRename, onEditTerm }: ControlsOverlayProps) {
constituenta,
isMutable,
isModified,
processing,
onRename,
onEditTerm
}: ControlsOverlayProps) {
return ( return (
<Overlay position='top-1 left-[4.1rem]' className='flex select-none'> <Overlay position='top-1 left-[4.1rem]' className='flex select-none'>
{isMutable || processing ? ( {!disabled || processing ? (
<MiniButton <MiniButton
title={ title={
isModified ? messages.unsaved : `Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}` modified ? messages.unsaved : `Редактировать словоформы термина: ${constituenta?.term_forms.length ?? 0}`
} }
noHover noHover
onClick={onEditTerm} onClick={onEditTerm}
icon={<LiaEdit size='1rem' className='icon-primary' />} icon={<LiaEdit size='1rem' className='icon-primary' />}
disabled={isModified} disabled={modified}
/> />
) : null} ) : null}
<div <div
@ -42,19 +35,19 @@ function ControlsOverlay({
'pt-1 pl-[1.375rem]', // prettier: split lines 'pt-1 pl-[1.375rem]', // prettier: split lines
'text-sm font-medium whitespace-nowrap', 'text-sm font-medium whitespace-nowrap',
'select-text cursor-default', 'select-text cursor-default',
!isMutable && !processing && 'pl-[2.8rem]' disabled && !processing && 'pl-[2.8rem]'
)} )}
> >
<span>Имя </span> <span>Имя </span>
<span className='ml-1'>{constituenta?.alias ?? ''}</span> <span className='ml-1'>{constituenta?.alias ?? ''}</span>
</div> </div>
{isMutable || processing ? ( {!disabled || processing ? (
<MiniButton <MiniButton
noHover noHover
title={isModified ? messages.unsaved : 'Переименовать конституенту'} title={modified ? messages.unsaved : 'Переименовать конституенту'}
onClick={onRename} onClick={onRename}
icon={<LiaEdit size='1rem' className='icon-primary' />} icon={<LiaEdit size='1rem' className='icon-primary' />}
disabled={isModified} disabled={modified}
/> />
) : null} ) : null}
</Overlay> </Overlay>

View File

@ -35,8 +35,8 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
const [toggleReset, setToggleReset] = useState(false); const [toggleReset, setToggleReset] = useState(false);
const disabled = useMemo( const disabled = useMemo(
() => !activeCst || !controller.isContentEditable, () => !activeCst || !controller.isContentEditable || controller.isProcessing,
[activeCst, controller.isContentEditable] [activeCst, controller.isContentEditable, controller.isProcessing]
); );
const isNarrow = useMemo(() => !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD, [windowSize]); const isNarrow = useMemo(() => !!windowSize.width && windowSize.width <= SIDELIST_LAYOUT_THRESHOLD, [windowSize]);
@ -79,10 +79,10 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
return ( return (
<> <>
{controller.isContentEditable || controller.isProcessing ? ( {controller.isContentEditable ? (
<ConstituentaToolbar <ConstituentaToolbar
isMutable={!disabled} disabled={disabled}
isModified={isModified} modified={isModified}
onMoveUp={controller.moveUp} onMoveUp={controller.moveUp}
onMoveDown={controller.moveDown} onMoveDown={controller.moveDown}
onSubmit={initiateSubmit} onSubmit={initiateSubmit}
@ -102,7 +102,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
onKeyDown={handleInput} onKeyDown={handleInput}
> >
<FormConstituenta <FormConstituenta
isMutable={!disabled} disabled={disabled}
showList={showList} showList={showList}
id={globals.constituenta_editor} id={globals.constituenta_editor}
constituenta={activeCst} constituenta={activeCst}

View File

@ -21,7 +21,7 @@ import ControlsOverlay from './ControlsOverlay';
export const ROW_SIZE_IN_CHARACTERS = 70; export const ROW_SIZE_IN_CHARACTERS = 70;
interface FormConstituentaProps { interface FormConstituentaProps {
isMutable: boolean; disabled: boolean;
showList: boolean; showList: boolean;
id?: string; id?: string;
@ -37,7 +37,7 @@ interface FormConstituentaProps {
} }
function FormConstituenta({ function FormConstituenta({
isMutable, disabled,
showList, showList,
id, id,
isModified, isModified,
@ -114,8 +114,8 @@ function FormConstituenta({
return ( return (
<div> <div>
<ControlsOverlay <ControlsOverlay
isMutable={isMutable} disabled={disabled}
isModified={isModified} modified={isModified}
processing={processing} processing={processing}
constituenta={constituenta} constituenta={constituenta}
onEditTerm={onEditTerm} onEditTerm={onEditTerm}
@ -134,14 +134,14 @@ function FormConstituenta({
value={term} value={term}
initialValue={constituenta?.term_raw ?? ''} initialValue={constituenta?.term_raw ?? ''}
resolved={constituenta?.term_resolved ?? ''} resolved={constituenta?.term_resolved ?? ''}
disabled={!isMutable} disabled={disabled}
onChange={newValue => setTerm(newValue)} onChange={newValue => setTerm(newValue)}
/> />
<TextArea <TextArea
id='cst_typification' id='cst_typification'
dense dense
noBorder noBorder
disabled disabled={true}
label='Типизация' label='Типизация'
rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1} rows={typification.length > ROW_SIZE_IN_CHARACTERS ? 2 : 1}
value={typification} value={typification}
@ -157,7 +157,7 @@ function FormConstituenta({
value={expression} value={expression}
activeCst={constituenta} activeCst={constituenta}
showList={showList} showList={showList}
disabled={!isMutable} disabled={disabled}
toggleReset={toggleReset} toggleReset={toggleReset}
onToggleList={onToggleList} onToggleList={onToggleList}
onChange={newValue => setExpression(newValue)} onChange={newValue => setExpression(newValue)}
@ -172,7 +172,7 @@ function FormConstituenta({
value={textDefinition} value={textDefinition}
initialValue={constituenta?.definition_raw ?? ''} initialValue={constituenta?.definition_raw ?? ''}
resolved={constituenta?.definition_resolved ?? ''} resolved={constituenta?.definition_resolved ?? ''}
disabled={!isMutable} disabled={disabled}
onChange={newValue => setTextDefinition(newValue)} onChange={newValue => setTextDefinition(newValue)}
/> />
<TextArea <TextArea
@ -181,15 +181,15 @@ function FormConstituenta({
label='Конвенция / Комментарий' label='Конвенция / Комментарий'
placeholder='Договоренность об интерпретации или пояснение' placeholder='Договоренность об интерпретации или пояснение'
value={convention} value={convention}
disabled={!isMutable} disabled={disabled}
rows={convention.length > 2 * ROW_SIZE_IN_CHARACTERS ? 3 : 2} rows={convention.length > 2 * ROW_SIZE_IN_CHARACTERS ? 3 : 2}
onChange={event => setConvention(event.target.value)} onChange={event => setConvention(event.target.value)}
/> />
{isMutable || processing ? ( {!disabled || processing ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center' className='self-center'
disabled={!isModified || !isMutable} disabled={disabled || !isModified}
icon={<FiSave size='1.25rem' />} icon={<FiSave size='1.25rem' />}
/> />
) : null} ) : null}

View File

@ -118,7 +118,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
/> />
<div className='flex flex-col'> <div className='flex flex-col'>
<Overlay position='top-[-0.25rem] right-[-0.25rem] flex'> <Overlay position='top-[-0.25rem] right-[-0.25rem] flex'>
{controller.isMutable || (controller.isMutable && controller.isProcessing) ? ( {controller.isMutable ? (
<> <>
<MiniButton <MiniButton
noHover noHover
@ -152,7 +152,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
label='Комментарий' label='Комментарий'
rows={3} rows={3}
value={comment} value={comment}
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable || controller.isProcessing}
onChange={event => setComment(event.target.value)} onChange={event => setComment(event.target.value)}
/> />
<div className='flex justify-between whitespace-nowrap'> <div className='flex justify-between whitespace-nowrap'>
@ -160,7 +160,7 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
id='schema_common' id='schema_common'
label='Общедоступная схема' label='Общедоступная схема'
title='Общедоступные схемы видны всем пользователям и могут быть изменены' title='Общедоступные схемы видны всем пользователям и могут быть изменены'
disabled={!controller.isContentEditable} disabled={!controller.isContentEditable || controller.isProcessing}
value={common} value={common}
setValue={value => setCommon(value)} setValue={value => setCommon(value)}
/> />
@ -168,17 +168,17 @@ function FormRSForm({ id, isModified, setIsModified }: FormRSFormProps) {
id='schema_immutable' id='schema_immutable'
label='Неизменная схема' label='Неизменная схема'
title='Только администраторы могут присваивать схемам неизменный статус' title='Только администраторы могут присваивать схемам неизменный статус'
disabled={!controller.isContentEditable || !user?.is_staff} disabled={!controller.isContentEditable || !user?.is_staff || controller.isProcessing}
value={canonical} value={canonical}
setValue={value => setCanonical(value)} setValue={value => setCanonical(value)}
/> />
</div> </div>
{controller.isContentEditable || (controller.isMutable && controller.isProcessing) ? ( {controller.isContentEditable ? (
<SubmitButton <SubmitButton
text='Сохранить изменения' text='Сохранить изменения'
className='self-center' className='self-center'
loading={processing} loading={processing}
disabled={!isModified || controller.isProcessing} disabled={!isModified}
icon={<FiSave size='1.25rem' />} icon={<FiSave size='1.25rem' />}
/> />
) : null} ) : null}

View File

@ -24,10 +24,10 @@ interface RSFormToolbarProps {
function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, onDestroy }: RSFormToolbarProps) { function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, onDestroy }: RSFormToolbarProps) {
const controller = useRSEdit(); const controller = useRSEdit();
const canSave = useMemo(() => modified && controller.isMutable, [modified, controller.isMutable]); const canSave = useMemo(() => modified && !controller.isProcessing, [modified, controller.isProcessing]);
return ( return (
<Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'> <Overlay position='top-1 right-1/2 translate-x-1/2' className='flex'>
{controller.isContentEditable || (controller.isMutable && controller.isProcessing) ? ( {controller.isContentEditable ? (
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')} titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
disabled={!canSave} disabled={!canSave}
@ -48,7 +48,6 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
{!anonymous ? ( {!anonymous ? (
<MiniButton <MiniButton
titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`} titleHtml={`Отслеживание <b>${subscribed ? 'включено' : 'выключено'}</b>`}
disabled={controller.isProcessing}
icon={ icon={
subscribed ? ( subscribed ? (
<FiBell size='1.25rem' className='icon-primary' /> <FiBell size='1.25rem' className='icon-primary' />
@ -56,6 +55,7 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
<FiBellOff size='1.25rem' className='clr-text-controls' /> <FiBellOff size='1.25rem' className='clr-text-controls' />
) )
} }
disabled={controller.isProcessing}
onClick={controller.toggleSubscribe} onClick={controller.toggleSubscribe}
/> />
) : null} ) : null}
@ -67,12 +67,12 @@ function RSFormToolbar({ modified, anonymous, subscribed, claimable, onSubmit, o
onClick={controller.claim} onClick={controller.claim}
/> />
) : null} ) : null}
{controller.isContentEditable || (controller.isMutable && controller.isProcessing) ? ( {controller.isMutable ? (
<MiniButton <MiniButton
title='Удалить схему' title='Удалить схему'
disabled={!controller.isMutable}
onClick={onDestroy}
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
disabled={!controller.isContentEditable || controller.isProcessing}
onClick={onDestroy}
/> />
) : null} ) : null}
<HelpButton topic={HelpTopic.RSFORM} offset={4} /> <HelpButton topic={HelpTopic.RSFORM} offset={4} />

View File

@ -51,7 +51,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
} }
function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) { function handleTableKey(event: React.KeyboardEvent<HTMLDivElement>) {
if (!controller.isMutable) { if (!controller.isContentEditable || controller.isProcessing) {
return; return;
} }
if (event.key === 'Delete' && selected.length > 0) { if (event.key === 'Delete' && selected.length > 0) {
@ -97,7 +97,7 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
return ( return (
<div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}> <div tabIndex={-1} className='outline-none' onKeyDown={handleTableKey}>
{controller.isContentEditable || (controller.isMutable && controller.isProcessing) ? ( {controller.isContentEditable ? (
<SelectedCounter <SelectedCounter
totalCount={controller.schema?.stats?.count_all ?? 0} totalCount={controller.schema?.stats?.count_all ?? 0}
selectedCount={selected.length} selectedCount={selected.length}
@ -105,20 +105,18 @@ function EditorRSList({ selected, setSelected, onOpenEdit }: EditorRSListProps)
/> />
) : null} ) : null}
{controller.isContentEditable || (controller.isMutable && controller.isProcessing) ? ( {controller.isContentEditable ? <RSListToolbar selectedCount={selected.length} /> : null}
<RSListToolbar selectedCount={selected.length} />
) : null}
<div <div
className={clsx('border-b', { className={clsx('border-b', {
'pt-[2.3rem]': controller.isContentEditable || controller.isProcessing, 'pt-[2.3rem]': controller.isContentEditable,
'relative top-[-1px]': !controller.isContentEditable && !controller.isProcessing 'relative top-[-1px]': !controller.isContentEditable
})} })}
/> />
<RSTable <RSTable
items={controller.schema?.items} items={controller.schema?.items}
maxHeight={tableHeight} maxHeight={tableHeight}
enableSelection={controller.isContentEditable || (controller.isMutable && controller.isProcessing)} enableSelection={controller.isContentEditable}
selected={rowSelection} selected={rowSelection}
setSelected={handleRowSelection} setSelected={handleRowSelection}
onEdit={onOpenEdit} onEdit={onOpenEdit}

View File

@ -31,25 +31,25 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')} titleHtml={prepareTooltip('Переместить вверх', 'Alt + вверх')}
icon={<BiUpvote size='1.25rem' className='icon-primary' />} icon={<BiUpvote size='1.25rem' className='icon-primary' />}
disabled={!controller.isMutable || nothingSelected} disabled={controller.isProcessing || nothingSelected}
onClick={controller.moveUp} onClick={controller.moveUp}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')} titleHtml={prepareTooltip('Переместить вниз', 'Alt + вниз')}
icon={<BiDownvote size='1.25rem' className='icon-primary' />} icon={<BiDownvote size='1.25rem' className='icon-primary' />}
disabled={!controller.isMutable || nothingSelected} disabled={controller.isProcessing || nothingSelected}
onClick={controller.moveDown} onClick={controller.moveDown}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')} titleHtml={prepareTooltip('Клонировать конституенту', 'Alt + V')}
icon={<BiDuplicate size='1.25rem' className='icon-green' />} icon={<BiDuplicate size='1.25rem' className='icon-green' />}
disabled={!controller.isMutable || selectedCount !== 1} disabled={controller.isProcessing || selectedCount !== 1}
onClick={controller.cloneCst} onClick={controller.cloneCst}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')} titleHtml={prepareTooltip('Добавить новую конституенту...', 'Alt + `')}
icon={<BiPlusCircle size='1.25rem' className='icon-green' />} icon={<BiPlusCircle size='1.25rem' className='icon-green' />}
disabled={!controller.isMutable} disabled={controller.isProcessing}
onClick={() => controller.createCst(undefined, false)} onClick={() => controller.createCst(undefined, false)}
/> />
<div ref={insertMenu.ref}> <div ref={insertMenu.ref}>
@ -57,7 +57,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
title='Добавить пустую конституенту' title='Добавить пустую конституенту'
hideTitle={insertMenu.isOpen} hideTitle={insertMenu.isOpen}
icon={<BiDownArrowCircle size='1.25rem' className='icon-green' />} icon={<BiDownArrowCircle size='1.25rem' className='icon-green' />}
disabled={!controller.isMutable} disabled={controller.isProcessing}
onClick={insertMenu.toggle} onClick={insertMenu.toggle}
/> />
<Dropdown isOpen={insertMenu.isOpen}> <Dropdown isOpen={insertMenu.isOpen}>
@ -74,7 +74,7 @@ function RSListToolbar({ selectedCount }: RSListToolbarProps) {
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранные', 'Delete')} titleHtml={prepareTooltip('Удалить выбранные', 'Delete')}
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
disabled={!controller.isMutable || nothingSelected} disabled={controller.isProcessing || nothingSelected}
onClick={controller.deleteCst} onClick={controller.deleteCst}
/> />
<HelpButton topic={HelpTopic.CSTLIST} offset={5} /> <HelpButton topic={HelpTopic.CSTLIST} offset={5} />

View File

@ -169,7 +169,7 @@ function EditorTermGraph({ selected, setSelected, onOpenEdit }: EditorTermGraphP
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
// Hotkeys implementation // Hotkeys implementation
if (!controller.isMutable) { if (!controller.isContentEditable || controller.isProcessing) {
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {

View File

@ -68,19 +68,19 @@ function GraphToolbar({
disabled={!is3D} disabled={!is3D}
onClick={toggleOrbit} onClick={toggleOrbit}
/> />
{controller.isMutable || controller.isProcessing ? ( {controller.isContentEditable ? (
<MiniButton <MiniButton
title='Новая конституента' title='Новая конституента'
icon={<BiPlusCircle size='1.25rem' className='icon-green' />} icon={<BiPlusCircle size='1.25rem' className='icon-green' />}
disabled={!controller.isMutable} disabled={controller.isProcessing}
onClick={onCreate} onClick={onCreate}
/> />
) : null} ) : null}
{controller.isMutable || controller.isProcessing ? ( {controller.isContentEditable ? (
<MiniButton <MiniButton
title='Удалить выбранные' title='Удалить выбранные'
icon={<BiTrash size='1.25rem' className='icon-red' />} icon={<BiTrash size='1.25rem' className='icon-red' />}
disabled={nothingSelected || !controller.isMutable} disabled={nothingSelected || controller.isProcessing}
onClick={onDelete} onClick={onDelete}
/> />
) : null} ) : null}

View File

@ -114,13 +114,9 @@ export const RSEditState = ({
const isMutable = useMemo(() => { const isMutable = useMemo(() => {
return ( return (
!model.loading && mode !== UserAccessMode.READER && ((model.isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
!model.processing &&
mode !== UserAccessMode.READER &&
((model.isOwned || (mode === UserAccessMode.ADMIN && user?.is_staff)) ?? false)
); );
}, [user?.is_staff, mode, model.isOwned, model.loading, model.processing]); }, [user?.is_staff, mode, model.isOwned]);
const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]); const isContentEditable = useMemo(() => isMutable && !model.isArchive, [isMutable, model.isArchive]);
const [showUpload, setShowUpload] = useState(false); const [showUpload, setShowUpload] = useState(false);

View File

@ -138,9 +138,9 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
<Dropdown isOpen={schemaMenu.isOpen}> <Dropdown isOpen={schemaMenu.isOpen}>
{user ? ( {user ? (
<DropdownButton <DropdownButton
disabled={!model.isClaimable && !model.isOwned}
text={model.isOwned ? 'Вы — владелец' : 'Стать владельцем'} text={model.isOwned ? 'Вы — владелец' : 'Стать владельцем'}
icon={<LuCrown size='1rem' className='icon-green' />} icon={<LuCrown size='1rem' className='icon-green' />}
disabled={!model.isClaimable && !model.isOwned}
onClick={!model.isOwned && model.isClaimable ? handleClaimOwner : undefined} onClick={!model.isOwned && model.isClaimable ? handleClaimOwner : undefined}
/> />
) : null} ) : null}
@ -151,9 +151,9 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
/> />
{user ? ( {user ? (
<DropdownButton <DropdownButton
disabled={model.isArchive}
text='Клонировать' text='Клонировать'
icon={<BiDuplicate size='1rem' className='icon-primary' />} icon={<BiDuplicate size='1rem' className='icon-primary' />}
disabled={model.isArchive}
onClick={handleClone} onClick={handleClone}
/> />
) : null} ) : null}
@ -162,11 +162,11 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
icon={<BiDownload size='1rem' className='icon-primary' />} icon={<BiDownload size='1rem' className='icon-primary' />}
onClick={handleDownload} onClick={handleDownload}
/> />
{user ? ( {controller.isContentEditable ? (
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable}
text='Загрузить из Экстеора' text='Загрузить из Экстеора'
icon={<BiUpload size='1rem' className='icon-red' />} icon={<BiUpload size='1rem' className='icon-red' />}
disabled={controller.isProcessing}
onClick={handleUpload} onClick={handleUpload}
/> />
) : null} ) : null}
@ -174,6 +174,7 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
<DropdownButton <DropdownButton
text='Удалить схему' text='Удалить схему'
icon={<BiTrash size='1rem' className='icon-red' />} icon={<BiTrash size='1rem' className='icon-red' />}
disabled={controller.isProcessing}
onClick={handleDelete} onClick={handleDelete}
/> />
) : null} ) : null}
@ -208,40 +209,40 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
/> />
<Dropdown isOpen={editMenu.isOpen}> <Dropdown isOpen={editMenu.isOpen}>
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable}
text='Шаблоны' text='Шаблоны'
title='Создать конституенту из шаблона' title='Создать конституенту из шаблона'
icon={<BiDiamond size='1rem' className='icon-green' />} icon={<BiDiamond size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing}
onClick={handleTemplates} onClick={handleTemplates}
/> />
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable}
text='Встраивание' text='Встраивание'
title='Импортировать совокупность конституент из другой схемы' title='Импортировать совокупность конституент из другой схемы'
icon={<LuBookCopy size='1rem' className='icon-green' />} icon={<LuBookCopy size='1rem' className='icon-green' />}
disabled={!controller.isContentEditable || controller.isProcessing}
onClick={handleInlineSynthesis} onClick={handleInlineSynthesis}
/> />
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable}
className='border-t-2' className='border-t-2'
text='Порядковые имена' text='Порядковые имена'
title='Присвоить порядковые имена и обновить выражения' title='Присвоить порядковые имена и обновить выражения'
icon={<LuWand2 size='1rem' className='icon-primary' />} icon={<LuWand2 size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || controller.isProcessing}
onClick={handleReindex} onClick={handleReindex}
/> />
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable || !controller.canProduceStructure}
text='Порождение структуры' text='Порождение структуры'
title='Раскрыть структуру типизации выделенной конституенты' title='Раскрыть структуру типизации выделенной конституенты'
icon={<LuNetwork size='1rem' className='icon-primary' />} icon={<LuNetwork size='1rem' className='icon-primary' />}
disabled={!controller.isContentEditable || !controller.canProduceStructure}
onClick={handleProduceStructure} onClick={handleProduceStructure}
/> />
<DropdownButton <DropdownButton
disabled={!controller.isContentEditable}
text='Отождествление' text='Отождествление'
title='Заменить вхождения одной конституенты на другую' title='Заменить вхождения одной конституенты на другую'
icon={<LuReplace size='1rem' className='icon-red' />} icon={<LuReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst} onClick={handleSubstituteCst}
disabled={!controller.isContentEditable || controller.isProcessing}
/> />
</Dropdown> </Dropdown>
</div> </div>
@ -289,17 +290,17 @@ function RSTabsMenu({ onDestroy }: RSTabsMenuProps) {
onClick={() => handleChangeMode(UserAccessMode.READER)} onClick={() => handleChangeMode(UserAccessMode.READER)}
/> />
<DropdownButton <DropdownButton
disabled={!model.isOwned}
text={labelAccessMode(UserAccessMode.OWNER)} text={labelAccessMode(UserAccessMode.OWNER)}
title={describeAccessMode(UserAccessMode.OWNER)} title={describeAccessMode(UserAccessMode.OWNER)}
icon={<LuCrown size='1rem' className='icon-primary' />} icon={<LuCrown size='1rem' className='icon-primary' />}
disabled={!model.isOwned}
onClick={() => handleChangeMode(UserAccessMode.OWNER)} onClick={() => handleChangeMode(UserAccessMode.OWNER)}
/> />
<DropdownButton <DropdownButton
disabled={!user?.is_staff}
text={labelAccessMode(UserAccessMode.ADMIN)} text={labelAccessMode(UserAccessMode.ADMIN)}
title={describeAccessMode(UserAccessMode.ADMIN)} title={describeAccessMode(UserAccessMode.ADMIN)}
icon={<BiMeteor size='1rem' className='icon-primary' />} icon={<BiMeteor size='1rem' className='icon-primary' />}
disabled={!user?.is_staff}
onClick={() => handleChangeMode(UserAccessMode.ADMIN)} onClick={() => handleChangeMode(UserAccessMode.ADMIN)}
/> />
</Dropdown> </Dropdown>