mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
R: Refactor menu bars and fix QR dialog styling
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
Some checks failed
Frontend CI / build (22.x) (push) Has been cancelled
This commit is contained in:
parent
fbdd561344
commit
9e50312d23
|
@ -9,6 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { Overlay } from '../Container';
|
||||||
import { Button, MiniButton, SubmitButton } from '../Control';
|
import { Button, MiniButton, SubmitButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
import { type Styling } from '../props';
|
import { type Styling } from '../props';
|
||||||
|
@ -103,13 +104,15 @@ export function ModalForm({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<Overlay className='z-modalOverlay'>
|
||||||
noPadding
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
className='float-right mt-2 mr-2'
|
icon={<IconClose size='1.25rem' />}
|
||||||
onClick={handleCancel}
|
className='float-right mt-2 mr-2'
|
||||||
/>
|
onClick={handleCancel}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { useDialogsStore } from '@/stores/dialogs';
|
||||||
import { PARAMETER } from '@/utils/constants';
|
import { PARAMETER } from '@/utils/constants';
|
||||||
import { prepareTooltip } from '@/utils/utils';
|
import { prepareTooltip } from '@/utils/utils';
|
||||||
|
|
||||||
|
import { Overlay } from '../Container';
|
||||||
import { Button, MiniButton } from '../Control';
|
import { Button, MiniButton } from '../Control';
|
||||||
import { IconClose } from '../Icons';
|
import { IconClose } from '../Icons';
|
||||||
|
|
||||||
|
@ -48,13 +49,15 @@ export function ModalView({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<MiniButton
|
<Overlay className='z-modalOverlay'>
|
||||||
noPadding
|
<MiniButton
|
||||||
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
noPadding
|
||||||
icon={<IconClose size='1.25rem' />}
|
titleHtml={prepareTooltip('Закрыть диалоговое окно', 'ESC')}
|
||||||
className='float-right mt-2 mr-2'
|
icon={<IconClose size='1.25rem' />}
|
||||||
onClick={hideDialog}
|
className='float-right mt-2 mr-2'
|
||||||
/>
|
onClick={hideDialog}
|
||||||
|
/>
|
||||||
|
</Overlay>
|
||||||
|
|
||||||
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
{header ? <h1 className='px-12 py-2 select-none'>{header}</h1> : null}
|
||||||
|
|
||||||
|
|
|
@ -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' />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -7,6 +7,7 @@ export { useUpdateItem } from './backend/useUpdateItem';
|
||||||
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
|
export { useUpdateTimestamp } from './backend/useUpdateTimestamp';
|
||||||
export { useVersionRestore } from './backend/useVersionRestore';
|
export { useVersionRestore } from './backend/useVersionRestore';
|
||||||
export { EditorLibraryItem } from './components/EditorLibraryItem';
|
export { EditorLibraryItem } from './components/EditorLibraryItem';
|
||||||
|
export { MenuRole } from './components/MenuRole';
|
||||||
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
|
export { MiniSelectorOSS } from './components/MiniSelectorOSS';
|
||||||
export { PickSchema } from './components/PickSchema';
|
export { PickSchema } from './components/PickSchema';
|
||||||
export { SelectLibraryItem } from './components/SelectLibraryItem';
|
export { SelectLibraryItem } from './components/SelectLibraryItem';
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,213 +1,22 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { useRoleStore, UserRole } from '@/features/users';
|
import { MenuRole } from '@/features/library';
|
||||||
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 { MenuEditOss } from './MenuEditOss';
|
||||||
|
import { MenuMain } from './MenuMain';
|
||||||
import { useOssEdit } from './OssEditContext';
|
import { useOssEdit } from './OssEditContext';
|
||||||
|
|
||||||
export function MenuOssTabs() {
|
export function MenuOssTabs() {
|
||||||
const { deleteSchema, promptRelocateConstituents, isMutable, isOwned, schema } = useOssEdit();
|
const { isOwned, schema } = useOssEdit();
|
||||||
const router = useConceptNavigation();
|
const { user } = useAuthSuspense();
|
||||||
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, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex border-r-2'>
|
<div className='flex border-r-2'>
|
||||||
<div ref={schemaMenu.ref}>
|
<MenuMain />
|
||||||
<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}
|
|
||||||
|
|
||||||
<Divider margins='mx-3 my-1' />
|
<MenuEditOss />
|
||||||
|
|
||||||
{!isAnonymous ? (
|
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
|
||||||
<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}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ export interface DlgShowQRProps {
|
||||||
export function DlgShowQR() {
|
export function DlgShowQR() {
|
||||||
const { target } = useDialogsStore(state => state.props as DlgShowQRProps);
|
const { target } = useDialogsStore(state => state.props as DlgShowQRProps);
|
||||||
return (
|
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'>
|
<div className='bg-[#ffffff] p-4 border'>
|
||||||
<QRCodeSVG value={target} size={256} />
|
<QRCodeSVG value={target} size={256} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,450 +1,21 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import fileDownload from 'js-file-download';
|
|
||||||
|
|
||||||
import { urls, useConceptNavigation } from '@/app';
|
|
||||||
import { useAuthSuspense } from '@/features/auth';
|
import { useAuthSuspense } from '@/features/auth';
|
||||||
import { AccessPolicy } from '@/features/library';
|
import { MenuRole } 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 { MenuEditSchema } from './MenuEditSchema';
|
||||||
|
import { MenuMain } from './MenuMain';
|
||||||
import { useRSEdit } from './RSEditContext';
|
import { useRSEdit } from './RSEditContext';
|
||||||
|
|
||||||
export function MenuRSTabs() {
|
export function MenuRSTabs() {
|
||||||
const router = useConceptNavigation();
|
const { user } = useAuthSuspense();
|
||||||
const { user, isAnonymous } = useAuthSuspense();
|
const { schema, isOwned } = useRSEdit();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex border-r-2'>
|
<div className='flex border-r-2'>
|
||||||
<div ref={schemaMenu.ref}>
|
<MenuMain />
|
||||||
<Button
|
<MenuEditSchema />
|
||||||
dense
|
<MenuRole isOwned={isOwned} isEditor={!!user.id && schema.editors.includes(user.id)} />
|
||||||
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}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ const reactCompilerConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
// Packages to include in main app bundle
|
// 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
|
// Rollup warnings that should not be displayed
|
||||||
const warningsToIgnore = [
|
const warningsToIgnore = [
|
||||||
|
|
Loading…
Reference in New Issue
Block a user