R: Refactor menu bars and fix QR dialog styling
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled

This commit is contained in:
Ivan 2025-02-23 16:54:11 +03:00
parent fbdd561344
commit 9e50312d23
13 changed files with 678 additions and 652 deletions

View File

@ -9,6 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils';
import { Overlay } from '../Container';
import { Button, MiniButton, SubmitButton } from '../Control';
import { IconClose } from '../Icons';
import { type Styling } from '../props';
@ -103,13 +104,15 @@ export function ModalForm({
</div>
) : null}
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2'
onClick={handleCancel}
/>
<Overlay className='z-modalOverlay'>
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2'
onClick={handleCancel}
/>
</Overlay>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}

View File

@ -9,6 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants';
import { prepareTooltip } from '@/utils/utils';
import { Overlay } from '../Container';
import { Button, MiniButton } from '../Control';
import { IconClose } from '../Icons';
@ -48,13 +49,15 @@ export function ModalView({
</div>
) : null}
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2'
onClick={hideDialog}
/>
<Overlay className='z-modalOverlay'>
<MiniButton
noPadding
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
icon={<IconClose size='1.25rem' />}
className='float-right mt-2 mr-2'
onClick={hideDialog}
/>
</Overlay>
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}

View File

@ -0,0 +1,21 @@
import { UserRole } from '@/features/users';
import { IconAdmin, IconEditor, IconOwner, IconReader } from '@/components/Icons';
interface IconRoleProps {
role: UserRole;
size?: string;
}
export function IconRole({ role, size = '1.25rem' }: IconRoleProps) {
switch (role) {
case UserRole.ADMIN:
return <IconAdmin size={size} className='icon-primary' />;
case UserRole.OWNER:
return <IconOwner size={size} className='icon-primary' />;
case UserRole.EDITOR:
return <IconEditor size={size} className='icon-primary' />;
case UserRole.READER:
return <IconReader size={size} className='icon-primary' />;
}
}

View File

@ -0,0 +1,90 @@
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconAlert } from '@/components/Icons';
import { IconRole } from './IconRole';
interface MenuRoleProps {
isOwned: boolean;
isEditor: boolean;
}
export function MenuRole({ isOwned, isEditor }: MenuRoleProps) {
const { user, isAnonymous } = useAuthSuspense();
const router = useConceptNavigation();
const accessMenu = useDropdown();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
function handleChangeMode(newMode: UserRole) {
accessMenu.hide();
setRole(newMode);
}
if (isAnonymous) {
return (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={<IconAlert size='1.25rem' className='icon-red' />}
onClick={() => router.push(urls.login)}
/>
);
}
return (
<div ref={accessMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={<IconRole role={role} size='1.25rem' />}
onClick={accessMenu.toggle}
/>
<Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}
icon={<IconRole role={UserRole.READER} size='1rem' />}
onClick={() => handleChangeMode(UserRole.READER)}
/>
<DropdownButton
text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)}
icon={<IconRole role={UserRole.EDITOR} size='1rem' />}
disabled={!isOwned && !isEditor}
onClick={() => handleChangeMode(UserRole.EDITOR)}
/>
<DropdownButton
text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)}
icon={<IconRole role={UserRole.OWNER} size='1rem' />}
disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)}
/>
<DropdownButton
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconRole role={UserRole.ADMIN} size='1rem' />}
disabled={!user.is_staff}
onClick={() => handleChangeMode(UserRole.ADMIN)}
/>
</Dropdown>
</div>
);
}

View File

@ -7,6 +7,7 @@ export { useUpdateItem } from './backend/useUpdateItem';
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
export { useVersionRestore } from './backend/useVersionRestore';
export { EditorLibraryItem } from './components/EditorLibraryItem';
export { MenuRole } from './components/MenuRole';
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
export { PickSchema } from './components/PickSchema';
export { SelectLibraryItem } from './components/SelectLibraryItem';

View File

