@perspective-dev/workspace 4.0.0 → 4.1.0

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
+ }
39
+
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
+ );
34
56
 
35
- export interface PerspectiveLayout<T> {
36
- children?: PerspectiveLayout<T>[];
37
- widgets?: T[];
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
+ );
212
340
 
213
- return { ...layout, viewers };
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
+ );
349
+
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
+ );
431
+
432
+ // const widgets = await Promise.all(tasks);
433
+
434
+ // );
272
435
 
273
- master.sizes &&
274
- this.masterPanel.setRelativeSizes(master.sizes);
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
  });
@@ -358,11 +521,6 @@ export class PerspectiveWorkspace extends SplitPanel {
358
521
  this.onPerspectiveSelect.bind(this),
359
522
  );
360
523
 
361
- widget.viewer.addEventListener(
362
- "perspective-click",
363
- this.onPerspectiveSelect.bind(this),
364
- );
365
-
366
524
  // TODO remove event listener
367
525
  this.masterPanel.addWidget(widget);
368
526
  }
@@ -385,13 +543,6 @@ export class PerspectiveWorkspace extends SplitPanel {
385
543
  } else {
386
544
  this._validate(table);
387
545
  }
388
-
389
- this.getAllWidgets().forEach((widget) => {
390
- const psp_widget = widget as PerspectiveViewerWidget;
391
- if (psp_widget.viewer.getAttribute("table") === name) {
392
- psp_widget.load(table);
393
- }
394
- });
395
546
  }
396
547
 
397
548
  _delete_listener(name: string) {
@@ -403,7 +554,7 @@ export class PerspectiveWorkspace extends SplitPanel {
403
554
  });
404
555
  }
405
556
 
