@perspective-dev/workspace 4.0.1 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,58 +12,209 @@
12
12
 
13
13
  import { find, toArray } from "@lumino/algorithm";
14
14
  import { CommandRegistry } from "@lumino/commands";
15
+
15
16
  import { SplitPanel, Panel, DockPanel } from "@lumino/widgets";
16
17
  import uniqBy from "lodash/uniqBy";
17
- import { DebouncedFunc, isEqual } from "lodash";
18
+ import { DebouncedFunc, DebouncedFuncLeading, isEqual } from "lodash";
19
+ import { throttle } from "lodash";
18
20
  import debounce from "lodash/debounce";
19
21
  import type {
20
22
  HTMLPerspectiveViewerElement,
21
23
  ViewerConfigUpdate,
22
24
  } from "@perspective-dev/viewer";
23
25
  import type * as psp from "@perspective-dev/client";
26
+ import type * as psp_viewer from "@perspective-dev/viewer";
24
27
  import injectedStyles from "../../../build/css/injected.css";
25
28
  import { PerspectiveDockPanel } from "./dockpanel";
26
29
  import { WorkspaceMenu } from "./menu";
27
30
  import { createCommands } from "./commands";
28
31
  import { PerspectiveViewerWidget } from "./widget";
29
- import { ObservableMap } from "../utils/observable_map";
30
32
 