@ -0,0 +1,50 @@
import { useAuthSuspense } from '@/features/auth';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconChild, IconEdit2 } from '@/components/Icons';
import { useMutatingOss } from '../../backend/useMutatingOss';
import { useOssEdit } from './OssEditContext';
export function MenuEditOss() {
const { isAnonymous } = useAuthSuspense();
const editMenu = useDropdown();
const { promptRelocateConstituents, isMutable } = useOssEdit();
const isProcessing = useMutatingOss();
function handleRelocate() {
editMenu.hide();
promptRelocateConstituents(undefined, []);
}
if (isAnonymous) {
return null;
}
return (
<div ref={editMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Редактирование'
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<DropdownButton
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={isProcessing}
onClick={handleRelocate}
/>
</Dropdown>
</div>
);
}

View File

@ -0,0 +1,99 @@
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { useRoleStore, UserRole } from '@/features/users';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import { IconDestroy, IconLibrary, IconMenu, IconNewItem, IconQR, IconShare } from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { generatePageQR, sharePage } from '@/utils/utils';
import { useMutatingOss } from '../../backend/useMutatingOss';
import { useOssEdit } from './OssEditContext';
export function MenuMain() {
const router = useConceptNavigation();
const { isMutable, deleteSchema } = useOssEdit();
const isProcessing = useMutatingOss();
const { isAnonymous } = useAuthSuspense();
const role = useRoleStore(state => state.role);
const showQR = useDialogsStore(state => state.showQR);
const schemaMenu = useDropdown();
function handleDelete() {
schemaMenu.hide();
deleteSchema();
}
function handleShare() {
schemaMenu.hide();
sharePage();
}
function handleCreateNew() {
router.push(urls.create_schema);
}
function handleShowQR() {
schemaMenu.hide();
showQR({ target: generatePageQR() });
}
return (
<div ref={schemaMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
/>
<DropdownButton
text='QR-код'
title='Показать QR-код схемы'
icon={<IconQR size='1rem' className='icon-primary' />}
onClick={handleShowQR}
/>
{isMutable ? (
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
<Divider margins='mx-3 my-1' />
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
onClick={handleCreateNew}
/>
) : null}
<DropdownButton
text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.library)}
/>
</Dropdown>
</div>
);
}

View File

@ -1,213 +1,22 @@
'use client';
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import {
IconAdmin,
IconAlert,
IconChild,
IconDestroy,
IconEdit2,
IconEditor,
IconLibrary,
IconMenu,
IconNewItem,
IconOwner,
IconReader,
IconShare
} from '@/components/Icons';
import { sharePage } from '@/utils/utils';
import { useMutatingOss } from '../../backend/useMutatingOss';
import { MenuRole } from '@/features/library';
import { MenuEditOss } from './MenuEditOss';
import { MenuMain } from './MenuMain';
import { useOssEdit } from './OssEditContext';
export function MenuOssTabs() {
const { deleteSchema, promptRelocateConstituents, isMutable, isOwned, schema } = useOssEdit();
const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense();
const isProcessing = useMutatingOss();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const accessMenu = useDropdown();
function handleDelete() {
schemaMenu.hide();
deleteSchema();
}
function handleShare() {
schemaMenu.hide();
sharePage();
}
function handleChangeRole(newMode: UserRole) {
accessMenu.hide();
setRole(newMode);
}
function handleCreateNew() {
router.push(urls.create_schema);
}
function handleLogin() {
router.push(urls.login);
}
function handleRelocate() {
editMenu.hide();
promptRelocateConstituents(undefined, []);
}
const { isOwned, schema } = useOssEdit();
const { user } = useAuthSuspense();
return (
<div className='flex border-r-2'>
<div ref={schemaMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
/>
{isMutable ? (
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
<MenuMain />
<Divider margins='mx-3 my-1' />
<MenuEditOss />
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
onClick={handleCreateNew}
/>
) : null}
<DropdownButton
text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.library)}
/>
</Dropdown>
</div>
{!isAnonymous ? (
<div ref={editMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Редактирование'
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={isMutable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<DropdownButton
text='Конституенты'
titleHtml='Перенос конституент</br>между схемами'
icon={<IconChild size='1rem' className='icon-green' />}
disabled={isProcessing}
onClick={handleRelocate}
/>
</Dropdown>
</div>
) : null}
{!isAnonymous ? (
<div ref={accessMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={
role === UserRole.ADMIN ? (
<IconAdmin size='1.25rem' className='icon-primary' />
) : role === UserRole.OWNER ? (
<IconOwner size='1.25rem' className='icon-primary' />
) : role === UserRole.EDITOR ? (
<IconEditor size='1.25rem' className='icon-primary' />
) : (
<IconReader size='1.25rem' className='icon-primary' />
)
}
onClick={accessMenu.toggle}
/>
<Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}
icon={<IconReader size='1rem' className='icon-primary' />}
onClick={() => handleChangeRole(UserRole.READER)}
/>
<DropdownButton
text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
onClick={() => handleChangeRole(UserRole.EDITOR)}
/>
<DropdownButton
text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!isOwned}
onClick={() => handleChangeRole(UserRole.OWNER)}
/>
<DropdownButton
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user.is_staff}
onClick={() => handleChangeRole(UserRole.ADMIN)}
/>
</Dropdown>
</div>
) : null}
{isAnonymous ? (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={<IconAlert size='1.25rem' className='icon-red' />}
onClick={handleLogin}
/>
) : null}
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
</div>
);
}

