single_model.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. import * as React from "react";
  2. import * as Mantine from "@mantine/core";
  3. import * as Icons from "@tabler/icons";
  4. import {D3OnionGraphData, D3GraphUpdater} from "../d3graph/reducers/onion_graph";
  5. import {D3GraphEditable, UserEditCallback} from "../d3graph/d3graph_editable";
  6. import {
  7. DeltaGraphState,
  8. fullDeltaId,
  9. deltaGraphReducer,
  10. } from "../d3graph/reducers/delta_graph";
  11. import {
  12. HistoryGraphState,
  13. initialHistoryGraph,
  14. fullVersionId,
  15. historyGraphReducer,
  16. } from "../d3graph/reducers/history_graph";
  17. import * as helpText from "./help_text";
  18. import {MergeView} from "./merge_view";
  19. import {D3Graph, emptyGraph, defaultGraphForces} from "../d3graph/d3graph";
  20. import {RountangleEditor} from "../rountangleEditor/RountangleEditor";
  21. import {InfoHoverCardOverlay} from "../info_hover_card";
  22. import {embed, Version, VersionRegistry} from "onion/version";
  23. import {PrimitiveDelta, PrimitiveRegistry} from "onion/primitive_delta";
  24. import {PrimitiveValue, UUID} from "onion/types";
  25. import {CompositeDelta, CompositeLevel} from "onion/composite_delta";
  26. import {GraphState} from "onion/graph_state";
  27. import {Delta} from "onion/delta";
  28. export const undoButtonHelpText = "Use the Undo/Redo buttons or the History panel to navigate to any version.";
  29. export interface VersionedModelState {
  30. version: Version; // the 'current version'
  31. graph: D3OnionGraphData; // the state what is displayed in the leftmost panel
  32. historyGraph: HistoryGraphState; // the state of what is displayed in the middle panel
  33. deltaGraphL1: DeltaGraphState; // the state of what is displayed in the rightmost panel
  34. deltaGraphL0: DeltaGraphState; // the state of what is displayed in the rightmost panel
  35. }
  36. interface VersionedModelCallbacks {
  37. onUserEdit?: UserEditCallback;
  38. onUndoClicked?: (parentVersion: Version, deltaToUndo: Delta) => void;
  39. onRedoClicked?: (childVersion: Version, deltaToRedo: Delta) => void;
  40. onVersionClicked?: (Version) => void;
  41. onMerge?: (outputs: Version[]) => void;
  42. }
  43. // Basically everything we need to construct the React components for:
  44. // - Graph state (+ optionally, a Rountangle Editor)
  45. // - History graph (+undo/redo buttons)
  46. // - Delta graph
  47. // , their state, and callbacks for updating their state.
  48. export function newVersionedModel({readonly}) {
  49. const versionRegistry = new VersionRegistry();
  50. const graphState = new GraphState();
  51. const compositeLevel = new CompositeLevel();
  52. // SVG coordinates to be used when adding a new node
  53. let x = 0;
  54. let y = 0;
  55. const initialState: VersionedModelState = {
  56. version: versionRegistry.initialVersion,
  57. graph: emptyGraph,
  58. historyGraph: initialHistoryGraph(versionRegistry.initialVersion),
  59. deltaGraphL1: emptyGraph,
  60. deltaGraphL0: emptyGraph,
  61. }
  62. // 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.
  63. let currentVersion = versionRegistry.initialVersion;
  64. function getCurrentVersion() {
  65. return currentVersion;
  66. }
  67. // This function may only be called from a functional React component.
  68. // Given setState callback, returns:
  69. // - Callback functions for updating the state
  70. // - A callback that constructs all React components (to be used in React render function)
  71. function getReducer(setState) {
  72. // Create and add a new version, and its deltas, without changing the current version
  73. const addDeltasAndVersion = (deltas: PrimitiveDelta[], description: string, parentHash: Buffer) => {
  74. const composite = compositeLevel.createComposite(deltas, description);
  75. const parentVersion = versionRegistry.lookupOptional(parentHash);
  76. if (parentVersion !== undefined) {
  77. const newVersion = versionRegistry.createVersion(parentVersion, composite);
  78. setState(({historyGraph, deltaGraphL1, deltaGraphL0, ...rest}) => {
  79. return {
  80. // add new version to history graph:
  81. historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version: newVersion}),
  82. // add the composite delta to the L1-graph + highlight it as 'active':
  83. deltaGraphL1: composite.deltas.length > 0 ? deltaGraphReducer(deltaGraphL1, {type: 'addDelta', delta: composite, active: false}) : deltaGraphL1, // never add an empty composite
  84. // add the primitive L0-deltas to the L0-graph + highlight them as 'active':
  85. deltaGraphL0: composite.deltas.reduce(
  86. (graph, delta) => deltaGraphReducer(graph, {type: 'addDelta', delta, active: false}),
  87. deltaGraphL0),
  88. ...rest,
  89. };
  90. });
  91. return newVersion;
  92. }
  93. };
  94. const createAndGotoNewVersion = (deltas: PrimitiveDelta[], description: string, parentVersion: Version = currentVersion): Version => {
  95. const newVersion = addDeltasAndVersion(deltas, description, parentVersion.hash) as Version;
  96. gotoVersion(newVersion);
  97. return newVersion;
  98. };
  99. const appendVersion = (version: Version) => {
  100. setState(({historyGraph, ...rest}) => {
  101. return {
  102. // add new version to history graph:
  103. historyGraph: historyGraphReducer(historyGraph, {type: 'addVersion', version}),
  104. ...rest,
  105. };
  106. });
  107. }
  108. // helper
  109. const setGraph = callback =>
  110. setState(({graph, ...rest}) => ({graph: callback(graph), ...rest}));
  111. const undoWithoutUpdatingHistoryGraph = (deltaToUndo) => {
  112. const d3Updater = new D3GraphUpdater(setGraph, x, y);
  113. graphState.unexec(deltaToUndo, d3Updater);
  114. setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
  115. deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaInactive', delta: deltaToUndo}),
  116. deltaGraphL0: deltaToUndo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaInactive', delta}),prevDeltaGraphL0),
  117. ...rest,
  118. }));
  119. };
  120. const redoWithoutUpdatingHistoryGraph = (deltaToRedo) => {
  121. const d3Updater = new D3GraphUpdater(setGraph, x, y);
  122. graphState.exec(deltaToRedo, d3Updater);
  123. setState(({deltaGraphL0: prevDeltaGraphL0, deltaGraphL1: prevDeltaGraphL1, ...rest}) => ({
  124. deltaGraphL1: deltaGraphReducer(prevDeltaGraphL1, {type: 'setDeltaActive', delta: deltaToRedo}),
  125. deltaGraphL0: deltaToRedo.deltas.reduce((prevDeltaGraphL0, delta) => deltaGraphReducer(prevDeltaGraphL0, {type: 'setDeltaActive', delta}),
  126. prevDeltaGraphL0),
  127. ...rest,
  128. }));
  129. };
  130. const undo = (parentVersion, deltaToUndo) => {
  131. undoWithoutUpdatingHistoryGraph(deltaToUndo);
  132. currentVersion = parentVersion;
  133. setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
  134. version: parentVersion,
  135. historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
  136. {type: 'highlightVersion', version: prevVersion, bold: false}),
  137. {type: 'highlightVersion', version: parentVersion, bold: true}),
  138. ...rest,
  139. }));
  140. };
  141. const redo = (childVersion, deltaToRedo) => {
  142. redoWithoutUpdatingHistoryGraph(deltaToRedo);
  143. currentVersion = childVersion;
  144. setState(({historyGraph: prevHistoryGraph, version: prevVersion, ...rest}) => ({
  145. version: childVersion,
  146. historyGraph: historyGraphReducer(historyGraphReducer(prevHistoryGraph,
  147. {type: 'highlightVersion', version: prevVersion, bold: false}),
  148. {type: 'highlightVersion', version: childVersion, bold: true}),
  149. ...rest,
  150. }));
  151. };
  152. const gotoVersion = (chosenVersion: Version) => {
  153. const path = currentVersion.findPathTo(chosenVersion);
  154. if (path === undefined) {
  155. throw new Error("Could not find path to version!");
  156. }
  157. for (const [linkType, delta] of path) {
  158. if (linkType === 'p') {
  159. undoWithoutUpdatingHistoryGraph(delta);
  160. }
  161. else if (linkType === 'c') {
  162. redoWithoutUpdatingHistoryGraph(delta);
  163. }
  164. }
  165. currentVersion = chosenVersion;
  166. setState(({historyGraph, version: oldVersion, ...rest}) => ({
  167. version: chosenVersion,
  168. historyGraph: historyGraphReducer(historyGraphReducer(historyGraph,
  169. {type: 'highlightVersion', version: oldVersion, bold: false}),
  170. {type: 'highlightVersion', version: chosenVersion, bold: true}),
  171. ...rest,
  172. }));
  173. };
  174. return {
  175. addDeltasAndVersion,
  176. gotoVersion,
  177. createAndGotoNewVersion,
  178. appendVersion,
  179. undo,
  180. redo,
  181. };
  182. }
  183. function getReactComponents(state: VersionedModelState, callbacks: VersionedModelCallbacks) {
  184. const graphStateComponent = <InfoHoverCardOverlay
  185. contents={readonly ? helpText.graphEditorReadonly : helpText.graphEditor}>
  186. {readonly ?
  187. <D3Graph graph={state.graph} forces={defaultGraphForces} />
  188. : <D3GraphEditable
  189. graph={state.graph}
  190. graphState={graphState}
  191. forces={defaultGraphForces}
  192. setNextNodePosition={(newX,newY) => {x = newX; y = newY;}}
  193. onUserEdit={callbacks.onUserEdit}
  194. />}
  195. </InfoHoverCardOverlay>;
  196. const deltaGraphL1Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
  197. <D3Graph graph={state.deltaGraphL1} forces={defaultGraphForces} />
  198. </InfoHoverCardOverlay>;
  199. const deltaGraphL0Component = <InfoHoverCardOverlay contents={helpText.deltaGraph}>
  200. <D3Graph graph={state.deltaGraphL0} forces={defaultGraphForces} />
  201. </InfoHoverCardOverlay>;
  202. const historyComponent = <InfoHoverCardOverlay contents={helpText.historyGraph}>
  203. <D3Graph graph={state.historyGraph} forces={defaultGraphForces}
  204. mouseUpHandler={(e, {x, y}, node) => node ? callbacks.onVersionClicked?.(node.obj) : undefined} />
  205. </InfoHoverCardOverlay>;
  206. const historyComponentWithMerge = <MergeView
  207. history={state.historyGraph}
  208. forces={defaultGraphForces}
  209. versionRegistry={versionRegistry}
  210. onMerge={outputs => callbacks.onMerge?.(outputs)}
  211. onGoto={version => callbacks.onVersionClicked?.(version)} />
  212. const rountangleEditor = <InfoHoverCardOverlay contents={helpText.rountangleEditor}>
  213. <RountangleEditor
  214. graph={state.graph}
  215. graphState={graphState}
  216. onUserEdit={callbacks.onUserEdit}
  217. />,
  218. </InfoHoverCardOverlay>;
  219. const makeUndoOrRedoButton = (parentsOrChildren, text, leftIcon?, rightIcon?, callback?) => {
  220. if (parentsOrChildren.length === 0) {
  221. return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} disabled>{text}</Mantine.Button>;
  222. }
  223. if (parentsOrChildren.length === 1) {
  224. return <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon} onClick={callback?.bind(null, parentsOrChildren[0][0], parentsOrChildren[0][1])}>{text}</Mantine.Button>;
  225. }
  226. return <Mantine.Menu shadow="md" position="bottom-start" trigger="hover" offset={0} transitionDuration={0}>
  227. <Mantine.Menu.Target>
  228. <Mantine.Button compact leftIcon={leftIcon} rightIcon={rightIcon}>{text} ({parentsOrChildren.length.toString()})</Mantine.Button>
  229. </Mantine.Menu.Target>
  230. <Mantine.Menu.Dropdown>
  231. {/*<Mantine.Menu.Label>{text}</Mantine.Menu.Label>*/}
  232. {parentsOrChildren.map(([parentOrChildVersion,deltaToUndoOrRedo]) =>
  233. <Mantine.Menu.Item key={fullDeltaId(deltaToUndoOrRedo)} onClick={callback?.bind(null, parentOrChildVersion, deltaToUndoOrRedo)}>{deltaToUndoOrRedo.getDescription()}</Mantine.Menu.Item>)}
  234. </Mantine.Menu.Dropdown>
  235. </Mantine.Menu>;
  236. }
  237. const undoButton = makeUndoOrRedoButton(state.version.parents, "Undo", <Icons.IconChevronLeft/>, null, callbacks.onUndoClicked);
  238. const redoButton = makeUndoOrRedoButton(state.version.children, "Redo", null, <Icons.IconChevronRight/>, callbacks.onRedoClicked);
  239. const undoRedoButtons = <>
  240. {undoButton}
  241. <Mantine.Space w="sm"/>
  242. {redoButton}
  243. </>;
  244. const stackedUndoButtons = state.version.parents.map(([parentVersion,deltaToUndo]) => {
  245. return (
  246. <div key={fullVersionId(parentVersion)}>
  247. <Mantine.Button fullWidth={true} compact={true} leftIcon={<Icons.IconChevronLeft size={18}/>} onClick={callbacks.onUndoClicked?.bind(null, parentVersion, deltaToUndo)}>
  248. UNDO {deltaToUndo.getDescription()}
  249. </Mantine.Button>
  250. <Mantine.Space h="xs"/>
  251. </div>
  252. );
  253. });
  254. const stackedRedoButtons = state.version.children.map(([childVersion,deltaToRedo]) => {
  255. return (
  256. <div key={fullVersionId(childVersion)}>
  257. <Mantine.Button style={{width: "100%"}} compact={true} rightIcon={<Icons.IconChevronRight size={18}/>} onClick={callbacks.onRedoClicked?.bind(null, childVersion, deltaToRedo)}>
  258. REDO {deltaToRedo.getDescription()}
  259. </Mantine.Button>
  260. <Mantine.Space h="xs"/>
  261. </div>
  262. );
  263. });
  264. const stackedUndoRedoButtons = (
  265. <Mantine.SimpleGrid cols={2}>
  266. <div>{stackedUndoButtons}</div>
  267. <div>{stackedRedoButtons}</div>
  268. </Mantine.SimpleGrid>
  269. );
  270. const makeTabs = (defaultTab: string, tabs: string[]) => {
  271. return <Mantine.Tabs defaultValue={defaultTab} keepMounted={false}>
  272. <Mantine.Tabs.List grow>
  273. {tabs.map(tab => ({
  274. editor: <Mantine.Tabs.Tab key={tab} value={tab}>Editor</Mantine.Tabs.Tab>,
  275. state: <Mantine.Tabs.Tab key={tab} value={tab}>State</Mantine.Tabs.Tab>,
  276. history: <Mantine.Tabs.Tab key={tab} value={tab}>History</Mantine.Tabs.Tab>,
  277. deltaL1: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L1)</Mantine.Tabs.Tab>,
  278. deltaL0: <Mantine.Tabs.Tab key={tab} value={tab}>Deltas (L0)</Mantine.Tabs.Tab>,
  279. }[tab]))}
  280. </Mantine.Tabs.List>
  281. <Mantine.Tabs.Panel value="state">
  282. {graphStateComponent}
  283. </Mantine.Tabs.Panel>
  284. <Mantine.Tabs.Panel value="editor">
  285. {rountangleEditor}
  286. </Mantine.Tabs.Panel>
  287. <Mantine.Tabs.Panel value="deltaL1">
  288. {deltaGraphL1Component}
  289. </Mantine.Tabs.Panel>
  290. <Mantine.Tabs.Panel value="deltaL0">
  291. {deltaGraphL0Component}
  292. </Mantine.Tabs.Panel>
  293. <Mantine.Tabs.Panel value="history">
  294. {historyComponent}
  295. </Mantine.Tabs.Panel>
  296. </Mantine.Tabs>;
  297. }
  298. // React components:
  299. return {
  300. graphStateComponent,
  301. rountangleEditor,
  302. deltaGraphL1Component,
  303. deltaGraphL0Component,
  304. historyComponent,
  305. historyComponentWithMerge,
  306. undoButton,
  307. redoButton,
  308. undoRedoButtons,
  309. stackedUndoRedoButtons,
  310. makeTabs,
  311. };
  312. }
  313. // State, reducers, etc.
  314. return {
  315. initialState,
  316. graphState,
  317. versionRegistry,
  318. getCurrentVersion,
  319. getReducer,
  320. getReactComponents,
  321. };
  322. }