123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- 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 = <InfoHoverCardOverlay
- contents={readonly ? helpText.graphEditorReadonly : helpText.graphEditor}>
- {readonly ?
- <D3Graph graph={state.graph} forces={defaultGraphForces} />
- : <D3GraphEditable
- graph={state.graph}
- graphState={graphState}
- forces={defaultGraphForces}
- setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
- onUserEdit={callbacks.onUserEdit}
- />}
- </InfoHoverCardOverlay>;
- const deltaGraphL1Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
- <D3Graph graph={state.deltaGraphL1} forces={defaultGraphForces} />
- </InfoHoverCardOverlay>;
- const deltaGraphL0Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
- <D3Graph graph={state.deltaGraphL0} forces={defaultGraphForces} />
- </InfoHoverCardOverlay>;
- const historyComponent = <InfoHoverCardOverlay contents={helpText.historyGraph}>
- <D3Graph graph={state.historyGraph} forces={defaultGraphForces}
- mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />
- </InfoHoverCardOverlay>;
- const historyComponentWithMerge = <MergeView
- history={state.historyGraph}
- forces={defaultGraphForces}
- versionRegistry={versionRegistry}
- onMerge={outputs => callbacks.onMerge?.(outputs)}
- onGoto={version => callbacks.onVersionClicked?.(version)} />
- const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
- <RountangleEditor
- graph={state.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} transitionDuration={0}>
- <Mantine.Menu.Target>
- <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
- </Mantine.Menu.Target>
- <Mantine.Menu.Dropdown>
- {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
- {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(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
- const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
- const undoRedoButtons = <>
- {undoButton}
- <Mantine.Space w="sm"/>
- {redoButton}
- </>;
- const stackedUndoButtons = state.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 = state.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>,
- history: <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="history">
- {historyComponent}
- </Mantine.Tabs.Panel>
- </Mantine.Tabs>;
- }
- // React components:
- return {
- graphStateComponent,
- rountangleEditor,
- deltaGraphL1Component,
- deltaGraphL0Component,
- historyComponent,
- historyComponentWithMerge,
- undoButton,
- redoButton,
- undoRedoButtons,
- stackedUndoRedoButtons,
- makeTabs,
- };
- }
- // State, reducers, etc.
- return {
- initialState,
- graphState,
- versionRegistry,
- getCurrentVersion,
- getReducer,
- getReactComponents,
- };
- }
|