406
- update_widget_for_viewer(viewer: HTMLPerspectiveViewerElement) {
557
+ async update_widget_for_viewer(viewer: HTMLPerspectiveViewerElement) {
407
558
  let slot_name = viewer.getAttribute("slot");
408
559
  if (!slot_name) {
409
560
  slot_name = this._gen_id();
@@ -418,10 +569,8 @@ export class PerspectiveWorkspace extends SplitPanel {
418
569
  `Undocked ${viewer.outerHTML}, creating default layout`,
419
570
  );
420
571
 
421
- const widget = this._createWidget({
422
- config: {
423
- table: viewer.getAttribute("table")!,
424
- },
572
+ const widget = await this._createWidget({
573
+ // config: {},
425
574
  viewer,
426
575
  });
427
576
 
@@ -467,7 +616,7 @@ export class PerspectiveWorkspace extends SplitPanel {
467
616
  const config = await widget.save();
468
617
  config.settings = false;
469
618
  config.title = config.title ? `${config.title} (*)` : "";
470
- const duplicate = this._createWidgetAndNode({
619
+ const duplicate = await this._createWidgetAndNode({
471
620
  config,
472
621
  slot: undefined,
473
622
  });
@@ -477,7 +626,7 @@ export class PerspectiveWorkspace extends SplitPanel {
477
626
  ref: widget,
478
627
  });
479
628
 
480
- await duplicate.task;
629
+ await duplicate.viewer.flush();
481
630
  }
482
631
 
483
632
  toggleMasterDetail(widget: PerspectiveViewerWidget) {
@@ -503,12 +652,14 @@ export class PerspectiveWorkspace extends SplitPanel {
503
652
 
504
653
  _maximize(widget: PerspectiveViewerWidget) {
505
654
  widget.viewer.classList.add("widget-maximize");
506
- this._minimizedLayout = this.dockpanel.saveLayout();
507
- this._minimizedLayoutSlots = PerspectiveDockPanel.mapWidgets(
508
- (widget: PerspectiveViewerWidget) =>
509
- widget.viewer.getAttribute("slot"),
510
- this.dockpanel.saveLayout(),
511
- );
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
+ }
512
663
 
513
664
  this._maximizedWidget = widget;
514
665
  this.dockpanel.mode = "single-document";
@@ -519,6 +670,7 @@ export class PerspectiveWorkspace extends SplitPanel {
519
670
  this._maximizedWidget!.viewer.classList.remove("widget-maximize");
520
671
  this.dockpanel.mode = "multiple-document";
521
672
  this.dockpanel.restoreLayout(this._minimizedLayout!);
673
+ this._minimizedLayout = undefined;
522
674
  }
523
675
 
524
676
  toggleSingleDocument(widget: PerspectiveViewerWidget) {
@@ -556,14 +708,7 @@ export class PerspectiveWorkspace extends SplitPanel {
556
708
  const config = await (
557
709
  event.target as HTMLPerspectiveViewerElement
558
710
  ).save();
559
- // perspective-select is already handled for hypergrid
560
711
 
561
- if (
562
- event.type === "perspective-click" &&
563
- (config.plugin === "Datagrid" || config.plugin === null)
564
- ) {
565
- return;
566
- }
567
712
  const candidates = new Set([
568
713
  ...(config["group_by"] || []),
569
714
  ...(config["split_by"] || []),
@@ -595,11 +740,6 @@ export class PerspectiveWorkspace extends SplitPanel {
595
740
  this.masterPanel.addWidget(widget);
596
741
  widget.isHidden && widget.show();
597
742
  widget.viewer.restyleElement();
598
- widget.viewer.addEventListener(
599
- "perspective-click",
600
- this.onPerspectiveSelect.bind(this),
601
- );
602
-
603
743
  widget.viewer.addEventListener(
604
744
  "perspective-select",
605
745
  this.onPerspectiveSelect.bind(this),
@@ -618,11 +758,6 @@ export class PerspectiveWorkspace extends SplitPanel {
618
758
  }
619
759
 
620
760
  widget.viewer.restyleElement();
621
- widget.viewer.removeEventListener(
622
- "perspective-click",
623
- this.onPerspectiveSelect.bind(this),
624
- );
625
-
626
761
  widget.viewer.removeEventListener(
627
762
  "perspective-select",
628
763
  this.onPerspectiveSelect.bind(this),
@@ -679,22 +814,31 @@ export class PerspectiveWorkspace extends SplitPanel {
679
814
  },
680
815
  );
681
816
 
682
- for (const table of this.tables.keys()) {
683
- let args;
684
- if (widget !== null) {
685
- args = {
686
- table,
687
- widget_name: widget.viewer.getAttribute("slot"),
688
- };
689
- } else {
690
- args = { table };
691
- }
817
+ (async () => {
818
+ for (const table of (
819
+ await Promise.all(
820
+ this.client.map((client) =>
821
+ client.get_hosted_table_names(),
822
+ ),
823
+ )
824
+ ).map((x) => x.flatMap((x: any) => x))) {
825
+ let args;
826
+ if (widget !== null) {
827
+ args = {
828
+ table,
829
+ widget_name:
830
+ widget.viewer.getAttribute("slot"),
831
+ };
832
+ } else {
833
+ args = { table };
834
+ }
692
835
 
693
- submenu.addItem({
694
- command: "workspace:new",
695
- args,
696
- });
697
- }
836
+ submenu.insertItem(0, {
837
+ command: "workspace:new",
838
+ args,
839
+ });
840
+ }
841
+ })();
698
842
 
699
843
  const widgets = PerspectiveDockPanel.getWidgets(
700
844
  this.dockpanel.saveLayout(),
@@ -837,38 +981,43 @@ export class PerspectiveWorkspace extends SplitPanel {
837
981
  this.setRelativeSizes(sizes);
838
982
  }
839
983
 
840
- addViewer(config: ViewerConfigUpdateExt, is_global_filter?: boolean) {
841
- if (this.dockpanel.mode === "single-document") {
842
- const _task = this._maximizedWidget!.viewer.toggleConfig(false);
843
- this._unmaximize();
844
- }
845
-
846
- const widget = this._createWidgetAndNode({ config });
847
- if (is_global_filter) {
848
- if (!this.masterPanel.isAttached) {
849
- this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE);
984
+ async addViewer(
985
+ config: psp_viewer.ViewerConfigUpdate,
986
+ is_global_filter?: boolean,
987
+ ) {
988
+ await this._mutex.lock(async () => {
989
+ if (this.dockpanel.mode === "single-document") {
990
+ const _task = this._maximizedWidget!.viewer.toggleConfig(false);
991
+ this._unmaximize();
850
992
  }
851
993
 
852
- this.masterPanel.addWidget(widget);
853
- } else {
854
- if (!this.detailPanel.isAttached) {
855
- this.addWidget(this.detailPanel);
994
+ const widget = await this._createWidgetAndNode({ config });
995
+ if (is_global_filter) {
996
+ if (!this.masterPanel.isAttached) {
997
+ this.setupMasterPanel(DEFAULT_WORKSPACE_SIZE);
998
+ }
999
+
1000
+ this.masterPanel.addWidget(widget);
1001
+ } else {
1002
+ if (!this.detailPanel.isAttached) {
1003
+ this.addWidget(this.detailPanel);
1004
+ }
1005
+ this.dockpanel.addWidget(widget, { mode: "split-right" });
856
1006
  }
857
- this.dockpanel.addWidget(widget, { mode: "split-right" });
858
- }
859
1007
 
860
- this.update();
1008
+ this.update();
1009
+ });
861
1010
  }
862
1011
 
863
1012
  /*********************************************************************
864
1013
  * Widget helper methods
865
1014
  */
866
1015
 
867
- _createWidgetAndNode({
1016
+ async _createWidgetAndNode({
868
1017
  config,
869
1018
  slot: slotname,
870
1019
  }: {
871
- config: ViewerConfigUpdateExt;
1020
+ config: psp_viewer.ViewerConfigUpdate;
872
1021
  slot?: string;
873
1022
  }) {
874
1023
  const node = this._createNode(slotname);
@@ -883,11 +1032,19 @@ export class PerspectiveWorkspace extends SplitPanel {
883
1032
  viewer.setAttribute("table", table);
884
1033
  }
885
1034
 
886
- return this._createWidget({
887
- config,
888
- elem: node as HTMLElement,
889
- viewer,
890
- });
1035
+ for (const client of this.client) {
1036
+ const tables = await client.get_hosted_table_names();
1037
+ if (table && tables.indexOf(table) > -1) {
1038
+ await viewer.load(client);
1039
+ return await this._createWidget({
1040
+ config,
1041
+ elem: node as HTMLElement,
1042
+ viewer,
1043
+ });
1044
+ }
1045
+ }
1046
+
1047
+ throw new Error(`Table "${table}" not found`);
891
1048
  }
892
1049
 
893
1050
  _gen_id() {
@@ -917,12 +1074,12 @@ export class PerspectiveWorkspace extends SplitPanel {
917
1074
  return node as HTMLElement;
918
1075
  }
919
1076
 
920
- _createWidget({
1077
+ async _createWidget({
921
1078
  config,
922
1079
  elem,
923
1080
  viewer,
924
1081
  }: {
925
- config: ViewerConfigUpdateExt;
1082
+ config?: psp_viewer.ViewerConfigUpdate;
926
1083
  elem?: Element;
927
1084
  viewer: HTMLPerspectiveViewerElement;
928
1085
  }) {
@@ -937,25 +1094,24 @@ export class PerspectiveWorkspace extends SplitPanel {
937
1094
  }
938
1095
  }
939
1096
 
940
- const table = this.tables.get(
941
- viewer.getAttribute("table") || config.table,
942
- );
943
-
944
- const widget = new PerspectiveViewerWidget({ node, viewer });
945
- widget.task = (async () => {
946
- if (table) {
947
- widget.load(table);
1097
+ const onAttach = () => {
1098
+ if (widget.viewer.parentElement !== this.element) {
1099
+ this.element.appendChild(widget.viewer);
948
1100
  }
949
1101
 
1102
+ const event = new CustomEvent("workspace-new-view", {
1103
+ detail: { config, widget },
1104
+ });
1105
+
1106
+ this.element.dispatchEvent(event);
1107
+ };
1108
+
1109
+ const widget = new PerspectiveViewerWidget({ node, viewer, onAttach });
1110
+ if (config) {
950
1111
  await widget.restore(config);
951
- })();
1112
+ }
952
1113
 
953
- const event = new CustomEvent("workspace-new-view", {
954
- detail: { config, widget },
955
- });
956
- this.element.dispatchEvent(event);
957
1114
  widget.title.closable = true;
958
- this.element.appendChild(widget.viewer);
959
1115
  this._addWidgetEventListeners(widget);
960
1116
  return widget;
961
1117
  }
@@ -965,12 +1121,6 @@ export class PerspectiveWorkspace extends SplitPanel {
965
1121
  this.listeners.get(widget)!();
966
1122
  }
967
1123
 
968
- const settings = (event: CustomEvent) => {
969
- if (!event.detail && this.dockpanel.mode === "single-document") {
970
- this._unmaximize();
971
- }
972
- };
973
-
974
1124
  const contextMenu = (event: MouseEvent) =>
975
1125
  this.showContextMenu(widget, event);
976
1126
 
@@ -984,20 +1134,86 @@ export class PerspectiveWorkspace extends SplitPanel {
984
1134
  : event.detail;
985
1135
 
986
1136
  widget.title.label = config.title;
1137
+ widget._title = config.title;
987
1138
  widget._is_pivoted = config.group_by?.length > 0;
988
1139
  };
989
1140
 
990
1141
  widget.node.addEventListener("contextmenu", contextMenu);
991
- widget.viewer.addEventListener("perspective-toggle-settings", settings);
1142
+
1143
+ // Settings
1144
+ const settings_before = (event: CustomEvent) => {
1145
+ if (event.detail && this.dockpanel.mode !== "single-document") {
1146
+ this._maximize(widget);
1147
+ }
1148
+ };
1149
+
1150
+ const settings_after = (event: CustomEvent) => {
1151
+ if (!event.detail && this.dockpanel.mode === "single-document") {
1152
+ this._unmaximize();
1153
+ }
1154
+ };
1155
+
1156
+ widget.viewer.addEventListener(
1157
+ "perspective-status-indicator-click",
1158
+ (event) => {
1159
+ widget._titlebar_callback?.(event as MouseEvent);
1160
+ },
1161
+ );
1162
+
1163
+ widget.viewer.addEventListener(
1164
+ "perspective-toggle-settings-before",
1165
+ settings_before,
1166
+ );
1167
+
1168
+ widget.viewer.addEventListener(
1169
+ "perspective-toggle-settings",
1170
+ settings_after,
1171
+ );
1172
+
1173
+ const delete_before = () => {
1174
+ if (!widget._deleted) {
1175
+ widget._deleted = true;
1176
+ widget.close();
1177
+ }
1178
+ };
1179
+
1180
+ const delete_after = (event: CustomEvent) => {
1181
+ widget._titlebar?.handleEvent(event.detail as PointerEvent);
1182
+ };
1183
+
1184
+ widget.viewer.addEventListener(
1185
+ "perspective-table-delete-before",
1186
+ delete_before,
1187
+ );
1188
+
1189
+ widget.viewer.addEventListener(
1190
+ "perspective-statusbar-pointerdown",
1191
+ delete_after,
1192
+ );
992
1193
 
993
1194
  // @ts-ignore
994
1195
  widget.viewer.addEventListener("perspective-config-update", updated);
995
1196
 
996
1197
  this.listeners.set(widget, () => {
997
1198
  widget.node.removeEventListener("contextmenu", contextMenu);
1199
+ widget.viewer.removeEventListener(
1200
+ "perspective-table-delete-before",
1201
+ delete_before,
1202
+ );
1203
+
1204
+ widget.viewer.removeEventListener(
1205
+ "perspective-table-delete",
1206
+ delete_after,
1207
+ );
1208
+
998
1209
  widget.viewer.removeEventListener(
999
1210
  "perspective-toggle-settings",
1000
- settings,
1211
+ settings_before,
1212
+ );
1213
+
1214
+ widget.viewer.removeEventListener(
1215
+ "perspective-toggle-settings",
1216
+ settings_after,
1001
1217
  );
1002
1218
 
1003
1219
  // @ts-ignore
@@ -1029,7 +1245,11 @@ export class PerspectiveWorkspace extends SplitPanel {
1029
1245
  *
1030
1246
  */
1031
1247
 
1248
+ _throttle?: DebouncedFuncLeading<() => Promise<void>>;
1249
+
1032
1250
  async workspaceUpdated() {
1251
+ // if (!this._throttle) {
1252
+ // this._throttle = throttle(async () => {
1033
1253
  const layout = await this.save();
1034
1254
  if (layout) {
1035
1255
  if (this._last_updated_state) {
@@ -1039,18 +1259,17 @@ export class PerspectiveWorkspace extends SplitPanel {
1039
1259
  }
1040
1260
 
1041
1261
  this._last_updated_state =
1042
- layout as any as PerspectiveWorkspaceConfig<string>;
1043
-
1044
- const tables: Record<string, psp.Table | Promise<psp.Table>> = {};
1045
- this.tables.forEach((value, key) => {
1046
- tables[key] = value;
1047
- });
1262
+ layout as any as PerspectiveWorkspaceConfig;
1048
1263
 
1049
1264
  this.element.dispatchEvent(
1050
1265
  new CustomEvent("workspace-layout-update", {
1051
- detail: { tables, layout },
1266
+ detail: { layout },
1052
1267
  }),
1053
1268
  );
1054
1269
  }
1270
+ // }, 0);
1271
+ // }
1272
+
1273
+ // this._throttle();
1055
1274
  }
1056
1275
  }