F: Simplify TGFlow tooltips
This commit is contained in:
parent
75106508e3
commit
dd79312056
|
@ -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}
|
||||||
>
|
>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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)]);
|
||||||
|
|
|
@ -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 (
|
||||||
|
|
|
@ -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))}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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={{
|
||||||
|
|
Loading…
Reference in New Issue
Block a user