View File

@ -13,7 +13,7 @@ export interface DlgShowQRProps {
export function DlgShowQR() {
const { target } = useDialogsStore(state => state.props as DlgShowQRProps);
return (
<ModalView className={clsx('w-[30rem]', 'py-12 pr-3 pl-6 flex gap-3 justify-center items-center')}>
<ModalView className={clsx('w-[25rem]', 'pb-6 pt-12 flex justify-center items-center')}>
<div className='bg-[#ffffff] p-4 border'>
<QRCodeSVG value={target} size={256} />
</div>

View File

@ -0,0 +1,182 @@
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import {
IconArchive,
IconEdit2,
IconGenerateNames,
IconGenerateStructure,
IconInlineSynthesis,
IconReplace,
IconSortList,
IconTemplates
} from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { promptUnsaved } from '@/utils/utils';
import { useMutatingRSForm } from '../../backend/useMutatingRSForm';
import { useProduceStructure } from '../../backend/useProduceStructure';
import { useResetAliases } from '../../backend/useResetAliases';
import { useRestoreOrder } from '../../backend/useRestoreOrder';
import { type IConstituenta } from '../../models/rsform';
import { canProduceStructure } from '../../models/rsformAPI';
import { useRSEdit } from './RSEditContext';
export function MenuEditSchema() {
const { isAnonymous } = useAuthSuspense();
const { isModified } = useModificationStore();
const router = useConceptNavigation();
const editMenu = useDropdown();
const { schema, activeCst, setSelected, isArchive, isContentEditable, promptTemplate, deselectAll } = useRSEdit();
const isProcessing = useMutatingRSForm();
const { resetAliases } = useResetAliases();
const { restoreOrder } = useRestoreOrder();
const { produceStructure } = useProduceStructure();
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
function handleReindex() {
editMenu.hide();
void resetAliases({ itemID: schema.id });
}
function handleRestoreOrder() {
editMenu.hide();
void restoreOrder({ itemID: schema.id });
}
function handleSubstituteCst() {
editMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showSubstituteCst({
schema: schema,
onSubstitute: data => setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)))
});
}
function handleTemplates() {
editMenu.hide();
promptTemplate();
}
function handleProduceStructure(targetCst: IConstituenta | null) {
editMenu.hide();
if (!targetCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
void produceStructure({
itemID: schema.id,
cstID: targetCst.id
}).then(cstList => {
if (cstList.length !== 0) {
setSelected(cstList);
}
});
}
function handleInlineSynthesis() {
editMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showInlineSynthesis({
receiver: schema,
onSynthesis: () => deselectAll()
});
}
if (isAnonymous) {
return null;
}
if (isArchive) {
return (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconArchive size='1.25rem' className='icon-primary' />}
onClick={event => router.push(urls.schema(schema.id), event.ctrlKey || event.metaKey)}
/>
);
}
return (
<div ref={editMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Редактирование'
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={isContentEditable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<DropdownButton
text='Шаблоны'
title='Создать конституенту из шаблона'
icon={<IconTemplates size='1rem' className='icon-green' />}
disabled={!isContentEditable || isProcessing}
onClick={handleTemplates}
/>
<DropdownButton
text='Встраивание'
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
disabled={!isContentEditable || isProcessing}
onClick={handleInlineSynthesis}
/>
<Divider margins='mx-3 my-1' />
<DropdownButton
text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!isContentEditable || isProcessing}
onClick={handleRestoreOrder}
/>
<DropdownButton
text='Порядковые имена'
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
disabled={!isContentEditable || isProcessing}
onClick={handleReindex}
/>
<DropdownButton
text='Порождение структуры'
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
disabled={!isContentEditable || isProcessing || !activeCst || !canProduceStructure(activeCst)}
onClick={() => handleProduceStructure(activeCst)}
/>
<DropdownButton
text='Отождествление'
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
icon={<IconReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst}
disabled={!isContentEditable || isProcessing}
/>
</Dropdown>
</div>
);
}

View File

@ -0,0 +1,197 @@
import fileDownload from 'js-file-download';
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { AccessPolicy } from '@/features/library';
import { LocationHead } from '@/features/library/models/library';
import { useRoleStore, UserRole } from '@/features/users';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import {
IconClone,
IconDestroy,
IconDownload,
IconLibrary,
IconMenu,
IconNewItem,
IconOSS,
IconQR,
IconShare,
IconUpload
} from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { tooltipText } from '@/utils/labels';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { useDownloadRSForm } from '../../backend/useDownloadRSForm';
import { useMutatingRSForm } from '../../backend/useMutatingRSForm';
import { useRSEdit } from './RSEditContext';
export function MenuMain() {
const router = useConceptNavigation();
const { schema, selected, deleteSchema, isArchive, isMutable, isContentEditable } = useRSEdit();
const isProcessing = useMutatingRSForm();
const { user, isAnonymous } = useAuthSuspense();
const role = useRoleStore(state => state.role);
const { isModified } = useModificationStore();
const { download } = useDownloadRSForm();
const showQR = useDialogsStore(state => state.showQR);
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const schemaMenu = useDropdown();
function calculateCloneLocation() {
const location = schema.location;
const head = location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user.is_staff ? location : LocationHead.USER;
}
if (schema.owner === user.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
}
function handleDelete() {
schemaMenu.hide();
deleteSchema();
}
function handleDownload() {
schemaMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
void download({
itemID: schema.id,
version: schema.version === 'latest' ? undefined : schema.version
}).then((data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}
function handleUpload() {
schemaMenu.hide();
showUpload({ itemID: schema.id });
}
function handleClone() {
schemaMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showClone({
base: schema,
initialLocation: calculateCloneLocation(),
selected: selected,
totalCount: schema.items.length
});
}
function handleShare() {
schemaMenu.hide();
sharePage();
}
function handleShowQR() {
schemaMenu.hide();
showQR({ target: generatePageQR() });
}
return (
<div ref={schemaMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
disabled={schema.access_policy !== AccessPolicy.PUBLIC}
/>
<DropdownButton
text='QR-код'
title='Показать QR-код схемы'
icon={<IconQR size='1rem' className='icon-primary' />}
onClick={handleShowQR}
/>
{!isAnonymous ? (
<DropdownButton
text='Клонировать'
icon={<IconClone size='1rem' className='icon-green' />}
disabled={isArchive}
onClick={handleClone}
/>
) : null}
<DropdownButton
text='Выгрузить в Экстеор'
icon={<IconDownload size='1rem' className='icon-primary' />}
onClick={handleDownload}
/>
{isContentEditable ? (
<DropdownButton
text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />}
disabled={isProcessing || schema.oss.length !== 0}
onClick={handleUpload}
/>
) : null}
{isMutable ? (
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
<Divider margins='mx-3 my-1' />
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.create_schema)}
/>
) : null}
{schema.oss.length > 0 ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(schema.oss[0].id))}
/>
) : null}
<DropdownButton
text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.library)}
/>
</Dropdown>
</div>
);
}

