M: Minor UI improvements

This commit is contained in:
Ivan 2025-07-28 21:39:02 +03:00
parent 72f7be3247
commit de108074b1
9 changed files with 205 additions and 172 deletions

View File

@ -12,6 +12,7 @@ import {
IconGraphInputs,
IconGraphMaximize,
IconGraphOutputs,
IconGraphSelection,
IconNewItem,
IconOSS,
IconPredecessor,
@ -101,6 +102,9 @@ export function HelpRSGraphTerm() {
<div className='dense w-84'>
<h2>Выделение</h2>
<ul>
<li>
<IconGraphSelection className='inline-icon' /> выделить связанные...
</li>
<li>
<IconGraphCollapse className='inline-icon' /> все влияющие
</li>

View File

@ -73,7 +73,7 @@ export function DlgCreateSynthesis() {
return (
<ModalForm
header='Создание операции'
header='Создание операции синтеза'
submitText='Создать'
canSubmit={isValid}
onSubmit={event => void methods.handleSubmit(onSubmit)(event)}

View File

@ -101,18 +101,15 @@ export function DlgEditOperation() {
<TabOperation />
</TabPanel>
{target.operation_type === OperationType.SYNTHESIS ? (
<TabPanel>
<TabArguments />
</TabPanel>
) : null}
{target.operation_type === OperationType.SYNTHESIS ? (
<TabPanel>
<TabPanel>{target.operation_type === OperationType.SYNTHESIS ? <TabArguments /> : null}</TabPanel>
<TabPanel>
{target.operation_type === OperationType.SYNTHESIS ? (
<Suspense fallback={<Loader />}>
<TabSubstitutions />
</Suspense>
</TabPanel>
) : null}
) : null}
</TabPanel>
</FormProvider>
</Tabs>
</ModalForm>

View File

@ -77,7 +77,7 @@ export function OssFlow() {
void updateLayout({ itemID: schema.id, data: getLayout() });
}
function handleCreateOperation() {
function handleCreateSynthesis() {
const targetPosition = screenToFlowPosition({ x: window.innerWidth / 2, y: window.innerHeight / 2 });
showCreateOperation({
manager: new LayoutManager(schema, getLayout()),
@ -184,13 +184,23 @@ export function OssFlow() {
withPreventDefault(handleSavePositions)(event);
return;
}
if (event.altKey && event.code === 'Key1') {
withPreventDefault(handleCreateBlock)(event);
return;
}
if (event.altKey && event.code === 'Key2') {
withPreventDefault(handleCreateOperation)(event);
return;
if (event.altKey) {
if (event.code === 'Digit1') {
withPreventDefault(handleCreateBlock)(event);
return;
}
if (event.code === 'Digit2') {
withPreventDefault(handleCreateSynthesis)(event);
return;
}
if (event.code === 'Digit3') {
withPreventDefault(handleImportSchema)(event);
return;
}
if (event.code === 'Digit4') {
withPreventDefault(handleCreateSynthesis)(event);
return;
}
}
if (event.key === 'Delete') {
withPreventDefault(handleDeleteSelected)(event);
@ -220,7 +230,7 @@ export function OssFlow() {
onCreateBlock={handleCreateBlock}
onCreateSchema={handleCreateSchema}
onImportSchema={handleImportSchema}
onCreateSynthesis={handleCreateOperation}
onCreateSynthesis={handleCreateSynthesis}
onDelete={handleDeleteSelected}
onResetPositions={resetGraph}
openContextMenu={openContextMenu}

View File

@ -113,9 +113,8 @@ export function ToolbarOssGraph({
return (
<div
className={cn(
'cc-tab-tools flex flex-col items-center',
'rounded-b-2xl',
'hover:bg-background backdrop-blur-xs',
'grid justify-items-center', //
'rounded-b-2xl hover:bg-background backdrop-blur-xs',
className
)}
{...restProps}

View File

@ -44,7 +44,7 @@ export function SchemasGuide({ schema }: SchemasGuideProps) {
<Tooltip
anchorSelect={`#${globalIDs.graph_schemas}`}
place='right'
className='grid max-w-100 break-words text-base'
className='z-topmost grid max-w-100 break-words text-base'
>
<div className='inline-flex items-center gap-2'>
<span className='w-3 h-3 border rounded-full' style={{ backgroundColor: colorBgSchemas(0) }} />

View File

@ -1,4 +1,5 @@
import { MiniButton } from '@/components/control';
import { Dropdown, DropdownButton, useDropdown } from '@/components/dropdown';
import {
IconGraphCollapse,
IconGraphCore,
@ -7,6 +8,7 @@ import {
IconGraphInverse,
IconGraphMaximize,
IconGraphOutputs,
IconGraphSelection,
IconPredecessor,
IconReset
} from '@/components/icons';
@ -31,6 +33,7 @@ export function ToolbarGraphSelection({
onChange,
...restProps
}: ToolbarGraphSelectionProps) {
const menu = useDropdown();
const emptySelection = selected.length === 0;
function handleSelectCore() {
@ -43,61 +46,77 @@ export function ToolbarGraphSelection({
}
return (
<div className={cn('cc-icons', className)} {...restProps}>
<div className={cn('cc-icons items-center', className)} {...restProps}>
<MiniButton
title='Сбросить выделение'
icon={<IconReset size='1.25rem' className='icon-primary' />}
onClick={() => onChange([])}
disabled={emptySelection}
/>
<div ref={menu.ref} onBlur={menu.handleBlur} className='flex items-center relative'>
<MiniButton
title='Выделить...'
hideTitle={menu.isOpen}
icon={<IconGraphSelection size='1.25rem' className='icon-primary' />}
onClick={menu.toggle}
disabled={emptySelection}
/>
<Dropdown isOpen={menu.isOpen} className='-translate-x-1/2'>
<DropdownButton
text='Влияющие'
title='Выделить все влияющие'
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandAllInputs(selected)])}
disabled={emptySelection}
/>
<DropdownButton
text='Зависимые'
title='Выделить все зависимые'
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandAllOutputs(selected)])}
disabled={emptySelection}
/>
<DropdownButton
text='Поставщики'
title='Выделить поставщиков'
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandInputs(selected)])}
disabled={emptySelection}
/>
<DropdownButton
text='Потребители'
title='Выделить потребителей'
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandOutputs(selected)])}
disabled={emptySelection}
/>
<DropdownButton
text='Максимизация'
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
aria-label='Максимизация - дополнение выделения конституентами, зависимыми только от выделенных'
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
onClick={() => onChange(graph.maximizePart(selected))}
disabled={emptySelection}
/>
</Dropdown>
</div>
<MiniButton
title='Выделить все влияющие'
icon={<IconGraphCollapse size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandAllInputs(selected)])}
disabled={emptySelection}
title='Выделить ядро'
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
onClick={handleSelectCore}
/>
<MiniButton
title='Выделить все зависимые'
icon={<IconGraphExpand size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandAllOutputs(selected)])}
disabled={emptySelection}
/>
<MiniButton
titleHtml='<b>Максимизация</b> <br/>дополнение выделения конституентами, <br/>зависимыми только от выделенных'
aria-label='Максимизация - дополнение выделения конституентами, зависимыми только от выделенных'
icon={<IconGraphMaximize size='1.25rem' className='icon-primary' />}
onClick={() => onChange(graph.maximizePart(selected))}
disabled={emptySelection}
/>
<MiniButton
title='Выделить поставщиков'
icon={<IconGraphInputs size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandInputs(selected)])}
disabled={emptySelection}
/>
<MiniButton
title='Выделить потребителей'
icon={<IconGraphOutputs size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...selected, ...graph.expandOutputs(selected)])}
disabled={emptySelection}
title='Выделить собственные'
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
onClick={handleSelectOwned}
/>
<MiniButton
title='Инвертировать'
icon={<IconGraphInverse size='1.25rem' className='icon-primary' />}
onClick={() => onChange([...graph.nodes.keys()].filter(item => !selected.includes(item)))}
/>
<MiniButton
title='Выделить ядро'
icon={<IconGraphCore size='1.25rem' className='icon-primary' />}
onClick={handleSelectCore}
/>
{isOwned ? (
<MiniButton
title='Выделить собственные'
icon={<IconPredecessor size='1.25rem' className='icon-primary' />}
onClick={handleSelectOwned}
/>
) : null}
</div>
);
}

