@memberjunction/ng-shared 5.51.0 → 6.1.0-edge.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.
Files changed (41) hide show
  1. package/LICENSE +7 -0
  2. package/dist/lib/base-dashboard.d.ts +16 -0
  3. package/dist/lib/base-dashboard.d.ts.map +1 -1
  4. package/dist/lib/base-dashboard.js +36 -5
  5. package/dist/lib/base-dashboard.js.map +1 -1
  6. package/dist/lib/base-resource-component.d.ts +33 -3
  7. package/dist/lib/base-resource-component.d.ts.map +1 -1
  8. package/dist/lib/base-resource-component.js +65 -11
  9. package/dist/lib/base-resource-component.js.map +1 -1
  10. package/dist/lib/explorer-breakpoint.service.d.ts +39 -0
  11. package/dist/lib/explorer-breakpoint.service.d.ts.map +1 -0
  12. package/dist/lib/explorer-breakpoint.service.js +57 -0
  13. package/dist/lib/explorer-breakpoint.service.js.map +1 -0
  14. package/dist/lib/navigation.interfaces.d.ts +13 -0
  15. package/dist/lib/navigation.interfaces.d.ts.map +1 -1
  16. package/dist/lib/navigation.interfaces.js.map +1 -1
  17. package/dist/lib/navigation.service.d.ts +70 -3
  18. package/dist/lib/navigation.service.d.ts.map +1 -1
  19. package/dist/lib/navigation.service.js +356 -22
  20. package/dist/lib/navigation.service.js.map +1 -1
  21. package/dist/lib/record-open-style.d.ts +88 -0
  22. package/dist/lib/record-open-style.d.ts.map +1 -0
  23. package/dist/lib/record-open-style.js +124 -0
  24. package/dist/lib/record-open-style.js.map +1 -0
  25. package/dist/lib/record-tab-icon.d.ts +10 -0
  26. package/dist/lib/record-tab-icon.d.ts.map +1 -0
  27. package/dist/lib/record-tab-icon.js +15 -0
  28. package/dist/lib/record-tab-icon.js.map +1 -0
  29. package/dist/lib/safe-detect-changes.d.ts +18 -0
  30. package/dist/lib/safe-detect-changes.d.ts.map +1 -0
  31. package/dist/lib/safe-detect-changes.js +29 -0
  32. package/dist/lib/safe-detect-changes.js.map +1 -0
  33. package/dist/lib/shared.service.d.ts +5 -0
  34. package/dist/lib/shared.service.d.ts.map +1 -1
  35. package/dist/lib/shared.service.js +16 -10
  36. package/dist/lib/shared.service.js.map +1 -1
  37. package/dist/public-api.d.ts +4 -0
  38. package/dist/public-api.d.ts.map +1 -1
  39. package/dist/public-api.js +4 -0
  40. package/dist/public-api.js.map +1 -1
  41. package/package.json +19 -17
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.interfaces.js","sourceRoot":"","sources":["../../src/lib/navigation.interfaces.ts"],"names":[],"mappings":"","sourcesContent":["/**\n * Options for controlling navigation behavior\n */\nexport interface NavigationOptions {\n /**\n * Force creation of a new tab instead of replacing temporary tabs\n * If not specified, the NavigationService will use global shift-key detection\n */\n forceNewTab?: boolean;\n\n /**\n * Create the tab as a pinned (permanent) tab\n */\n pinTab?: boolean;\n\n /**\n * Focus the tab after creation (default: true)\n */\n focusTab?: boolean;\n\n /**\n * Replace the currently active tab instead of creating a new one\n */\n replaceActive?: boolean;\n\n /**\n * Initial values to populate when creating a new record.\n * Should be a plain object with field names as keys.\n */\n newRecordValues?: Record<string, unknown>;\n\n /**\n * URL query parameters to set on the tab after navigation.\n * Use null values to remove a query param.\n */\n queryParams?: Record<string, string | null>;\n}\n"]}
1
+ {"version":3,"file":"navigation.interfaces.js","sourceRoot":"","sources":["../../src/lib/navigation.interfaces.ts"],"names":[],"mappings":"","sourcesContent":["import { RecordSourceContext } from './record-open-style';\n\n/**\n * Options for controlling navigation behavior\n */\nexport interface NavigationOptions {\n /**\n * Origin override for record opens (records-style origin crumb).\n * - omitted: NavigationService snapshots the active app/tab — correct for\n * in-tab surfaces, WRONG for overlays/dialogs (it blames the page behind\n * them).\n * - a RecordSourceContext: used verbatim. Overlay surfaces set at least\n * `sourceLabel` ('Conversation', 'Search', ...), plus app/nav-label when\n * there is a real place to return to.\n * - 'none': the open has no meaningful origin (e.g. recreating a closed\n * tab from browser history) — no crumb is rendered.\n */\n recordSource?: RecordSourceContext | 'none';\n\n /**\n * Force creation of a new tab instead of replacing temporary tabs\n * If not specified, the NavigationService will use global shift-key detection\n */\n forceNewTab?: boolean;\n\n /**\n * Create the tab as a pinned (permanent) tab\n */\n pinTab?: boolean;\n\n /**\n * Focus the tab after creation (default: true)\n */\n focusTab?: boolean;\n\n /**\n * Replace the currently active tab instead of creating a new one\n */\n replaceActive?: boolean;\n\n /**\n * Initial values to populate when creating a new record.\n * Should be a plain object with field names as keys.\n */\n newRecordValues?: Record<string, unknown>;\n\n /**\n * URL query parameters to set on the tab after navigation.\n * Use null values to remove a query param.\n */\n queryParams?: Record<string, string | null>;\n}\n"]}
@@ -1,6 +1,7 @@
1
1
  import { OnDestroy } from '@angular/core';
