@memberjunction/ng-shared 5.51.0 → 6.1.0-edge.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.
- package/LICENSE +7 -0
- package/dist/lib/base-dashboard.d.ts +16 -0
- package/dist/lib/base-dashboard.d.ts.map +1 -1
- package/dist/lib/base-dashboard.js +36 -5
- package/dist/lib/base-dashboard.js.map +1 -1
- package/dist/lib/base-resource-component.d.ts +33 -3
- package/dist/lib/base-resource-component.d.ts.map +1 -1
- package/dist/lib/base-resource-component.js +65 -11
- package/dist/lib/base-resource-component.js.map +1 -1
- package/dist/lib/explorer-breakpoint.service.d.ts +39 -0
- package/dist/lib/explorer-breakpoint.service.d.ts.map +1 -0
- package/dist/lib/explorer-breakpoint.service.js +57 -0
- package/dist/lib/explorer-breakpoint.service.js.map +1 -0
- package/dist/lib/home-pin.service.d.ts.map +1 -1
- package/dist/lib/home-pin.service.js +0 -2
- package/dist/lib/home-pin.service.js.map +1 -1
- package/dist/lib/navigation.interfaces.d.ts +13 -0
- package/dist/lib/navigation.interfaces.d.ts.map +1 -1
- package/dist/lib/navigation.interfaces.js.map +1 -1
- package/dist/lib/navigation.service.d.ts +70 -8
- package/dist/lib/navigation.service.d.ts.map +1 -1
- package/dist/lib/navigation.service.js +356 -50
- package/dist/lib/navigation.service.js.map +1 -1
- package/dist/lib/record-open-style.d.ts +88 -0
- package/dist/lib/record-open-style.d.ts.map +1 -0
- package/dist/lib/record-open-style.js +124 -0
- package/dist/lib/record-open-style.js.map +1 -0
- package/dist/lib/record-tab-icon.d.ts +10 -0
- package/dist/lib/record-tab-icon.d.ts.map +1 -0
- package/dist/lib/record-tab-icon.js +15 -0
- package/dist/lib/record-tab-icon.js.map +1 -0
- package/dist/lib/safe-detect-changes.d.ts +18 -0
- package/dist/lib/safe-detect-changes.d.ts.map +1 -0
- package/dist/lib/safe-detect-changes.js +29 -0
- package/dist/lib/safe-detect-changes.js.map +1 -0
- package/dist/lib/shared.service.d.ts +5 -1
- package/dist/lib/shared.service.d.ts.map +1 -1
- package/dist/lib/shared.service.js +16 -14
- package/dist/lib/shared.service.js.map +1 -1
- package/dist/public-api.d.ts +4 -0
- package/dist/public-api.d.ts.map +1 -1
- package/dist/public-api.js +4 -0
- package/dist/public-api.js.map +1 -1
- package/package.json +22 -20
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { Injectable } from '@angular/core';
|
|
2
|
+
import { IsRecordTabsStyle, RECORDS_RESOURCE_TYPE, IsRecordsTabConfiguration } from './record-open-style';
|
|
3
|
+
import { CompositeKey } from '@memberjunction/core';
|
|
2
4
|
import { fromEvent, BehaviorSubject, Subject } from 'rxjs';
|
|
3
5
|
import { map, distinctUntilChanged } from 'rxjs/operators';
|
|
4
6
|
import { UUIDsEqual } from '@memberjunction/global';
|
|
@@ -348,24 +350,57 @@ export class NavigationService {
|
|
|
348
350
|
return tabId;
|
|
349
351
|
}
|
|
350
352
|
/**
|
|
351
|
-
* Open an entity record view
|
|
352
|
-
*
|
|
353
|
+
* Open an entity record view. Behavior forks on the deployment's
|
|
354
|
+
* record-open style (see record-open-style.ts):
|
|
355
|
+
* - 'records' (default): the record ALWAYS opens as its own tab, assigned
|
|
356
|
+
* to the app the user is standing in (no Home reassignment, no nav flip).
|
|
357
|
+
* Re-opening a record that's already open focuses its existing tab,
|
|
358
|
+
* browser-style. The shell's Records pill + strip are the visible UI.
|
|
359
|
+
* - 'classic': the original Home-first assignment and tab semantics.
|
|
360
|
+
* Both styles capture the source-app trail into the tab configuration for
|
|
361
|
+
* context UI and diagnostics.
|
|
353
362
|
*/
|
|
354
363
|
OpenEntityRecord(entityName, recordPkey, options) {
|
|
355
|
-
const
|
|
356
|
-
const
|
|
357
|
-
|
|
364
|
+
const tabsMode = IsRecordTabsStyle();
|
|
365
|
+
const activeApp = this.appManager.GetActiveApp();
|
|
366
|
+
const appId = tabsMode && activeApp ? activeApp.ID : this.getDefaultApplicationId();
|
|
367
|
+
const appColor = tabsMode && activeApp ? activeApp.GetColor() : this.getDefaultAppColor();
|
|
358
368
|
const recordId = recordPkey.ToURLSegment();
|
|
369
|
+
// Browser-style dedup: if this exact record is already open in a tab,
|
|
370
|
+
// focus that tab instead of opening a second copy
|
|
371
|
+
if (tabsMode) {
|
|
372
|
+
const existing = this.findOpenRecordTab(entityName, recordId);
|
|
373
|
+
if (existing) {
|
|
374
|
+
// RE-CAPTURE the origin: re-opening means "I came from somewhere
|
|
375
|
+
// NEW" — the crumb must point at where the user is standing for
|
|
376
|
+
// THIS open, not wherever they were when the tab was first created
|
|
377
|
+
// (possibly days ago, possibly under an older origin schema).
|
|
378
|
+
this.refreshSourceContext(existing.id, options);
|
|
379
|
+
// The activation assert applies to RE-opens too — the same-click
|
|
380
|
+
// stale-stomp that motivated it for fresh opens is equally possible
|
|
381
|
+
// here, and without it the symptom is maddening: opening a record
|
|
382
|
+
// works the FIRST time and silently fails every time after.
|
|
383
|
+
this.workspaceManager.SetActiveTab(existing.id);
|
|
384
|
+
this.assertRecordActivation(existing.id);
|
|
385
|
+
return existing.id;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
let forceNew = tabsMode || this.shouldForceNewTab(options);
|
|
359
389
|
const request = {
|
|
360
390
|
ApplicationId: appId,
|
|
361
391
|
Title: `${entityName} - ${recordId}`,
|
|
362
392
|
Configuration: {
|
|
363
|
-
resourceType:
|
|
393
|
+
resourceType: RECORDS_RESOURCE_TYPE,
|
|
364
394
|
Entity: entityName, // Must use 'Entity' (capital E) - expected by record-resource.component
|
|
365
|
-
recordId: recordId // Also needed in Configuration for tab-container.component to populate ResourceRecordID
|
|
395
|
+
recordId: recordId, // Also needed in Configuration for tab-container.component to populate ResourceRecordID
|
|
396
|
+
...this.resolveSourceContext(options)
|
|
366
397
|
},
|
|
367
398
|
ResourceRecordId: recordId,
|
|
368
|
-
IsPinned: options?.pinTab || false
|
|
399
|
+
IsPinned: options?.pinTab || false,
|
|
400
|
+
// Records style: opening a record must not pin the nav tab (see
|
|
401
|
+
// TabRequest.PreservePinState) — a pinned nav tab forces the main tab
|
|
402
|
+
// bar visible on every nav page.
|
|
403
|
+
PreservePinState: tabsMode
|
|
369
404
|
};
|
|
370
405
|
// Handle transition from single-resource mode
|
|
371
406
|
forceNew = this.handleSingleResourceModeTransition(forceNew, request);
|
|
@@ -376,8 +411,269 @@ export class NavigationService {
|
|
|
376
411
|
else {
|
|
377
412
|
tabId = this.workspaceManager.OpenTab(request, appColor);
|
|
378
413
|
}
|
|
414
|
+
if (tabsMode) {
|
|
415
|
+
this.assertRecordActivation(tabId);
|
|
416
|
+
}
|
|
379
417
|
return tabId;
|
|
380
418
|
}
|
|
419
|
+
/**
|
|
420
|
+
* Opening a record means the record WINS the activation race — assert it
|
|
421
|
+
* after the synchronous open flow settles. Surfaces that both emit a
|
|
422
|
+
* record-open AND run their own internal navigation in the same click
|
|
423
|
+
* (e.g. a grid row's select handler updating query params from a config
|
|
424
|
+
* snapshot captured before the open) can otherwise stomp the activation
|
|
425
|
+
* with stale state, leaving the tab open but never shown. Skipped when the
|
|
426
|
+
* tab was closed in the meantime.
|
|
427
|
+
*/
|
|
428
|
+
assertRecordActivation(tabId) {
|
|
429
|
+
setTimeout(() => {
|
|
430
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
431
|
+
if (config?.tabs.some(t => t.id === tabId) && config.activeTabId !== tabId) {
|
|
432
|
+
this.workspaceManager.SetActiveTab(tabId);
|
|
433
|
+
}
|
|
434
|
+
}, 0);
|
|
435
|
+
}
|
|
436
|
+
/** Find an already-open tab showing this exact record (records-style dedup) */
|
|
437
|
+
findOpenRecordTab(entityName, recordId) {
|
|
438
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
439
|
+
const match = config?.tabs.find(t => t.resourceRecordId === recordId &&
|
|
440
|
+
t.configuration?.['resourceType'] === RECORDS_RESOURCE_TYPE &&
|
|
441
|
+
t.configuration?.['Entity'] === entityName);
|
|
442
|
+
return match ? { id: match.id } : null;
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* Overwrite a record tab's origin with a freshly-resolved one (the dedup
|
|
446
|
+
* re-open path). All source keys are explicitly cleared first — a shallow
|
|
447
|
+
* config merge would otherwise let fields from the OLD origin shape leak
|
|
448
|
+
* through (e.g. a parent-record origin's sourceRecordEntity surviving a
|
|
449
|
+
* re-open from a nav page, or pre-schema captures keeping stale labels).
|
|
450
|
+
*/
|
|
451
|
+
refreshSourceContext(tabId, options) {
|
|
452
|
+
this.workspaceManager.UpdateTabConfiguration(tabId, {
|
|
453
|
+
sourceAppId: undefined,
|
|
454
|
+
sourceAppName: undefined,
|
|
455
|
+
sourceNavLabel: undefined,
|
|
456
|
+
sourceNavItemName: undefined,
|
|
457
|
+
sourceTabId: undefined,
|
|
458
|
+
sourceLabel: undefined,
|
|
459
|
+
sourceQueryParams: undefined,
|
|
460
|
+
sourceRecordEntity: undefined,
|
|
461
|
+
sourceRecordId: undefined,
|
|
462
|
+
...this.resolveSourceContext(options)
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Resolve the source context to stamp into a record tab's configuration.
|
|
467
|
+
* Callers that are workspace tabs omit options.recordSource and get the
|
|
468
|
+
* active app/tab snapshot; overlay surfaces pass their own context;
|
|
469
|
+
* 'none' means no origin (no crumb).
|
|
470
|
+
*/
|
|
471
|
+
resolveSourceContext(options) {
|
|
472
|
+
if (options?.recordSource === 'none') {
|
|
473
|
+
return {};
|
|
474
|
+
}
|
|
475
|
+
if (options?.recordSource) {
|
|
476
|
+
const src = options.recordSource;
|
|
477
|
+
const context = {};
|
|
478
|
+
if (src.sourceAppId)
|
|
479
|
+
context['sourceAppId'] = src.sourceAppId;
|
|
480
|
+
if (src.sourceAppName)
|
|
481
|
+
context['sourceAppName'] = src.sourceAppName;
|
|
482
|
+
if (src.sourceNavLabel)
|
|
483
|
+
context['sourceNavLabel'] = src.sourceNavLabel;
|
|
484
|
+
if (src.sourceTabId)
|
|
485
|
+
context['sourceTabId'] = src.sourceTabId;
|
|
486
|
+
if (src.sourceLabel)
|
|
487
|
+
context['sourceLabel'] = src.sourceLabel;
|
|
488
|
+
if (src.sourceQueryParams && Object.keys(src.sourceQueryParams).length > 0) {
|
|
489
|
+
context['sourceQueryParams'] = src.sourceQueryParams;
|
|
490
|
+
}
|
|
491
|
+
return context;
|
|
492
|
+
}
|
|
493
|
+
return this.captureSourceContext();
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Snapshot of where the user was when the record was
|
|
497
|
+
* opened — the source app and (when the active tab belongs to that app and
|
|
498
|
+
* isn't itself a record) the nav page they were on. Rendered by the
|
|
499
|
+
* breadcrumb strip as "App › Page › Record".
|
|
500
|
+
*/
|
|
501
|
+
captureSourceContext() {
|
|
502
|
+
const activeApp = this.appManager.GetActiveApp();
|
|
503
|
+
if (!activeApp) {
|
|
504
|
+
return {};
|
|
505
|
+
}
|
|
506
|
+
const context = {
|
|
507
|
+
sourceAppId: activeApp.ID,
|
|
508
|
+
sourceAppName: activeApp.Name
|
|
509
|
+
};
|
|
510
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
511
|
+
const activeTab = config?.tabs.find(t => t.id === config.activeTabId);
|
|
512
|
+
if (!activeTab || !UUIDsEqual(activeTab.applicationId, activeApp.ID) || !activeTab.title) {
|
|
513
|
+
return context;
|
|
514
|
+
}
|
|
515
|
+
if (IsRecordsTabConfiguration(activeTab.configuration)) {
|
|
516
|
+
// Opened from INSIDE another record → the crumb points at the PARENT
|
|
517
|
+
// record (Matt's record→record design call). Identity, not just the
|
|
518
|
+
// tab id, so the return survives tab closure (re-open via dedup).
|
|
519
|
+
const parentEntity = activeTab.configuration?.['Entity'];
|
|
520
|
+
const parentRecordId = activeTab.resourceRecordId || activeTab.configuration?.['recordId'];
|
|
521
|
+
if (typeof parentEntity === 'string' && typeof parentRecordId === 'string' && parentRecordId) {
|
|
522
|
+
context['sourceTabId'] = activeTab.id;
|
|
523
|
+
context['sourceLabel'] = activeTab.title;
|
|
524
|
+
context['sourceRecordEntity'] = parentEntity;
|
|
525
|
+
context['sourceRecordId'] = parentRecordId;
|
|
526
|
+
}
|
|
527
|
+
return context;
|
|
528
|
+
}
|
|
529
|
+
context['sourceNavLabel'] = activeTab.title;
|
|
530
|
+
// Canonical nav identity alongside the display title: titles mutate
|
|
531
|
+
// (Data Explorer renames its tab to the drilled entity), so the return
|
|
532
|
+
// path verifies/resolves by navItemName and shows the label.
|
|
533
|
+
const navItemName = activeTab.configuration?.['navItemName'];
|
|
534
|
+
if (typeof navItemName === 'string' && navItemName.length > 0) {
|
|
535
|
+
context['sourceNavItemName'] = navItemName;
|
|
536
|
+
}
|
|
537
|
+
// The exact tab, not just its label — the origin crumb's return path
|
|
538
|
+
// prefers reactivating this tab (state intact) over re-resolving the
|
|
539
|
+
// nav item by label.
|
|
540
|
+
context['sourceTabId'] = activeTab.id;
|
|
541
|
+
// Snapshot the tab's query params too: dashboards keep their DRILL-IN
|
|
542
|
+
// state there (e.g. Data Explorer's entity section), and it can drift
|
|
543
|
+
// or be stomped between open and return. The crumb restores the page
|
|
544
|
+
// AS IT WAS, not just the tab.
|
|
545
|
+
const queryParams = activeTab.configuration?.['queryParams'];
|
|
546
|
+
if (queryParams && typeof queryParams === 'object' && Object.keys(queryParams).length > 0) {
|
|
547
|
+
context['sourceQueryParams'] = { ...queryParams };
|
|
548
|
+
}
|
|
549
|
+
return context;
|
|
550
|
+
}
|
|
551
|
+
/**
|
|
552
|
+
* Switch to an app's LANDING page — its default nav item in its BASE
|
|
553
|
+
* state (drifted section params cleared, reset force-delivered so the
|
|
554
|
+
* dashboard actually returns to its landing even if its state diverged).
|
|
555
|
+
* The origin crumb's APP segment uses this: classic breadcrumb semantics,
|
|
556
|
+
* each level goes to that level's home.
|
|
557
|
+
*/
|
|
558
|
+
async SwitchToAppHome(appId) {
|
|
559
|
+
const app = this.appManager.GetAllApps().find(a => UUIDsEqual(a.ID, appId));
|
|
560
|
+
if (!app) {
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
const navItems = await app.GetNavItems();
|
|
564
|
+
const home = navItems.find(i => i.isDefault) ?? navItems[0];
|
|
565
|
+
await this.SwitchToApp(appId, home?.Label);
|
|
566
|
+
const tabId = this.workspaceManager.GetActiveTabId();
|
|
567
|
+
if (!tabId) {
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
const tab = this.workspaceManager.GetTab(tabId);
|
|
571
|
+
const current = (tab?.configuration?.['queryParams'] || {});
|
|
572
|
+
const clear = {};
|
|
573
|
+
for (const key of Object.keys(current)) {
|
|
574
|
+
clear[key] = null;
|
|
575
|
+
}
|
|
576
|
+
if (Object.keys(clear).length > 0) {
|
|
577
|
+
this.applyQueryParamsToTab(tabId, clear);
|
|
578
|
+
}
|
|
579
|
+
this.NotifyQueryParamsChanged(tabId, {}, true);
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* Take the user back to where they were when a record was opened (the
|
|
583
|
+
* origin crumb's click path). Fidelity ladder:
|
|
584
|
+
* 1. The exact source tab, if it's still open — reactivate it with all its
|
|
585
|
+
* state (query params, scroll, cached component) intact.
|
|
586
|
+
* 2. Otherwise re-resolve by app + nav label via SwitchToApp, which reuses
|
|
587
|
+
* a matching open tab or opens the nav item fresh.
|
|
588
|
+
* No-ops when the context has no sourceAppId (capture was best-effort).
|
|
589
|
+
*/
|
|
590
|
+
async ReturnToRecordSource(origin) {
|
|
591
|
+
// Parent-RECORD origin (opened from inside another record): activate the
|
|
592
|
+
// parent's tab while it still shows that record, else RE-OPEN the parent
|
|
593
|
+
// — OpenEntityRecord's dedup makes the re-open land on (or recreate)
|
|
594
|
+
// exactly that record. No app flip either way: record tabs live in the
|
|
595
|
+
// records region regardless of app.
|
|
596
|
+
if (origin.sourceRecordEntity && origin.sourceRecordId) {
|
|
597
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
598
|
+
const sourceTab = origin.sourceTabId ? config?.tabs.find(t => t.id === origin.sourceTabId) : undefined;
|
|
599
|
+
const stillParent = !!sourceTab &&
|
|
600
|
+
IsRecordsTabConfiguration(sourceTab.configuration) &&
|
|
601
|
+
sourceTab.configuration?.['Entity'] === origin.sourceRecordEntity &&
|
|
602
|
+
sourceTab.resourceRecordId === origin.sourceRecordId;
|
|
603
|
+
if (sourceTab && stillParent) {
|
|
604
|
+
this.workspaceManager.SetActiveTab(sourceTab.id);
|
|
605
|
+
return;
|
|
606
|
+
}
|
|
607
|
+
const parentKey = new CompositeKey();
|
|
608
|
+
parentKey.SimpleLoadFromURLSegment(origin.sourceRecordId);
|
|
609
|
+
this.OpenEntityRecord(origin.sourceRecordEntity, parentKey);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (!origin.sourceAppId) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
if (origin.sourceTabId) {
|
|
616
|
+
const config = this.workspaceManager.GetConfiguration();
|
|
617
|
+
const sourceTab = config?.tabs.find(t => t.id === origin.sourceTabId);
|
|
618
|
+
// Tab ids are NOT stable identities for nav pages: the temp-tab
|
|
619
|
+
// replacement model (WorkspaceStateManager.OpenTab) reuses the same
|
|
620
|
+
// tab id when the user navigates elsewhere — the captured id can now
|
|
621
|
+
// hold a DIFFERENT page in a DIFFERENT app. Trust it only while the
|
|
622
|
+
// tab still shows the captured page; otherwise fall through to
|
|
623
|
+
// re-resolving by app + nav item below. Prefer navItemName for the
|
|
624
|
+
// page check — TITLES MUTATE (Data Explorer renames its tab to the
|
|
625
|
+
// drilled entity), and a mutated title must not disqualify the tab.
|
|
626
|
+
const tabNavItemName = sourceTab?.configuration?.['navItemName'];
|
|
627
|
+
const samePage = origin.sourceNavItemName && typeof tabNavItemName === 'string'
|
|
628
|
+
? tabNavItemName === origin.sourceNavItemName
|
|
629
|
+
: (!origin.sourceNavLabel || sourceTab?.title === origin.sourceNavLabel);
|
|
630
|
+
const stillSourcePage = !!sourceTab &&
|
|
631
|
+
(!origin.sourceAppId || UUIDsEqual(sourceTab.applicationId, origin.sourceAppId)) &&
|
|
632
|
+
samePage;
|
|
633
|
+
if (sourceTab && stillSourcePage) {
|
|
634
|
+
// Restore the tab's query params to the capture-time snapshot BEFORE
|
|
635
|
+
// activating, so a cached resource component reattaches against the
|
|
636
|
+
// right config (see SwitchToApp's queryParams doc). REPLACE
|
|
637
|
+
// semantics: null out params the tab has gained since capture, then
|
|
638
|
+
// lay the snapshot over — "back" means the page as it was, and
|
|
639
|
+
// drill-in state (e.g. Data Explorer's entity section) lives here.
|
|
640
|
+
const currentParams = (sourceTab.configuration?.['queryParams'] || {});
|
|
641
|
+
const restore = {};
|
|
642
|
+
for (const key of Object.keys(currentParams)) {
|
|
643
|
+
restore[key] = null;
|
|
644
|
+
}
|
|
645
|
+
Object.assign(restore, origin.sourceQueryParams ?? {});
|
|
646
|
+
if (Object.keys(restore).length > 0) {
|
|
647
|
+
this.applyQueryParamsToTab(sourceTab.id, restore);
|
|
648
|
+
}
|
|
649
|
+
// FORCE delivery: the component's state may have drifted while its
|
|
650
|
+
// last-delivered params already equal the snapshot — the reactive
|
|
651
|
+
// path would drop the restore as a duplicate (crumb lands on the
|
|
652
|
+
// dashboard's home instead of the captured section).
|
|
653
|
+
if (origin.sourceQueryParams && Object.keys(origin.sourceQueryParams).length > 0) {
|
|
654
|
+
this.NotifyQueryParamsChanged(sourceTab.id, origin.sourceQueryParams, true);
|
|
655
|
+
}
|
|
656
|
+
// Set the app context first so the shell's nav renders the right
|
|
657
|
+
// app the moment the tab activates (activation alone would also
|
|
658
|
+
// flip it, but explicit sequencing avoids a visible two-step).
|
|
659
|
+
await this.appManager.SetActiveApp(sourceTab.applicationId);
|
|
660
|
+
this.workspaceManager.SetActiveTab(sourceTab.id);
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
// Re-resolve by the CANONICAL nav-item name when we have it — the label
|
|
665
|
+
// is a display title that may have mutated into something SwitchToApp's
|
|
666
|
+
// nav-item lookup can't match (e.g. a drilled entity name).
|
|
667
|
+
await this.SwitchToApp(origin.sourceAppId, origin.sourceNavItemName ?? origin.sourceNavLabel, origin.sourceQueryParams);
|
|
668
|
+
// Same drift-proofing as the exact-tab path: force the delivery so the
|
|
669
|
+
// restore can't be deduped away.
|
|
670
|
+
if (origin.sourceQueryParams && Object.keys(origin.sourceQueryParams).length > 0) {
|
|
671
|
+
const targetTabId = this.workspaceManager.GetActiveTabId();
|
|
672
|
+
if (targetTabId) {
|
|
673
|
+
this.NotifyQueryParamsChanged(targetTabId, origin.sourceQueryParams, true);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
381
677
|
/**
|
|
382
678
|
* Open a view
|
|
383
679
|
* Uses Home app if available, otherwise falls back to active app or system app
|
|
@@ -434,34 +730,6 @@ export class NavigationService {
|
|
|
434
730
|
return this.workspaceManager.OpenTab(request, appColor);
|
|
435
731
|
}
|
|
436
732
|
}
|
|
437
|
-
/**
|
|
438
|
-
* Open a report
|
|
439
|
-
* Uses Home app if available, otherwise falls back to active app or system app
|
|
440
|
-
*/
|
|
441
|
-
OpenReport(reportId, reportName, options) {
|
|
442
|
-
const appId = this.getDefaultApplicationId();
|
|
443
|
-
const appColor = this.getDefaultAppColor();
|
|
444
|
-
let forceNew = this.shouldForceNewTab(options);
|
|
445
|
-
const request = {
|
|
446
|
-
ApplicationId: appId,
|
|
447
|
-
Title: reportName,
|
|
448
|
-
Configuration: {
|
|
449
|
-
resourceType: 'Reports',
|
|
450
|
-
reportId,
|
|
451
|
-
recordId: reportId // Also needed in Configuration for tab-container.component to populate ResourceRecordID
|
|
452
|
-
},
|
|
453
|
-
ResourceRecordId: reportId,
|
|
454
|
-
IsPinned: options?.pinTab || false
|
|
455
|
-
};
|
|
456
|
-
// Handle transition from single-resource mode
|
|
457
|
-
forceNew = this.handleSingleResourceModeTransition(forceNew, request);
|
|
458
|
-
if (forceNew) {
|
|
459
|
-
return this.workspaceManager.OpenTabForced(request, appColor);
|
|
460
|
-
}
|
|
461
|
-
else {
|
|
462
|
-
return this.workspaceManager.OpenTab(request, appColor);
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
733
|
/**
|
|
466
734
|
* Open an artifact
|
|
467
735
|
* Artifacts are versioned content containers (reports, dashboards, UI components, etc.)
|
|
@@ -558,30 +826,45 @@ export class NavigationService {
|
|
|
558
826
|
* @param options Navigation options including optional newRecordValues for pre-populating fields
|
|
559
827
|
*/
|
|
560
828
|
OpenNewEntityRecord(entityName, options) {
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
829
|
+
// Records style: new-record tabs are RECORD tabs and get the same
|
|
830
|
+
// treatment as OpenEntityRecord — assigned to the ACTIVE app, always a
|
|
831
|
+
// new tab, nav pins preserved, origin captured. Without the forceNew,
|
|
832
|
+
// the non-forced OpenTab consumed the user's nav page as the
|
|
833
|
+
// replaceable temp tab and CONVERTED it into a records-region tab —
|
|
834
|
+
// silently destroying the page they were on.
|
|
835
|
+
const tabsMode = IsRecordTabsStyle();
|
|
836
|
+
const activeApp = this.appManager.GetActiveApp();
|
|
837
|
+
const appId = tabsMode && activeApp ? activeApp.ID : this.getDefaultApplicationId();
|
|
838
|
+
const appColor = tabsMode && activeApp ? activeApp.GetColor() : this.getDefaultAppColor();
|
|
839
|
+
let forceNew = tabsMode || this.shouldForceNewTab(options);
|
|
564
840
|
const request = {
|
|
565
841
|
ApplicationId: appId,
|
|
566
842
|
Title: `New ${entityName}`,
|
|
567
843
|
Configuration: {
|
|
568
|
-
resourceType:
|
|
844
|
+
resourceType: RECORDS_RESOURCE_TYPE,
|
|
569
845
|
Entity: entityName, // Must use 'Entity' (capital E) - expected by record-resource.component
|
|
570
846
|
recordId: '', // Empty recordId indicates new record
|
|
571
847
|
isNew: true, // Flag to indicate this is a new record
|
|
572
|
-
NewRecordValues: options?.newRecordValues // Pass through initial values if provided
|
|
848
|
+
NewRecordValues: options?.newRecordValues, // Pass through initial values if provided
|
|
849
|
+
...this.resolveSourceContext(options)
|
|
573
850
|
},
|
|
574
851
|
ResourceRecordId: '', // Empty for new records
|
|
575
|
-
IsPinned: options?.pinTab || false
|
|
852
|
+
IsPinned: options?.pinTab || false,
|
|
853
|
+
PreservePinState: tabsMode
|
|
576
854
|
};
|
|
577
855
|
// Handle transition from single-resource mode
|
|
578
856
|
forceNew = this.handleSingleResourceModeTransition(forceNew, request);
|
|
857
|
+
let tabId;
|
|
579
858
|
if (forceNew) {
|
|
580
|
-
|
|
859
|
+
tabId = this.workspaceManager.OpenTabForced(request, appColor);
|
|
581
860
|
}
|
|
582
861
|
else {
|
|
583
|
-
|
|
862
|
+
tabId = this.workspaceManager.OpenTab(request, appColor);
|
|
584
863
|
}
|
|
864
|
+
if (tabsMode) {
|
|
865
|
+
this.assertRecordActivation(tabId);
|
|
866
|
+
}
|
|
867
|
+
return tabId;
|
|
585
868
|
}
|
|
586
869
|
/**
|
|
587
870
|
* Open a universal search results tab for the given query.
|
|
@@ -680,15 +963,38 @@ export class NavigationService {
|
|
|
680
963
|
if (!app) {
|
|
681
964
|
return;
|
|
682
965
|
}
|
|
683
|
-
|
|
966
|
+
// Records style: record tabs are ASSIGNED to apps but live in their own
|
|
967
|
+
// region — switching to an app must surface its NAV pages, so records
|
|
968
|
+
// don't count here. Without this filter, the active record tab (which
|
|
969
|
+
// belongs to the target app) satisfied the "already on this app's tab"
|
|
970
|
+
// check and the whole switch silently no-oped — the dead origin-crumb
|
|
971
|
+
// click when the crumb only knew the app. Records DOCKED to the
|
|
972
|
+
// workspace are excluded too (identity check, not region membership):
|
|
973
|
+
// switch-to-app must land on a page, never a record. Classic style
|
|
974
|
+
// keeps records as ordinary tabs.
|
|
975
|
+
const appTabs = IsRecordTabsStyle()
|
|
976
|
+
? this.workspaceManager.GetAppTabs(appId).filter(t => !IsRecordsTabConfiguration(t.configuration))
|
|
977
|
+
: this.workspaceManager.GetAppTabs(appId);
|
|
684
978
|
// If a specific nav item is requested
|
|
685
979
|
if (navItemName) {
|
|
686
980
|
const navItems = await app.GetNavItems();
|
|
687
981
|
const navItem = navItems.find(item => item.Label === navItemName);
|
|
688
982
|
if (navItem) {
|
|
689
|
-
// Check if there's already a tab for this nav item
|
|
690
|
-
|
|
691
|
-
|
|
983
|
+
// Check if there's already a tab for this nav item. Identity check
|
|
984
|
+
// uses configuration.navItemName when the tab carries it — TITLES
|
|
985
|
+
// LIE: same-driver nav switches reuse the live tab and can leave a
|
|
986
|
+
// stale title behind (e.g. Data Explorer's tab titled "Data" while
|
|
987
|
+
// its content and navItemName say "Dashboards"). Matching that
|
|
988
|
+
// chimera by title activated the WRONG page instead of reopening
|
|
989
|
+
// the requested nav item.
|
|
990
|
+
const existingTab = appTabs.find(tab => {
|
|
991
|
+
const tabNav = tab.configuration?.['navItemName'];
|
|
992
|
+
if (typeof tabNav === 'string' && tabNav.length > 0) {
|
|
993
|
+
return tabNav === navItem.Label;
|
|
994
|
+
}
|
|
995
|
+
return tab.title === navItem.Label ||
|
|
996
|
+
(tab.configuration?.['route'] === navItem.Route && !!navItem.Route);
|
|
997
|
+
});
|
|
692
998
|
if (existingTab) {
|
|
693
999
|
// Switch to existing tab
|
|
694
1000
|
this.workspaceManager.SetActiveTab(existingTab.id);
|
|
@@ -795,8 +1101,8 @@ export class NavigationService {
|
|
|
795
1101
|
* Called by the shell when back/forward navigation changes query params on the active tab.
|
|
796
1102
|
* The notification includes the tab ID so only the component in that tab reacts.
|
|
797
1103
|
*/
|
|
798
|
-
NotifyQueryParamsChanged(tabId, params) {
|
|
799
|
-
this.queryParamChanged$.next({ TabId: tabId, Params: params });
|
|
1104
|
+
NotifyQueryParamsChanged(tabId, params, force = false) {
|
|
1105
|
+
this.queryParamChanged$.next({ TabId: tabId, Params: params, Force: force });
|
|
800
1106
|
}
|
|
801
1107
|
/**
|
|
802
1108
|
* Reactively observe the query params for a specific tab.
|