flexdesk 0.4.2 → 0.4.3
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.
- package/README.md +12 -0
- package/dist/charts.js +2 -2
- package/dist/{chunk-XKDTIT4Q.js → chunk-4RGQSRMC.js} +2 -2
- package/dist/{chunk-QIU5S2RU.js → chunk-P4AALM3A.js} +2 -2
- package/dist/{chunk-QIU5S2RU.js.map → chunk-P4AALM3A.js.map} +2 -2
- package/dist/tiles.js +2 -2
- package/dist/widgets.js +1 -1
- package/dist/wm.js +206 -165
- package/dist/wm.js.map +3 -3
- package/package.json +1 -1
- package/src/tiling/tile_breadcrumb.js +9 -8
- package/src/tiling/wm.js +222 -175
- package/src/ui/components/data_table.js +5 -1
- /package/dist/{chunk-XKDTIT4Q.js.map → chunk-4RGQSRMC.js.map} +0 -0
package/package.json
CHANGED
|
@@ -44,20 +44,21 @@ export function mountTileBreadcrumb(kind, props, ctx) {
|
|
|
44
44
|
// before the renderer threaded wm through — still navigates.
|
|
45
45
|
const getWm = () => ctx?.wm || window.__twm?.wm || null;
|
|
46
46
|
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// jumping back to the primary tile.
|
|
47
|
+
// Every crumb CLIMBS: `wm.navigateUp` (C35), not `wm.navigate`. A crumb
|
|
48
|
+
// means "up to this level of where I am", and from a record whose list is
|
|
49
|
+
// already open that is what Backspace means too, so the WM answers both
|
|
50
|
+
// with one rule and the breadcrumb never works out which crumb is the
|
|
51
|
+
// parent. Passing `ctx` keeps the click inside its own tile or floating
|
|
52
|
+
// window.
|
|
54
53
|
const navigate = (k, p = {}) => {
|
|
55
54
|
const wm = getWm();
|
|
56
55
|
if (!wm) {
|
|
57
56
|
console.error('[breadcrumb] no WM available — click ignored', k);
|
|
58
57
|
return;
|
|
59
58
|
}
|
|
60
|
-
if (typeof wm.
|
|
59
|
+
if (typeof wm.navigateUp === 'function') {
|
|
60
|
+
wm.navigateUp(k, p, { ctx });
|
|
61
|
+
} else if (typeof wm.navigate === 'function') {
|
|
61
62
|
wm.navigate(k, p, { ctx, dest: 'origin' });
|
|
62
63
|
} else if (typeof wm.openInPrimary === 'function') {
|
|
63
64
|
// Fallback for older WM revisions.
|
package/src/tiling/wm.js
CHANGED
|
@@ -88,7 +88,7 @@ export class WindowManager {
|
|
|
88
88
|
this.promoteInPlace = !!promoteInPlace;
|
|
89
89
|
/** C32. Whether Backspace on an ENTITY tab closes it and returns to a list
|
|
90
90
|
* that is already open, instead of turning the tab into a second copy of
|
|
91
|
-
* that list. See `
|
|
91
|
+
* that list. See `_returnToOpenList`. Default off: it changes what the key
|
|
92
92
|
* does, and an existing embedder upgrading must not find tabs closing
|
|
93
93
|
* where they used to navigate. */
|
|
94
94
|
this.backToOpenList = !!backToOpenList;
|
|
@@ -261,69 +261,50 @@ export class WindowManager {
|
|
|
261
261
|
* methods (openFromContext / openInLeaf / navigateActiveTab) —
|
|
262
262
|
* they all record history by default. */
|
|
263
263
|
navigateBack() {
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
//
|
|
269
|
-
|
|
270
|
-
// tile is explicitly focused (panels excluded). If the focused
|
|
271
|
-
// leaf is a panel, walk back on the primary content leaf
|
|
272
|
-
// instead (Backspace from the nav panel should still navigate
|
|
273
|
-
// the user's content area).
|
|
274
|
-
const focusedId = tree.focusedLeafId;
|
|
275
|
-
const primaryId = tree.primaryLeafId();
|
|
276
|
-
const focusedLeaf = focusedId ? tree.get(focusedId) : null;
|
|
277
|
-
const focusedKind = focusedLeaf?.content?.kind || '';
|
|
278
|
-
const id = (focusedId && focusedKind
|
|
279
|
-
&& !focusedKind.startsWith('panel:')
|
|
280
|
-
&& focusedKind !== 'window-placeholder')
|
|
281
|
-
? focusedId : primaryId;
|
|
282
|
-
if (!id) return;
|
|
283
|
-
// (1) Per-tab history pop, if anything was recorded.
|
|
284
|
-
const prior = tree.popActiveTabHistory?.(id);
|
|
285
|
-
if (prior && prior.kind) {
|
|
286
|
-
// Cross-page back entries carry a `topNav` marker — pop
|
|
287
|
-
// routes via swapToPage so we land on the correct page.
|
|
288
|
-
const leafNow = tree.get(id);
|
|
289
|
-
const curKind = leafNow?.content?.kind;
|
|
290
|
-
const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
|
|
291
|
-
if (prior.topNav && curTopNav && prior.topNav !== curTopNav) {
|
|
292
|
-
tree.swapToPage?.(
|
|
293
|
-
id,
|
|
294
|
-
{ kind: prior.kind, props: prior.props || {},
|
|
295
|
-
title: prior.title || _tabTitle(prior.kind, prior.props) },
|
|
296
|
-
curTopNav, prior.topNav,
|
|
297
|
-
{ recordHistory: false },
|
|
298
|
-
);
|
|
299
|
-
} else {
|
|
300
|
-
tree.replaceActiveTabContent(
|
|
301
|
-
id,
|
|
302
|
-
{ kind: prior.kind, props: prior.props || {} },
|
|
303
|
-
prior.title || _tabTitle(prior.kind, prior.props),
|
|
304
|
-
{ recordHistory: false },
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
tree.focus(id);
|
|
308
|
-
this.renderer.render();
|
|
309
|
-
this._persist();
|
|
310
|
-
this._notifyChange('navigate-back');
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
// (2) C32. An entity tab with nowhere left to go back to, beside a list
|
|
314
|
-
// of its own section that is already open: close it and land there.
|
|
315
|
-
if (this.backToOpenList && this._backToOpenList(id)) return;
|
|
264
|
+
const scope = this._backScope();
|
|
265
|
+
if (!scope) return;
|
|
266
|
+
// (1) Per-tab history, browser-style. Tiles only: a window keeps none.
|
|
267
|
+
if (scope.leafId && this._popTabHistory(scope.leafId)) return;
|
|
268
|
+
// (2) C32. A record beside an open list of its section closes onto it.
|
|
269
|
+
if (this._returnToOpenList(scope)) return;
|
|
316
270
|
// (3) Taxonomy fallback — the user's path is gone, so walk the
|
|
317
|
-
// canonical parent instead. An ENTITY-level parent wins (the
|
|
318
|
-
// instance one level up,
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
const
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
271
|
+
// canonical parent instead, in place. An ENTITY-level parent wins (the
|
|
272
|
+
// instance one level up); failing that, the KIND-level parent.
|
|
273
|
+
const active = this._scopeActiveTab(scope);
|
|
274
|
+
if (!active) return;
|
|
275
|
+
const up = this.taxonomy.parentOf(active.kind, active.props || {});
|
|
276
|
+
const parentKind = up ? up.kind : this.taxonomy.parentKindFor(active.kind);
|
|
277
|
+
if (parentKind) this._navigateInScope(scope, parentKind, up ? (up.props || {}) : {});
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* C35. CLIMB TO A LEVEL: what a breadcrumb crumb means.
|
|
282
|
+
*
|
|
283
|
+
* `navigate` goes to exactly the target. A crumb says something different:
|
|
284
|
+
* "take me up to this level of where I am". From a record, the level it
|
|
285
|
+
* names is usually a list of the record's own section, and when such a list
|
|
286
|
+
* is already open that is the SAME request Backspace makes, so it goes
|
|
287
|
+
* through the same rule, `_returnToOpenList`, and closes the record onto the
|
|
288
|
+
* open list instead of turning the record into a second copy of it.
|
|
289
|
+
*
|
|
290
|
+
* Every crumb calls this, so no crumb needs to know whether it is the
|
|
291
|
+
* immediate parent. A crumb that names an entity (an epic above a story), a
|
|
292
|
+
* record with no list of its section open, or content that is not a record
|
|
293
|
+
* navigates in place, exactly as `navigate(…, { dest: 'origin' })` would.
|
|
294
|
+
*
|
|
295
|
+
* @param {string} kind
|
|
296
|
+
* @param {object} [props]
|
|
297
|
+
* @param {{ctx?: object}} [opts] the crumb's tile or window context
|
|
298
|
+
*/
|
|
299
|
+
navigateUp(kind, props = {}, { ctx = null } = {}) {
|
|
300
|
+
const scope = this._scopeFromCtx(ctx);
|
|
301
|
+
if (!scope) return this.navigate(kind, props, { ctx, dest: 'origin' });
|
|
302
|
+
const active = this._scopeActiveTab(scope);
|
|
303
|
+
const aimsAtOwnList = !_isEntityTab({ props })
|
|
304
|
+
&& _isEntityTab(active)
|
|
305
|
+
&& this.taxonomy.topNavFor(kind) === this.taxonomy.topNavFor(active.kind);
|
|
306
|
+
if (aimsAtOwnList && this._returnToOpenList(scope)) return;
|
|
307
|
+
this._navigateInScope(scope, kind, props);
|
|
327
308
|
}
|
|
328
309
|
|
|
329
310
|
/** True when `navigateBack` would do something user-visible — i.e.
|
|
@@ -372,137 +353,181 @@ export class WindowManager {
|
|
|
372
353
|
this._persist();
|
|
373
354
|
}
|
|
374
355
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
*
|
|
384
|
-
*
|
|
385
|
-
*
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
* for an open tab of the SAME section that is NOT one. That is the list or
|
|
392
|
-
* landing page the record belongs to. Close the record and activate it.
|
|
393
|
-
*
|
|
394
|
-
* NEAREST TO THE LEFT FIRST. New tabs are appended at the end, so the tab a
|
|
395
|
-
* record was opened from sits to its left; when several lists of the section
|
|
396
|
-
* are open, the nearest one on that side is the one it most plausibly came
|
|
397
|
-
* from. The right side is searched only after the left has nothing.
|
|
398
|
-
*
|
|
399
|
-
* DECLINES, and leaves step (3) to walk up in place, whenever there is no
|
|
400
|
-
* such tab: a record that is the tile's only tab, or one whose section has no
|
|
401
|
-
* list open. That keeps the one guarantee the whole rule exists for — Back
|
|
402
|
-
* never leaves two copies of a list — without ever closing a record into a
|
|
403
|
-
* tile that has nothing of its own to show.
|
|
404
|
-
*
|
|
405
|
-
* Matching is by SECTION (`topNavFor`) and not by kind, because the list a
|
|
406
|
-
* record was opened from is often not the record's taxonomy parent: a root
|
|
407
|
-
* landing page that renders the section, or a list that a flattened taxonomy
|
|
408
|
-
* files beside the record rather than above it.
|
|
409
|
-
*
|
|
410
|
-
* @returns {boolean} whether it closed a tab
|
|
411
|
-
*/
|
|
412
|
-
_backToOpenList(leafId) {
|
|
356
|
+
// ── Back and up: one scope model, one rule ─────────────────────
|
|
357
|
+
//
|
|
358
|
+
// A SCOPE is where a navigation happens: `{ leafId }` for a tile or
|
|
359
|
+
// `{ windowId }` for a floating window. Backspace and `navigateUp` both
|
|
360
|
+
// resolve one, and everything below works on a scope, so tiles and windows
|
|
361
|
+
// cannot drift into two versions of the same behaviour.
|
|
362
|
+
|
|
363
|
+
/** The scope Backspace acts on. C34: a floating window the user last
|
|
364
|
+
* clicked in, because the tree's focus never moves to a window; else the
|
|
365
|
+
* focused content tile; else the primary tile. Panels are never a scope:
|
|
366
|
+
* Backspace from the nav panel walks the content area. */
|
|
367
|
+
_backScope() {
|
|
368
|
+
if (this._backWindowId && this._windowToLeaf.has(this._backWindowId)) {
|
|
369
|
+
return { windowId: this._backWindowId };
|
|
370
|
+
}
|
|
371
|
+
this._backWindowId = null;
|
|
413
372
|
const tree = this._tree();
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
if (!section) return false;
|
|
373
|
+
const focusedId = tree.focusedLeafId;
|
|
374
|
+
const focusedKind = focusedId ? tree.get(focusedId)?.content?.kind || '' : '';
|
|
375
|
+
const id = (focusedId && focusedKind
|
|
376
|
+
&& !focusedKind.startsWith('panel:')
|
|
377
|
+
&& focusedKind !== 'window-placeholder')
|
|
378
|
+
? focusedId : tree.primaryLeafId();
|
|
379
|
+
return id ? { leafId: id } : null;
|
|
380
|
+
}
|
|
423
381
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
382
|
+
/** The scope a content context names: its window, or its tile. Null for a
|
|
383
|
+
* panel or a context the WM does not know. */
|
|
384
|
+
_scopeFromCtx(ctx) {
|
|
385
|
+
if (ctx?.windowId && this._windowToLeaf.has(ctx.windowId)) return { windowId: ctx.windowId };
|
|
386
|
+
const leaf = ctx?.leafId ? this._tree().get(ctx.leafId) : null;
|
|
387
|
+
const k = leaf?.content?.kind || '';
|
|
388
|
+
if (leaf && leaf.kind === 'leaf' && k && !k.startsWith('panel:')) return { leafId: leaf.id };
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
429
391
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
392
|
+
/** A scope's tab list and active index. A window promoted with no strip, or
|
|
393
|
+
* a tile with no tab array, reads as a single tab. */
|
|
394
|
+
_scopeTabs(scope) {
|
|
395
|
+
if (scope?.windowId) {
|
|
396
|
+
const rec = this._windowToLeaf.get(scope.windowId);
|
|
397
|
+
if (!rec) return null;
|
|
398
|
+
const tabs = Array.isArray(rec.tabs) && rec.tabs.length ? rec.tabs : [rec.original].filter(Boolean);
|
|
399
|
+
return { tabs, activeIdx: Math.max(0, Math.min(tabs.length - 1, rec.activeTabIdx || 0)) };
|
|
400
|
+
}
|
|
401
|
+
const leaf = scope?.leafId ? this._tree().get(scope.leafId) : null;
|
|
402
|
+
if (!leaf || leaf.kind !== 'leaf') return null;
|
|
403
|
+
const tabs = Array.isArray(leaf.tabs) && leaf.tabs.length ? leaf.tabs : [leaf.content].filter(Boolean);
|
|
404
|
+
return { tabs, activeIdx: Math.max(0, Math.min(tabs.length - 1, leaf.activeTabIdx || 0)) };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
_scopeActiveTab(scope) {
|
|
408
|
+
const src = this._scopeTabs(scope);
|
|
409
|
+
return src ? src.tabs[src.activeIdx] || null : null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/** Replace what a scope shows, in place: the tile's active tab (recording
|
|
413
|
+
* history) or the window's content. */
|
|
414
|
+
_navigateInScope(scope, kind, props = {}) {
|
|
415
|
+
if (scope.windowId) return this.openInWindow(scope.windowId, kind, props);
|
|
416
|
+
return this.navigateActiveTab(scope.leafId, kind, props);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Pop the active tab's history, if it has any. */
|
|
420
|
+
_popTabHistory(id) {
|
|
421
|
+
const tree = this._tree();
|
|
422
|
+
const prior = tree.popActiveTabHistory?.(id);
|
|
423
|
+
if (!prior || !prior.kind) return false;
|
|
424
|
+
// Cross-page back entries carry a `topNav` marker — pop
|
|
425
|
+
// routes via swapToPage so we land on the correct page.
|
|
426
|
+
const curKind = tree.get(id)?.content?.kind;
|
|
427
|
+
const curTopNav = curKind ? this.taxonomy.topNavFor(curKind) : null;
|
|
428
|
+
const title = prior.title || _tabTitle(prior.kind, prior.props);
|
|
429
|
+
if (prior.topNav && curTopNav && prior.topNav !== curTopNav) {
|
|
430
|
+
tree.swapToPage?.(id, { kind: prior.kind, props: prior.props || {}, title },
|
|
431
|
+
curTopNav, prior.topNav, { recordHistory: false });
|
|
432
|
+
} else {
|
|
433
|
+
tree.replaceActiveTabContent(id, { kind: prior.kind, props: prior.props || {} },
|
|
434
|
+
title, { recordHistory: false });
|
|
435
|
+
}
|
|
436
|
+
tree.focus(id);
|
|
434
437
|
this.renderer.render();
|
|
435
438
|
this._persist();
|
|
436
|
-
this._notifyChange('navigate-back
|
|
439
|
+
this._notifyChange('navigate-back');
|
|
437
440
|
return true;
|
|
438
441
|
}
|
|
439
442
|
|
|
440
443
|
/**
|
|
441
|
-
* C34.
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
*
|
|
446
|
-
*
|
|
447
|
-
*
|
|
448
|
-
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
*
|
|
444
|
+
* C32 / C34 / C35. BACK FROM A RECORD RETURNS TO THE LIST; IT DOES NOT MAKE ONE.
|
|
445
|
+
*
|
|
446
|
+
* The gesture this is for: a list, a record opened from it into a new tab,
|
|
447
|
+
* then Backspace or the list's crumb in that record. Rewriting the record
|
|
448
|
+
* into its parent would leave the list open twice. The user's model is a
|
|
449
|
+
* browser's: the list is still open, so going back closes the record and
|
|
450
|
+
* shows the list again. This function is the ONLY place that rule lives.
|
|
451
|
+
*
|
|
452
|
+
* Applies when `backToOpenList` is on and the scope's active tab is a RECORD
|
|
453
|
+
* (its props carry an `id`). The list is an open tab that is not a record
|
|
454
|
+
* and belongs to the same SECTION (`topNavFor`), matched by section and not
|
|
455
|
+
* by kind because a record's list is often not its taxonomy parent: a root
|
|
456
|
+
* landing that renders the section, or a list filed beside the record.
|
|
457
|
+
*
|
|
458
|
+
* Where it looks, in order:
|
|
459
|
+
* - the scope's own tabs, nearest to the LEFT first (new tabs are appended,
|
|
460
|
+
* so the tab a record came from sits to its left), then to the right;
|
|
461
|
+
* - for a WINDOW only, then any tile of the window's desktop, primary tile
|
|
462
|
+
* first, activating a list tab there. Closing a window's last tab closes
|
|
463
|
+
* the window, which discards it; a plain close never docks back.
|
|
464
|
+
*
|
|
465
|
+
* Declines, leaving the caller to navigate in place, when there is no such
|
|
466
|
+
* list, so Back never closes a record into a tile with nothing to show.
|
|
467
|
+
*
|
|
468
|
+
* @returns {boolean} whether it closed the record
|
|
452
469
|
*/
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
if (!
|
|
457
|
-
const
|
|
458
|
-
const activeIdx = Math.max(0, Math.min(tabs.length - 1, rec.activeTabIdx || 0));
|
|
470
|
+
_returnToOpenList(scope) {
|
|
471
|
+
if (!this.backToOpenList) return false;
|
|
472
|
+
const src = this._scopeTabs(scope);
|
|
473
|
+
if (!src) return false;
|
|
474
|
+
const { tabs, activeIdx } = src;
|
|
459
475
|
const active = tabs[activeIdx];
|
|
460
|
-
if (!active) return
|
|
461
|
-
const section = this.
|
|
462
|
-
|
|
476
|
+
if (!_isEntityTab(active)) return false;
|
|
477
|
+
const section = this.taxonomy.topNavFor(active.kind);
|
|
478
|
+
if (!section) return false;
|
|
463
479
|
const isList = (t) => !!t && !_isEntityTab(t) && this.taxonomy.topNavFor(t.kind) === section;
|
|
464
480
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
this._windowTabAction(winId, 'close', { idx: activeIdx });
|
|
472
|
-
this.showWindowTab(winId, own > activeIdx ? own - 1 : own);
|
|
481
|
+
const own = _nearestIndex(tabs, activeIdx, isList);
|
|
482
|
+
if (own >= 0 && tabs.length > 1) {
|
|
483
|
+
const landing = own > activeIdx ? own - 1 : own;
|
|
484
|
+
if (scope.windowId) {
|
|
485
|
+
this._windowTabAction(scope.windowId, 'close', { idx: activeIdx });
|
|
486
|
+
this.showWindowTab(scope.windowId, landing);
|
|
473
487
|
return true;
|
|
474
488
|
}
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
489
|
+
const tree = this._tree();
|
|
490
|
+
tree.removeLeafTab(scope.leafId, activeIdx);
|
|
491
|
+
tree.setActiveLeafTab(scope.leafId, landing);
|
|
492
|
+
tree.focus(scope.leafId);
|
|
493
|
+
this.renderer.render();
|
|
494
|
+
this._persist();
|
|
495
|
+
this._notifyChange('navigate-back-close');
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
if (!scope.windowId) return false;
|
|
499
|
+
|
|
500
|
+
const rec = this._windowToLeaf.get(scope.windowId);
|
|
501
|
+
const tree = (this.desktops.desktops[rec.desktopIdx] || this.desktops.active()).tree;
|
|
502
|
+
const primaryId = tree.primaryLeafId();
|
|
503
|
+
const leaves = tree.leaves?.() || [];
|
|
504
|
+
const ordered = [...leaves.filter((l) => l.id === primaryId), ...leaves.filter((l) => l.id !== primaryId)];
|
|
505
|
+
for (const leaf of ordered) {
|
|
506
|
+
const ltabs = Array.isArray(leaf.tabs) ? leaf.tabs : [];
|
|
507
|
+
const cur = Math.max(0, Math.min(ltabs.length - 1, leaf.activeTabIdx || 0));
|
|
508
|
+
const hit = isList(ltabs[cur]) ? cur : ltabs.findIndex(isList);
|
|
509
|
+
if (hit < 0) continue;
|
|
510
|
+
if (this._backWindowId === scope.windowId) this._backWindowId = null;
|
|
511
|
+
if (Array.isArray(rec.tabs) && rec.tabs.length > 1) {
|
|
512
|
+
this._windowTabAction(scope.windowId, 'close', { idx: activeIdx });
|
|
513
|
+
} else {
|
|
514
|
+
try { rec.window.close({ force: true }); } catch { /* already gone */ }
|
|
515
|
+
}
|
|
516
|
+
if (this.desktops.active().tree === tree) {
|
|
517
|
+
tree.setActiveLeafTab(leaf.id, hit);
|
|
518
|
+
tree.focus(leaf.id);
|
|
519
|
+
this.renderer.render();
|
|
497
520
|
}
|
|
521
|
+
this._persist();
|
|
522
|
+
this._notifyChange('navigate-back-close');
|
|
523
|
+
return true;
|
|
498
524
|
}
|
|
499
|
-
|
|
500
|
-
const up = this.taxonomy.parentOf?.(active.kind, active.props || {});
|
|
501
|
-
const parentKind = up ? up.kind : this.taxonomy.parentKindFor(active.kind);
|
|
502
|
-
if (parentKind) this.openInWindow(winId, parentKind, up ? (up.props || {}) : {});
|
|
503
|
-
return true;
|
|
525
|
+
return false;
|
|
504
526
|
}
|
|
505
527
|
|
|
528
|
+
/** Navigate inside the current tab — preserves every other tab in
|
|
529
|
+
* the leaf. Used by Backspace + breadcrumb segments + anything
|
|
530
|
+
* else that should walk WITHIN the tile rather than reset it. */
|
|
506
531
|
navigateActiveTab(leafId, kind, props = {}) {
|
|
507
532
|
const tree = this._tree();
|
|
508
533
|
const leaf = tree.get(leafId);
|
|
@@ -640,6 +665,20 @@ export class WindowManager {
|
|
|
640
665
|
tree.swapToPage(leafId,
|
|
641
666
|
{ kind, props, title },
|
|
642
667
|
currentTopNav, targetTopNav);
|
|
668
|
+
// C35. THE SAME PAGE WITH DIFFERENT PROPS IS A DIFFERENT VIEW. swapToPage
|
|
669
|
+
// finds an existing tab by kind + props.id, and a list has no id, so a
|
|
670
|
+
// filter clicked while that list is showing matched the open list tab
|
|
671
|
+
// and re-activated it unchanged: the click did nothing. The active tab
|
|
672
|
+
// must carry the requested props. A BARE open (no props, a top-nav
|
|
673
|
+
// click) is left alone, because putting back the tab you left, filter
|
|
674
|
+
// and all, is the point of archiving page tabs. BugDesk's own tiling
|
|
675
|
+
// fork carried this correction; it is upstream now.
|
|
676
|
+
const _leaf = tree.get(leafId);
|
|
677
|
+
const _active = _leaf?.tabs?.[_leaf.activeTabIdx];
|
|
678
|
+
if (_active && _active.kind === kind && Object.keys(props || {}).length > 0
|
|
679
|
+
&& JSON.stringify(_active.props || {}) !== JSON.stringify(props || {})) {
|
|
680
|
+
tree.replaceActiveTabContent(leafId, { kind, props }, title, { recordHistory: false });
|
|
681
|
+
}
|
|
643
682
|
tree.focus(leafId);
|
|
644
683
|
if (spawned) this._canonicalize(tree, this.desktops.active());
|
|
645
684
|
this.renderer.render();
|
|
@@ -3807,6 +3846,14 @@ function _swapSiblings(tree, aId, bId) {
|
|
|
3807
3846
|
|
|
3808
3847
|
/** A tab showing ONE record rather than a list: its props name an `id`. The same
|
|
3809
3848
|
* test `swapToPage` applies to "the caller asked for a specific entity". */
|
|
3849
|
+
/** The index nearest to `from` whose tab passes `test`: leftward first, then
|
|
3850
|
+
* rightward. -1 when none does. */
|
|
3851
|
+
function _nearestIndex(tabs, from, test) {
|
|
3852
|
+
for (let i = from - 1; i >= 0; i--) if (test(tabs[i])) return i;
|
|
3853
|
+
for (let i = from + 1; i < tabs.length; i++) if (test(tabs[i])) return i;
|
|
3854
|
+
return -1;
|
|
3855
|
+
}
|
|
3856
|
+
|
|
3810
3857
|
function _isEntityTab(tab) {
|
|
3811
3858
|
return !!tab && tab.props != null && tab.props.id != null && tab.props.id !== '';
|
|
3812
3859
|
}
|
|
@@ -1467,7 +1467,11 @@ export class DataTable {
|
|
|
1467
1467
|
const isServerSide = typeof onPageChange === 'function';
|
|
1468
1468
|
const processedRows = isServerSide ? rows : this._getProcessedRows();
|
|
1469
1469
|
const offset = isServerSide ? (configOffset || 0) : this._state.offset;
|
|
1470
|
-
|
|
1470
|
+
// NO PAGINATION MEANS NO PAGES. This used to slice to `pageSize` either
|
|
1471
|
+
// way, so a table built with `pagination: false` silently showed its
|
|
1472
|
+
// first hundred rows and offered no control to reach the rest.
|
|
1473
|
+
const pageRows = isServerSide || !this.config.pagination
|
|
1474
|
+
? processedRows : processedRows.slice(offset, offset + pageSize);
|
|
1471
1475
|
|
|
1472
1476
|
const table = document.createElement('table');
|
|
1473
1477
|
table.className = readonly ? 'twm-preview-table twm-preview-table--readonly' : 'twm-preview-table';
|
|
File without changes
|