2
2
  import { WorkspaceStateManager, NavItem, ApplicationManager } from '@memberjunction/ng-base-application';
3
3
  import { NavigationOptions } from './navigation.interfaces';
4
+ import { RecordSourceContext } from './record-open-style';
4
5
  import { CompositeKey } from '@memberjunction/core';
5
6
  import { BehaviorSubject, Subject, Observable } from 'rxjs';
6
7
  import type { AppContextSnapshot } from '@memberjunction/ai-core-plus';
@@ -14,6 +15,13 @@ import * as i0 from "@angular/core";
14
15
  export interface QueryParamChangeEvent {
15
16
  TabId: string;
16
17
  Params: Record<string, string>;
18
+ /**
19
+ * Bypass the receiving component's duplicate-delivery guard. Set by
20
+ * explicit restore navigations (e.g. the origin crumb): the component's
21
+ * state may have DRIFTED from the last-delivered params, in which case a
22
+ * restore to those same params would otherwise be dropped as a duplicate.
23
+ */
24
+ Force?: boolean;
17
25
  }
18
26
  /**
19
27
  * Event emitted when a resource component reports its agent context or tools.
@@ -226,10 +234,69 @@ export declare class NavigationService implements OnDestroy {
226
234
  */
227
235
  OpenNavItem(appId: string, navItem: NavItem, appColor: string, options?: NavigationOptions): string;
228
236
  /**
229
- * Open an entity record view
230
- * Uses Home app if available, otherwise falls back to active app or system app
237
+ * Open an entity record view. Behavior forks on the deployment's
238
+ * record-open style (see record-open-style.ts):
239
+ * - 'records' (default): the record ALWAYS opens as its own tab, assigned
240
+ * to the app the user is standing in (no Home reassignment, no nav flip).
241
+ * Re-opening a record that's already open focuses its existing tab,
242
+ * browser-style. The shell's Records pill + strip are the visible UI.
243
+ * - 'classic': the original Home-first assignment and tab semantics.
244
+ * Both styles capture the source-app trail into the tab configuration for
245
+ * context UI and diagnostics.
231
246
  */
232
247
  OpenEntityRecord(entityName: string, recordPkey: CompositeKey, options?: NavigationOptions): string;
