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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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