|
@@ -58,14 +58,21 @@ function makeOverlayHelpIcon(background, helpIcon) {
|
|
|
);
|
|
|
}
|
|
|
|
|
|
+// 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({generateUUID, primitiveRegistry, 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,
|
|
@@ -74,12 +81,21 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
dependencyGraphL0: 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.
|
|
|
- // It creates the state, and a number of state update functions.
|
|
|
- function getReducer([state, setState]) {
|
|
|
+ // 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) {
|
|
|
const setGraph = callback =>
|
|
|
setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
|
|
|
|
|
|
+ // Create and add a new version, and its deltas, without changing the current version
|
|
|
const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
|
|
|
let composite;
|
|
|
try {
|
|
@@ -108,21 +124,21 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
});
|
|
|
}
|
|
|
};
|
|
|
- const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, newVersionCallback?: (Version)=>void) => {
|
|
|
+ const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string): Version => {
|
|
|
const composite = compositeLevel.createComposite(deltas, description);
|
|
|
+ const newVersion = versionRegistry.createVersion(currentVersion, composite);
|
|
|
+ currentVersion = newVersion;
|
|
|
|
|
|
// update graph state:
|
|
|
const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
|
|
|
graphState.exec(composite, d3Updater);
|
|
|
|
|
|
// update rest of state:
|
|
|
- setState(({version: curVersion, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
|
|
|
- const newVersion = versionRegistry.createVersion(curVersion, composite);
|
|
|
- newVersionCallback?.(newVersion);
|
|
|
+ setState(({version: oldVersion, historyGraph, dependencyGraphL1, dependencyGraphL0, ...rest}) => {
|
|
|
return {
|
|
|
version: newVersion,
|
|
|
// add new version to history graph + highlight the new version as the current version:
|
|
|
- historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), curVersion, newVersion),
|
|
|
+ historyGraph: setCurrentVersion(appendToHistoryGraph(historyGraph, newVersion), oldVersion, newVersion),
|
|
|
// add the composite delta to the L1-graph + highlight it as 'active':
|
|
|
dependencyGraphL1: composite.deltas.length > 0 ? addDeltaAndActivate(dependencyGraphL1, composite) : dependencyGraphL1, // never add an empty composite
|
|
|
// add the primitive L0-deltas to the L0-graph + highlight them as 'active':
|
|
@@ -133,6 +149,8 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
...rest,
|
|
|
};
|
|
|
});
|
|
|
+
|
|
|
+ return newVersion;
|
|
|
};
|
|
|
const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
|
|
|
const d3Updater = new D3GraphStateUpdater(setGraph, x, y);
|
|
@@ -154,6 +172,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
};
|
|
|
const undo = (parentVersion, deltaToUndo) => {
|
|
|
undoWithoutUpdatingHistoryGraph(deltaToUndo);
|
|
|
+ currentVersion = parentVersion;
|
|
|
setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
|
|
|
version: parentVersion,
|
|
|
historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, parentVersion),
|
|
@@ -162,6 +181,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
};
|
|
|
const redo = (childVersion, deltaToRedo) => {
|
|
|
redoWithoutUpdatingHistoryGraph(deltaToRedo);
|
|
|
+ currentVersion = childVersion;
|
|
|
setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
|
|
|
version: childVersion,
|
|
|
historyGraph: setCurrentVersion(prevHistoryGraph, prevVersion, childVersion),
|
|
@@ -169,7 +189,7 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
}));
|
|
|
};
|
|
|
const gotoVersion = (chosenVersion: Version) => {
|
|
|
- const path = state.version.findPathTo(chosenVersion);
|
|
|
+ const path = currentVersion.findPathTo(chosenVersion);
|
|
|
if (path === undefined) {
|
|
|
throw new Error("Could not find path to version!");
|
|
|
}
|
|
@@ -181,130 +201,129 @@ export function newVersionedModel({generateUUID, primitiveRegistry, readonly}) {
|
|
|
redoWithoutUpdatingHistoryGraph(delta);
|
|
|
}
|
|
|
}
|
|
|
- setState(({historyGraph, version, ...rest}) => ({
|
|
|
+ currentVersion = chosenVersion;
|
|
|
+ setState(({historyGraph, version: oldVersion, ...rest}) => ({
|
|
|
version: chosenVersion,
|
|
|
- historyGraph: setCurrentVersion(historyGraph, version, chosenVersion),
|
|
|
+ historyGraph: setCurrentVersion(historyGraph, oldVersion, chosenVersion),
|
|
|
...rest,
|
|
|
}));
|
|
|
};
|
|
|
|
|
|
- const getReactComponents = (callbacks: VersionedModelCallbacks) => {
|
|
|
- const graphStateComponent = makeOverlayHelpIcon(readonly ?
|
|
|
- <Graph graph={state.graph} forces={graphForces} />
|
|
|
- : <EditableGraph
|
|
|
- graph={state.graph}
|
|
|
- graphState={graphState}
|
|
|
- forces={graphForces}
|
|
|
- generateUUID={generateUUID}
|
|
|
- primitiveRegistry={primitiveRegistry}
|
|
|
- setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
|
|
|
- onUserEdit={callbacks.onUserEdit}
|
|
|
- />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
|
|
|
-
|
|
|
- const depGraphL1Component = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
|
|
|
- HelpIcons.depGraph);
|
|
|
- const depGraphL0Component = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
|
|
|
- HelpIcons.depGraph);
|
|
|
-
|
|
|
- const historyComponent = makeOverlayHelpIcon(
|
|
|
- <Graph graph={state.historyGraph} forces={graphForces}
|
|
|
- mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
|
|
|
- HelpIcons.historyGraph);
|
|
|
+ return {
|
|
|
+ addDeltasAndVersion,
|
|
|
+ gotoVersion,
|
|
|
+ createAndGotoNewVersion,
|
|
|
+ undo,
|
|
|
+ redo,
|
|
|
+ };
|
|
|
+ }
|
|
|
|
|
|
- const rountangleEditor = makeOverlayHelpIcon(
|
|
|
- <RountangleEditor
|
|
|
+ function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) {
|
|
|
+ const graphStateComponent = makeOverlayHelpIcon(readonly ?
|
|
|
+ <Graph graph={state.graph} forces={graphForces} />
|
|
|
+ : <EditableGraph
|
|
|
graph={state.graph}
|
|
|
+ graphState={graphState}
|
|
|
+ forces={graphForces}
|
|
|
generateUUID={generateUUID}
|
|
|
primitiveRegistry={primitiveRegistry}
|
|
|
- graphState={graphState}
|
|
|
+ setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
|
|
|
onUserEdit={callbacks.onUserEdit}
|
|
|
- />,
|
|
|
- HelpIcons.rountangleEditor);
|
|
|
+ />, readonly ? HelpIcons.graphEditorReadonly : HelpIcons.graphEditor);
|
|
|
|
|
|
- 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 depGraphL1Component = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.dependencyGraphL1} forces={graphForces} />,
|
|
|
+ HelpIcons.depGraph);
|
|
|
+ const depGraphL0Component = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.dependencyGraphL0} forces={graphForces} />,
|
|
|
+ HelpIcons.depGraph);
|
|
|
|
|
|
- }
|
|
|
- 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 historyComponent = makeOverlayHelpIcon(
|
|
|
+ <Graph graph={state.historyGraph} forces={graphForces}
|
|
|
+ mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />,
|
|
|
+ HelpIcons.historyGraph);
|
|
|
|
|
|
- const undoRedoButtons = <>
|
|
|
- {undoButton}
|
|
|
- {redoButton}
|
|
|
- </>;
|
|
|
+ const rountangleEditor = makeOverlayHelpIcon(
|
|
|
+ <RountangleEditor
|
|
|
+ graph={state.graph}
|
|
|
+ generateUUID={generateUUID}
|
|
|
+ primitiveRegistry={primitiveRegistry}
|
|
|
+ graphState={graphState}
|
|
|
+ onUserEdit={callbacks.onUserEdit}
|
|
|
+ />,
|
|
|
+ HelpIcons.rountangleEditor);
|
|
|
|
|
|
- const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
|
|
|
- return (
|
|
|
- <div key={fullVersionId(parentVersion)}>
|
|
|
- <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev 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.IconPlayerTrackNext 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 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>;
|
|
|
|
|
|
- return {
|
|
|
- graphStateComponent,
|
|
|
- rountangleEditor,
|
|
|
- depGraphL1Component,
|
|
|
- depGraphL0Component,
|
|
|
- historyComponent,
|
|
|
- undoButton,
|
|
|
- redoButton,
|
|
|
- undoRedoButtons,
|
|
|
- stackedUndoRedoButtons,
|
|
|
- };
|
|
|
}
|
|
|
+ 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}
|
|
|
+ {redoButton}
|
|
|
+ </>;
|
|
|
+
|
|
|
+ const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
|
|
|
+ return (
|
|
|
+ <div key={fullVersionId(parentVersion)}>
|
|
|
+ <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconPlayerTrackPrev 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.IconPlayerTrackNext 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>
|
|
|
+ );
|
|
|
|
|
|
return {
|
|
|
- state,
|
|
|
- getReactComponents,
|
|
|
- callbacks: {
|
|
|
- addDeltasAndVersion,
|
|
|
- gotoVersion,
|
|
|
- createAndGotoNewVersion,
|
|
|
- undo,
|
|
|
- redo,
|
|
|
- },
|
|
|
+ graphStateComponent,
|
|
|
+ rountangleEditor,
|
|
|
+ depGraphL1Component,
|
|
|
+ depGraphL0Component,
|
|
|
+ historyComponent,
|
|
|
+ undoButton,
|
|
|
+ redoButton,
|
|
|
+ undoRedoButtons,
|
|
|
+ stackedUndoRedoButtons,
|
|
|
};
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
initialState,
|
|
|
+ getCurrentVersion,
|
|
|
getReducer,
|
|
|
+ getReactComponents,
|
|
|
};
|
|
|
}
|