248
+ /**
249
+ * Opening a record means the record WINS the activation race — assert it
250
+ * after the synchronous open flow settles. Surfaces that both emit a
251
+ * record-open AND run their own internal navigation in the same click
252
+ * (e.g. a grid row's select handler updating query params from a config
253
+ * snapshot captured before the open) can otherwise stomp the activation
254
+ * with stale state, leaving the tab open but never shown. Skipped when the
255
+ * tab was closed in the meantime.
256
+ */
257
+ private assertRecordActivation;
258
+ /** Find an already-open tab showing this exact record (records-style dedup) */
259
+ private findOpenRecordTab;
260
+ /**
261
+ * Overwrite a record tab's origin with a freshly-resolved one (the dedup
262
+ * re-open path). All source keys are explicitly cleared first — a shallow
263
+ * config merge would otherwise let fields from the OLD origin shape leak
264
+ * through (e.g. a parent-record origin's sourceRecordEntity surviving a
265
+ * re-open from a nav page, or pre-schema captures keeping stale labels).
266
+ */
267
+ private refreshSourceContext;
268
+ /**
269
+ * Resolve the source context to stamp into a record tab's configuration.
270
+ * Callers that are workspace tabs omit options.recordSource and get the
271
+ * active app/tab snapshot; overlay surfaces pass their own context;
272
+ * 'none' means no origin (no crumb).
273
+ */
274
+ private resolveSourceContext;
275
+ /**
276
+ * Snapshot of where the user was when the record was
277
+ * opened — the source app and (when the active tab belongs to that app and
278
+ * isn't itself a record) the nav page they were on. Rendered by the
279
+ * breadcrumb strip as "App › Page › Record".
280
+ */
281
+ private captureSourceContext;
282
+ /**
283
+ * Switch to an app's LANDING page — its default nav item in its BASE
284
+ * state (drifted section params cleared, reset force-delivered so the
285
+ * dashboard actually returns to its landing even if its state diverged).
286
+ * The origin crumb's APP segment uses this: classic breadcrumb semantics,
287
+ * each level goes to that level's home.
288
+ */
289
+ SwitchToAppHome(appId: string): Promise<void>;
290
+ /**
291
+ * Take the user back to where they were when a record was opened (the
292
+ * origin crumb's click path). Fidelity ladder:
293
+ * 1. The exact source tab, if it's still open — reactivate it with all its
294
+ * state (query params, scroll, cached component) intact.
295
+ * 2. Otherwise re-resolve by app + nav label via SwitchToApp, which reuses
296
+ * a matching open tab or opens the nav item fresh.
297
+ * No-ops when the context has no sourceAppId (capture was best-effort).
298
+ */
299
+ ReturnToRecordSource(origin: RecordSourceContext): Promise<void>;
233
300
  /**
234
301
  * Open a view
235
302
  * Uses Home app if available, otherwise falls back to active app or system app
@@ -337,7 +404,7 @@ export declare class NavigationService implements OnDestroy {
337
404
  * Called by the shell when back/forward navigation changes query params on the active tab.
338
405
  * The notification includes the tab ID so only the component in that tab reacts.
339
406
  */
340
- NotifyQueryParamsChanged(tabId: string, params: Record<string, string>): void;
407
+ NotifyQueryParamsChanged(tabId: string, params: Record<string, string>, force?: boolean): void;
341
408
  /**
342
409
  * Reactively observe the query params for a specific tab.
343
410
  *
@@ -1 +1 @@
1
- {"version":3,"file":"navigation.service.d.ts","sourceRoot":"","sources":["../../src/lib/navigation.service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"navigation.service.d.ts","sourceRoot":"","sources":["../../src/lib/navigation.service.ts"],"names":[],"mappings":"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"}
@@ -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
- * Uses Home app if available, otherwise falls back to active app or system app
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 appId = this.getDefaultApplicationId();
356
- const appColor = this.getDefaultAppColor();
357
- let forceNew = this.shouldForceNewTab(options);
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: 'Records',
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
@@ -558,30 +854,45 @@ export class NavigationService {
558
854
  * @param options Navigation options including optional newRecordValues for pre-populating fields
559
855
  */
560
856
  OpenNewEntityRecord(entityName, options) {
561
- const appId = this.getDefaultApplicationId();
562
- const appColor = this.getDefaultAppColor();
563
- let forceNew = this.shouldForceNewTab(options);
857
+ // Records style: new-record tabs are RECORD tabs and get the same
858
+ // treatment as OpenEntityRecord — assigned to the ACTIVE app, always a
859
+ // new tab, nav pins preserved, origin captured. Without the forceNew,
860
+ // the non-forced OpenTab consumed the user's nav page as the
861
+ // replaceable temp tab and CONVERTED it into a records-region tab —
862
+ // silently destroying the page they were on.
863
+ const tabsMode = IsRecordTabsStyle();
864
+ const activeApp = this.appManager.GetActiveApp();
865
+ const appId = tabsMode && activeApp ? activeApp.ID : this.getDefaultApplicationId();
866
+ const appColor = tabsMode && activeApp ? activeApp.GetColor() : this.getDefaultAppColor();
867
+ let forceNew = tabsMode || this.shouldForceNewTab(options);
564
868
  const request = {
565
869
  ApplicationId: appId,
566
870
  Title: `New ${entityName}`,
567
871
  Configuration: {
568
- resourceType: 'Records',
872
+ resourceType: RECORDS_RESOURCE_TYPE,
569
873
  Entity: entityName, // Must use 'Entity' (capital E) - expected by record-resource.component
570
874
  recordId: '', // Empty recordId indicates new record
571
875
  isNew: true, // Flag to indicate this is a new record
572
- NewRecordValues: options?.newRecordValues // Pass through initial values if provided
876
+ NewRecordValues: options?.newRecordValues, // Pass through initial values if provided
877
+ ...this.resolveSourceContext(options)
573
878
  },
574
879
  ResourceRecordId: '', // Empty for new records
575
- IsPinned: options?.pinTab || false
880
+ IsPinned: options?.pinTab || false,
881
+ PreservePinState: tabsMode
576
882
  };
577
883
  // Handle transition from single-resource mode
578
884
  forceNew = this.handleSingleResourceModeTransition(forceNew, request);
885
+ let tabId;
579
886
  if (forceNew) {
580
- return this.workspaceManager.OpenTabForced(request, appColor);
887
+ tabId = this.workspaceManager.OpenTabForced(request, appColor);
581
888
  }
582
889
  else {
583
- return this.workspaceManager.OpenTab(request, appColor);
890
+ tabId = this.workspaceManager.OpenTab(request, appColor);
891
+ }
892
+ if (tabsMode) {
893
+ this.assertRecordActivation(tabId);
584
894
  }
895
+ return tabId;
585
896
  }