View File

@ -1,450 +1,21 @@
'use client';
import fileDownload from 'js-file-download';
import { urls, useConceptNavigation } from '@/app';
import { useAuthSuspense } from '@/features/auth';
import { AccessPolicy } from '@/features/library';
import { LocationHead } from '@/features/library/models/library';
import { useRoleStore, UserRole } from '@/features/users';
import { describeUserRole, labelUserRole } from '@/features/users/labels';
import { Divider } from '@/components/Container';
import { Button } from '@/components/Control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/Dropdown';
import {
IconAdmin,
IconAlert,
IconArchive,
IconClone,
IconDestroy,
IconDownload,
IconEdit2,
IconEditor,
IconGenerateNames,
IconGenerateStructure,
IconInlineSynthesis,
IconLibrary,
IconMenu,
IconNewItem,
IconOSS,
IconOwner,
IconQR,
IconReader,
IconReplace,
IconShare,
IconSortList,
IconTemplates,
IconUpload
} from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs';
import { useModificationStore } from '@/stores/modification';
import { EXTEOR_TRS_FILE } from '@/utils/constants';
import { tooltipText } from '@/utils/labels';
import { generatePageQR, promptUnsaved, sharePage } from '@/utils/utils';
import { useDownloadRSForm } from '../../backend/useDownloadRSForm';
import { useMutatingRSForm } from '../../backend/useMutatingRSForm';
import { useProduceStructure } from '../../backend/useProduceStructure';
import { useResetAliases } from '../../backend/useResetAliases';
import { useRestoreOrder } from '../../backend/useRestoreOrder';
import { canProduceStructure } from '../../models/rsformAPI';
import { MenuRole } from '@/features/library';
import { MenuEditSchema } from './MenuEditSchema';
import { MenuMain } from './MenuMain';
import { useRSEdit } from './RSEditContext';
export function MenuRSTabs() {
const router = useConceptNavigation();
const { user, isAnonymous } = useAuthSuspense();
const {
activeCst,
schema,
selected,
setSelected,
deleteSchema,
promptTemplate,
deselectAll,
isArchive,
isMutable,
isContentEditable,
isOwned
} = useRSEdit();
const role = useRoleStore(state => state.role);
const setRole = useRoleStore(state => state.setRole);
const { isModified } = useModificationStore();
const isProcessing = useMutatingRSForm();
const { resetAliases } = useResetAliases();
const { restoreOrder } = useRestoreOrder();
const { produceStructure } = useProduceStructure();
const { download } = useDownloadRSForm();
const showInlineSynthesis = useDialogsStore(state => state.showInlineSynthesis);
const showQR = useDialogsStore(state => state.showQR);
const showSubstituteCst = useDialogsStore(state => state.showSubstituteCst);
const showClone = useDialogsStore(state => state.showCloneLibraryItem);
const showUpload = useDialogsStore(state => state.showUploadRSForm);
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const accessMenu = useDropdown();
const structureEnabled = !!activeCst && canProduceStructure(activeCst);
function calculateCloneLocation() {
const location = schema.location;
const head = location.substring(0, 2) as LocationHead;
if (head === LocationHead.LIBRARY) {
return user.is_staff ? location : LocationHead.USER;
}
if (schema.owner === user.id) {
return location;
}
return head === LocationHead.USER ? LocationHead.USER : location;
}
function handleDelete() {
schemaMenu.hide();
deleteSchema();
}
function handleDownload() {
schemaMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
const fileName = (schema.alias ?? 'Schema') + EXTEOR_TRS_FILE;
void download({
itemID: schema.id,
version: schema.version === 'latest' ? undefined : schema.version
}).then((data: Blob) => {
try {
fileDownload(data, fileName);
} catch (error) {
console.error(error);
}
});
}
function handleUpload() {
schemaMenu.hide();
showUpload({ itemID: schema.id });
}
function handleClone() {
schemaMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showClone({
base: schema,
initialLocation: calculateCloneLocation(),
selected: selected,
totalCount: schema.items.length
});
}
function handleShare() {
schemaMenu.hide();
sharePage();
}
function handleShowQR() {
schemaMenu.hide();
showQR({ target: generatePageQR() });
}
function handleReindex() {
editMenu.hide();
void resetAliases({ itemID: schema.id });
}
function handleRestoreOrder() {
editMenu.hide();
void restoreOrder({ itemID: schema.id });
}
function handleSubstituteCst() {
editMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showSubstituteCst({
schema: schema,
onSubstitute: data => setSelected(prev => prev.filter(id => !data.substitutions.find(sub => sub.original === id)))
});
}
function handleTemplates() {
editMenu.hide();
promptTemplate();
}
function handleProduceStructure() {
editMenu.hide();
if (!activeCst) {
return;
}
if (isModified && !promptUnsaved()) {
return;
}
void produceStructure({
itemID: schema.id,
cstID: activeCst.id
}).then(cstList => {
if (cstList.length !== 0) {
setSelected(cstList);
}
});
}
function handleInlineSynthesis() {
editMenu.hide();
if (isModified && !promptUnsaved()) {
return;
}
showInlineSynthesis({
receiver: schema,
onSynthesis: () => deselectAll()
});
}
function handleChangeMode(newMode: UserRole) {
accessMenu.hide();
setRole(newMode);
}
function handleCreateNew() {
router.push(urls.create_schema);
}
function handleLogin() {
router.push(urls.login);
}
const { user } = useAuthSuspense();
const { schema, isOwned } = useRSEdit();
return (
<div className='flex border-r-2'>
<div ref={schemaMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Меню'
hideTitle={schemaMenu.isOpen}
icon={<IconMenu size='1.25rem' className='clr-text-controls' />}
className='h-full pl-2'
onClick={schemaMenu.toggle}
/>
<Dropdown isOpen={schemaMenu.isOpen}>
<DropdownButton
text='Поделиться'
titleHtml={tooltipText.shareItem(schema.access_policy === AccessPolicy.PUBLIC)}
icon={<IconShare size='1rem' className='icon-primary' />}
onClick={handleShare}
disabled={schema.access_policy !== AccessPolicy.PUBLIC}
/>
<DropdownButton
text='QR-код'
title='Показать QR-код схемы'
icon={<IconQR size='1rem' className='icon-primary' />}
onClick={handleShowQR}
/>
{!isAnonymous ? (
<DropdownButton
text='Клонировать'
icon={<IconClone size='1rem' className='icon-green' />}
disabled={isArchive}
onClick={handleClone}
/>
) : null}
<DropdownButton
text='Выгрузить в Экстеор'
icon={<IconDownload size='1rem' className='icon-primary' />}
onClick={handleDownload}
/>
{isContentEditable ? (
<DropdownButton
text='Загрузить из Экстеор'
icon={<IconUpload size='1rem' className='icon-red' />}
disabled={isProcessing || schema.oss.length !== 0}
onClick={handleUpload}
/>
) : null}
{isMutable ? (
<DropdownButton
text='Удалить схему'
icon={<IconDestroy size='1rem' className='icon-red' />}
disabled={isProcessing || role < UserRole.OWNER}
onClick={handleDelete}
/>
) : null}
<Divider margins='mx-3 my-1' />
{!isAnonymous ? (
<DropdownButton
text='Создать новую схему'
icon={<IconNewItem size='1rem' className='icon-primary' />}
onClick={handleCreateNew}
/>
) : null}
{schema.oss.length > 0 ? (
<DropdownButton
text='Перейти к ОСС'
icon={<IconOSS size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.oss(schema.oss[0].id))}
/>
) : null}
<DropdownButton
text='Библиотека'
icon={<IconLibrary size='1rem' className='icon-primary' />}
onClick={() => router.push(urls.library)}
/>
</Dropdown>
</div>
{!isArchive && !isAnonymous ? (
<div ref={editMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title='Редактирование'
hideTitle={editMenu.isOpen}
className='h-full px-2'
icon={<IconEdit2 size='1.25rem' className={isContentEditable ? 'icon-green' : 'icon-red'} />}
onClick={editMenu.toggle}
/>
<Dropdown isOpen={editMenu.isOpen}>
<DropdownButton
text='Шаблоны'
title='Создать конституенту из шаблона'
icon={<IconTemplates size='1rem' className='icon-green' />}
disabled={!isContentEditable || isProcessing}
onClick={handleTemplates}
/>
<DropdownButton
text='Встраивание'
titleHtml='Импортировать совокупность <br/>конституент из другой схемы'
icon={<IconInlineSynthesis size='1rem' className='icon-green' />}
disabled={!isContentEditable || isProcessing}
onClick={handleInlineSynthesis}
/>
<Divider margins='mx-3 my-1' />
<DropdownButton
text='Упорядочить список'
titleHtml='Упорядочить список, исходя из <br/>логики типов и связей конституент'
icon={<IconSortList size='1rem' className='icon-primary' />}
disabled={!isContentEditable || isProcessing}
onClick={handleRestoreOrder}
/>
<DropdownButton
text='Порядковые имена'
titleHtml='Присвоить порядковые имена <br/>и обновить выражения'
icon={<IconGenerateNames size='1rem' className='icon-primary' />}
disabled={!isContentEditable || isProcessing}
onClick={handleReindex}
/>
<DropdownButton
text='Порождение структуры'
titleHtml='Раскрыть структуру типизации <br/>выделенной конституенты'
icon={<IconGenerateStructure size='1rem' className='icon-primary' />}
disabled={!isContentEditable || !structureEnabled || isProcessing}
onClick={handleProduceStructure}
/>
<DropdownButton
text='Отождествление'
titleHtml='Заменить вхождения <br/>одной конституенты на другую'
icon={<IconReplace size='1rem' className='icon-red' />}
onClick={handleSubstituteCst}
disabled={!isContentEditable || isProcessing}
/>
</Dropdown>
</div>
) : null}
{isArchive && !isAnonymous ? (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Архив</b>: Редактирование запрещено<br />Перейти к актуальной версии'
hideTitle={accessMenu.isOpen}
className='h-full px-2'
icon={<IconArchive size='1.25rem' className='icon-primary' />}
onClick={event => router.push(urls.schema(schema.id), event.ctrlKey || event.metaKey)}
/>
) : null}
{!isAnonymous ? (
<div ref={accessMenu.ref}>
<Button
dense
noBorder
noOutline
tabIndex={-1}
title={`Режим ${labelUserRole(role)}`}
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={
role === UserRole.ADMIN ? (
<IconAdmin size='1.25rem' className='icon-primary' />
) : role === UserRole.OWNER ? (
<IconOwner size='1.25rem' className='icon-primary' />
) : role === UserRole.EDITOR ? (
<IconEditor size='1.25rem' className='icon-primary' />
) : (
<IconReader size='1.25rem' className='icon-primary' />
)
}
onClick={accessMenu.toggle}
/>
<Dropdown isOpen={accessMenu.isOpen}>
<DropdownButton
text={labelUserRole(UserRole.READER)}
title={describeUserRole(UserRole.READER)}
icon={<IconReader size='1rem' className='icon-primary' />}
onClick={() => handleChangeMode(UserRole.READER)}
/>
<DropdownButton
text={labelUserRole(UserRole.EDITOR)}
title={describeUserRole(UserRole.EDITOR)}
icon={<IconEditor size='1rem' className='icon-primary' />}
disabled={!isOwned && (!user.id || !schema.editors.includes(user.id))}
onClick={() => handleChangeMode(UserRole.EDITOR)}
/>
<DropdownButton
text={labelUserRole(UserRole.OWNER)}
title={describeUserRole(UserRole.OWNER)}
icon={<IconOwner size='1rem' className='icon-primary' />}
disabled={!isOwned}
onClick={() => handleChangeMode(UserRole.OWNER)}
/>
<DropdownButton
text={labelUserRole(UserRole.ADMIN)}
title={describeUserRole(UserRole.ADMIN)}
icon={<IconAdmin size='1rem' className='icon-primary' />}
disabled={!user.is_staff}
onClick={() => handleChangeMode(UserRole.ADMIN)}
/>
</Dropdown>
</div>
) : null}
{isAnonymous ? (
<Button
dense
noBorder
noOutline
tabIndex={-1}
titleHtml='<b>Анонимный режим</b><br />Войти в Портал'
hideTitle={accessMenu.isOpen}
className='h-full pr-2'
icon={<IconAlert size='1.25rem' className='icon-red' />}
onClick={handleLogin}
/>
) : null}
<MenuMain />
<MenuEditSchema />
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
</div>
);
}

View File

@ -11,7 +11,7 @@ const reactCompilerConfig = {
};
// Packages to include in main app bundle
const inlinePackages = ['react', 'react-router', 'react-dom'];
const inlinePackages = ['react', 'react-router', 'react-dom', 'global', 'react-scan'];
// Rollup warnings that should not be displayed
const warningsToIgnore = [