View File

@ -3,7 +3,7 @@
import { useEffect, useRef } from 'react';
import { type Edge, MarkerType, type Node, useEdgesState, useNodesState, useOnSelectionChange } from 'reactflow';
import { DiagramFlow, useReactFlow, useStoreApi } from '@/components/flow/diagram-flow';
import { DiagramFlow, useReactFlow } from '@/components/flow/diagram-flow';
import { useMainHeight } from '@/stores/app-layout';
import { PARAMETER } from '@/utils/constants';
import { withPreventDefault } from '@/utils/utils';
@ -12,10 +12,7 @@ import { useMutatingRSForm } from '../../../backend/use-mutating-rsform';
import { TGEdgeTypes } from '../../../components/term-graph/graph/tg-edge-types';
import { TGNodeTypes } from '../../../components/term-graph/graph/tg-node-types';
import { SelectColoring } from '../../../components/term-graph/select-coloring';
import { ToolbarFocusedCst } from '../../../components/term-graph/toolbar-focused-cst';
import { ToolbarGraphSelection } from '../../../components/toolbar-graph-selection';
import { applyLayout, type TGNodeData } from '../../../models/graph-api';
import { isBasicConcept } from '../../../models/rsform-api';
import { useTermGraphStore } from '../../../stores/term-graph';
import { useRSEdit } from '../rsedit-context';
@ -36,20 +33,9 @@ export const flowOptions = {
export function TGFlow() {
const mainHeight = useMainHeight();
const { fitView, viewportInitialized } = useReactFlow();
const store = useStoreApi();
const { addSelectedNodes } = store.getState();
const isProcessing = useMutatingRSForm();
const {
isContentEditable,
schema,
selected,
setSelected,
promptDeleteCst,
focusCst,
setFocus,
deselectAll,
navigateCst
} = useRSEdit();
const { isContentEditable, schema, selected, setSelected, promptDeleteCst, focusCst, setFocus, navigateCst } =
useRSEdit();
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([]);
const [edges, setEdges] = useEdgesState<Edge>([]);
@ -59,11 +45,8 @@ export function TGFlow() {
function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id));
if (ids.length === 0) {
deselectAll();
} else {
setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
}
setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
}
useOnSelectionChange({
onChange: onSelectionChange
@ -130,11 +113,6 @@ export function TGFlow() {
);
}
function handleSetSelected(newSelection: number[]) {
setSelected(newSelection);
addSelectedNodes(newSelection.map(id => String(id)));
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (isProcessing) {
return;
@ -166,26 +144,7 @@ export function TGFlow() {
return (
<div className='relative' tabIndex={-1} onKeyDown={handleKeyDown}>
<div className='cc-tab-tools flex flex-col items-center rounded-b-2xl backdrop-blur-xs'>
<ToolbarTermGraph />
{focusCst ? (
<ToolbarFocusedCst
focus={focusCst} //
resetFocus={() => setFocus(null)}
/>
) : (
<ToolbarGraphSelection
graph={schema.graph}
isCore={cstID => {
const cst = schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type);
}}
isOwned={schema.inheritance.length > 0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined}
value={selected}
onChange={handleSetSelected}
/>
)}
</div>
<ToolbarTermGraph className='cc-tab-tools' />
<div className='absolute z-pop top-24 sm:top-16 left-2 sm:left-3 w-54 flex flex-col pointer-events-none'>
<span className='px-2 pb-1 select-none whitespace-nowrap backdrop-blur-xs rounded-xl w-fit'>

View File

@ -1,9 +1,12 @@
import { useReactFlow } from 'reactflow';
import { useReactFlow, useStoreApi } from 'reactflow';
import { HelpTopic } from '@/features/help';
import { BadgeHelp } from '@/features/help/components/badge-help';
import { type ILibraryItemReference } from '@/features/library';
import { MiniSelectorOSS } from '@/features/library/components/mini-selector-oss';
import { ToolbarFocusedCst } from '@/features/rsform/components/term-graph/toolbar-focused-cst';
import { ToolbarGraphSelection } from '@/features/rsform/components/toolbar-graph-selection';
import { isBasicConcept } from '@/features/rsform/models/rsform-api';
import { MiniButton } from '@/components/control';
import {
@ -18,6 +21,7 @@ import {
IconTextOff,
IconTypeGraph
} from '@/components/icons';
import { cn } from '@/components/utils';
import { useDialogsStore } from '@/stores/dialogs';
import { PARAMETER } from '@/utils/constants';
@ -28,17 +32,23 @@ import { useRSEdit } from '../rsedit-context';
import { flowOptions } from './tg-flow';
export function ToolbarTermGraph() {
interface ToolbarTermGraphProps {
className?: string;
}
export function ToolbarTermGraph({ className }: ToolbarTermGraphProps) {
const isProcessing = useMutatingRSForm();
const {
schema, //
schema,
selected,
setSelected,
setFocus,
navigateOss,
isContentEditable,
canDeleteSelected,
createCst,
promptDeleteCst
promptDeleteCst,
focusCst
} = useRSEdit();
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const showParams = useDialogsStore(state => state.showGraphParams);
@ -47,6 +57,8 @@ export function ToolbarTermGraph() {
const toggleClustering = useTermGraphStore(state => state.toggleClustering);
const { fitView } = useReactFlow();
const store = useStoreApi();
const { addSelectedNodes } = store.getState();
function handleShowTypeGraph() {
const typeInfo = schema.items.map(item => ({
@ -86,69 +98,102 @@ export function ToolbarTermGraph() {
navigateOss(newValue.id, event.ctrlKey || event.metaKey);
}
function handleSetSelected(newSelection: number[]) {
setSelected(newSelection);
addSelectedNodes(newSelection.map(id => String(id)));
}
return (
<div className='cc-icons'>
{schema.oss.length > 0 ? <MiniSelectorOSS items={schema.oss} onSelect={handleSelectOss} /> : null}
<MiniButton
title='Настройки фильтрации узлов и связей'
icon={<IconFilter size='1.25rem' className='icon-primary' />}
onClick={showParams}
/>
<MiniButton
title='Задать фокус конституенту'
icon={<IconFocus size='1.25rem' className='icon-primary' />}
disabled={selected.length !== 1}
onClick={handleSetFocus}
/>
<MiniButton
title='Граф целиком'
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
onClick={handleFitView}
/>
<MiniButton
title={!filter.noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!filter.noText ? (
<IconText size='1.25rem' className='icon-green' />
) : (
<IconTextOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleText}
/>
<MiniButton
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
icon={
!filter.foldDerived ? (
<IconClustering size='1.25rem' className='icon-green' />
) : (
<IconClusteringOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleClustering}
/>
{isContentEditable ? (
<div
className={cn(
'grid justify-items-center', //
'rounded-b-2xl hover:bg-background backdrop-blur-xs',
className
)}
>
<div className='cc-icons'>
{schema.oss.length > 0 ? <MiniSelectorOSS items={schema.oss} onSelect={handleSelectOss} /> : null}
<MiniButton
title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
onClick={handleCreateCst}
disabled={isProcessing}
title='Настройки фильтрации узлов и связей'
icon={<IconFilter size='1.25rem' className='icon-primary' />}
onClick={showParams}
/>
) : null}
{isContentEditable ? (
<MiniButton
title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={handleDeleteCst}
disabled={!canDeleteSelected || isProcessing}
title='Задать фокус конституенту'
icon={<IconFocus size='1.25rem' className='icon-primary' />}
disabled={selected.length !== 1}
onClick={handleSetFocus}
/>
) : null}
<MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней'
onClick={handleShowTypeGraph}
/>
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
<MiniButton
title='Граф целиком'
icon={<IconFitImage size='1.25rem' className='icon-primary' />}
onClick={handleFitView}
/>
<MiniButton
title={!filter.noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={
!filter.noText ? (
<IconText size='1.25rem' className='icon-green' />
) : (
<IconTextOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleText}
/>
<MiniButton
title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
icon={
!filter.foldDerived ? (
<IconClustering size='1.25rem' className='icon-green' />
) : (
<IconClusteringOff size='1.25rem' className='icon-primary' />
)
}
onClick={toggleClustering}
/>
<MiniButton
icon={<IconTypeGraph size='1.25rem' className='icon-primary' />}
title='Граф ступеней'
onClick={handleShowTypeGraph}
/>
<BadgeHelp topic={HelpTopic.UI_GRAPH_TERM} contentClass='sm:max-w-160' offset={4} />
</div>
<div className='cc-icons items-center'>
{focusCst ? (
<ToolbarFocusedCst
focus={focusCst} //
resetFocus={() => setFocus(null)}
/>
) : (
<ToolbarGraphSelection
graph={schema.graph}
isCore={cstID => {
const cst = schema.cstByID.get(cstID);
return !!cst && isBasicConcept(cst.cst_type);
}}
isOwned={schema.inheritance.length > 0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined}
value={selected}
onChange={handleSetSelected}
/>
)}
{isContentEditable ? (
<MiniButton
title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />}
onClick={handleCreateCst}
disabled={isProcessing}
/>
) : null}
{isContentEditable ? (
<MiniButton
title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />}
onClick={handleDeleteCst}
disabled={!canDeleteSelected || isProcessing}
/>
) : null}
</div>
</div>
);
}