586
897
  /**
587
898
  * Open a universal search results tab for the given query.
@@ -680,15 +991,38 @@ export class NavigationService {
680
991
  if (!app) {
681
992
  return;
682
993
  }
683
- const appTabs = this.workspaceManager.GetAppTabs(appId);
994
+ // Records style: record tabs are ASSIGNED to apps but live in their own
995
+ // region — switching to an app must surface its NAV pages, so records
996
+ // don't count here. Without this filter, the active record tab (which
997
+ // belongs to the target app) satisfied the "already on this app's tab"
998
+ // check and the whole switch silently no-oped — the dead origin-crumb
999
+ // click when the crumb only knew the app. Records DOCKED to the
1000
+ // workspace are excluded too (identity check, not region membership):
1001
+ // switch-to-app must land on a page, never a record. Classic style
1002
+ // keeps records as ordinary tabs.
1003
+ const appTabs = IsRecordTabsStyle()
1004
+ ? this.workspaceManager.GetAppTabs(appId).filter(t => !IsRecordsTabConfiguration(t.configuration))
1005
+ : this.workspaceManager.GetAppTabs(appId);
684
1006
  // If a specific nav item is requested
685
1007
  if (navItemName) {
686
1008
  const navItems = await app.GetNavItems();
687
1009
  const navItem = navItems.find(item => item.Label === navItemName);
688
1010
  if (navItem) {
689
- // Check if there's already a tab for this nav item
690
- const existingTab = appTabs.find(tab => tab.title === navItem.Label ||
691
- (tab.configuration?.['route'] === navItem.Route && navItem.Route));
1011
+ // Check if there's already a tab for this nav item. Identity check
1012
+ // uses configuration.navItemName when the tab carries it — TITLES
1013
+ // LIE: same-driver nav switches reuse the live tab and can leave a
1014
+ // stale title behind (e.g. Data Explorer's tab titled "Data" while
1015
+ // its content and navItemName say "Dashboards"). Matching that
1016
+ // chimera by title activated the WRONG page instead of reopening
1017
+ // the requested nav item.
1018
+ const existingTab = appTabs.find(tab => {
1019
+ const tabNav = tab.configuration?.['navItemName'];
1020
+ if (typeof tabNav === 'string' && tabNav.length > 0) {
1021
+ return tabNav === navItem.Label;
1022
+ }
1023
+ return tab.title === navItem.Label ||
1024
+ (tab.configuration?.['route'] === navItem.Route && !!navItem.Route);
1025
+ });
692
1026
  if (existingTab) {
693
1027
  // Switch to existing tab
694
1028
  this.workspaceManager.SetActiveTab(existingTab.id);
@@ -795,8 +1129,8 @@ export class NavigationService {
795
1129
  * Called by the shell when back/forward navigation changes query params on the active tab.
796
1130
  * The notification includes the tab ID so only the component in that tab reacts.
797
1131
  */
798
- NotifyQueryParamsChanged(tabId, params) {
799
- this.queryParamChanged$.next({ TabId: tabId, Params: params });
1132
+ NotifyQueryParamsChanged(tabId, params, force = false) {
1133
+ this.queryParamChanged$.next({ TabId: tabId, Params: params, Force: force });
800
1134
  }
801
1135
  /**
802
1136
  * Reactively observe the query params for a specific tab.