31
- const DEFAULT_WORKSPACE_SIZE = [1, 3];
33
+ class AsyncMutex {
34
+ _lock: Promise<unknown> | null;
32
35
 
33
- let ID_COUNTER = 0;
36
+ constructor() {
37
+ this._lock = null;
38
+ }
34
39
 
35
- export interface PerspectiveLayout<T> {
36
- children?: PerspectiveLayout<T>[];
37
- widgets?: T[];
40
+ lock<A>(continuation: () => Promise<A>): Promise<A> {
41
+ if (this._lock !== null) {
42
+ return this._lock.then(() => this.lock(continuation));
43
+ }
44
+
45
+ this._lock = new Promise((x, y) =>
46
+ continuation()
47
+ .then((z) => {
48
+ this._lock = null;
49
+ x(z);
50
+ })
51
+ .catch((e) => {
52
+ this._lock = null;
53
+ y(e);
54
+ }),
55
+ );
56
+
57
+ return this._lock as Promise<A>;
58
+ }
59
+ }
60
+
61
+ export type PerspectiveSplitArea = {
62
+ type: "split-area";
38
63
  sizes: number[];
64
+ orientation: "horizontal" | "vertical";
65
+ children: PerspectiveLayout[];
66
+ };
67
+
68
+ export type PerspectiveTabArea = {
69
+ type: "tab-area";
70
+ currentIndex: number;
71
+ widgets: string[];
72
+ };
73
+
74
+ export type PerspectiveLayout = PerspectiveSplitArea | PerspectiveTabArea;
75
+
76
+ export interface PerspectiveWorkspaceConfig {
77
+ sizes: number[];
78
+ viewers: Record<string, psp_viewer.ViewerConfigUpdate>;
79
+ detail: { main: PerspectiveLayout | null };
80
+ master?: {
81
+ sizes: number[];
82
+ widgets: string[];
83
+ };
39
84
  }
40
85
 
41
- export interface ViewerConfigUpdateExt extends ViewerConfigUpdate {
42
- table: string;
86
+ const DEFAULT_WORKSPACE_SIZE = [1, 3];
87
+ let ID_COUNTER = 0;
88
+
89
+ export function genId(workspace: PerspectiveWorkspaceConfig) {
90
+ let i = `PERSPECTIVE_GENERATED_ID_${ID_COUNTER++}`;
91
+ if (Object.keys(workspace.viewers).includes(i)) {
92
+ i = genId(workspace);
93
+ }
94
+ return i;
43
95
  }
44
96
 
45
- export interface PerspectiveWorkspaceConfig<T> {
46
- sizes: number[];
47
- master: PerspectiveLayout<T>;
48
- detail: PerspectiveLayout<T>;
49
- viewers: Record<string, ViewerConfigUpdateExt>;
97
+ /// This function takes a workspace config and viewer config and adds the
98
+ /// viewer config to the workspace config, returning a new workspace config.
99
+ /// This is a slightly different algorithm from the Lumino one,
100
+ /// which will be used on internal workspace actions (such as duplication).
101
+ /// It currently attaches the viewer using a split-right style,
102
+ /// (see Lumino docklayout.ts for documentation on insert modes).
103
+ export function addViewer(
104
+ workspace: PerspectiveWorkspaceConfig,
105
+ config: psp_viewer.ViewerConfigUpdate,
106
+ id: string,
107
+ ): PerspectiveWorkspaceConfig {
108
+ const GOLDEN_RATIO = 0.618;
109
+ /// ensures that the sum of the input is 1
110
+ /// keeps the relative size of the elements
111
+ function normalize(sizes: number[]) {
112
+ const sum = sizes.reduce((a, b) => a + b, 0);
113
+ return sum === 1 ? sizes : sizes.map((size) => size / sum);
114
+ }
115
+
116
+ if (workspace.detail.main === null) {
117
+ return {
118
+ sizes: workspace.sizes,
119
+ viewers: {
120
+ ...workspace.viewers,
121
+ [id]: config,
122
+ },
123
+ detail: {
124
+ main: {
125
+ type: "split-area",
126
+ sizes: [1],
127
+ orientation: "horizontal",
128
+ children: [
129
+ {
130
+ type: "tab-area",
131
+ currentIndex: 0,
132
+ widgets: [id],
133
+ },
134
+ ],
135
+ },
136
+ },
137
+ master: workspace.master,
138
+ };
139
+ } else if (
140
+ workspace.detail.main.type === "tab-area" ||
141
+ (workspace.detail.main.type === "split-area" &&
142
+ workspace.detail.main.orientation === "vertical")
143
+ ) {
144
+ return {
145
+ sizes: workspace.sizes,
146
+ viewers: {
147
+ ...workspace.viewers,
148
+ [id]: config,
149
+ },
150
+ detail: {
151
+ main: {
152
+ type: "split-area",
153
+ sizes: [0.5, 0.5],
154
+ orientation: "horizontal",
155
+ children: [
156
+ workspace.detail.main,
157
+ {
158
+ type: "tab-area",
159
+ currentIndex: 0,
160
+ widgets: [id],
161
+ },
162
+ ],
163
+ },
164
+ },
165
+ master: workspace.master,
166
+ };
167
+ } else if (
168
+ workspace.detail.main.type === "split-area" &&
169
+ workspace.detail.main.orientation === "horizontal"
170
+ ) {
171
+ return {
172
+ sizes: workspace.sizes,
173
+ viewers: {
174
+ ...workspace.viewers,
175
+ [id]: config,
176
+ },
177
+ detail: {
178
+ main: {
179
+ type: "split-area",
180
+ sizes: normalize([
181
+ ...normalize(workspace.detail.main.sizes),
182
+ GOLDEN_RATIO,
183
+ ]),
184
+ orientation: "horizontal",
185
+ children: [
186
+ ...workspace.detail.main.children,
187
+ {
188
+ type: "tab-area",
189
+ currentIndex: 0,
190
+ widgets: [id],
191
+ },
192
+ ],
193
+ },
194
+ },
195
+ master: workspace.master,
196
+ };
197
+ } else {
198
+ throw new Error("Unknown workspace state");
199
+ }
50
200
  }
51
201
 
52
202
  export class PerspectiveWorkspace extends SplitPanel {
53
203
  private dockpanel: PerspectiveDockPanel;
54
204
  private detailPanel: Panel;
55
205
  private masterPanel: SplitPanel;
206
+ client: psp.Client[];
56
207
  element: HTMLElement;
57
208
  menu_elem: HTMLElement;
58
- private _tables: ObservableMap<string, psp.Table | Promise<psp.Table>>;
59
209
  private listeners: WeakMap<PerspectiveViewerWidget, () => void>;
60
210
  private indicator: HTMLElement;
61
211
  private commands: CommandRegistry;
62
212
  private _menu?: WorkspaceMenu;
63
- private _minimizedLayoutSlots?: DockPanel.ILayoutConfig;
213
+ private _minimizedLayoutSlots?: Promise<DockPanel.ILayoutConfig>;
64
214
  private _minimizedLayout?: DockPanel.ILayoutConfig;
65
215
  private _maximizedWidget?: PerspectiveViewerWidget;
66
- private _last_updated_state?: PerspectiveWorkspaceConfig<string>;
216
+ private _last_updated_state?: PerspectiveWorkspaceConfig;
217
+ _mutex: AsyncMutex;
67
218
  // private _context_menu?: Menu & { init_overlay?: () => void };
68
219
 
69
220
  constructor(element: HTMLElement) {
@@ -76,17 +227,15 @@ export class PerspectiveWorkspace extends SplitPanel {
76
227
  this.detailPanel.addWidget(this.dockpanel);
77
228
  this.masterPanel = new SplitPanel({ orientation: "vertical" });
78
229
  this.masterPanel.addClass("master-panel");
230
+ this._mutex = new AsyncMutex();
79
231
  this.dockpanel.layoutModified.connect(() => {
80
232
  this.workspaceUpdated();
81
233
  });
82
234
 
83
235
  this.addWidget(this.detailPanel);
84
- this.spacing = 6;
85
236
  this.element = element;
86
237
  this.listeners = new WeakMap();
87
- this._tables = new ObservableMap();
88
- this._tables.addSetListener(this._set_listener.bind(this));
89
- this._tables.addDeleteListener(this._delete_listener.bind(this));
238
+ this.client = [];
90
239
  this.indicator = this.init_indicator();
91
240
  this.commands = createCommands(this, this.indicator);
92
241
  this.menu_elem = document.createElement("perspective-workspace-menu");
@@ -112,6 +261,10 @@ export class PerspectiveWorkspace extends SplitPanel {
112
261
  }
113
262
 
114
263
  init_indicator() {
264
+ const exists = document.querySelector("body > perspective-indicator");
265
+ if (exists) {
266
+ return exists as HTMLElement;
267
+ }
115
268
  const indicator = document.createElement("perspective-indicator");
116
269
  indicator.style.position = "fixed";
117
270
  indicator.style.pointerEvents = "none";
@@ -136,147 +289,158 @@ export class PerspectiveWorkspace extends SplitPanel {
136
289
  *
137
290
  */
138
291
 
139
- addTable(name: string, table: Promise<psp.Table>) {
140
- this.tables.set(name, table);
141
- }
142
-
143
- getTable(name: string): psp.Table | Promise<psp.Table> {
144
- return this.tables.get(name);
145
- }
146
-
147
- removeTable(name: string) {
148
- return this.tables.delete(name);
149
- }
150
-
151
- replaceTable(name: string, table: Promise<psp.Table>) {
152
- this.tables.set(name, table);
153
- }
154
-
155
- get tables() {
156
- return this._tables;
157
- }
158
-
159
- async save() {
160
- const is_settings = this.dockpanel.mode === "single-document";
161
- let detail = is_settings
162
- ? this._minimizedLayoutSlots
163
- : PerspectiveDockPanel.mapWidgets(
164
- (widget) =>
165
- // this.getWidgetByName(widget)!.viewer.getAttribute("slot")
166
- (widget as PerspectiveViewerWidget).viewer.getAttribute(
167
- "slot",
168
- ),
169
- this.dockpanel.saveLayout(),
170
- );
171
-
172
- const layout = {
173
- sizes: [...this.relativeSizes()],
174
- detail,
175
- master: undefined as
176
- | { widgets: string[]; sizes: number[] }
177
- | undefined,
178
- };
179
-
180
- if (this.masterPanel.isAttached) {
181
- const master = {
182
- widgets: this.masterPanel.widgets.map(
183
- (widget) =>
184
- (widget as PerspectiveViewerWidget).viewer.getAttribute(
185
- "slot",
186
- )!,
187
- ),
188
- sizes: [...this.masterPanel.relativeSizes()],
292
+ async save(): Promise<PerspectiveWorkspaceConfig> {
293
+ return await this._mutex.lock(async () => {
294
+ const is_settings = this.dockpanel.mode === "single-document";
295
+ let detail = is_settings
296
+ ? await this._minimizedLayoutSlots
297
+ : await PerspectiveDockPanel.mapWidgets(
298
+ async (widget) =>
299
+ (
300
+ widget as PerspectiveViewerWidget
301
+ ).viewer.getAttribute("slot"),
302
+ this.dockpanel.saveLayout(),
303
+ );
304
+
305
+ const layout: PerspectiveWorkspaceConfig = {
306
+ sizes: [...this.relativeSizes()],
307
+ detail: detail as { main: PerspectiveLayout },
308
+ viewers: {},
309
+ master: undefined as
310
+ | { widgets: string[]; sizes: number[] }
311
+ | undefined,
189
312
  };
190
- layout.master = master;
191
- }
192
-
193
- const viewers: Record<string, ViewerConfigUpdate> = {};
194
- for (const widget of this.masterPanel.widgets) {
195
- const psp_widget = widget as PerspectiveViewerWidget;
196
- viewers[psp_widget.viewer.getAttribute("slot")!] =
197
- await psp_widget.save();
198
- }
199
313
 
200
- const widgets = PerspectiveDockPanel.getWidgets(
201
- is_settings ? this._minimizedLayout! : this.dockpanel.saveLayout(),
202
- );
314
+ if (this.masterPanel.isAttached) {
315
+ const master = {
316
+ widgets: this.masterPanel.widgets.map(
317
+ (widget) =>
318
+ (
319
+ widget as PerspectiveViewerWidget
320
+ ).viewer.getAttribute("slot")!,
321
+ ),
322
+ sizes: [...this.masterPanel.relativeSizes()],
323
+ };
324
+
325
+ layout.master = master;
326
+ }
203
327
 
204
- await Promise.all(
205
- widgets.map(async (widget) => {
328
+ // const viewers: Record<string, ViewerConfigUpdate> = {};
329
+ for (const widget of this.masterPanel.widgets) {
206
330
  const psp_widget = widget as PerspectiveViewerWidget;
207
- const slot = psp_widget.viewer.getAttribute("slot")!;
208
- viewers[slot] = await psp_widget.save();
209
- viewers[slot]!.settings = false;
210
- }),
211
- );
331
+ layout.viewers[psp_widget.viewer.getAttribute("slot")!] =
332
+ await psp_widget.save();
333
+ }
334
+
335
+ const widgets = PerspectiveDockPanel.getWidgets(
336
+ is_settings
337
+ ? this._minimizedLayout!
338
+ : this.dockpanel.saveLayout(),
339
+ );
340
+
341
+ await Promise.all(
342
+ widgets.map(async (widget) => {
343
+ const psp_widget = widget as PerspectiveViewerWidget;
344
+ const slot = psp_widget.viewer.getAttribute("slot")!;
345
+ layout.viewers[slot] = await psp_widget.save();
346
+ layout.viewers[slot]!.settings = false;
347
+ }),
348
+ );
212
349
 
213
- return { ...layout, viewers };
350
+ return layout;
351
+ });
214
352
  }
215
353
 
216
- async restore(value: PerspectiveWorkspaceConfig<string>) {
217
- const {
218
- sizes,
219
- master,
220
- detail,
221
- viewers: viewer_configs = {},
222
- } = structuredClone(value);
354
+ async restore(value: PerspectiveWorkspaceConfig) {
355
+ await this._mutex.lock(async () => {
356
+ const {
357
+ sizes,
358
+ master,
359
+ detail,
360
+ viewers: viewer_configs = {},
361
+ } = structuredClone(value);
362
+
363
+ if (master && master.widgets!.length > 0) {
364
+ this.setupMasterPanel(sizes || DEFAULT_WORKSPACE_SIZE);
365
+ } else {
366
+ if (this.masterPanel.isAttached) {
367
+ this.detailPanel.removeClass("has-master-panel");
368
+ this.masterPanel.close();
369
+ }
223
370
 
224
- if (master && master.widgets!.length > 0) {
225
- this.setupMasterPanel(sizes || DEFAULT_WORKSPACE_SIZE);
226
- } else {
227
- if (this.masterPanel.isAttached) {
228
- this.detailPanel.removeClass("has-master-panel");
229
- this.masterPanel.close();
371
+ this.addWidget(this.detailPanel);
230
372
  }
231
373
 
232
- this.addWidget(this.detailPanel);
233
- }
374
+ let tasks: Promise<void>[] = [];
234
375
 
235
- let tasks: Promise<void>[] = [];
376
+ // Using ES generators as context managers ..
377
+ for (const viewers of this._capture_viewers()) {
378
+ for (const widgets of this._capture_widgets()) {
379
+ for (const v of viewers) {
380
+ v.removeAttribute("class");
381
+ }
236
382
 
237
- // Using ES generators as context managers ..
238
- for (const viewers of this._capture_viewers()) {
239
- for (const widgets of this._capture_widgets()) {
240
- for (const v of viewers) {
241
- v.removeAttribute("class");
242
- }
383
+ const callback = this._restore_callback.bind(
384
+ this,
385
+ viewer_configs,
386
+ viewers,
387
+ widgets,
388
+ );
243
389
 
244
- const callback = this._restore_callback.bind(
245
- this,
246
- viewer_configs,
247
- viewers,
248
- widgets,
249
- );
390
+ if (detail) {
391
+ const detailLayout =
392
+ await PerspectiveDockPanel.mapWidgets(
393
+ (name: string) =>
394
+ callback.bind(this, false)(name),
395
+ detail,
396
+ );
397
+
398
+ this.dockpanel.mode = "multiple-document";
399
+ this.dockpanel.restoreLayout(detailLayout);
400
+ tasks = tasks.concat(
401
+ PerspectiveDockPanel.getWidgets(detailLayout).map(
402
+ (x) =>
403
+ (
404
+ x as PerspectiveViewerWidget
405
+ ).viewer.flush(),
406
+ ),
407
+ );
408
+ }
250
409
 
251
- if (detail) {
252
- const detailLayout = PerspectiveDockPanel.mapWidgets(
253
- (name: string) => callback.bind(this, false)(name),
254
- detail,
255
- );
410
+ if (master) {
411
+ // tasks = tasks.concat(
256
412
 
257
- this.dockpanel.mode = "multiple-document";
258
- this.dockpanel.restoreLayout(detailLayout);
259
- tasks = tasks.concat(
260
- PerspectiveDockPanel.getWidgets(detailLayout).map(
261
- (x) => (x as PerspectiveViewerWidget).task!,
262
- ),
263
- );
264
- }
413
+ const tasks2: any[] = [],
414
+ names: string[] = [];
415
+ master.widgets!.map((name) => {
416
+ names.push(name);
417
+ tasks2.push(callback.bind(this, true)(name));
418
+ return name;
419
+ });
265
420
 
266
- if (master) {
267
- tasks = tasks.concat(
268
- master.widgets!.map(
269
- (name) => callback.bind(this, true)(name).task!,
270
- ),
271
- );
421
+ // return name;
422
+ tasks.push(
423
+ Promise.all(tasks2).then((x) => {
424
+ master.widgets = master.widgets!.map((name) => {
425
+ const idx = names.indexOf(name);
426
+ const task = x[idx];
427
+ return task;
428
+ });
429
+ }),
430
+ );
272
431
 
273
- master.sizes &&
274
- this.masterPanel.setRelativeSizes(master.sizes);
432
+ // const widgets = await Promise.all(tasks);
433
+
434
+ // );
435
+
436
+ master.sizes &&
437
+ this.masterPanel.setRelativeSizes(master.sizes);
438
+ }
275
439
  }
276
440
  }
277
- }
278
441
 
279
- await Promise.all(tasks);
442
+ await Promise.all(tasks);
443
+ });
280
444
  }
281
445
 
282
446
  *_capture_widgets() {
@@ -313,8 +477,8 @@ export class PerspectiveWorkspace extends SplitPanel {
313
477
  }
314
478
  }
315
479
 
316
- _restore_callback(
317
- viewers: Record<string, ViewerConfigUpdateExt>,
480
+ async _restore_callback(
481
+ viewers: Record<string, psp_viewer.ViewerConfigUpdate>,
318
482
  starting_viewers: HTMLPerspectiveViewerElement[],
319
483
  starting_widgets: PerspectiveViewerWidget[],
320
484
  master: boolean,
@@ -331,16 +495,15 @@ export class PerspectiveWorkspace extends SplitPanel {
331
495
  if (viewer) {
332
496
  widget = starting_widgets.find((x) => x.viewer === viewer);
333
497
  if (widget) {
334
- widget.load(this.tables.get(viewer_config.table));
335
- widget.restore({ ...viewer_config });
498
+ await widget.restore({ ...viewer_config });
336
499
  } else {
337
- widget = this._createWidget({
500
+ widget = await this._createWidget({
338
501
  config: { ...viewer_config },
339
502
  viewer,
340
503
  });
341
504
  }
342
505
  } else if (viewer_config) {
343
- widget = this._createWidgetAndNode({
506
+ widget = await this._createWidgetAndNode({
344
507
  config: { ...viewer_config },
345
508
  slot: widgetName,
346
509
  });
@@ -380,13 +543,6 @@ export class PerspectiveWorkspace extends SplitPanel {
380
543
  } else {
381
544
  this._validate(table);
382
545
  }
383
-
384
- this.getAllWidgets().forEach((widget) => {
385
- const psp_widget = widget as PerspectiveViewerWidget;
386
- if (psp_widget.viewer.getAttribute("table") === name) {
387
- psp_widget.load(table);
388
- }
389
- });
390
546
  }
391
547
 
392
548
  _delete_listener(name: string) {
@@ -398,7 +554,7 @@ export class PerspectiveWorkspace extends SplitPanel {
398
554
  });
399
555
  }
400
556
 
401
- update_widget_for_viewer(viewer: HTMLPerspectiveViewerElement) {
557
+ async update_widget_for_viewer(viewer: HTMLPerspectiveViewerElement) {
402
558
  let slot_name = viewer.getAttribute("slot");
403
559
  if (!slot_name) {
404
560
  slot_name = this._gen_id();
@@ -413,10 +569,8 @@ export class PerspectiveWorkspace extends SplitPanel {
413
569
  `Undocked ${viewer.outerHTML}, creating default layout`,
414
570
  );
415
571
 
416
- const widget = this._createWidget({
417
- config: {
418
- table: viewer.getAttribute("table")!,
419
- },
572
+ const widget = await this._createWidget({
573
+ // config: {},
420
574
  viewer,
421
575
  });
422
576
 
@@ -462,7 +616,7 @@ export class PerspectiveWorkspace extends SplitPanel {
462
616
  const config = await widget.save();
463
617
  config.settings = false;
464
618
  config.title = config.title ? `${config.title} (*)` : "";
465
- const duplicate = this._createWidgetAndNode({
619
+ const duplicate = await this._createWidgetAndNode({
466
620
  config,
467
621
  slot: undefined,
468
622
  });
@@ -472,7 +626,7 @@ export class PerspectiveWorkspace extends SplitPanel {
472
626
  ref: widget,
473
627
  });
474
628
 
475
- await duplicate.task;
629
+ await duplicate.viewer.flush();
476
630
  }
477
631
 
478
632
  toggleMasterDetail(widget: PerspectiveViewerWidget) {
@@ -498,12 +652,14 @@ export class PerspectiveWorkspace extends SplitPanel {
498
652
 
499
653
  _maximize(widget: PerspectiveViewerWidget) {
500
654
  widget.viewer.classList.add("widget-maximize");
501
- this._minimizedLayout = this.dockpanel.saveLayout();
502
- this._minimizedLayoutSlots = PerspectiveDockPanel.mapWidgets(
503
- (widget: PerspectiveViewerWidget) =>
504
- widget.viewer.getAttribute("slot"),
505
- this.dockpanel.saveLayout(),
506
- );
655
+ if (!this._minimizedLayout) {
656
+ this._minimizedLayout = this.dockpanel.saveLayout();
657
+ this._minimizedLayoutSlots = PerspectiveDockPanel.mapWidgets(
658
+ async (widget: PerspectiveViewerWidget) =>
659
+ widget.viewer.getAttribute("slot"),
660
+ this.dockpanel.saveLayout(),
661
+ );
662
+ }
507
663
 
508
664
  this._maximizedWidget = widget;
509
665
  this.dockpanel.mode = "single-document";
@@ -514,6 +670,7 @@ export class PerspectiveWorkspace extends SplitPanel {
514
670
  this._maximizedWidget!.viewer.classList.remove("widget-maximize");
515
671
  this.dockpanel.mode = "multiple-document";
516
672
  this.dockpanel.restoreLayout(this._minimizedLayout!);
673
+ this._minimizedLayout = undefined;
517
674
  }
518
675
 
519
676
  toggleSingleDocument(widget: PerspectiveViewerWidget) {
@@ -657,22 +814,29 @@ export class PerspectiveWorkspace extends SplitPanel {
657
814
  },
658
815
  );
659
816
 
660
- for (const table of this.tables.keys()) {
661
- let args;
662
- if (widget !== null) {
663
- args = {
664
- table,
665
- widget_name: widget.viewer.getAttribute("slot"),
666
- };
667
- } else {
668
- args = { table };
669
- }
817
+ (async () => {
818
+ const names = await Promise.all(
819
+ this.client.map((c) => c.get_hosted_table_names()),
820
+ ).then((x) => x.flat());
670
821
 
671
- submenu.addItem({
672
- command: "workspace:new",
673
- args,
674
- });
675
- }
822
+ for (const table of names) {
823
+ let args;
824
+ if (widget !== null) {
825
+ args = {
826
+ table,
827
+ widget_name:
828
+ widget.viewer.getAttribute("slot"),
829
+ };
830
+ } else {
831
+ args = { table };
832
+ }
833
+
834
+ submenu.insertItem(0, {
835
+ command: "workspace:new",
836
+ args,
837
+ });
838
+ }
839
+ })();
676
840
 
677
841
  const widgets = PerspectiveDockPanel.getWidgets(
678
842
  this.dockpanel.saveLayout(),
@@ -815,38 +979,43 @@ export class PerspectiveWorkspace extends SplitPanel {
815
979
  this.setRelativeSizes(sizes);
816
980
  }
817
981
 
818
- addViewer(config: ViewerConfigUpdateExt, is_global_filter?: boolean) {
819
- if (this.dockpanel.mode === "single-document") {
820
- const _task = this._maximizedWidget!.viewer.toggleConfig(false);
821
- this._unmaximize();
822
- }
823
-
824
- const widget = this._createWidgetAndNode({ config });
825
- if (is_global_filter) {
826
- if (!this.masterPanel.isAttached) {
827
- this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE);
982
+ async addViewer(
983
+ config: psp_viewer.ViewerConfigUpdate,
984
+ is_global_filter?: boolean,
985
+ ) {
986
+ await this._mutex.lock(async () => {
987
+ if (this.dockpanel.mode === "single-document") {
988
+ const _task = this._maximizedWidget!.viewer.toggleConfig(false);
989
+ this._unmaximize();
828
990
  }
829
991
 
830
- this.masterPanel.addWidget(widget);
831
- } else {
832
- if (!this.detailPanel.isAttached) {
833
- this.addWidget(this.detailPanel);
992
+ const widget = await this._createWidgetAndNode({ config });
993
+ if (is_global_filter) {
994
+ if (!this.masterPanel.isAttached) {
995
+ this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE);
996
+ }
997
+
998
+ this.masterPanel.addWidget(widget);
999
+ } else {
1000
+ if (!this.detailPanel.isAttached) {
1001
+ this.addWidget(this.detailPanel);
1002
+ }
1003
+ this.dockpanel.addWidget(widget, { mode: "split-right" });
834
1004
  }
835
- this.dockpanel.addWidget(widget, { mode: "split-right" });
836
- }
837
1005
 
838
- this.update();
1006
+ this.update();
1007
+ });
839
1008
  }
840
1009
 
841
1010
  /*********************************************************************
842
1011
  * Widget helper methods
843
1012
  */
844
1013
 
845
- _createWidgetAndNode({
1014
+ async _createWidgetAndNode({
846
1015
  config,
847
1016
  slot: slotname,
848
1017
  }: {
849
- config: ViewerConfigUpdateExt;
1018
+ config: psp_viewer.ViewerConfigUpdate;
850
1019
  slot?: string;
851
1020
  }) {
852
1021
  const node = this._createNode(slotname);
@@ -861,11 +1030,19 @@ export class PerspectiveWorkspace extends SplitPanel {
861
1030
  viewer.setAttribute("table", table);
862
1031
  }
863
1032
 
864
- return this._createWidget({
865
- config,
866
- elem: node as HTMLElement,
867
- viewer,
868
- });
1033
+ for (const client of this.client) {
1034
+ const tables = await client.get_hosted_table_names();
1035
+ if (table && tables.indexOf(table) > -1) {
1036
+ await viewer.load(client);
1037
+ return await this._createWidget({
1038
+ config,
1039
+ elem: node as HTMLElement,
1040
+ viewer,
1041
+ });
1042
+ }
1043
+ }
1044
+
1045
+ throw new Error(`Table "${table}" not found`);
869
1046
  }
870
1047
 
871
1048
  _gen_id() {
@@ -895,12 +1072,12 @@ export class PerspectiveWorkspace extends SplitPanel {
895
1072
  return node as HTMLElement;
896
1073
  }
897
1074
 
898
- _createWidget({
1075
+ async _createWidget({
899
1076
  config,
900
1077
  elem,
901
1078
  viewer,
902
1079
  }: {
903
- config: ViewerConfigUpdateExt;
1080
+ config?: psp_viewer.ViewerConfigUpdate;
904
1081
  elem?: Element;
905
1082
  viewer: HTMLPerspectiveViewerElement;
906
1083
  }) {
@@ -915,25 +1092,24 @@ export class PerspectiveWorkspace extends SplitPanel {
915
1092
  }
916
1093
  }
917
1094
 
918
- const table = this.tables.get(
919
- viewer.getAttribute("table") || config.table,
920
- );
921
-
922
- const widget = new PerspectiveViewerWidget({ node, viewer });
923
- widget.task = (async () => {
924
- if (table) {
925
- widget.load(table);
1095
+ const onAttach = () => {
1096
+ if (widget.viewer.parentElement !== this.element) {
1097
+ this.element.appendChild(widget.viewer);
926
1098
  }
927
1099
 
1100
+ const event = new CustomEvent("workspace-new-view", {
1101
+ detail: { config, widget },
1102
+ });
1103
+
1104
+ this.element.dispatchEvent(event);
1105
+ };
1106
+
1107
+ const widget = new PerspectiveViewerWidget({ node, viewer, onAttach });
1108
+ if (config) {
928
1109
  await widget.restore(config);
929
- })();
1110
+ }
930
1111
 
931
- const event = new CustomEvent("workspace-new-view", {
932
- detail: { config, widget },
933
- });
934
- this.element.dispatchEvent(event);
935
1112
  widget.title.closable = true;
936
- this.element.appendChild(widget.viewer);
937
1113
  this._addWidgetEventListeners(widget);
938
1114
  return widget;
939
1115
  }
@@ -943,12 +1119,6 @@ export class PerspectiveWorkspace extends SplitPanel {
943
1119
  this.listeners.get(widget)!();
944
1120
  }
945
1121
 
946
- const settings = (event: CustomEvent) => {
947
- if (!event.detail && this.dockpanel.mode === "single-document") {
948
- this._unmaximize();
949
- }
950
- };
951
-
952
1122
  const contextMenu = (event: MouseEvent) =>
953
1123
  this.showContextMenu(widget, event);
954
1124
 
@@ -962,20 +1132,86 @@ export class PerspectiveWorkspace extends SplitPanel {
962
1132
  : event.detail;
963
1133
 
964
1134
  widget.title.label = config.title;
1135
+ widget._title = config.title;
965
1136
  widget._is_pivoted = config.group_by?.length > 0;
966
1137
  };
967
1138
 
968
1139
  widget.node.addEventListener("contextmenu", contextMenu);
969
- widget.viewer.addEventListener("perspective-toggle-settings", settings);
1140
+
1141
+ // Settings
1142
+ const settings_before = (event: CustomEvent) => {
1143
+ if (event.detail && this.dockpanel.mode !== "single-document") {
1144
+ this._maximize(widget);
1145
+ }
1146
+ };
1147
+
1148
+ const settings_after = (event: CustomEvent) => {
1149
+ if (!event.detail && this.dockpanel.mode === "single-document") {
1150
+ this._unmaximize();
1151
+ }
1152
+ };
1153
+
1154
+ widget.viewer.addEventListener(
1155
+ "perspective-status-indicator-click",
1156
+ (event) => {
1157
+ widget._titlebar_callback?.(event as MouseEvent);
1158
+ },
1159
+ );
1160
+
1161
+ widget.viewer.addEventListener(
1162
+ "perspective-toggle-settings-before",
1163
+ settings_before,
1164
+ );
1165
+
1166
+ widget.viewer.addEventListener(
1167
+ "perspective-toggle-settings",
1168
+ settings_after,
1169
+ );
1170
+
1171
+ const delete_before = () => {
1172
+ if (!widget._deleted) {
1173
+ widget._deleted = true;
1174
+ widget.close();
1175
+ }
1176
+ };
1177
+
1178
+ const delete_after = (event: CustomEvent) => {
1179
+ widget._titlebar?.handleEvent(event.detail as PointerEvent);
1180
+ };
1181
+
1182
+ widget.viewer.addEventListener(
1183
+ "perspective-table-delete-before",
1184
+ delete_before,
1185
+ );
1186
+
1187
+ widget.viewer.addEventListener(
1188
+ "perspective-statusbar-pointerdown",
1189
+ delete_after,
1190
+ );
970
1191
 
971
1192
  // @ts-ignore
972
1193
  widget.viewer.addEventListener("perspective-config-update", updated);
973
1194
 
974
1195
  this.listeners.set(widget, () => {
975
1196
  widget.node.removeEventListener("contextmenu", contextMenu);
1197
+ widget.viewer.removeEventListener(
1198
+ "perspective-table-delete-before",
1199
+ delete_before,
1200
+ );
1201
+
1202
+ widget.viewer.removeEventListener(
1203
+ "perspective-table-delete",
1204
+ delete_after,
1205
+ );
1206
+
1207
+ widget.viewer.removeEventListener(
1208
+ "perspective-toggle-settings",
1209
+ settings_before,
1210
+ );
1211
+
976
1212
  widget.viewer.removeEventListener(
977
1213
  "perspective-toggle-settings",
978
- settings,
1214
+ settings_after,
979
1215
  );
980
1216
 
981
1217
  // @ts-ignore
@@ -1007,7 +1243,11 @@ export class PerspectiveWorkspace extends SplitPanel {
1007
1243
  *
1008
1244
  */
1009
1245
 
1246
+ _throttle?: DebouncedFuncLeading<() => Promise<void>>;
1247
+
1010
1248
  async workspaceUpdated() {
1249
+ // if (!this._throttle) {
1250
+ // this._throttle = throttle(async () => {
1011
1251
  const layout = await this.save();
1012
1252
  if (layout) {
1013
1253
  if (this._last_updated_state) {
@@ -1017,18 +1257,17 @@ export class PerspectiveWorkspace extends SplitPanel {
1017
1257
  }
1018
1258
 
1019
1259
  this._last_updated_state =
1020
- layout as any as PerspectiveWorkspaceConfig<string>;
1021
-
1022
- const tables: Record<string, psp.Table | Promise<psp.Table>> = {};
1023
- this.tables.forEach((value, key) => {
1024
- tables[key] = value;
1025
- });
1260
+ layout as any as PerspectiveWorkspaceConfig;
1026
1261
 
1027
1262
  this.element.dispatchEvent(
1028
1263
  new CustomEvent("workspace-layout-update", {
1029
- detail: { tables, layout },
1264
+ detail: { layout },
1030
1265
  }),
1031
1266
  );
1032
1267
  }
1268
+ // }, 0);
1269
+ // }
1270
+
1271
+ // this._throttle();
1033
1272
  }
1034
1273
  }