Skip to content

Commit f2edb75

Browse files
committed
Make saving and restoring workspaces faster and more reliable
1 parent a10fb5b commit f2edb75

3 files changed

Lines changed: 68 additions & 28 deletions

File tree

src/ts/core/common/mutation-observer.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,15 @@ const observeElement = (
4040
export const waitForSelectorToExist = (
4141
selector: string,
4242
observeInside: HTMLElement = document.body
43+
): Promise<HTMLElement> => waitForSelectionToExist(element => element.querySelector(selector), observeInside)
44+
45+
export const waitForSelectionToExist = (
46+
selectionFn: (element: HTMLElement) => HTMLElement | null,
47+
observeInside = document.body
4348
): Promise<HTMLElement> => {
4449
return new Promise(resolve => {
4550
const resolveIfElementExists = () => {
46-
const element = observeInside.querySelector(selector) as HTMLElement
51+
const element = selectionFn(observeInside)
4752
if (element) {
4853
resolve(element)
4954
return true

src/ts/core/features/spatial-mode/graph-visualization.ts

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export class GraphVisualization {
7070
static instance: GraphVisualization | null
7171

7272
private cy: cytoscape.Core
73+
private layout: cytoscape.Layouts | null
7374
private synchronizer: SpatialDomSynchronizer
7475
viewport: SpatialViewport
7576

@@ -78,6 +79,7 @@ export class GraphVisualization {
7879
container,
7980
style: getCytoscapeStyles(),
8081
})
82+
this.layout = null
8183
this.synchronizer = new SpatialDomSynchronizer(this.cy)
8284
this.viewport = new SpatialViewport(this.cy)
8385
}
@@ -190,7 +192,9 @@ export class GraphVisualization {
190192
node.style('height', panelElement.offsetHeight + 20)
191193
}
192194
})
193-
this.cy
195+
196+
this.layout?.stop()
197+
this.layout = this.cy
194198
.layout({
195199
name: 'cola',
196200
fit: false,
@@ -207,10 +211,16 @@ export class GraphVisualization {
207211
// @ts-ignore if maxSimulationTime is too low, the layout doesn't actually run
208212
nodeSpacing: SpatialSettings.getNodeSpacing(),
209213
})
210-
.stop()
211214
.run()
212215

213-
return this.cy.promiseOn('layoutstop')
216+
return this.waitForLayout()
217+
}
218+
219+
private async waitForLayout() {
220+
if (this.layout) {
221+
await this.layout.promiseOn('layoutstop')
222+
this.layout = null
223+
}
214224
}
215225

216226
save(): GraphData {
@@ -230,8 +240,9 @@ export class GraphVisualization {
230240
}
231241
}
232242

233-
load(savedData: GraphData) {
234-
this.cy.stop()
243+
async load(savedData: GraphData) {
244+
this.layout?.stop()
245+
await this.waitForLayout()
235246
this.cy.batch(() => {
236247
savedData.nodes.forEach(({id, position, width, height}) => {
237248
let node = this.cy.getElementById(id)
@@ -252,9 +263,13 @@ export class GraphVisualization {
252263
},
253264
})
254265
})
255-
this.cy.zoom(savedData.zoom)
256-
this.cy.pan(savedData.pan)
257266
})
267+
// Stop viewport animations that may have started up after adding nodes
268+
this.cy.stop(true, true)
269+
await delay(10)
270+
271+
this.cy.zoom(savedData.zoom)
272+
this.cy.pan(savedData.pan)
258273
}
259274

260275
cleanup() {
Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,63 @@
11
import {Selectors} from 'src/core/roam/selectors'
22
import {Mouse} from 'src/core/common/mouse'
33
import {assumeExists} from 'src/core/common/assert'
4-
import {delay} from 'src/core/common/async'
54

65
import {GraphVisualization} from './graph-visualization'
6+
import {waitForSelectionToExist} from 'src/core/common/mutation-observer'
7+
import {RoamPanel} from 'src/core/roam/panel/roam-panel'
78

89
export const saveWorkspace = (graph: GraphVisualization) => {
910
const graphData = graph.save()
1011
if (graphData.nodes.some(({id}) => id.includes('Mentions of') || id.includes('block-input-'))) {
1112
window.alert("Saving block outline and mention panels isn't supported yet")
1213
}
13-
const bulletsForPages = graphData.nodes.map(({id}) => `- [[${id}]]`).join('\n')
14-
const graphJsonCodeBlock = '```' + JSON.stringify(graphData) + '```'
15-
navigator.clipboard.writeText(bulletsForPages + '\n' + graphJsonCodeBlock)
14+
const bulletsForPages = graphData.nodes.map(({id}) => ` - [[${id}]]`).join('\n')
15+
const graphJsonCodeBlock = ' - ```' + JSON.stringify(graphData) + '```'
16+
navigator.clipboard.writeText('#[[Roam Toolkit Workspace]]' + '\n' + bulletsForPages + '\n' + graphJsonCodeBlock)
1617
}
1718

1819
export const restoreWorkspace = async (graph: GraphVisualization) => {
19-
// first, open all blocks in the page so the dom nodes exist
20-
const links = Array.from(document.querySelectorAll(`${Selectors.mainContent} ${Selectors.block} ${Selectors.link}`))
21-
// ignore the links inside complex pages
22-
const outerLinks = links.filter(link => !link.parentElement?.closest(Selectors.link)) as HTMLElement[]
23-
// Open sidebar pages
24-
outerLinks.forEach(link => Mouse.leftClick(link, true))
25-
// Open main page
26-
Mouse.leftClick(outerLinks[0])
27-
2820
/** assume the format of the page is:
29-
* - [[main page]]
30-
* - [[side page 1]]
31-
* - [[side page 2]]
32-
* ```$JSON```
21+
* - #[[Roam Toolkit Workspace]]
22+
* - [[main page]]
23+
* - [[side page 1]]
24+
* - [[side page 2]]
25+
* - ```$JSON```
3326
*/
27+
const savedWorkspaceBlocks = document
28+
.querySelector('[data-tag="Roam Toolkit Workspace"]')
29+
?.closest(Selectors.blockContainer)
30+
if (!savedWorkspaceBlocks) {
31+
return
32+
}
3433
const firstCodeBlockLine = assumeExists(
35-
document.querySelector(`${Selectors.mainContent} .CodeMirror-line`)
34+
savedWorkspaceBlocks.querySelector(`${Selectors.mainContent} .CodeMirror-line`)
3635
) as HTMLElement
3736
const graphData = JSON.parse(firstCodeBlockLine.innerText)
3837

38+
if (graphData.nodes) {
39+
await openAllLinksInBlocks(savedWorkspaceBlocks)
40+
await graph.load(graphData)
41+
}
42+
}
43+
44+
const openAllLinksInBlocks = async (blockContainer: Element) => {
45+
// first, open all blocks in the page so the dom nodes exist
46+
const [, ...links] = Array.from(blockContainer.querySelectorAll(Selectors.link))
47+
// ignore the links inside complex pages
48+
const outerLinks = links.filter(link => !link.parentElement?.closest(Selectors.link)) as HTMLElement[]
49+
const [mainLink, ...sidebarLinks] = outerLinks
50+
// Open sidebar pages
51+
sidebarLinks.forEach(link => Mouse.leftClick(link, true))
52+
// Open main page
53+
Mouse.leftClick(mainLink)
54+
3955
// Wait for all pages to finish opening
40-
await delay(500 + outerLinks.length * 200)
56+
await Promise.all(outerLinks.map(assertLinkWasOpened))
57+
}
4158

42-
graph.load(graphData)
59+
const assertLinkWasOpened = async (linkElement: HTMLElement) => {
60+
const panelId = assumeExists((linkElement.closest('[data-link-title]') as HTMLElement)?.dataset?.linkTitle)
61+
// Wait for all pages to finish opening
62+
await waitForSelectionToExist(() => RoamPanel.getPanel(panelId))
4363
}

0 commit comments

Comments
 (0)