M: Improve UI for creating new elements

This commit is contained in:
Ivan 2025-07-08 11:22:43 +03:00
parent 9d8405fc36
commit cac508451d
5 changed files with 116 additions and 38 deletions

View File

@ -1,5 +1,6 @@
// Search new icons at https://reactsvgicons.com/ // Search new icons at https://reactsvgicons.com/
// Note: save this file using Ctrl + K, Ctrl + Shift + S to disable autoformat // Note: save this file using Ctrl + K, Ctrl + Shift + S to disable autoformat
// Note: For Cursor hotkeys are Ctrl + M, Ctrl + Shift + S
/* eslint-disable simple-import-sort/exports */ /* eslint-disable simple-import-sort/exports */
// ==== General actions ======= // ==== General actions =======

View File

@ -1,7 +1,6 @@
'use client'; 'use client';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { urls, useConceptNavigation } from '@/app';
import { useLibrary } from '@/features/library/backend/use-library'; import { useLibrary } from '@/features/library/backend/use-library';
import { DropdownButton } from '@/components/dropdown'; import { DropdownButton } from '@/components/dropdown';
@ -33,7 +32,6 @@ interface MenuOperationProps {
} }
export function MenuOperation({ operation, onHide }: MenuOperationProps) { export function MenuOperation({ operation, onHide }: MenuOperationProps) {
const router = useConceptNavigation();
const { items: libraryItems } = useLibrary(); const { items: libraryItems } = useLibrary();
const { schema, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit(); const { schema, navigateOperationSchema, isMutable, canDeleteOperation } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
@ -134,7 +132,7 @@ export function MenuOperation({ operation, onHide }: MenuOperationProps) {
void inputCreate({ void inputCreate({
itemID: schema.id, itemID: schema.id,
data: { target: operation.id, layout: getLayout() } data: { target: operation.id, layout: getLayout() }
}).then(new_schema => router.push({ path: urls.schema(new_schema.id), force: true })); });
} }
function handleRelocateConstituents() { function handleRelocateConstituents() {

View File

@ -163,14 +163,16 @@ export function OssFlow() {
handleSavePositions(); handleSavePositions();
return; return;
} }
if ((event.ctrlKey || event.metaKey) && event.code === 'KeyQ') { if (event.altKey && event.code === 'Key1') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (event.shiftKey) {
handleCreateBlock(); handleCreateBlock();
} else { return;
handleCreateOperation();
} }
if (event.altKey && event.code === 'Key2') {
event.preventDefault();
event.stopPropagation();
handleCreateOperation();
return; return;
} }
if (event.key === 'Delete') { if (event.key === 'Delete') {
@ -200,8 +202,10 @@ export function OssFlow() {
<ToolbarOssGraph <ToolbarOssGraph
className='absolute z-pop top-8 right-1/2 translate-x-1/2' className='absolute z-pop top-8 right-1/2 translate-x-1/2'
onCreateOperation={handleCreateOperation}
onCreateBlock={handleCreateBlock} onCreateBlock={handleCreateBlock}
onCreateSchema={handleCreateOperation}
onImportSchema={handleCreateOperation}
onCreateSynthesis={handleCreateOperation}
onDelete={handleDeleteSelected} onDelete={handleDeleteSelected}
onResetPositions={resetGraph} onResetPositions={resetGraph}
openContextMenu={openContextMenu} openContextMenu={openContextMenu}

View File

@ -2,16 +2,21 @@
import React from 'react'; import React from 'react';
import { useAuthSuspense } from '@/features/auth/backend/use-auth';
import { HelpTopic } from '@/features/help'; import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components/badge-help'; import { BadgeHelp } from '@/features/help/components/badge-help';
import { IconShowSidebar } from '@/features/library/components/icon-show-sidebar'; import { IconShowSidebar } from '@/features/library/components/icon-show-sidebar';
import { type OssNode } from '@/features/oss/models/oss-layout'; import { type OssNode } from '@/features/oss/models/oss-layout';
import { MiniButton } from '@/components/control'; import { MiniButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import { import {
IconClustering,
IconConceptBlock, IconConceptBlock,
IconDestroy, IconDestroy,
IconDownload,
IconEdit2, IconEdit2,
IconFilter,
IconFitImage, IconFitImage,
IconNewItem, IconNewItem,
IconReset, IconReset,
@ -22,7 +27,7 @@ import { type Styling } from '@/components/props';
import { cn } from '@/components/utils'; import { cn } from '@/components/utils';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences'; import { usePreferencesStore } from '@/stores/preferences';
import { isIOS, prepareTooltip } from '@/utils/utils'; import { isIOS, notImplemented, prepareTooltip } from '@/utils/utils';
import { useMutatingOss } from '../../../backend/use-mutating-oss'; import { useMutatingOss } from '../../../backend/use-mutating-oss';
import { useUpdateLayout } from '../../../backend/use-update-layout'; import { useUpdateLayout } from '../../../backend/use-update-layout';
@ -33,8 +38,10 @@ import { useOssFlow } from './oss-flow-context';
import { useGetLayout } from './use-get-layout'; import { useGetLayout } from './use-get-layout';
interface ToolbarOssGraphProps extends Styling { interface ToolbarOssGraphProps extends Styling {
onCreateOperation: () => void;
onCreateBlock: () => void; onCreateBlock: () => void;
onCreateSchema: () => void;
onImportSchema: () => void;
onCreateSynthesis: () => void;
onDelete: () => void; onDelete: () => void;
onResetPositions: () => void; onResetPositions: () => void;
@ -44,8 +51,10 @@ interface ToolbarOssGraphProps extends Styling {
} }
export function ToolbarOssGraph({ export function ToolbarOssGraph({
onCreateOperation,
onCreateBlock, onCreateBlock,
onCreateSchema,
onImportSchema,
onCreateSynthesis,
onDelete, onDelete,
onResetPositions, onResetPositions,
@ -58,18 +67,25 @@ export function ToolbarOssGraph({
const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit(); const { schema, selectedItems, isMutable, canDeleteOperation: canDelete } = useOssEdit();
const isProcessing = useMutatingOss(); const isProcessing = useMutatingOss();
const { resetView, nodes } = useOssFlow(); const { resetView, nodes } = useOssFlow();
const selectedOperation =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
const selectedBlock =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
const getLayout = useGetLayout(); const getLayout = useGetLayout();
const { updateLayout } = useUpdateLayout(); const { updateLayout } = useUpdateLayout();
const { user } = useAuthSuspense();
const menu = useDropdown();
const showOptions = useDialogsStore(state => state.showOssOptions); const showOptions = useDialogsStore(state => state.showOssOptions);
const showSidePanel = usePreferencesStore(state => state.showOssSidePanel); const showSidePanel = usePreferencesStore(state => state.showOssSidePanel);
const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel); const toggleShowSidePanel = usePreferencesStore(state => state.toggleShowOssSidePanel);
const selectedOperation =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.OPERATION ? selectedItems[0] : null;
const selectedBlock =
selectedItems.length === 1 && selectedItems[0].nodeType === NodeType.BLOCK ? selectedItems[0] : null;
function handleMenuToggle() {
hideContextMenu();
menu.toggle();
}
function handleShowOptions() { function handleShowOptions() {
showOptions(); showOptions();
} }
@ -127,40 +143,80 @@ export function ToolbarOssGraph({
<BadgeHelp topic={HelpTopic.UI_OSS_GRAPH} contentClass='sm:max-w-160' offset={4} /> <BadgeHelp topic={HelpTopic.UI_OSS_GRAPH} contentClass='sm:max-w-160' offset={4} />
</div> </div>
{isMutable ? ( {isMutable ? (
<div className='cc-icons'> <div className='cc-icons items-start'>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
aria-label='Сохранить изменения' aria-label='Сохранить изменения'
titleHtml={prepareTooltip('Сохранить изменения', 'Ctrl + S')}
hideTitle={menu.isOpen}
icon={<IconSave size='1.25rem' className='icon-primary' />} icon={<IconSave size='1.25rem' className='icon-primary' />}
onClick={handleSavePositions} onClick={handleSavePositions}
disabled={isProcessing} disabled={isProcessing}
/> />
<MiniButton <MiniButton
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
hideTitle={isContextMenuOpen}
aria-label='Редактировать выбранную' aria-label='Редактировать выбранную'
titleHtml={prepareTooltip('Редактировать выбранную', isIOS() ? '' : 'Правый клик')}
hideTitle={isContextMenuOpen || menu.isOpen}
icon={<IconEdit2 size='1.25rem' className='icon-primary' />} icon={<IconEdit2 size='1.25rem' className='icon-primary' />}
onClick={handleEditItem} onClick={handleEditItem}
disabled={selectedItems.length !== 1 || isProcessing} disabled={selectedItems.length !== 1 || isProcessing}
/> />
<div ref={menu.ref} onBlur={menu.handleBlur} className='relative'>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Новая операция', 'Ctrl + Q')} title='Добавить...'
aria-label='Новая операция' hideTitle={menu.isOpen}
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
onClick={onCreateOperation} onClick={handleMenuToggle}
disabled={isProcessing} disabled={isProcessing}
/> />
<MiniButton <Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
titleHtml={prepareTooltip('Новый блок', 'Ctrl + Shift + Q')} <DropdownButton
aria-label='Новый блок' text='Новый блок'
icon={<IconConceptBlock size='1.25rem' className='icon-green' />} titleHtml={prepareTooltip('Новый блок', 'Alt + 1')}
icon={<IconConceptBlock size='1.25rem' className='text-constructive' />}
onClick={onCreateBlock} onClick={onCreateBlock}
disabled={isProcessing}
/> />
<DropdownButton
text='Новая КС'
titleHtml={prepareTooltip('Новая концептуальная схема', 'Alt + 2')}
icon={<IconNewItem size='1.25rem' className='text-constructive' />}
onClick={onCreateSchema}
/>
<DropdownButton
text='Импорт КС'
titleHtml={prepareTooltip('Импорт концептуальной схемы', 'Alt + 3')}
icon={<IconDownload size='1.25rem' className='text-constructive' />}
onClick={onImportSchema}
/>
<DropdownButton
text='Синтез'
titleHtml={prepareTooltip('Синтез концептуальных схем', 'Alt + 4')}
icon={<IconConceptBlock size='1.25rem' className='text-primary' />}
onClick={onCreateSynthesis}
/>
{user.is_staff ? (
<DropdownButton
disabled
text='Фильтр'
titleHtml={prepareTooltip('Фильтрация конституент', 'Alt + 5')}
icon={<IconFilter size='1.25rem' className='icon-primary' />}
onClick={notImplemented}
/>
) : null}
{user.is_staff ? (
<DropdownButton
disabled
text='Релятивизация'
titleHtml={prepareTooltip('Релятивизация концептуальных схем', 'Alt + 6')}
icon={<IconClustering size='1.25rem' className='icon-primary' />}
onClick={notImplemented}
/>
) : null}
</Dropdown>
</div>
<MiniButton <MiniButton
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
aria-label='Удалить выбранную' aria-label='Удалить выбранную'
titleHtml={prepareTooltip('Удалить выбранную', 'Delete')}
hideTitle={menu.isOpen}
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={onDelete} onClick={onDelete}
disabled={ disabled={

View File

@ -176,6 +176,25 @@ export function sharePage() {
.catch(console.error); .catch(console.error);
} }
/**
* Show error message about not implemented function.
*/
export function notImplemented() {
toast.error('Данная функция еще не реализована');
console.error('Not implemented');
}
/**
* Wrap event handler to prevent default and stop propagation.
*/
export function withPreventDefault<T extends React.SyntheticEvent>(handler: (event: T) => void) {
return (event: T) => {
event.preventDefault();
event.stopPropagation();
handler(event);
};
}
/** /**
* Remove html tags from target string. * Remove html tags from target string.
*/ */