import * as React from "react"; import * as Mantine from "@mantine/core"; import * as Icons from "@tabler/icons"; import {D3OnionGraphData, D3GraphUpdater} from "../d3graph/reducers/onion_graph"; import {D3GraphEditable, UserEditCallback} from "../d3graph/d3graph_editable"; import { DeltaGraphState, fullDeltaId, deltaGraphReducer, } from "../d3graph/reducers/delta_graph"; import { HistoryGraphState, initialHistoryGraph, fullVersionId, historyGraphReducer, } from "../d3graph/reducers/history_graph"; import * as helpText from "./help_text"; import {MergeView} from "./merge_view"; import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph"; import {RountangleEditor} from "../rountangleEditor/RountangleEditor"; import {InfoHoverCardOverlay} from "../info_hover_card"; import {embed, Version, VersionRegistry} from "onion/version"; import {PrimitiveDelta, PrimitiveRegistry} from "onion/primitive_delta"; import {PrimitiveValue, UUID} from "onion/types"; import {CompositeDelta, CompositeLevel} from "onion/composite_delta"; import {GraphState} from "onion/graph_state"; import {Delta} from "onion/delta"; export const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version."; export interface VersionedModelState { version: Version; // the 'current version' graph: D3OnionGraphData; // the state what is displayed in the leftmost panel historyGraph: HistoryGraphState; // the state of what is displayed in the middle panel deltaGraphL1: DeltaGraphState; // the state of what is displayed in the rightmost panel deltaGraphL0: DeltaGraphState; // the state of what is displayed in the rightmost panel } interface VersionedModelCallbacks { onUserEdit?: UserEditCallback; onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void; onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void; onVersionClicked?: (Version) => void; onMerge?: (outputs: Version[]) => void; } // Basically everything we need to construct the React components for: // - Graph state (+ optionally, a Rountangle Editor) // - History graph (+undo/redo buttons) // - Delta graph // , their state, and callbacks for updating their state. export function newVersionedModel({readonly}) { const versionRegistry = new VersionRegistry(); const graphState = new GraphState(); const compositeLevel = new CompositeLevel(); // SVG coordinates to be used when adding a new node let x = 0; let y = 0; const initialState: VersionedModelState = { version: versionRegistry.initialVersion, graph: emptyGraph, historyGraph: initialHistoryGraph(versionRegistry.initialVersion), deltaGraphL1: emptyGraph, deltaGraphL0: emptyGraph, } // The "current version" is both part of the React state (for rendering undo/redo buttons) and a local variable here, such that we can get the current version (synchronously), even outside of a setState-callback. let currentVersion = versionRegistry.initialVersion; function getCurrentVersion() { return currentVersion; } // This function may only be called from a functional React component. // Given setState callback, returns: // - Callback functions for updating the state // - A callback that constructs all React components (to be used in React render function) function getReducer(setState) { // Create and add a new version, and its deltas, without changing the current version const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => { const composite = compositeLevel.createComposite(deltas, description); const parentVersion = versionRegistry.lookupOptional(parentHash); if (parentVersion !== undefined) { const newVersion = versionRegistry.createVersion(parentVersion, composite); setState(({historyGraph, deltaGraphL1, deltaGraphL0, ...rest}) => { return { // add new version to history graph: historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}), // add the composite delta to the L1-graph + highlight it as 'active': deltaGraphL1: composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1, // never add an empty composite // add the primitive L0-deltas to the L0-graph + highlight them as 'active': deltaGraphL0: composite.deltas.reduce( (graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}), deltaGraphL0), ...rest, }; }); return newVersion; } }; const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion): Version => { const newVersion = addDeltasAndVersion(deltas, description, parentVersion.hash) as Version; gotoVersion(newVersion); return newVersion; }; const appendVersion = (version: Version) => { setState(({historyGraph, ...rest}) => { return { // add new version to history graph: historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version}), ...rest, }; }); } // helper const setGraph = callback => setState(({graph, ...rest}) => ({graph: callback(graph), ...rest})); const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => { const d3Updater = new D3GraphUpdater(setGraph, x, y); graphState.unexec(deltaToUndo, d3Updater); setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({ deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}), deltaGraphL0: deltaToUndo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaInactive', delta}),prevDeltaGraphL0), ...rest, })); }; const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => { const d3Updater = new D3GraphUpdater(setGraph, x, y); graphState.exec(deltaToRedo, d3Updater); setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({ deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}), deltaGraphL0: deltaToRedo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaActive', delta}), prevDeltaGraphL0), ...rest, })); }; const undo = (parentVersion, deltaToUndo) => { undoWithoutUpdatingHistoryGraph(deltaToUndo); currentVersion = parentVersion; setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({ version: parentVersion, historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph, {type: 'highlightVersion', version: prevVersion, bold: false}), {type: 'highlightVersion', version: parentVersion, bold: true}), ...rest, })); }; const redo = (childVersion, deltaToRedo) => { redoWithoutUpdatingHistoryGraph(deltaToRedo); currentVersion = childVersion; setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({ version: childVersion, historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph, {type: 'highlightVersion', version: prevVersion, bold: false}), {type: 'highlightVersion', version: childVersion, bold: true}), ...rest, })); }; const gotoVersion = (chosenVersion: Version) => { const path = currentVersion.findPathTo(chosenVersion); if (path === undefined) { throw new Error("Could not find path to version!"); } for (const [linkType, delta] of path) { if (linkType === 'p') { undoWithoutUpdatingHistoryGraph(delta); } else if (linkType === 'c') { redoWithoutUpdatingHistoryGraph(delta); } } currentVersion = chosenVersion; setState(({historyGraph, version: oldVersion, ...rest}) => ({ version: chosenVersion, historyGraph: historyGraphReducer(historyGraphReducer(historyGraph, {type: 'highlightVersion', version: oldVersion, bold: false}), {type: 'highlightVersion', version: chosenVersion, bold: true}), ...rest, })); }; return { addDeltasAndVersion, gotoVersion, createAndGotoNewVersion, appendVersion, undo, redo, }; } function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) { const graphStateComponent = {readonly ? : {x = newX; y = newY;}} onUserEdit={callbacks.onUserEdit} />} ; const deltaGraphL1Component = ; const deltaGraphL0Component = ; const historyComponent = node ? callbacks.onVersionClicked?.(node.obj) : undefined} /> ; const historyComponentWithMerge = callbacks.onMerge?.(outputs)} onGoto={version => callbacks.onVersionClicked?.(version)} /> const rountangleEditor = , ; const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => { if (parentsOrChildren.length === 0) { return {text}; } if (parentsOrChildren.length === 1) { return {text}; } return {text} ({parentsOrChildren.length.toString()}) {/*{text}*/} {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) => {deltaToUndoOrRedo.getDescription()})} ; } const undoButton = makeUndoOrRedoButton(state.version.parents, "Undo", , null, callbacks.onUndoClicked); const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, , callbacks.onRedoClicked); const undoRedoButtons = <> {undoButton} {redoButton} ; const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => { return (
} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}> UNDO {deltaToUndo.getDescription()}
); }); const stackedRedoButtons = state.version.children.map(([childVersion,deltaToRedo]) => { return (
} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}> REDO {deltaToRedo.getDescription()}
); }); const stackedUndoRedoButtons = (
{stackedUndoButtons}
{stackedRedoButtons}
); const makeTabs = (defaultTab: string, tabs: string[]) => { return {tabs.map(tab => ({ editor: Editor, state: State, history: History, deltaL1: Deltas (L1), deltaL0: Deltas (L0), }[tab]))} {graphStateComponent} {rountangleEditor} {deltaGraphL1Component} {deltaGraphL0Component} {historyComponent} ; } // React components: return { graphStateComponent, rountangleEditor, deltaGraphL1Component, deltaGraphL0Component, historyComponent, historyComponentWithMerge, undoButton, redoButton, undoRedoButtons, stackedUndoRedoButtons, makeTabs, }; } // State, reducers, etc. return { initialState, graphState, versionRegistry, getCurrentVersion, getReducer, getReactComponents, }; }