F: Simplify TGFlow tooltips

This commit is contained in:
Ivan 2025-02-25 12:42:00 +03:00
parent 75106508e3
commit dd79312056
8 changed files with 163 additions and 232 deletions

View File

@ -37,7 +37,7 @@ export const GlobalTooltips = () => {
<Tooltip <Tooltip
clickable clickable
id={globalIDs.constituenta_tooltip} id={globalIDs.constituenta_tooltip}
layer='z-modalTooltip' layer='z-topmost'
className='max-w-[30rem]' className='max-w-[30rem]'
hidden={!hoverCst} hidden={!hoverCst}
> >
@ -47,7 +47,7 @@ export const GlobalTooltips = () => {
</Tooltip> </Tooltip>
<Tooltip <Tooltip
id={globalIDs.operation_tooltip} id={globalIDs.operation_tooltip}
layer='z-modalTooltip' layer='z-topmost'
className='max-w-[35rem] max-h-[40rem] dense' className='max-w-[35rem] max-h-[40rem] dense'
hidden={!hoverOperation} hidden={!hoverOperation}
> >

View File

@ -119,7 +119,6 @@ export function PickMultiConstituenta({
isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited} isOwned={cstID => !schema.cstByID.get(cstID)?.is_inherited}
value={value} value={value}
onChange={onChange} onChange={onChange}
emptySelection={value.length === 0}
className='w-fit' className='w-fit'
/> />
</div> </div>

View File

@ -21,7 +21,6 @@ interface ToolbarGraphSelectionProps extends Styling {
graph: Graph; graph: Graph;
isCore: (item: number) => boolean; isCore: (item: number) => boolean;
isOwned?: (item: number) => boolean; isOwned?: (item: number) => boolean;
emptySelection?: boolean;
} }
export function ToolbarGraphSelection({ export function ToolbarGraphSelection({
@ -31,9 +30,10 @@ export function ToolbarGraphSelection({
isCore, isCore,
isOwned, isOwned,
onChange, onChange,
emptySelection,
...restProps ...restProps
}: ToolbarGraphSelectionProps) { }: ToolbarGraphSelectionProps) {
const emptySelection = selected.length === 0;
function handleSelectCore() { function handleSelectCore() {
const core = [...graph.nodes.keys()].filter(isCore); const core = [...graph.nodes.keys()].filter(isCore);
onChange([...core, ...graph.expandInputs(core)]); onChange([...core, ...graph.expandInputs(core)]);

View File

@ -4,16 +4,10 @@ interface SelectedCounterProps {
totalCount: number; totalCount: number;
selectedCount: number; selectedCount: number;
position?: string; position?: string;
hideZero?: boolean;
} }
export function SelectedCounter({ export function SelectedCounter({ totalCount, selectedCount, position = 'top-0 left-0' }: SelectedCounterProps) {
totalCount, if (selectedCount === 0) {
selectedCount,
hideZero,
position = 'top-0 left-0'
}: SelectedCounterProps) {
if (selectedCount === 0 && hideZero) {
return null; return null;
} }
return ( return (

View File

@ -1,11 +1,8 @@
'use client'; 'use client';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { import {
type Edge, type Edge,
getNodesBounds,
getViewportForBounds,
MarkerType, MarkerType,
type Node, type Node,
ReactFlow, ReactFlow,
@ -15,21 +12,16 @@ import {
useReactFlow, useReactFlow,
useStoreApi useStoreApi
} from 'reactflow'; } from 'reactflow';
import clsx from 'clsx';
import { toPng } from 'html-to-image';
import { useDebounce } from 'use-debounce';
import { Overlay } from '@/components/Container'; import { Overlay } from '@/components/Container';
import { useMainHeight } from '@/stores/appLayout'; import { useMainHeight } from '@/stores/appLayout';
import { useDialogsStore } from '@/stores/dialogs'; import { useTooltipsStore } from '@/stores/tooltips';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { CstType } from '../../../backend/types'; import { CstType } from '../../../backend/types';
import { useMutatingRSForm } from '../../../backend/useMutatingRSForm'; import { useMutatingRSForm } from '../../../backend/useMutatingRSForm';
import { colorBgGraphNode } from '../../../colors'; import { colorBgGraphNode } from '../../../colors';
import { InfoConstituenta } from '../../../components/InfoConstituenta';
import { ToolbarGraphSelection } from '../../../components/ToolbarGraphSelection'; import { ToolbarGraphSelection } from '../../../components/ToolbarGraphSelection';
import { type IConstituenta, type IRSForm } from '../../../models/rsform'; import { type IConstituenta, type IRSForm } from '../../../models/rsform';
import { isBasicConcept } from '../../../models/rsformAPI'; import { isBasicConcept } from '../../../models/rsformAPI';
@ -46,12 +38,8 @@ import { ToolbarFocusedCst } from './ToolbarFocusedCst';
import { ToolbarTermGraph } from './ToolbarTermGraph'; import { ToolbarTermGraph } from './ToolbarTermGraph';
import { ViewHidden } from './ViewHidden'; import { ViewHidden } from './ViewHidden';
const ZOOM_MAX = 3; export const ZOOM_MAX = 3;
const ZOOM_MIN = 0.25; export const ZOOM_MIN = 0.25;
// ratio to client size used to determine which side of screen popup should be
const HOVER_LIMIT_X = 0.4;
const HOVER_LIMIT_Y = 0.6;
export function TGFlow() { export function TGFlow() {
const mainHeight = useMainHeight(); const mainHeight = useMainHeight();
@ -65,19 +53,17 @@ export function TGFlow() {
selected, selected,
setSelected, setSelected,
navigateCst, navigateCst,
createCst,
toggleSelect, toggleSelect,
canDeleteSelected, canDeleteSelected,
promptDeleteCst promptDeleteCst
} = useRSEdit(); } = useRSEdit();
const showParams = useDialogsStore(state => state.showGraphParams);
const filter = useTermGraphStore(state => state.filter); const filter = useTermGraphStore(state => state.filter);
const setFilter = useTermGraphStore(state => state.setFilter);
const coloring = useTermGraphStore(state => state.coloring); const coloring = useTermGraphStore(state => state.coloring);
const setColoring = useTermGraphStore(state => state.setColoring); const setColoring = useTermGraphStore(state => state.setColoring);
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
const [nodes, setNodes, onNodesChange] = useNodesState([]); const [nodes, setNodes, onNodesChange] = useNodesState([]);
const [edges, setEdges] = useEdgesState([]); const [edges, setEdges] = useEdgesState([]);
@ -85,14 +71,7 @@ export function TGFlow() {
const filteredGraph = produceFilteredGraph(schema, filter, focusCst); const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
const [hidden, setHidden] = useState<number[]>([]); const [hidden, setHidden] = useState<number[]>([]);
const [isDragging, setIsDragging] = useState(false);
const [hoverID, setHoverID] = useState<number | null>(null);
const hoverCst = hoverID && schema.cstByID.get(hoverID);
const [hoverCstDebounced] = useDebounce(hoverCst, PARAMETER.graphPopupDelay);
const [hoverLeft, setHoverLeft] = useState(true);
const [needReset, setNeedReset] = useState(true); const [needReset, setNeedReset] = useState(true);
const [toggleResetView, setToggleResetView] = useState(false);
function onSelectionChange({ nodes }: { nodes: Node[] }) { function onSelectionChange({ nodes }: { nodes: Node[] }) {
const ids = nodes.map(node => Number(node.id)); const ids = nodes.map(node => Number(node.id));
@ -115,7 +94,6 @@ export function TGFlow() {
} }
}); });
setHidden(newDismissed); setHidden(newDismissed);
setHoverID(null);
}, [schema, filteredGraph]); }, [schema, filteredGraph]);
const resetNodes = useCallback(() => { const resetNodes = useCallback(() => {
@ -177,62 +155,11 @@ export function TGFlow() {
resetNodes(); resetNodes();
}, [needReset, schema, resetNodes, flow.viewportInitialized]); }, [needReset, schema, resetNodes, flow.viewportInitialized]);
useEffect(() => {
setTimeout(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, PARAMETER.minimalTimeout);
}, [toggleResetView, flow, focusCst, filter]);
function handleSetSelected(newSelection: number[]) { function handleSetSelected(newSelection: number[]) {
setSelected(newSelection); setSelected(newSelection);
addSelectedNodes(newSelection.map(id => String(id))); addSelectedNodes(newSelection.map(id => String(id)));
} }
function handleCreateCst() {
const definition = selected.map(id => schema.cstByID.get(id)!.alias).join(' ');
createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
}
function handleDeleteCst() {
if (!canDeleteSelected) {
return;
}
promptDeleteCst();
}
function handleSaveImage() {
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
if (canvas === null) {
toast.error(errorMsg.imageFailed);
return;
}
const imageWidth = PARAMETER.ossImageWidth;
const imageHeight = PARAMETER.ossImageHeight;
const nodesBounds = getNodesBounds(nodes);
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
toPng(canvas, {
backgroundColor: APP_COLORS.bgDefault,
width: imageWidth,
height: imageHeight,
style: {
width: String(imageWidth),
height: String(imageHeight),
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom * 2})`
}
})
.then(dataURL => {
const a = document.createElement('a');
a.setAttribute('download', `${schema.alias}.png`);
a.setAttribute('href', dataURL);
a.click();
})
.catch(error => {
console.error(error);
toast.error(errorMsg.imageFailed);
});
}
function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) { function handleKeyDown(event: React.KeyboardEvent<HTMLDivElement>) {
if (isProcessing) { if (isProcessing) {
return; return;
@ -240,7 +167,7 @@ export function TGFlow() {
if (event.key === 'Escape') { if (event.key === 'Escape') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setFocusCst(null); handleSetFocus(null);
handleSetSelected([]); handleSetSelected([]);
return; return;
} }
@ -250,31 +177,24 @@ export function TGFlow() {
if (event.key === 'Delete') { if (event.key === 'Delete') {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
handleDeleteCst(); if (canDeleteSelected) {
promptDeleteCst();
}
return; return;
} }
} }
function handleFoldDerived() {
setFilter({
...filter,
foldDerived: !filter.foldDerived
});
setTimeout(() => {
setToggleResetView(prev => !prev);
}, PARAMETER.graphRefreshDelay);
}
function handleSetFocus(cstID: number | null) { function handleSetFocus(cstID: number | null) {
if (cstID === null) { if (cstID === null) {
setFocusCst(null); setFocusCst(null);
} else { } else {
const target = schema.cstByID.get(cstID) ?? null; const target = schema.cstByID.get(cstID) ?? null;
setFocusCst(prev => (prev === target ? null : target)); setFocusCst(prev => (prev === target ? null : target));
if (target) { }
setSelected([]); setSelected([]);
} setTimeout(() => {
} flow.fitView({ duration: PARAMETER.zoomDuration });
}, PARAMETER.minimalTimeout);
} }
function handleNodeContextMenu(event: React.MouseEvent, cstID: number) { function handleNodeContextMenu(event: React.MouseEvent, cstID: number) {
@ -289,32 +209,18 @@ export function TGFlow() {
navigateCst(cstID); navigateCst(cstID);
} }
function handleNodeEnter(event: React.MouseEvent, cstID: number) { function handleNodeEnter(cstID: number) {
setHoverID(cstID); const cst = schema.cstByID.get(cstID);
setHoverLeft( if (cst) {
event.clientX / window.innerWidth >= HOVER_LIMIT_X || event.clientY / window.innerHeight >= HOVER_LIMIT_Y setActiveCst(cst);
); }
} }
return ( return (
<> <>
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'> <Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
<ToolbarTermGraph <ToolbarTermGraph />
noText={filter.noText} {focusCst ? <ToolbarFocusedCst focusedCst={focusCst} onResetFocus={() => handleSetFocus(null)} /> : null}
foldDerived={filter.foldDerived}
showParamsDialog={() => showParams()}
onCreate={handleCreateCst}
onDelete={handleDeleteCst}
onFitView={() => setToggleResetView(prev => !prev)}
onSaveImage={handleSaveImage}
toggleFoldDerived={handleFoldDerived}
toggleNoText={() =>
setFilter({
...filter,
noText: !filter.noText
})
}
/>
{!focusCst ? ( {!focusCst ? (
<ToolbarGraphSelection <ToolbarGraphSelection
graph={schema.graph} graph={schema.graph}
@ -325,59 +231,17 @@ export function TGFlow() {
isOwned={schema.inheritance.length > 0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined} isOwned={schema.inheritance.length > 0 ? cstID => !schema.cstByID.get(cstID)?.is_inherited : undefined}
value={selected} value={selected}
onChange={handleSetSelected} onChange={handleSetSelected}
emptySelection={selected.length === 0}
/>
) : null}
{focusCst ? (
<ToolbarFocusedCst
center={focusCst}
reset={() => handleSetFocus(null)}
showInputs={filter.focusShowInputs}
showOutputs={filter.focusShowOutputs}
toggleShowInputs={() =>
setFilter({
...filter,
focusShowInputs: !filter.focusShowInputs
})
}
toggleShowOutputs={() =>
setFilter({
...filter,
focusShowOutputs: !filter.focusShowOutputs
})
}
/> />
) : null} ) : null}
</Overlay> </Overlay>
<div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}> <div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}>
<SelectedCounter <SelectedCounter
hideZero
totalCount={schema.stats?.count_all ?? 0} totalCount={schema.stats?.count_all ?? 0}
selectedCount={selected.length} selectedCount={selected.length}
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]' position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]'
/> />
{hoverCstDebounced ? (
<Overlay
layer='z-tooltip'
position={clsx('top-[3.5rem]', { 'left-[2.6rem]': hoverLeft, 'right-[2.6rem]': !hoverLeft })}
className={clsx(
'w-[25rem] max-h-[calc(100dvh-15rem)]',
'px-3',
'cc-scroll-y cc-fade-in',
'border shadow-md',
'clr-input cc-fade-in',
'text-sm'
)}
style={{
opacity: !isDragging && hoverCst && hoverCst === hoverCstDebounced ? 1 : 0
}}
>
<InfoConstituenta className='pt-1 pb-2' data={hoverCstDebounced} />
</Overlay>
) : null}
<Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1 pointer-events-none'> <Overlay position='top-[6.15rem] sm:top-[5.9rem] left-0' className='flex gap-1 pointer-events-none'>
<div className='flex flex-col ml-2 w-[13.5rem]'> <div className='flex flex-col ml-2 w-[13.5rem]'>
<GraphSelectors schema={schema} coloring={coloring} onChangeColoring={setColoring} /> <GraphSelectors schema={schema} coloring={coloring} onChangeColoring={setColoring} />
@ -405,10 +269,7 @@ export function TGFlow() {
edgeTypes={TGEdgeTypes} edgeTypes={TGEdgeTypes}
maxZoom={ZOOM_MAX} maxZoom={ZOOM_MAX}
minZoom={ZOOM_MIN} minZoom={ZOOM_MIN}
onNodeDragStart={() => setIsDragging(true)} onNodeMouseEnter={(_, node) => handleNodeEnter(Number(node.id))}
onNodeDragStop={() => setIsDragging(false)}
onNodeMouseEnter={(event, node) => handleNodeEnter(event, Number(node.id))}
onNodeMouseLeave={() => setHoverID(null)}
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))} onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
onNodeContextMenu={(event, node) => handleNodeContextMenu(event, Number(node.id))} onNodeContextMenu={(event, node) => handleNodeContextMenu(event, Number(node.id))}
/> />

View File

@ -1,5 +1,7 @@
'use client'; 'use client';
import { useTermGraphStore } from '@/features/rsform/stores/termGraph';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/Icons'; import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/Icons';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
@ -8,35 +10,40 @@ import { type IConstituenta } from '../../../models/rsform';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
interface ToolbarFocusedCstProps { interface ToolbarFocusedCstProps {
center: IConstituenta; focusedCst: IConstituenta;
showInputs: boolean; onResetFocus: () => void;
showOutputs: boolean;
reset: () => void;
toggleShowInputs: () => void;
toggleShowOutputs: () => void;
} }
export function ToolbarFocusedCst({ export function ToolbarFocusedCst({ focusedCst, onResetFocus }: ToolbarFocusedCstProps) {
center,
reset,
showInputs,
showOutputs,
toggleShowInputs,
toggleShowOutputs
}: ToolbarFocusedCstProps) {
const { deselectAll } = useRSEdit(); const { deselectAll } = useRSEdit();
const filter = useTermGraphStore(state => state.filter);
const setFilter = useTermGraphStore(state => state.setFilter);
function resetSelection() { function resetSelection() {
reset(); onResetFocus();
deselectAll(); deselectAll();
} }
function handleShowInputs() {
setFilter({
...filter,
focusShowInputs: !filter.focusShowInputs
});
}
function handleShowOutputs() {
setFilter({
...filter,
focusShowOutputs: !filter.focusShowOutputs
});
}
return ( return (
<div className='items-center cc-icons'> <div className='items-center cc-icons'>
<div className='w-[7.8rem] text-right select-none' style={{ color: APP_COLORS.fgPurple }}> <div className='w-[7.8rem] text-right select-none' style={{ color: APP_COLORS.fgPurple }}>
Фокус Фокус
<b className='px-1'> {center.alias} </b> <b className='px-1'> {focusedCst.alias} </b>
</div> </div>
<MiniButton <MiniButton
titleHtml='Сбросить фокус' titleHtml='Сбросить фокус'
@ -44,14 +51,14 @@ export function ToolbarFocusedCst({
onClick={resetSelection} onClick={resetSelection}
/> />
<MiniButton <MiniButton
title={showInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'} title={filter.focusShowInputs ? 'Скрыть поставщиков' : 'Отобразить поставщиков'}
icon={<IconGraphInputs size='1.25rem' className={showInputs ? 'icon-green' : 'icon-primary'} />} icon={<IconGraphInputs size='1.25rem' className={filter.focusShowInputs ? 'icon-green' : 'icon-primary'} />}
onClick={toggleShowInputs} onClick={handleShowInputs}
/> />
<MiniButton <MiniButton
title={showOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'} title={filter.focusShowOutputs ? 'Скрыть потребителей' : 'Отобразить потребителей'}
icon={<IconGraphOutputs size='1.25rem' className={showOutputs ? 'icon-green' : 'icon-primary'} />} icon={<IconGraphOutputs size='1.25rem' className={filter.focusShowOutputs ? 'icon-green' : 'icon-primary'} />}
onClick={toggleShowOutputs} onClick={handleShowOutputs}
/> />
</div> </div>
); );

View File

@ -1,7 +1,12 @@
import { toast } from 'react-toastify';
import { getNodesBounds, getViewportForBounds, useNodes, useReactFlow } from 'reactflow';
import clsx from 'clsx'; import clsx from 'clsx';
import { toPng } from 'html-to-image';
import { BadgeHelp, HelpTopic } from '@/features/help'; import { BadgeHelp, HelpTopic } from '@/features/help';
import { MiniSelectorOSS } from '@/features/library'; import { MiniSelectorOSS } from '@/features/library';
import { CstType } from '@/features/rsform/backend/types';
import { useTermGraphStore } from '@/features/rsform/stores/termGraph';
import { MiniButton } from '@/components/Control'; import { MiniButton } from '@/components/Control';
import { import {
@ -17,39 +22,34 @@ import {
IconTypeGraph IconTypeGraph
} from '@/components/Icons'; } from '@/components/Icons';
import { useDialogsStore } from '@/stores/dialogs'; import { useDialogsStore } from '@/stores/dialogs';
import { usePreferencesStore } from '@/stores/preferences';
import { APP_COLORS } from '@/styling/colors';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { errorMsg } from '@/utils/labels';
import { useMutatingRSForm } from '../../../backend/useMutatingRSForm'; import { useMutatingRSForm } from '../../../backend/useMutatingRSForm';
import { useRSEdit } from '../RSEditContext'; import { useRSEdit } from '../RSEditContext';
interface ToolbarTermGraphProps { import { ZOOM_MAX, ZOOM_MIN } from './TGFlow';
noText: boolean;
foldDerived: boolean;
showParamsDialog: () => void; export function ToolbarTermGraph() {
onCreate: () => void;
onDelete: () => void;
onFitView: () => void;
onSaveImage: () => void;
toggleFoldDerived: () => void;
toggleNoText: () => void;
}
export function ToolbarTermGraph({
noText,
foldDerived,
toggleNoText,
toggleFoldDerived,
showParamsDialog,
onCreate,
onDelete,
onFitView,
onSaveImage
}: ToolbarTermGraphProps) {
const isProcessing = useMutatingRSForm(); const isProcessing = useMutatingRSForm();
const darkMode = usePreferencesStore(state => state.darkMode);
const {
schema, //
selected,
navigateOss,
isContentEditable,
canDeleteSelected,
createCst,
promptDeleteCst
} = useRSEdit();
const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph); const showTypeGraph = useDialogsStore(state => state.showShowTypeGraph);
const { schema, navigateOss, isContentEditable, canDeleteSelected } = useRSEdit(); const showParams = useDialogsStore(state => state.showGraphParams);
const filter = useTermGraphStore(state => state.filter);
const setFilter = useTermGraphStore(state => state.setFilter);
const nodes = useNodes();
const flow = useReactFlow();
function handleShowTypeGraph() { function handleShowTypeGraph() {
const typeInfo = schema.items.map(item => ({ const typeInfo = schema.items.map(item => ({
@ -60,6 +60,74 @@ export function ToolbarTermGraph({
showTypeGraph({ items: typeInfo }); showTypeGraph({ items: typeInfo });
} }
function handleCreateCst() {
const definition = selected.map(id => schema.cstByID.get(id)!.alias).join(' ');
createCst(selected.length === 0 ? CstType.BASE : CstType.TERM, false, definition);
}
function handleDeleteCst() {
if (!canDeleteSelected || isProcessing) {
return;
}
promptDeleteCst();
}
function handleToggleNoText() {
setFilter({
...filter,
noText: !filter.noText
});
}
function handleSaveImage() {
const canvas: HTMLElement | null = document.querySelector('.react-flow__viewport');
if (canvas === null) {
toast.error(errorMsg.imageFailed);
return;
}
const imageWidth = PARAMETER.ossImageWidth;
const imageHeight = PARAMETER.ossImageHeight;
const nodesBounds = getNodesBounds(nodes);
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
toPng(canvas, {
backgroundColor: darkMode ? APP_COLORS.bgDefaultDark : APP_COLORS.bgDefaultLight,
width: imageWidth,
height: imageHeight,
style: {
width: String(imageWidth),
height: String(imageHeight),
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom * 2})`
}
})
.then(dataURL => {
const a = document.createElement('a');
a.setAttribute('download', `${schema.alias}.png`);
a.setAttribute('href', dataURL);
a.click();
})
.catch(error => {
console.error(error);
toast.error(errorMsg.imageFailed);
});
}
function handleFitView() {
setTimeout(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, PARAMETER.minimalTimeout);
}
function handleFoldDerived() {
setFilter({
...filter,
foldDerived: !filter.foldDerived
});
setTimeout(() => {
flow.fitView({ duration: PARAMETER.zoomDuration });
}, PARAMETER.graphRefreshDelay);
}
return ( return (
<div className='cc-icons'> <div className='cc-icons'>
{schema.oss.length > 0 ? ( {schema.oss.length > 0 ? (
@ -71,41 +139,41 @@ export function ToolbarTermGraph({
<MiniButton <MiniButton
title='Настройки фильтрации узлов и связей' title='Настройки фильтрации узлов и связей'
icon={<IconFilter size='1.25rem' className='icon-primary' />} icon={<IconFilter size='1.25rem' className='icon-primary' />}
onClick={showParamsDialog} onClick={showParams}
/> />
<MiniButton <MiniButton
icon={<IconFitImage size='1.25rem' className='icon-primary' />} icon={<IconFitImage size='1.25rem' className='icon-primary' />}
title='Граф целиком' title='Граф целиком'
onClick={onFitView} onClick={handleFitView}
/> />
<MiniButton <MiniButton
title={!noText ? 'Скрыть текст' : 'Отобразить текст'} title={!filter.noText ? 'Скрыть текст' : 'Отобразить текст'}
icon={ icon={
!noText ? ( !filter.noText ? (
<IconText size='1.25rem' className='icon-green' /> <IconText size='1.25rem' className='icon-green' />
) : ( ) : (
<IconTextOff size='1.25rem' className='icon-primary' /> <IconTextOff size='1.25rem' className='icon-primary' />
) )
} }
onClick={toggleNoText} onClick={handleToggleNoText}
/> />
<MiniButton <MiniButton
title={!foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'} title={!filter.foldDerived ? 'Скрыть порожденные' : 'Отобразить порожденные'}
icon={ icon={
!foldDerived ? ( !filter.foldDerived ? (
<IconClustering size='1.25rem' className='icon-green' /> <IconClustering size='1.25rem' className='icon-green' />
) : ( ) : (
<IconClusteringOff size='1.25rem' className='icon-primary' /> <IconClusteringOff size='1.25rem' className='icon-primary' />
) )
} }
onClick={toggleFoldDerived} onClick={handleFoldDerived}
/> />
{isContentEditable ? ( {isContentEditable ? (
<MiniButton <MiniButton
title='Новая конституента' title='Новая конституента'
icon={<IconNewItem size='1.25rem' className='icon-green' />} icon={<IconNewItem size='1.25rem' className='icon-green' />}
disabled={isProcessing} disabled={isProcessing}
onClick={onCreate} onClick={handleCreateCst}
/> />
) : null} ) : null}
{isContentEditable ? ( {isContentEditable ? (
@ -113,7 +181,7 @@ export function ToolbarTermGraph({
title='Удалить выбранные' title='Удалить выбранные'
icon={<IconDestroy size='1.25rem' className='icon-red' />} icon={<IconDestroy size='1.25rem' className='icon-red' />}
disabled={!canDeleteSelected || isProcessing} disabled={!canDeleteSelected || isProcessing}
onClick={onDelete} onClick={handleDeleteCst}
/> />
) : null} ) : null}
<MiniButton <MiniButton
@ -124,7 +192,7 @@ export function ToolbarTermGraph({
<MiniButton <MiniButton
icon={<IconImage size='1.25rem' className='icon-primary' />} icon={<IconImage size='1.25rem' className='icon-primary' />}
title='Сохранить изображение' title='Сохранить изображение'
onClick={onSaveImage} onClick={handleSaveImage}
/> />
<BadgeHelp <BadgeHelp
topic={HelpTopic.UI_GRAPH_TERM} topic={HelpTopic.UI_GRAPH_TERM}

View File

@ -3,6 +3,7 @@
import { Handle, Position } from 'reactflow'; import { Handle, Position } from 'reactflow';
import { APP_COLORS } from '@/styling/colors'; import { APP_COLORS } from '@/styling/colors';
import { globalIDs } from '@/utils/constants';
const DESCRIPTION_THRESHOLD = 15; const DESCRIPTION_THRESHOLD = 15;
const LABEL_THRESHOLD = 3; const LABEL_THRESHOLD = 3;
@ -39,6 +40,7 @@ export function TGNode(node: TGNodeInternal) {
backgroundColor: !node.selected ? node.data.fill : APP_COLORS.bgActiveSelection, backgroundColor: !node.selected ? node.data.fill : APP_COLORS.bgActiveSelection,
fontSize: node.data.label.length > LABEL_THRESHOLD ? FONT_SIZE_MED : FONT_SIZE_MAX fontSize: node.data.label.length > LABEL_THRESHOLD ? FONT_SIZE_MED : FONT_SIZE_MAX
}} }}
data-tooltip-id={globalIDs.constituenta_tooltip}
> >
<div <div
style={{ style={{