mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
R: Rework TGFlow rerenders
This commit is contained in:
parent
fcdaa81836
commit
0fec8f9d61
|
@ -4,8 +4,7 @@ import { Overlay } from '@/components/Container';
|
|||
import { SelectSingle } from '@/components/Input';
|
||||
|
||||
import { mapLabelColoring } from '../../../labels';
|
||||
import { type IRSForm } from '../../../models/rsform';
|
||||
import { type GraphColoring } from '../../../stores/termGraph';
|
||||
import { type GraphColoring, useTermGraphStore } from '../../../stores/termGraph';
|
||||
|
||||
import { SchemasGuide } from './SchemasGuide';
|
||||
|
||||
|
@ -15,19 +14,16 @@ import { SchemasGuide } from './SchemasGuide';
|
|||
const SelectorGraphColoring: { value: GraphColoring; label: string }[] = //
|
||||
[...mapLabelColoring.entries()].map(item => ({ value: item[0], label: item[1] }));
|
||||
|
||||
interface GraphSelectorsProps {
|
||||
schema: IRSForm;
|
||||
coloring: GraphColoring;
|
||||
onChangeColoring: (newValue: GraphColoring) => void;
|
||||
}
|
||||
export function GraphSelectors() {
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||
|
||||
export function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSelectorsProps) {
|
||||
return (
|
||||
<div className='border rounded-b-none select-none clr-input rounded-t-md pointer-events-auto'>
|
||||
<Overlay position='right-[2.5rem] top-[0.25rem]'>
|
||||
{coloring === 'status' ? <BadgeHelp topic={HelpTopic.UI_CST_STATUS} className='min-w-[25rem]' /> : null}
|
||||
{coloring === 'type' ? <BadgeHelp topic={HelpTopic.UI_CST_CLASS} className='min-w-[25rem]' /> : null}
|
||||
{coloring === 'schemas' ? <SchemasGuide schema={schema} /> : null}
|
||||
{coloring === 'schemas' ? <SchemasGuide /> : null}
|
||||
</Overlay>
|
||||
<SelectSingle
|
||||
noBorder
|
||||
|
@ -35,7 +31,7 @@ export function GraphSelectors({ schema, coloring, onChangeColoring }: GraphSele
|
|||
options={SelectorGraphColoring}
|
||||
isSearchable={false}
|
||||
value={coloring ? { value: coloring, label: mapLabelColoring.get(coloring) } : null}
|
||||
onChange={data => onChangeColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
onChange={data => setColoring(data?.value ?? SelectorGraphColoring[0].value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -5,14 +5,11 @@ import { IconHelp } from '@/components/Icons';
|
|||
import { globalIDs, prefixes } from '@/utils/constants';
|
||||
|
||||
import { colorBgSchemas } from '../../../colors';
|
||||
import { type IRSForm } from '../../../models/rsform';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface SchemasGuideProps {
|
||||
schema: IRSForm;
|
||||
}
|
||||
|
||||
export function SchemasGuide({ schema }: SchemasGuideProps) {
|
||||
export function SchemasGuide() {
|
||||
const { items: libraryItems } = useLibrary();
|
||||
const { schema } = useRSEdit();
|
||||
|
||||
const schemas = (() => {
|
||||
const processed = new Set<number>();
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
import { Overlay } from '@/components/Container';
|
||||
|
||||
interface SelectedCounterProps {
|
||||
totalCount: number;
|
||||
selectedCount: number;
|
||||
position?: string;
|
||||
}
|
||||
|
||||
export function SelectedCounter({ totalCount, selectedCount, position = 'top-0 left-0' }: SelectedCounterProps) {
|
||||
if (selectedCount === 0) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Overlay position={`px-2 ${position}`} className='select-none whitespace-nowrap cc-blur rounded-xl'>
|
||||
Выбор {selectedCount} из {totalCount}
|
||||
</Overlay>
|
||||
);
|
||||
}
|
|
@ -15,30 +15,27 @@ import {
|
|||
|
||||
import { Overlay } from '@/components/Container';
|
||||
import { useMainHeight } from '@/stores/appLayout';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { CstType } from '../../../backend/types';
|
||||
import { useMutatingRSForm } from '../../../backend/useMutatingRSForm';
|
||||
import { colorBgGraphNode } from '../../../colors';
|
||||
import { ToolbarGraphSelection } from '../../../components/ToolbarGraphSelection';
|
||||
import { type IConstituenta, type IRSForm } from '../../../models/rsform';
|
||||
import { type IConstituenta } from '../../../models/rsform';
|
||||
import { isBasicConcept } from '../../../models/rsformAPI';
|
||||
import { type GraphFilterParams, useTermGraphStore } from '../../../stores/termGraph';
|
||||
import { useTermGraphStore } from '../../../stores/termGraph';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
import { TGEdgeTypes } from './graph/TGEdgeTypes';
|
||||
import { applyLayout } from './graph/TGLayout';
|
||||
import { type TGNodeData } from './graph/TGNode';
|
||||
import { TGNodeTypes } from './graph/TGNodeTypes';
|
||||
import { GraphSelectors } from './GraphSelectors';
|
||||
import { SelectedCounter } from './SelectedCounter';
|
||||
import { ToolbarFocusedCst } from './ToolbarFocusedCst';
|
||||
import { ToolbarTermGraph } from './ToolbarTermGraph';
|
||||
import { useFilteredGraph } from './useFilteredGraph';
|
||||
import { ViewHidden } from './ViewHidden';
|
||||
|
||||
export const ZOOM_MAX = 3;
|
||||
export const ZOOM_MIN = 0.25;
|
||||
export const VIEW_PADDING = 0.3;
|
||||
|
||||
export function TGFlow() {
|
||||
const mainHeight = useMainHeight();
|
||||
|
@ -51,50 +48,38 @@ export function TGFlow() {
|
|||
schema,
|
||||
selected,
|
||||
setSelected,
|
||||
navigateCst,
|
||||
toggleSelect,
|
||||
canDeleteSelected,
|
||||
promptDeleteCst
|
||||
promptDeleteCst,
|
||||
focusCst,
|
||||
setFocus,
|
||||
deselectAll
|
||||
} = useRSEdit();
|
||||
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const setColoring = useTermGraphStore(state => state.setColoring);
|
||||
|
||||
const [needReset, setNeedReset] = useState(true);
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
||||
const [edges, setEdges] = useEdgesState([]);
|
||||
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
|
||||
const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
|
||||
const [hidden, setHidden] = useState<number[]>([]);
|
||||
|
||||
const [needReset, setNeedReset] = useState(true);
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const { filteredGraph, hidden } = useFilteredGraph();
|
||||
|
||||
function onSelectionChange({ nodes }: { nodes: Node[] }) {
|
||||
const ids = nodes.map(node => Number(node.id));
|
||||
if (ids.length === 0) {
|
||||
setSelected([]);
|
||||
deselectAll();
|
||||
} else {
|
||||
setSelected(prev => [...prev.filter(nodeID => !filteredGraph.hasNode(nodeID)), ...ids]);
|
||||
}
|
||||
}
|
||||
|
||||
useOnSelectionChange({
|
||||
onChange: onSelectionChange
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newDismissed: number[] = [];
|
||||
schema.items.forEach(cst => {
|
||||
if (!filteredGraph.nodes.has(cst.id)) {
|
||||
newDismissed.push(cst.id);
|
||||
}
|
||||
});
|
||||
setHidden(newDismissed);
|
||||
}, [schema, filteredGraph]);
|
||||
setNeedReset(true);
|
||||
}, [schema, filter, focusCst]);
|
||||
|
||||
const resetNodes = useCallback(() => {
|
||||
const newNodes: Node<TGNodeData>[] = [];
|
||||
const newNodes: Node<IConstituenta>[] = [];
|
||||
filteredGraph.nodes.forEach(node => {
|
||||
const cst = schema.cstByID.get(node.id);
|
||||
if (cst) {
|
||||
|
@ -103,10 +88,7 @@ export function TGFlow() {
|
|||
type: 'concept',
|
||||
selected: selected.includes(node.id),
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
fill: focusCst === cst ? APP_COLORS.bgPurple : colorBgGraphNode(cst, coloring),
|
||||
cst: cst
|
||||
}
|
||||
data: cst
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -137,11 +119,11 @@ export function TGFlow() {
|
|||
|
||||
setNodes(newNodes);
|
||||
setEdges(newEdges);
|
||||
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, selected, focusCst, coloring]);
|
||||
|
||||
useEffect(() => {
|
||||
setNeedReset(true);
|
||||
}, [schema, focusCst, coloring, filter]);
|
||||
setTimeout(() => {
|
||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||
}, PARAMETER.minimalTimeout);
|
||||
}, [schema, filteredGraph, setNodes, setEdges, filter.noText, selected, fitView]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!needReset || !viewportInitialized) {
|
||||
|
@ -163,8 +145,7 @@ export function TGFlow() {
|
|||
if (event.key === 'Escape') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSetFocus(null);
|
||||
handleSetSelected([]);
|
||||
setFocus(null);
|
||||
return;
|
||||
}
|
||||
if (!isContentEditable) {
|
||||
|
@ -180,36 +161,11 @@ export function TGFlow() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleSetFocus(cstID: number | null) {
|
||||
if (cstID === null) {
|
||||
setFocusCst(null);
|
||||
} else {
|
||||
const target = schema.cstByID.get(cstID) ?? null;
|
||||
setFocusCst(prev => (prev === target ? null : target));
|
||||
}
|
||||
setSelected([]);
|
||||
setTimeout(() => {
|
||||
fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, PARAMETER.minimalTimeout);
|
||||
}
|
||||
|
||||
function handleNodeContextMenu(event: React.MouseEvent, cstID: number) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handleSetFocus(cstID);
|
||||
}
|
||||
|
||||
function handleNodeDoubleClick(event: React.MouseEvent, cstID: number) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateCst(cstID);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Overlay position='cc-tab-tools' className='flex flex-col items-center rounded-b-2xl cc-blur'>
|
||||
<ToolbarTermGraph />
|
||||
{focusCst ? <ToolbarFocusedCst focusedCst={focusCst} onResetFocus={() => handleSetFocus(null)} /> : null}
|
||||
<ToolbarFocusedCst />
|
||||
{!focusCst ? (
|
||||
<ToolbarGraphSelection
|
||||
graph={schema.graph}
|
||||
|
@ -225,24 +181,15 @@ export function TGFlow() {
|
|||
</Overlay>
|
||||
|
||||
<div className='cc-fade-in' tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<SelectedCounter
|
||||
totalCount={schema.stats?.count_all ?? 0}
|
||||
selectedCount={selected.length}
|
||||
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem]'
|
||||
/>
|
||||
|
||||
<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]'>
|
||||
<GraphSelectors schema={schema} coloring={coloring} onChangeColoring={setColoring} />
|
||||
<ViewHidden
|
||||
items={hidden}
|
||||
selected={selected}
|
||||
schema={schema}
|
||||
coloringScheme={coloring}
|
||||
toggleSelection={toggleSelect}
|
||||
setFocus={handleSetFocus}
|
||||
/>
|
||||
<Overlay
|
||||
position='top-[4.4rem] sm:top-[4.1rem] left-[0.5rem] sm:left-[0.65rem] w-[13.5rem]'
|
||||
className='flex flex-col pointer-events-none'
|
||||
>
|
||||
<div className='px-2 pb-1 select-none whitespace-nowrap cc-blur rounded-xl'>
|
||||
Выбор {selected.length} из {schema.stats?.count_all ?? 0}
|
||||
</div>
|
||||
<GraphSelectors />
|
||||
<ViewHidden items={hidden} />
|
||||
</Overlay>
|
||||
|
||||
<div className='relative outline-hidden w-[100dvw]' style={{ height: mainHeight }}>
|
||||
|
@ -258,71 +205,10 @@ export function TGFlow() {
|
|||
edgeTypes={TGEdgeTypes}
|
||||
maxZoom={ZOOM_MAX}
|
||||
minZoom={ZOOM_MIN}
|
||||
onNodeDoubleClick={(event, node) => handleNodeDoubleClick(event, Number(node.id))}
|
||||
onNodeContextMenu={(event, node) => handleNodeContextMenu(event, Number(node.id))}
|
||||
onContextMenu={event => event.preventDefault()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ====== Internals =========
|
||||
function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
||||
const filtered = schema.graph.clone();
|
||||
const allowedTypes: CstType[] = (() => {
|
||||
const result: CstType[] = [];
|
||||
if (params.allowBase) result.push(CstType.BASE);
|
||||
if (params.allowStruct) result.push(CstType.STRUCTURED);
|
||||
if (params.allowTerm) result.push(CstType.TERM);
|
||||
if (params.allowAxiom) result.push(CstType.AXIOM);
|
||||
if (params.allowFunction) result.push(CstType.FUNCTION);
|
||||
if (params.allowPredicate) result.push(CstType.PREDICATE);
|
||||
if (params.allowConstant) result.push(CstType.CONSTANT);
|
||||
if (params.allowTheorem) result.push(CstType.THEOREM);
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (params.noHermits) {
|
||||
filtered.removeIsolated();
|
||||
}
|
||||
if (params.noTemplates) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst !== focusCst && cst.is_template) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (allowedTypes.length < Object.values(CstType).length) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst !== focusCst && !allowedTypes.includes(cst.cst_type)) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!focusCst && params.foldDerived) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst.spawner) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (focusCst) {
|
||||
const includes: number[] = [
|
||||
focusCst.id,
|
||||
...focusCst.spawn,
|
||||
...(params.focusShowInputs ? schema.graph.expandInputs([focusCst.id]) : []),
|
||||
...(params.focusShowOutputs ? schema.graph.expandOutputs([focusCst.id]) : [])
|
||||
];
|
||||
schema.items.forEach(cst => {
|
||||
if (!includes.includes(cst.id)) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (params.noTransitive) {
|
||||
filtered.transitiveReduction();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
|
|
@ -6,23 +6,16 @@ import { MiniButton } from '@/components/Control';
|
|||
import { IconGraphInputs, IconGraphOutputs, IconReset } from '@/components/Icons';
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
|
||||
import { type IConstituenta } from '../../../models/rsform';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface ToolbarFocusedCstProps {
|
||||
focusedCst: IConstituenta;
|
||||
onResetFocus: () => void;
|
||||
}
|
||||
|
||||
export function ToolbarFocusedCst({ focusedCst, onResetFocus }: ToolbarFocusedCstProps) {
|
||||
const { deselectAll } = useRSEdit();
|
||||
export function ToolbarFocusedCst() {
|
||||
const { setFocus, focusCst } = useRSEdit();
|
||||
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const setFilter = useTermGraphStore(state => state.setFilter);
|
||||
|
||||
function resetSelection() {
|
||||
onResetFocus();
|
||||
deselectAll();
|
||||
setFocus(null);
|
||||
}
|
||||
|
||||
function handleShowInputs() {
|
||||
|
@ -39,11 +32,15 @@ export function ToolbarFocusedCst({ focusedCst, onResetFocus }: ToolbarFocusedCs
|
|||
});
|
||||
}
|
||||
|
||||
if (!focusCst) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='items-center cc-icons'>
|
||||
<div className='w-[7.8rem] text-right select-none' style={{ color: APP_COLORS.fgPurple }}>
|
||||
Фокус
|
||||
<b className='px-1'> {focusedCst.alias} </b>
|
||||
<b className='px-1'> {focusCst.alias} </b>
|
||||
</div>
|
||||
<MiniButton
|
||||
titleHtml='Сбросить фокус'
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { toast } from 'react-toastify';
|
||||
import { getNodesBounds, getViewportForBounds, useNodes, useReactFlow } from 'reactflow';
|
||||
import { getNodesBounds, getViewportForBounds, useReactFlow } from 'reactflow';
|
||||
import clsx from 'clsx';
|
||||
import { toPng } from 'html-to-image';
|
||||
|
||||
|
@ -30,7 +30,7 @@ import { errorMsg } from '@/utils/labels';
|
|||
import { useMutatingRSForm } from '../../../backend/useMutatingRSForm';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
import { ZOOM_MAX, ZOOM_MIN } from './TGFlow';
|
||||
import { VIEW_PADDING, ZOOM_MAX, ZOOM_MIN } from './TGFlow';
|
||||
|
||||
export function ToolbarTermGraph() {
|
||||
const isProcessing = useMutatingRSForm();
|
||||
|
@ -48,8 +48,8 @@ export function ToolbarTermGraph() {
|
|||
const showParams = useDialogsStore(state => state.showGraphParams);
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const setFilter = useTermGraphStore(state => state.setFilter);
|
||||
const nodes = useNodes();
|
||||
const flow = useReactFlow();
|
||||
|
||||
const { fitView, getNodes } = useReactFlow();
|
||||
|
||||
function handleShowTypeGraph() {
|
||||
const typeInfo = schema.items.map(item => ({
|
||||
|
@ -88,7 +88,7 @@ export function ToolbarTermGraph() {
|
|||
|
||||
const imageWidth = PARAMETER.ossImageWidth;
|
||||
const imageHeight = PARAMETER.ossImageHeight;
|
||||
const nodesBounds = getNodesBounds(nodes);
|
||||
const nodesBounds = getNodesBounds(getNodes());
|
||||
const viewport = getViewportForBounds(nodesBounds, imageWidth, imageHeight, ZOOM_MIN, ZOOM_MAX);
|
||||
toPng(canvas, {
|
||||
backgroundColor: darkMode ? APP_COLORS.bgDefaultDark : APP_COLORS.bgDefaultLight,
|
||||
|
@ -114,7 +114,7 @@ export function ToolbarTermGraph() {
|
|||
|
||||
function handleFitView() {
|
||||
setTimeout(() => {
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
fitView({ duration: PARAMETER.zoomDuration, padding: VIEW_PADDING });
|
||||
}, PARAMETER.minimalTimeout);
|
||||
}
|
||||
|
||||
|
@ -123,9 +123,6 @@ export function ToolbarTermGraph() {
|
|||
...filter,
|
||||
foldDerived: !filter.foldDerived
|
||||
});
|
||||
setTimeout(() => {
|
||||
flow.fitView({ duration: PARAMETER.zoomDuration });
|
||||
}, PARAMETER.graphRefreshDelay);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -12,36 +12,35 @@ import { APP_COLORS } from '@/styling/colors';
|
|||
import { globalIDs, PARAMETER, prefixes } from '@/utils/constants';
|
||||
|
||||
import { colorBgGraphNode } from '../../../colors';
|
||||
import { type IRSForm } from '../../../models/rsform';
|
||||
import { type GraphColoring, useTermGraphStore } from '../../../stores/termGraph';
|
||||
import { type IConstituenta } from '../../../models/rsform';
|
||||
import { useTermGraphStore } from '../../../stores/termGraph';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
interface ViewHiddenProps {
|
||||
schema: IRSForm;
|
||||
items: number[];
|
||||
selected: number[];
|
||||
coloringScheme: GraphColoring;
|
||||
|
||||
toggleSelection: (cstID: number) => void;
|
||||
setFocus: (cstID: number) => void;
|
||||
}
|
||||
|
||||
export function ViewHidden({ items, selected, toggleSelection, setFocus, schema, coloringScheme }: ViewHiddenProps) {
|
||||
export function ViewHidden({ items }: ViewHiddenProps) {
|
||||
const windowSize = useWindowSize();
|
||||
const localSelected = items.filter(id => selected.includes(id));
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
const { navigateCst, setFocus, schema, selected, toggleSelect } = useRSEdit();
|
||||
|
||||
const { navigateCst } = useRSEdit();
|
||||
const localSelected = items.filter(id => selected.includes(id));
|
||||
const isFolded = useTermGraphStore(state => state.foldHidden);
|
||||
const toggleFolded = useTermGraphStore(state => state.toggleFoldHidden);
|
||||
const setActiveCst = useTooltipsStore(state => state.setActiveCst);
|
||||
const hiddenHeight = useFitHeight(windowSize.isSmall ? '10.4rem + 2px' : '12.5rem + 2px');
|
||||
|
||||
function handleClick(cstID: number, event: React.MouseEvent<Element>) {
|
||||
if (event.ctrlKey || event.metaKey) {
|
||||
setFocus(cstID);
|
||||
} else {
|
||||
toggleSelection(cstID);
|
||||
function handleClick(event: React.MouseEvent<Element>, cstID: number) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
toggleSelect(cstID);
|
||||
}
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent<HTMLElement>, target: IConstituenta) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setFocus(target);
|
||||
}
|
||||
|
||||
if (items.length <= 0) {
|
||||
|
@ -92,14 +91,13 @@ export function ViewHidden({ items, selected, toggleSelection, setFocus, schema,
|
|||
>
|
||||
{items.map(cstID => {
|
||||
const cst = schema.cstByID.get(cstID)!;
|
||||
const id = `${prefixes.cst_hidden_list}${cst.alias}`;
|
||||
return (
|
||||
<button
|
||||
key={id}
|
||||
key={`${prefixes.cst_hidden_list}${cst.alias}`}
|
||||
type='button'
|
||||
className='min-w-[3rem] rounded-md text-center select-none'
|
||||
style={{
|
||||
backgroundColor: colorBgGraphNode(cst, coloringScheme),
|
||||
backgroundColor: colorBgGraphNode(cst, coloring),
|
||||
...(localSelected.includes(cstID)
|
||||
? {
|
||||
outlineWidth: '2px',
|
||||
|
@ -108,7 +106,8 @@ export function ViewHidden({ items, selected, toggleSelection, setFocus, schema,
|
|||
}
|
||||
: {})
|
||||
}}
|
||||
onClick={event => handleClick(cstID, event)}
|
||||
onClick={event => handleClick(event, cstID)}
|
||||
onContextMenu={event => handleContextMenu(event, cst)}
|
||||
onDoubleClick={() => navigateCst(cstID)}
|
||||
data-tooltip-id={globalIDs.constituenta_tooltip}
|
||||
onMouseEnter={() => setActiveCst(cst)}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { type Edge, type Node } from 'reactflow';
|
||||
import dagre from '@dagrejs/dagre';
|
||||
|
||||
import { type IConstituenta } from '@/features/rsform/models/rsform';
|
||||
|
||||
import { PARAMETER } from '@/utils/constants';
|
||||
|
||||
import { type TGNodeData } from './TGNode';
|
||||
|
||||
export function applyLayout(nodes: Node<TGNodeData>[], edges: Edge[], subLabels?: boolean) {
|
||||
export function applyLayout(nodes: Node<IConstituenta>[], edges: Edge[], subLabels: boolean) {
|
||||
const dagreGraph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
|
||||
dagreGraph.setGraph({
|
||||
rankdir: 'TB',
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
import { Handle, Position } from 'reactflow';
|
||||
|
||||
import { type IConstituenta } from '@/features/rsform/models/rsform';
|
||||
import { useTermGraphStore } from '@/features/rsform/stores/termGraph';
|
||||
|
||||
import { APP_COLORS } from '@/styling/colors';
|
||||
import { globalIDs } from '@/utils/constants';
|
||||
|
||||
import { colorBgGraphNode } from '../../../../colors';
|
||||
import { type IConstituenta } from '../../../../models/rsform';
|
||||
import { useTermGraphStore } from '../../../../stores/termGraph';
|
||||
import { useRSEdit } from '../../RSEditContext';
|
||||
|
||||
const DESCRIPTION_THRESHOLD = 15;
|
||||
const LABEL_THRESHOLD = 3;
|
||||
|
||||
|
@ -15,17 +17,12 @@ const FONT_SIZE_MAX = 14;
|
|||
const FONT_SIZE_MED = 12;
|
||||
const FONT_SIZE_MIN = 10;
|
||||
|
||||
export interface TGNodeData {
|
||||
fill: string;
|
||||
cst: IConstituenta;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents graph AST node internal data.
|
||||
*/
|
||||
interface TGNodeInternal {
|
||||
id: string;
|
||||
data: TGNodeData;
|
||||
data: IConstituenta;
|
||||
selected: boolean;
|
||||
dragging: boolean;
|
||||
xPos: number;
|
||||
|
@ -33,9 +30,24 @@ interface TGNodeInternal {
|
|||
}
|
||||
|
||||
export function TGNode(node: TGNodeInternal) {
|
||||
const { focusCst, setFocus: setFocusCst, navigateCst } = useRSEdit();
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
const label = node.data.cst.alias;
|
||||
const description = !filter.noText ? node.data.cst.term_resolved : '';
|
||||
const coloring = useTermGraphStore(state => state.coloring);
|
||||
|
||||
const label = node.data.alias;
|
||||
const description = !filter.noText ? node.data.term_resolved : '';
|
||||
|
||||
function handleContextMenu(event: React.MouseEvent<HTMLElement>) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
setFocusCst(focusCst === node.data ? null : node.data);
|
||||
}
|
||||
|
||||
function handleDoubleClick(event: React.MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
navigateCst(node.data.id);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -43,11 +55,18 @@ export function TGNode(node: TGNodeInternal) {
|
|||
<div
|
||||
className='w-full h-full cursor-default flex items-center justify-center rounded-full'
|
||||
style={{
|
||||
backgroundColor: !node.selected ? node.data.fill : APP_COLORS.bgActiveSelection,
|
||||
backgroundColor: node.selected
|
||||
? APP_COLORS.bgActiveSelection
|
||||
: focusCst === node.data
|
||||
? APP_COLORS.bgPurple
|
||||
: colorBgGraphNode(node.data, coloring),
|
||||
fontSize: label.length > LABEL_THRESHOLD ? FONT_SIZE_MED : FONT_SIZE_MAX
|
||||
}}
|
||||
data-tooltip-id={globalIDs.tooltip}
|
||||
data-tooltip-html={describeCstNode(node.data.cst)}
|
||||
data-tooltip-html={describeCstNode(node.data)}
|
||||
data-tooltip-hidden={node.dragging}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
|
@ -66,6 +85,8 @@ export function TGNode(node: TGNodeInternal) {
|
|||
style={{
|
||||
fontSize: description.length > DESCRIPTION_THRESHOLD ? FONT_SIZE_MIN : FONT_SIZE_MED
|
||||
}}
|
||||
onContextMenu={handleContextMenu}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
<div className='absolute top-0 px-1 left-0 text-center w-full line-clamp-3 hover:line-clamp-none'>
|
||||
{description}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { CstType } from '../../../backend/types';
|
||||
import { type IConstituenta, type IRSForm } from '../../../models/rsform';
|
||||
import { type GraphFilterParams, useTermGraphStore } from '../../../stores/termGraph';
|
||||
import { useRSEdit } from '../RSEditContext';
|
||||
|
||||
export function useFilteredGraph() {
|
||||
const { schema, focusCst } = useRSEdit();
|
||||
const filter = useTermGraphStore(state => state.filter);
|
||||
|
||||
const filteredGraph = produceFilteredGraph(schema, filter, focusCst);
|
||||
const hidden = schema.items.filter(cst => !filteredGraph.hasNode(cst.id)).map(cst => cst.id);
|
||||
|
||||
return { filteredGraph, hidden };
|
||||
}
|
||||
|
||||
// ====== Internals =========
|
||||
function produceFilteredGraph(schema: IRSForm, params: GraphFilterParams, focusCst: IConstituenta | null) {
|
||||
const filtered = schema.graph.clone();
|
||||
const allowedTypes: CstType[] = (() => {
|
||||
const result: CstType[] = [];
|
||||
if (params.allowBase) result.push(CstType.BASE);
|
||||
if (params.allowStruct) result.push(CstType.STRUCTURED);
|
||||
if (params.allowTerm) result.push(CstType.TERM);
|
||||
if (params.allowAxiom) result.push(CstType.AXIOM);
|
||||
if (params.allowFunction) result.push(CstType.FUNCTION);
|
||||
if (params.allowPredicate) result.push(CstType.PREDICATE);
|
||||
if (params.allowConstant) result.push(CstType.CONSTANT);
|
||||
if (params.allowTheorem) result.push(CstType.THEOREM);
|
||||
return result;
|
||||
})();
|
||||
|
||||
if (params.noHermits) {
|
||||
filtered.removeIsolated();
|
||||
}
|
||||
if (params.noTemplates) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst !== focusCst && cst.is_template) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (allowedTypes.length < Object.values(CstType).length) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst !== focusCst && !allowedTypes.includes(cst.cst_type)) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (!focusCst && params.foldDerived) {
|
||||
schema.items.forEach(cst => {
|
||||
if (cst.spawner) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (focusCst) {
|
||||
const includes: number[] = [
|
||||
focusCst.id,
|
||||
...focusCst.spawn,
|
||||
...(params.focusShowInputs ? schema.graph.expandInputs([focusCst.id]) : []),
|
||||
...(params.focusShowOutputs ? schema.graph.expandOutputs([focusCst.id]) : [])
|
||||
];
|
||||
schema.items.forEach(cst => {
|
||||
if (!includes.includes(cst.id)) {
|
||||
filtered.foldNode(cst.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (params.noTransitive) {
|
||||
filtered.transitiveReduction();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
|
@ -31,6 +31,7 @@ export enum RSTabID {
|
|||
export interface IRSEditContext {
|
||||
schema: IRSForm;
|
||||
selected: number[];
|
||||
focusCst: IConstituenta | null;
|
||||
activeCst: IConstituenta | null;
|
||||
activeVersion?: number;
|
||||
|
||||
|
@ -48,6 +49,7 @@ export interface IRSEditContext {
|
|||
|
||||
deleteSchema: () => void;
|
||||
|
||||
setFocus: (newValue: IConstituenta | null) => void;
|
||||
setSelected: React.Dispatch<React.SetStateAction<number[]>>;
|
||||
select: (target: number) => void;
|
||||
deselect: (target: number) => void;
|
||||
|
@ -103,6 +105,7 @@ export const RSEditState = ({
|
|||
|
||||
const [selected, setSelected] = useState<number[]>([]);
|
||||
const canDeleteSelected = selected.length > 0 && selected.every(id => !schema.cstByID.get(id)?.is_inherited);
|
||||
const [focusCst, setFocusCst] = useState<IConstituenta | null>(null);
|
||||
|
||||
const activeCst = selected.length === 0 ? null : schema.cstByID.get(selected[selected.length - 1])!;
|
||||
|
||||
|
@ -125,6 +128,11 @@ export const RSEditState = ({
|
|||
[schema, adjustRole, isOwned, user, adminMode]
|
||||
);
|
||||
|
||||
function handleSetFocus(newValue: IConstituenta | null) {
|
||||
setFocusCst(newValue);
|
||||
setSelected([]);
|
||||
}
|
||||
|
||||
function navigateVersion(versionID?: number) {
|
||||
router.push(urls.schema(schema.id, versionID));
|
||||
}
|
||||
|
@ -311,6 +319,7 @@ export const RSEditState = ({
|
|||
<RSEditContext
|
||||
value={{
|
||||
schema,
|
||||
focusCst,
|
||||
selected,
|
||||
activeCst,
|
||||
activeVersion,
|
||||
|
@ -329,6 +338,7 @@ export const RSEditState = ({
|
|||
|
||||
deleteSchema,
|
||||
|
||||
setFocus: handleSetFocus,
|
||||
setSelected,
|
||||
select: (target: number) => setSelected(prev => [...prev, target]),
|
||||
deselect: (target: number) => setSelected(prev => prev.filter(id => id !== target)),
|
||||
|
|
|
@ -29,7 +29,7 @@ export function RSTabs({ activeID, activeTab }: RSTabsProps) {
|
|||
|
||||
const hideFooter = useAppLayoutStore(state => state.hideFooter);
|
||||
const { setIsModified } = useModificationStore();
|
||||
const { schema, selected, setSelected, navigateRSForm } = useRSEdit();
|
||||
const { schema, selected, setSelected, deselectAll, navigateRSForm } = useRSEdit();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const oldTitle = document.title;
|
||||
|
@ -46,11 +46,11 @@ export function RSTabs({ activeID, activeTab }: RSTabsProps) {
|
|||
if (activeID && schema.cstByID.has(activeID)) {
|
||||
setSelected([activeID]);
|
||||
} else {
|
||||
setSelected([]);
|
||||
deselectAll();
|
||||
}
|
||||
}
|
||||
return () => hideFooter(false);
|
||||
}, [activeTab, activeID, setSelected, schema, hideFooter, setIsModified]);
|
||||
}, [activeTab, activeID, setSelected, deselectAll, schema, hideFooter, setIsModified]);
|
||||
|
||||
function onSelectTab(index: number, last: number, event: Event) {
|
||||
if (last === index) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user