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