| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378 |
- 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 {GraphView, Renderer} from "./graph_view";
- import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
- import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
- import {InfoHoverCardOverlay} from "../info_hover_card";
- import {OnionContext, OnionContextType} from "../onion_context";
- import {Version, VersionRegistry, Embeddings} 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";
- import {DeltaParser} from "onion/delta_parser";
- 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 newOnion({readonly, primitiveRegistry, 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;
- // 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;
- }
- function useOnion(overridenCallbacks: (any) => VersionedModelCallbacks) {
- const [version, setVersion] = React.useState<Version>(versionRegistry.initialVersion);
- const [graph, setGraph] = React.useState<D3OnionGraphData>(emptyGraph);
- const [historyGraph, setHistoryGraph] = React.useState<HistoryGraphState>(initialHistoryGraph(versionRegistry.initialVersion));
- const [deltaGraphL1, setDeltaGraphL1] = React.useState<DeltaGraphState>(emptyGraph);
- const [deltaGraphL0, setDeltaGraphL0] = React.useState<DeltaGraphState>(emptyGraph);
- // Reducer
- // Create and add a new version, and its deltas, without changing the current version
- const createVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer, embeddings: (Version) => Embeddings = () => new Map()) => {
- const parentVersion = versionRegistry.lookupOptional(parentHash);
- if (parentVersion !== undefined) {
- // The following is not very efficient, and it looks weird and hacky, but it works.
- // Cleaner looking solution would be to implement a function version.getGraphState() ...
- let prevVersion = currentVersion;
- gotoVersion(parentVersion);
- const dependencies = compositeLevel.findCompositeDependencies(deltas, graphState.composites)
- const composite = compositeLevel.createComposite(deltas, description, dependencies);
- gotoVersion(prevVersion); // go back
- const newVersion = versionRegistry.createVersion(parentVersion, composite, embeddings);
- setHistoryGraph(historyGraph => historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}));
- setDeltaGraphL1(deltaGraphL1 => composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1);
- setDeltaGraphL0(deltaGraphL0 => composite.deltas.reduce((graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}), deltaGraphL0));
- return newVersion;
- }
- };
- const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion, embeddings: (Version) => Embeddings = () => new Map()): Version => {
- const newVersion = createVersion(deltas, description, parentVersion.hash, embeddings) as Version;
- gotoVersion(newVersion);
- return newVersion;
- };
- // Idempotent
- const appendVersions = (versions: Version[]) => {
- setHistoryGraph(historyGraph => {
- const versionsToAdd: Version[] = [];
- const addIfDontHaveYet = version => {
- if (!(historyGraph.nodes.some(n => n.obj === version) || versionsToAdd.includes(version))) {
- collectVersions(version);
- }
- }
- const collectVersions = version => {
- // first add child, then parent
- // this prevents infinite recursion when a version explicitly embeds itself.
- versionsToAdd.push(version);
- for (const [parent] of version.parents) {
- addIfDontHaveYet(parent);
- }
- for (const {version: guest} of version.embeddings.values()) {
- addIfDontHaveYet(guest);
- }
- }
- for (const v of versions) {
- collectVersions(v);
- }
- return versionsToAdd.reduceRight((historyGraph, version) => historyGraphReducer(historyGraph, {type: 'addVersion', version}), historyGraph);
- })
- }
- // // Idempotent
- // const appendDelta = (delta: CompositeDelta) => {
- // setState(({deltaGraphL0, deltaGraphL1, ...rest}) => {
- // return {
- // deltaGraphL1: deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta, active: false}),
- // deltaGraphL0: delta.reduce(
- // (graph, delta) => deltaGraphReducer(deltaGraphL0, {type: 'addDelta', delta, active: false}),
- // deltaGraphL0),
- // ...rest,
- // };
- // });
- // }
- const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
- const d3Updater = new D3GraphUpdater(setGraph, x, y);
- graphState.unexec(deltaToUndo, d3Updater);
- setDeltaGraphL0(deltaGraphL0 => deltaToUndo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaInactive', delta}), deltaGraphL0));
- setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}));
- };
- const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
- const d3Updater = new D3GraphUpdater(setGraph, x, y);
- graphState.exec(deltaToRedo, d3Updater);
- setDeltaGraphL0(deltaGraphL0 => deltaToRedo.deltas.reduce((deltaGraphL0, delta) => deltaGraphReducer(deltaGraphL0, {type: 'setDeltaActive', delta}), deltaGraphL0));
- setDeltaGraphL1(deltaGraphL1 => deltaGraphReducer(deltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}));
- };
- const undo = (parentVersion, deltaToUndo) => {
- undoWithoutUpdatingHistoryGraph(deltaToUndo);
- currentVersion = parentVersion;
- setVersion(prevVersion => {
- setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
- {type: 'highlightVersion', version: prevVersion, bold: false}),
- {type: 'highlightVersion', version: parentVersion, bold: true}));
- return parentVersion;
- });
- };
- const redo = (childVersion, deltaToRedo) => {
- redoWithoutUpdatingHistoryGraph(deltaToRedo);
- currentVersion = childVersion;
- setVersion(prevVersion => {
- setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
- {type: 'highlightVersion', version: prevVersion, bold: false}),
- {type: 'highlightVersion', version: childVersion, bold: true}));
- return childVersion;
- });
- };
- 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;
- setVersion(prevVersion => {
- setHistoryGraph(historyGraph => historyGraphReducer(historyGraphReducer(historyGraph,
- {type: 'highlightVersion', version: prevVersion, bold: false}),
- {type: 'highlightVersion', version: chosenVersion, bold: true}));
- return chosenVersion;
- });
- };
- const reducer = {
- createVersion,
- gotoVersion,
- createAndGotoNewVersion,
- appendVersions,
- undo,
- redo,
- };
- // Components
- const defaultCallbacks = {
- onUserEdit: createAndGotoNewVersion,
- onUndoClicked: undo,
- onRedoClicked: redo,
- onVersionClicked: gotoVersion,
- onMerge: appendVersions,
- };
- const callbacks = Object.assign({}, defaultCallbacks, overridenCallbacks(reducer));
- const graphStateComponent = readonly ?
- <GraphView graphData={graph} help={helpText.graphEditorReadonly} mouseUpHandler={()=>{}} />
- : <InfoHoverCardOverlay contents={helpText.graphEditor}>
- <D3GraphEditable
- graph={graph}
- graphState={graphState}
- forces={defaultGraphForces}
- setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
- onUserEdit={callbacks.onUserEdit}
- />
- </InfoHoverCardOverlay>;
- const deltaComponentProps = {
- help: helpText.deltaGraph,
- defaultRenderer: ('graphviz' as Renderer),
- mouseUpHandler: (e, {x,y}, node) => {
- if (node) {
- alert(JSON.stringify(node.obj.serialize(), null, 2));
- }
- },
- };
- const deltaGraphL0Component = <GraphView<Delta,null> graphData={deltaGraphL0} {...deltaComponentProps} />;
- const deltaGraphL1Component = <GraphView<Delta,null> graphData={deltaGraphL1} {...deltaComponentProps} />;
- const historyComponentWithMerge = <MergeView
- history={historyGraph}
- forces={defaultGraphForces}
- versionRegistry={versionRegistry}
- onMerge={outputs => callbacks.onMerge?.(outputs)}
- onGoto={version => callbacks.onVersionClicked?.(version)}
- appendVersions={reducer.appendVersions}
- {...{primitiveRegistry, compositeLevel, createVersion}}
- />;
- const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
- <RountangleEditor
- graph={graph}
- graphState={graphState}
- onUserEdit={callbacks.onUserEdit}
- />
- </InfoHoverCardOverlay>;
- const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
- if (parentsOrChildren.length === 0) {
- return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
- }
- if (parentsOrChildren.length === 1) {
- return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
- }
- return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionProps={{duration:0}}>
- <Mantine.Menu.Target>
- <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
- </Mantine.Menu.Target>
- <Mantine.Menu.Dropdown>
- {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
- <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
- </Mantine.Menu.Dropdown>
- </Mantine.Menu>;
- }
- const undoButton = makeUndoOrRedoButton(version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
- const redoButton = makeUndoOrRedoButton(version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
- const undoRedoButtons = <>
- {undoButton}
- <Mantine.Space w="sm"/>
- {redoButton}
- </>;
- const stackedUndoButtons = version.parents.map(([parentVersion,deltaToUndo]) => {
- return (
- <div key={fullVersionId(parentVersion)}>
- <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconChevronLeft size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
- UNDO {deltaToUndo.getDescription()}
- </Mantine.Button>
- <Mantine.Space h="xs"/>
- </div>
- );
- });
- const stackedRedoButtons = version.children.map(([childVersion,deltaToRedo]) => {
- return (
- <div key={fullVersionId(childVersion)}>
- <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconChevronRight size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
- REDO {deltaToRedo.getDescription()}
- </Mantine.Button>
- <Mantine.Space h="xs"/>
- </div>
- );
- });
- const stackedUndoRedoButtons = (
- <Mantine.SimpleGrid cols={2}>
- <div>{stackedUndoButtons}</div>
- <div>{stackedRedoButtons}</div>
- </Mantine.SimpleGrid>
- );
- const makeTabs = (defaultTab: string, tabs: string[]) => {
- return <Mantine.Tabs defaultValue={defaultTab} keepMounted={false}>
- <Mantine.Tabs.List grow>
- {tabs.map(tab => ({
- editor: <Mantine.Tabs.Tab key={tab} value={tab}>Editor</Mantine.Tabs.Tab>,
- state: <Mantine.Tabs.Tab key={tab} value={tab}>State</Mantine.Tabs.Tab>,
- merge: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
- deltaL1: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L1)</Mantine.Tabs.Tab>,
- deltaL0: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L0)</Mantine.Tabs.Tab>,
- }[tab]))}
- </Mantine.Tabs.List>
- <Mantine.Tabs.Panel value="state">
- {graphStateComponent}
- </Mantine.Tabs.Panel>
- <Mantine.Tabs.Panel value="editor">
- {rountangleEditor}
- </Mantine.Tabs.Panel>
- <Mantine.Tabs.Panel value="deltaL1">
- {deltaGraphL1Component}
- </Mantine.Tabs.Panel>
- <Mantine.Tabs.Panel value="deltaL0">
- {deltaGraphL0Component}
- </Mantine.Tabs.Panel>
- <Mantine.Tabs.Panel value="merge">
- {historyComponentWithMerge}
- </Mantine.Tabs.Panel>
- </Mantine.Tabs>;
- }
- return {
- state: {
- version,
- },
- reducer,
- components: {
- graphStateComponent,
- rountangleEditor,
- deltaGraphL1Component,
- deltaGraphL0Component,
- historyComponentWithMerge,
- undoButton,
- redoButton,
- undoRedoButtons,
- stackedUndoRedoButtons,
- makeTabs,
- },
- };
- }
- return {
- graphState,
- useOnion,
- }
- }
|