@memberjunction/ng-explorer-core 6.2.0-edge.4 → 6.2.0-edge.6

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 (76) hide show
  1. package/README.md +7 -2
  2. package/dist/app-routing.module.d.ts.map +1 -1
  3. package/dist/app-routing.module.js +20 -10
  4. package/dist/app-routing.module.js.map +1 -1
  5. package/dist/generated/lazy-feature-config.d.ts +1 -1
  6. package/dist/generated/lazy-feature-config.d.ts.map +1 -1
  7. package/dist/generated/lazy-feature-config.js +6 -3
  8. package/dist/generated/lazy-feature-config.js.map +1 -1
  9. package/dist/lib/oauth/oauth-callback.component.d.ts +2 -0
  10. package/dist/lib/oauth/oauth-callback.component.d.ts.map +1 -1
  11. package/dist/lib/oauth/oauth-callback.component.js +10 -10
  12. package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
  13. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +17 -3
  14. package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
  15. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +211 -13
  16. package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
  17. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +12 -3
  18. package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
  19. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +18 -3
  20. package/dist/lib/resource-wrappers/chat-tasks-resource.component.js.map +1 -1
  21. package/dist/lib/resource-wrappers/record-form-mode.d.ts +44 -0
  22. package/dist/lib/resource-wrappers/record-form-mode.d.ts.map +1 -0
  23. package/dist/lib/resource-wrappers/record-form-mode.js +47 -0
  24. package/dist/lib/resource-wrappers/record-form-mode.js.map +1 -0
  25. package/dist/lib/resource-wrappers/record-resource.component.d.ts +56 -1
  26. package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
  27. package/dist/lib/resource-wrappers/record-resource.component.js +124 -9
  28. package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
  29. package/dist/lib/resource-wrappers/search-results-resource.component.d.ts +3 -3
  30. package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
  31. package/dist/lib/resource-wrappers/search-results-resource.component.js +5 -10
  32. package/dist/lib/resource-wrappers/search-results-resource.component.js.map +1 -1
  33. package/dist/lib/resource-wrappers/standard-form-create.d.ts +4 -0
  34. package/dist/lib/resource-wrappers/standard-form-create.d.ts.map +1 -0
  35. package/dist/lib/resource-wrappers/standard-form-create.js +6 -0
  36. package/dist/lib/resource-wrappers/standard-form-create.js.map +1 -0
  37. package/dist/lib/resource-wrappers/view-resource.component.d.ts +18 -1
  38. package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
  39. package/dist/lib/resource-wrappers/view-resource.component.js +105 -39
  40. package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
  41. package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts +16 -3
  42. package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts.map +1 -1
  43. package/dist/lib/server-connectivity/server-connectivity-banner.component.js +17 -3
  44. package/dist/lib/server-connectivity/server-connectivity-banner.component.js.map +1 -1
  45. package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
  46. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +12 -8
  47. package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
  48. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +6 -0
  49. package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
  50. package/dist/lib/shell/components/tabs/component-cache-manager.js +18 -3
  51. package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
  52. package/dist/lib/shell/components/tabs/tab-container.component.d.ts +36 -0
  53. package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
  54. package/dist/lib/shell/components/tabs/tab-container.component.js +188 -47
  55. package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
  56. package/dist/lib/shell/deep-link-workspace.d.ts +25 -0
  57. package/dist/lib/shell/deep-link-workspace.d.ts.map +1 -0
  58. package/dist/lib/shell/deep-link-workspace.js +49 -0
  59. package/dist/lib/shell/deep-link-workspace.js.map +1 -0
  60. package/dist/lib/shell/shell-chrome-policy.d.ts +6 -0
  61. package/dist/lib/shell/shell-chrome-policy.d.ts.map +1 -1
  62. package/dist/lib/shell/shell-chrome-policy.js +1 -0
  63. package/dist/lib/shell/shell-chrome-policy.js.map +1 -1
  64. package/dist/lib/shell/shell.component.d.ts +35 -0
  65. package/dist/lib/shell/shell.component.d.ts.map +1 -1
  66. package/dist/lib/shell/shell.component.js +348 -78
  67. package/dist/lib/shell/shell.component.js.map +1 -1
  68. package/dist/lib/single-record/single-record.component.d.ts +39 -2
  69. package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
  70. package/dist/lib/single-record/single-record.component.js +99 -12
  71. package/dist/lib/single-record/single-record.component.js.map +1 -1
  72. package/dist/lib/user-notifications/user-notifications.component.d.ts +10 -2
  73. package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
  74. package/dist/lib/user-notifications/user-notifications.component.js +77 -22
  75. package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
  76. package/package.json +57 -57
@@ -1,11 +1,11 @@
1
1
  import { Component, ViewChild, HostListener, inject } from '@angular/core';
2
2
  import { AngularAdapterService } from '@memberjunction/ng-react';
3
- import { NavigationEnd } from '@angular/router';
3
+ import { NavigationEnd, NavigationError, NavigationCancel } from '@angular/router';
4
4
  import { combineLatest, Subject } from 'rxjs';
5
5
  import { filter, takeUntil } from 'rxjs/operators';
6
6
  import { LogStatus, LogError, StartupManager, CompositeKey, EncodeNewRecordValuesForURL, IsNewEntityRecordUrlId, NEW_ENTITY_RECORD_URL_ID, NEW_RECORD_VALUES_QUERY_PARAM, RecordUrlMatchesTab, ResourceUrlsEquivalent } from '@memberjunction/core';
7
7
  import { MJEventType, MJGlobal, uuidv4, UUIDsEqual } from '@memberjunction/global';
8
- import { EventCodes, SharedService, SYSTEM_APP_ID, SetRecordOpenStyle, IsRecordsRegionTab, IsRecordsTabConfiguration } from '@memberjunction/ng-shared';
8
+ import { EventCodes, SharedService, SYSTEM_APP_ID, SetRecordOpenStyle, IsRecordsRegionTab, IsRecordsTabConfiguration, NormalizeRecordIdForMatch, FormatEntityRecordDisplayTitle, ResolveRecordTabDisplayTitle, FORM_MODE_QUERY_PARAM, ReadStandardFormQuery } from '@memberjunction/ng-shared';
9
9
  import { MJNotificationService } from '@memberjunction/ng-notifications';
10
10
  import { IsOmnibarAvailable, IsOmnibarEnabledForUser, OMNIBAR_PROMO_DISMISSED_KEY, OMNIBAR_USER_SETTING_KEY } from '../omnibar/omnibar-user-setting';
11
11
  import { GetOmnibarShortcutLabel } from '../omnibar/omnibar-shortcut';
@@ -18,6 +18,7 @@ import { BaseAngularComponent } from '@memberjunction/ng-base-types';
18
18
  import { ApplyShellChromePolicy, BaseShellChromePolicy } from './shell-chrome-policy';
19
19
  import { SetReadinessBeacon } from './readiness-beacon';
20
20
  import { FindDashboardTabForUrl, OpenDashboardForUrl, ParseAppDashboardUrl } from './dashboard-tab-for-url';
21
+ import { ApplyDeepLinkToRestoredWorkspace } from './deep-link-workspace';
21
22
  import * as i0 from "@angular/core";
22
23
  import * as i1 from "@memberjunction/ng-base-application";
23
24
  import * as i2 from "@memberjunction/ng-shared";
@@ -927,6 +928,9 @@ export class ShellComponent extends BaseAngularComponent {
927
928
  // to 'auto' (compact under a handful of apps, launcher otherwise)
928
929
  appSwitcherStyle: rawStyle === 'launcher' || rawStyle === 'compact' ? rawStyle : 'auto',
929
930
  appNav: engine.GetBoolean('Shell.AppNav.Enabled', true),
931
+ // The same key explorer-app read directly before the overlay went through the policy;
932
+ // explorer-app now reads the resolved bit from ChromeFlags.
933
+ chatOverlay: engine.GetBoolean('Shell.ChatOverlay.Enabled', true),
930
934
  // Resolved (and pushed to collaborators) in resolveRecordOpenStyle()
931
935
  // during initializeShell — NEVER as a getter side effect; getters
932
936
  // run at change detection's whim and startup correctness must not
@@ -939,6 +943,16 @@ export class ShellComponent extends BaseAngularComponent {
939
943
  }
940
944
  return flags;
941
945
  }
946
+ /**
947
+ * The resolved chrome, for collaborators outside the shell. explorer-app reads `chatOverlay`
948
+ * from here (through its view query on this component) so the overlay follows the host's
949
+ * chrome policy with ONE policy instance and ONE `Changed`; the memoization above makes a
950
+ * read from a parent's change detection a field access. Read-only on purpose: this IS the
951
+ * memo, and a consumer writing to it would poison the shell's own chrome.
952
+ */
953
+ get ChromeFlags() {
954
+ return this.chromeFlags;
955
+ }
942
956
  /** The policy's answer may have changed: forget the cached flags and repaint. */
943
957
  onChromePolicyChanged() {
944
958
  this._chromeFlags = null;
@@ -1258,18 +1272,33 @@ export class ShellComponent extends BaseAngularComponent {
1258
1272
  }
1259
1273
  async ngOnInit() {
1260
1274
  try {
1275
+ let loggedIn = false;
1276
+ let shellInitialized = false;
1277
+ const initShellOnce = async () => {
1278
+ if (shellInitialized) {
1279
+ return;
1280
+ }
1281
+ shellInitialized = true;
1282
+ this.initialNavigationComplete = true;
1283
+ await this.InitializeShell();
1284
+ };
1285
+ // Subscribe to router settlement events (NavigationEnd, NavigationError on unmatched routes,
1286
+ // or NavigationCancel) before LoggedIn fires so we never miss a navigation that settled early
1287
+ // or hang forever when a bad URL triggers NavigationError and resets to '/'.
1288
+ this.subscriptions.push(this.router.events.pipe(filter((event) => event instanceof NavigationEnd ||
1289
+ event instanceof NavigationError ||
1290
+ event instanceof NavigationCancel), filter(() => !this.initialNavigationComplete)).subscribe(async () => {
1291
+ this.initialNavigationComplete = true;
1292
+ if (loggedIn) {
1293
+ await initShellOnce();
1294
+ }
1295
+ }));
1261
1296
  MJGlobal.Instance.GetEventListener(true).subscribe(async (loginEvent) => {
1262
1297
  if (loginEvent.event === MJEventType.LoggedIn) {
1263
- if (this.authBase.initialPath === "/") {
1264
- // Base route - no need to wait for NavigationEnd
1265
- await this.InitializeShell();
1266
- }
1267
- else {
1268
- // Deep link route - wait for NavigationEnd to ensure router URL is correct
1269
- this.router.events.pipe(filter((event) => event instanceof NavigationEnd), filter(() => !this.initialNavigationComplete)).subscribe(async () => {
1270
- this.initialNavigationComplete = true;
1271
- await this.InitializeShell();
1272
- });
1298
+ loggedIn = true;
1299
+ if (this.authBase.initialPath === "/" || this.initialNavigationComplete) {
1300
+ // Base route or initial router navigation already settled
1301
+ await initShellOnce();
1273
1302
  }
1274
1303
  }
1275
1304
  });
@@ -1309,8 +1338,14 @@ export class ShellComponent extends BaseAngularComponent {
1309
1338
  // Check the current URL to determine if we're loading from a URL-based navigation
1310
1339
  const currentUrl = this.router.url;
1311
1340
  this.urlBasedNavigation = currentUrl.includes('/app/') || currentUrl.includes('/resource/');
1312
- // Wait for workspace initialization to complete before allowing any tab operations
1313
- await this.workspaceManager.Initialize(user.ID);
1341
+ // Wait for workspace initialization to complete before allowing any tab operations.
1342
+ // On a deep link the URL's query params are written into the matching restored tab
1343
+ // BEFORE the configuration is published, so the URL — not the saved workspace — is what
1344
+ // that tab's component first sees (see ApplyDeepLinkToRestoredWorkspace).
1345
+ const startupUrl = currentUrl;
1346
+ const startupPath = startupUrl.split('?')[0];
1347
+ const startupIsDeepLink = startupPath.length > 1 && startupPath !== '/';
1348
+ await this.workspaceManager.Initialize(user.ID, startupIsDeepLink ? { PrepareConfiguration: config => this.applyStartupDeepLink(config, startupUrl) } : undefined);
1314
1349
  // A saved tab of a nav item that moved to another app becomes a tab of that app's nav item: once now, before the
1315
1350
  // shell syncs the URL or the active app from the saved tabs, and again each time the user's app list changes.
1316
1351
  this.subscriptions.push(await this.navigationService.WatchMovedNavItemTabs());
@@ -1368,6 +1403,13 @@ export class ShellComponent extends BaseAngularComponent {
1368
1403
  // Nav-app list is now populated/changed — refresh the precomputed arrays
1369
1404
  // bound in the header template.
1370
1405
  this.recomputeNavBarApps();
1406
+ // An app-locked session (magic link) whose user cannot read, or has no, User Application rows
1407
+ // still gets its scoped app: adopt it before deciding there are no apps. Adopting re-emits
1408
+ // Applications, which re-enters this handler with the app present.
1409
+ const sessionLockedId = this.authBase.GetSessionScope()?.restrictedToApplicationId;
1410
+ if (sessionLockedId && this.appManager.AdoptSessionApplication(sessionLockedId)) {
1411
+ return;
1412
+ }
1371
1413
  // Handle the case where user has no apps at all (only after loading is complete)
1372
1414
  if (apps.length === 0) {
1373
1415
  await this.handleNoAppsAvailable();
@@ -1392,23 +1434,31 @@ export class ShellComponent extends BaseAngularComponent {
1392
1434
  // already on the scoped app — fall through to normal handling below
1393
1435
  }
1394
1436
  }
1395
- // Check if URL specifies an app by parsing the browser URL
1437
+ // Check if URL specifies an app by parsing the browser URL (strip query string and fragment first)
1396
1438
  const currentUrl = this.router.url;
1397
- const appMatch = currentUrl.match(/\/app\/([^\/]+)/);
1439
+ const currentUrlPath = (currentUrl || '').split('#')[0].split('?')[0];
1440
+ const appMatch = currentUrlPath.match(/\/app\/([^\/?#]+)/);
1398
1441
  if (appMatch) {
1399
1442
  const routeAppPath = decodeURIComponent(appMatch[1]);
1400
1443
  // Find the app from the URL by Path (or Name for backwards compatibility)
1401
- const urlApp = this.appManager.GetAppByPath(routeAppPath);
1444
+ const urlApp = this.appManager.GetAppByPath(routeAppPath) || this.appManager.GetAppByName(routeAppPath);
1402
1445
  if (urlApp) {
1403
1446
  // Set the app from URL - takes precedence over workspace restoration
1404
1447
  await this.appManager.SetActiveApp(urlApp.ID);
1405
1448
  // If the URL is just /app/:appName (no nav item), create default tab
1406
- const hasNavItem = currentUrl.match(/\/app\/[^\/]+\/[^\/]+/);
1449
+ const hasNavItem = currentUrlPath.match(/\/app\/[^\/]+\/[^\/]+/);
1407
1450
  if (!hasNavItem) {
1408
1451
  const existingTabs = this.workspaceManager.GetAppTabs(urlApp.ID);
1409
1452
  if (existingTabs.length === 0) {
1410
1453
  const tabRequest = await urlApp.CreateDefaultTab();
1411
1454
  if (tabRequest) {
1455
+ const urlParams = this.extractQueryParamsFromUrl(currentUrl);
1456
+ if (Object.keys(urlParams).length > 0) {
1457
+ tabRequest.Configuration = {
1458
+ ...(tabRequest.Configuration || {}),
1459
+ queryParams: urlParams
1460
+ };
1461
+ }
1412
1462
  this.tabService.OpenTab(tabRequest);
1413
1463
  }
1414
1464
  }
@@ -1531,17 +1581,15 @@ export class ShellComponent extends BaseAngularComponent {
1531
1581
  // Only restore workspace state when the URL is bare root (/) or empty.
1532
1582
  const initialUrl = this.router.url.split('?')[0]; // ignore query params for this check
1533
1583
  const isDeepLink = initialUrl.length > 1 && initialUrl !== '/';
1534
- const initConfig = this.workspaceManager.GetConfiguration();
1584
+ let deepLinkResolved = false;
1535
1585
  if (isDeepLink) {
1536
1586
  // User navigated to a specific URL — sync workspace to match the URL,
1537
1587
  // not the other way around. The ResourceResolver will have created a tab
1538
1588
  // for this URL; we just need to activate it.
1539
- await this.syncWorkspaceWithUrl(this.router.url);
1589
+ deepLinkResolved = await this.syncWorkspaceWithUrl(this.router.url);
1540
1590
  }
1541
- else if (initConfig && initConfig.activeTabId) {
1542
- // Bare root URL — restore last workspace state
1543
- await this.syncActiveAppWithTab(initConfig);
1544
- this.syncUrlWithWorkspace(initConfig);
1591
+ if (!deepLinkResolved && !this.pendingAppPath) {
1592
+ await this.restoreOrOpenFallbackTab();
1545
1593
  }
1546
1594
  // Force change detection to sync Angular's expected values after all async
1547
1595
  // state changes (apps loaded, searchableEntities populated, etc.) to prevent
@@ -1552,6 +1600,24 @@ export class ShellComponent extends BaseAngularComponent {
1552
1600
  // does this on construction (#4802). Non-blocking; Preload() logs and retries on demand.
1553
1601
  this.reactAdapter.Preload();
1554
1602
  }
1603
+ /**
1604
+ * PrepareConfiguration hook for the startup deep link: when the URL carries query params and
1605
+ * resolves to a tab already in the restored workspace, that tab takes the URL's params and
1606
+ * becomes active. Runs before any tab component mounts.
1607
+ */
1608
+ async applyStartupDeepLink(config, url) {
1609
+ const urlParams = this.extractQueryParamsFromUrl(url);
1610
+ if (Object.keys(urlParams).length === 0 || !config.tabs?.length) {
1611
+ return config;
1612
+ }
1613
+ // findTabForUrl resolves /app/... URLs through the user's app list, which ApplicationManager
1614
+ // is still loading when workspace initialization calls this hook; resolving now would match no
1615
+ // app and drop the deep link. Waiting is cheap (the app list loads in parallel with the
1616
+ // workspace) and resolves even when loading fails.
1617
+ await this.appManager.WhenReady();
1618
+ const tab = await this.findTabForUrl(url, config.tabs);
1619
+ return tab ? ApplyDeepLinkToRestoredWorkspace(config, tab.id, urlParams, new Date().toISOString()) : config;
1620
+ }
1555
1621
  /** @deprecated Use {@link InitializeShell}. */
1556
1622
  async initializeShell() {
1557
1623
  return this.InitializeShell();
@@ -1624,10 +1690,11 @@ export class ShellComponent extends BaseAngularComponent {
1624
1690
  this.titleService.reset();
1625
1691
  return;
1626
1692
  }
1693
+ const resolvedTabTitle = ResolveRecordTabDisplayTitle(activeTab, this.ProviderToUse);
1627
1694
  // Get the tab's application ID
1628
1695
  const tabAppId = activeTab.applicationId;
1629
1696
  if (!tabAppId) {
1630
- this.titleService.setResourceName(activeTab.title || null);
1697
+ this.titleService.setResourceName(resolvedTabTitle);
1631
1698
  return;
1632
1699
  }
1633
1700
  // Check if this is a system tab (not associated with a registered app)
@@ -1636,7 +1703,7 @@ export class ShellComponent extends BaseAngularComponent {
1636
1703
  this.cdr.detectChanges();
1637
1704
  // Don't try to set active app - SYSTEM_APP_ID has no registered app
1638
1705
  // Update browser title with just the tab title (no app context)
1639
- this.titleService.setContext(null, activeTab.title || 'Explorer');
1706
+ this.titleService.setContext(null, resolvedTabTitle || 'Explorer');
1640
1707
  return;
1641
1708
  }
1642
1709
  // Not a system tab - clear the flag
@@ -1650,7 +1717,7 @@ export class ShellComponent extends BaseAngularComponent {
1650
1717
  // stays wherever the user is. Records DOCKED to the workspace fall
1651
1718
  // through: they are ordinary main-layout tabs and DO flip app context.
1652
1719
  if (this.RecordTabsStyle && IsRecordsRegionTab(activeTab.configuration)) {
1653
- this.titleService.setContext(this.ActiveApp?.Name || null, activeTab.title || null);
1720
+ this.titleService.setContext(this.ActiveApp?.Name || null, resolvedTabTitle);
1654
1721
  return;
1655
1722
  }
1656
1723
  // Check if active app needs to be updated
@@ -1664,7 +1731,7 @@ export class ShellComponent extends BaseAngularComponent {
1664
1731
  // (which can be stale when switching between apps in single-resource mode).
1665
1732
  const app = this.appManager.GetAppById(tabAppId);
1666
1733
  const appName = app?.Name || null;
1667
- const tabTitle = activeTab.configuration?.['navItemName'] || activeTab.title || null;
1734
+ const tabTitle = activeTab.configuration?.['navItemName'] || resolvedTabTitle || null;
1668
1735
  this.titleService.setContext(appName, tabTitle);
1669
1736
  }
1670
1737
  /**
@@ -1697,10 +1764,35 @@ export class ShellComponent extends BaseAngularComponent {
1697
1764
  // Suppress ResourceResolver for this navigation - we're just syncing the URL
1698
1765
  // to reflect the current active tab, not requesting a new tab to be opened
1699
1766
  this.tabService.SuppressNextResolve();
1700
- this.router.navigateByUrl(resourceUrl);
1767
+ // Leaving a record that was deleted: replace its history entry so
1768
+ // browser Back can't reopen it as a blank form.
1769
+ const leavingRecord = this.parseRecordUrl(currentUrl);
1770
+ const replaceUrl = !!leavingRecord &&
1771
+ this.tabService.ConsumeRecordUrlReplacement(leavingRecord.EntityName, leavingRecord.RecordId);
1772
+ this.router.navigateByUrl(resourceUrl, { replaceUrl });
1701
1773
  }
1702
1774
  }
1703
1775
  }
1776
+ /**
1777
+ * The record a URL shows, from either record URL shape
1778
+ * (`/app/:app/record/:entity/:id` or legacy `/resource/record/:entity/:id`),
1779
+ * or null when it is not a record URL. Query string and fragment ignored.
1780
+ */
1781
+ parseRecordUrl(url) {
1782
+ const urlPath = url.split(/[?#]/)[0];
1783
+ const appRecordMatch = urlPath.match(/^\/app\/[^\/]+\/record\/([^\/]+)\/(.+)$/);
1784
+ const legacyRecordMatch = appRecordMatch ? null : urlPath.match(/^\/resource\/record\/([^\/]+)\/(.+)$/);
1785
+ const match = appRecordMatch ?? legacyRecordMatch;
1786
+ if (!match) {
1787
+ return null;
1788
+ }
1789
+ try {
1790
+ return { EntityName: decodeURIComponent(match[1]), RecordId: NormalizeRecordIdForMatch(decodeURIComponent(match[2])) };
1791
+ }
1792
+ catch {
1793
+ return null;
1794
+ }
1795
+ }
1704
1796
  /**
1705
1797
  * Sync workspace state with the current URL: find the tab that matches it and activate that tab.
1706
1798
  *
@@ -1716,11 +1808,12 @@ export class ShellComponent extends BaseAngularComponent {
1716
1808
  */
1717
1809
  async syncWorkspaceWithUrl(url, navigatedAt = this.lastNavigationAt) {
1718
1810
  const config = this.workspaceManager.GetConfiguration();
1719
- if (!config?.tabs?.length) {
1720
- return;
1811
+ if (!config?.tabs) {
1812
+ return false;
1721
1813
  }
1722
- // Find the tab that matches this URL
1723
- const matchingTab = await this.findTabForUrl(url, config.tabs);
1814
+ // Find the tab that matches this URL (null when workspace has no tabs yet,
1815
+ // which falls through to handleMissingTabForUrl below so deep links still open).
1816
+ const matchingTab = config.tabs.length > 0 ? await this.findTabForUrl(url, config.tabs) : null;
1724
1817
  if (matchingTab && matchingTab.id !== config.activeTabId) {
1725
1818
  /**
1726
1819
  * ORDERING. A url sync must not override an activation that happened AFTER this url was
@@ -1763,16 +1856,37 @@ export class ShellComponent extends BaseAngularComponent {
1763
1856
  // stays because it says what the comparison relies on, but removing it changes no behaviour and
1764
1857
  // breaks no test — do not read it as the thing that handles NaN.
1765
1858
  if (Number.isFinite(activatedAt) && activatedAt > navigatedAt && activatedAt <= Date.now()) {
1766
- return;
1859
+ return true;
1860
+ }
1861
+ // If the URL carries query params that differ from the matching tab's config, sync them
1862
+ // before activating so syncUrlWithWorkspace doesn't overwrite the URL with stale params.
1863
+ // When the URL is a bare app-level URL (/app/:appName) with no query params, preserve the
1864
+ // default tab's saved queryParams state (matching ApplyDeepLinkToRestoredWorkspace).
1865
+ const urlParams = this.extractQueryParamsFromUrl(url);
1866
+ const isBareAppOnlyUrl = /^\/app\/[^\/]+$/.test(url.split('?')[0]) && Object.keys(urlParams).length === 0;
1867
+ const tabParams = (matchingTab.configuration?.['queryParams'] || {});
1868
+ if (!isBareAppOnlyUrl && !this.queryParamsEqual(urlParams, tabParams) && typeof this.workspaceManager.UpdateTabConfiguration === 'function') {
1869
+ this.urlBasedNavigation = true;
1870
+ try {
1871
+ this.workspaceManager.UpdateTabConfiguration(matchingTab.id, {
1872
+ queryParams: Object.keys(urlParams).length > 0 ? urlParams : undefined
1873
+ });
1874
+ this.navigationService?.NotifyQueryParamsChanged?.(matchingTab.id, urlParams);
1875
+ }
1876
+ finally {
1877
+ this.urlBasedNavigation = false;
1878
+ }
1767
1879
  }
1768
1880
  // Activate the matching tab
1769
1881
  this.workspaceManager.SetActiveTab(matchingTab.id);
1882
+ return true;
1770
1883
  }
1771
1884
  else if (matchingTab && matchingTab.id === config.activeTabId) {
1772
1885
  // Same tab is already active, but query params may have changed (back/forward within nav item)
1773
1886
  const urlParams = this.extractQueryParamsFromUrl(url);
1887
+ const isBareAppOnlyUrl = /^\/app\/[^\/]+$/.test(url.split('?')[0]) && Object.keys(urlParams).length === 0;
1774
1888
  const tabParams = (matchingTab.configuration?.['queryParams'] || {});
1775
- if (!this.queryParamsEqual(urlParams, tabParams)) {
1889
+ if (!isBareAppOnlyUrl && !this.queryParamsEqual(urlParams, tabParams)) {
1776
1890
  // URL is source of truth during back/forward — update tab config to match.
1777
1891
  this.urlBasedNavigation = true;
1778
1892
  try {
@@ -1785,9 +1899,60 @@ export class ShellComponent extends BaseAngularComponent {
1785
1899
  this.urlBasedNavigation = false;
1786
1900
  }
1787
1901
  }
1902
+ return true;
1788
1903
  }
1789
1904
  else if (!matchingTab) {
1790
- await this.handleMissingTabForUrl(url);
1905
+ const handled = await this.handleMissingTabForUrl(url);
1906
+ return handled !== false;
1907
+ }
1908
+ return false;
1909
+ }
1910
+ /**
1911
+ * Restores the active workspace tab (syncing the active app and URL) or, when no active tab
1912
+ * exists (such as a fresh workspace after an invalid deep link suppressed default tab creation),
1913
+ * opens a default tab on the active or landing application so the loading screen always clears.
1914
+ */
1915
+ async restoreOrOpenFallbackTab() {
1916
+ const config = this.workspaceManager.GetConfiguration();
1917
+ const activeTab = config?.activeTabId
1918
+ ? config.tabs?.find(tab => tab.id === config.activeTabId)
1919
+ : undefined;
1920
+ if (config && activeTab) {
1921
+ await this.syncActiveAppWithTab(config);
1922
+ await this.syncUrlWithWorkspace(config);
1923
+ return;
1924
+ }
1925
+ if (config?.tabs && config.tabs.length > 0) {
1926
+ const fallbackTab = config.tabs[0];
1927
+ this.workspaceManager.SetActiveTab(fallbackTab.id);
1928
+ const updatedConfig = this.workspaceManager.GetConfiguration() ?? { ...config, activeTabId: fallbackTab.id };
1929
+ await this.syncActiveAppWithTab(updatedConfig);
1930
+ await this.syncUrlWithWorkspace(updatedConfig);
1931
+ return;
1932
+ }
1933
+ let activeApp = this.appManager.GetActiveApp();
1934
+ if (!activeApp) {
1935
+ const allApps = this.appManager.GetAllApps?.() ?? [];
1936
+ if (allApps.length > 0) {
1937
+ await this.openLandingApp(allApps);
1938
+ activeApp = this.appManager.GetActiveApp();
1939
+ }
1940
+ }
1941
+ if (activeApp) {
1942
+ const existingAppTabs = this.workspaceManager.GetAppTabs?.(activeApp.ID) ?? [];
1943
+ if (existingAppTabs.length === 0) {
1944
+ const defaultTabRequest = await activeApp.CreateDefaultTab();
1945
+ if (defaultTabRequest) {
1946
+ this.tabService.OpenTab(defaultTabRequest);
1947
+ }
1948
+ }
1949
+ const latestConfig = this.workspaceManager.GetConfiguration();
1950
+ if (latestConfig?.activeTabId) {
1951
+ await this.syncUrlWithWorkspace(latestConfig);
1952
+ }
1953
+ else {
1954
+ this.router.navigateByUrl(this.appManager.GetAppUrl(activeApp), { replaceUrl: true });
1955
+ }
1791
1956
  }
1792
1957
  }
1793
1958
  /**
@@ -1800,9 +1965,16 @@ export class ShellComponent extends BaseAngularComponent {
1800
1965
  if (queryIndex === -1)
1801
1966
  return {};
1802
1967
  const params = new URLSearchParams(cleanUrl.substring(queryIndex + 1));
1803
- const result = {};
1804
- params.forEach((value, key) => { result[key] = value; });
1805
- return result;
1968
+ return this.urlSearchParamsToRecord(params);
1969
+ }
1970
+ urlSearchParamsToRecord(params) {
1971
+ const safeEntries = [];
1972
+ params.forEach((value, key) => {
1973
+ if (key !== '__proto__' && key !== 'constructor' && key !== 'prototype') {
1974
+ safeEntries.push([key, value]);
1975
+ }
1976
+ });
1977
+ return Object.fromEntries(safeEntries);
1806
1978
  }
1807
1979
  /**
1808
1980
  * Compare two query param records for equality, normalizing encoding differences
@@ -1819,6 +1991,7 @@ export class ShellComponent extends BaseAngularComponent {
1819
1991
  /**
1820
1992
  * Handle the case where no tab matches the URL during back/forward navigation.
1821
1993
  * Creates new tabs for resources when the original tab was replaced.
1994
+ * Returns true when a valid resource/tab was opened, or false when the URL did not resolve.
1822
1995
  */
1823
1996
  async handleMissingTabForUrl(url) {
1824
1997
  const urlPath = url.split('?')[0];
@@ -1832,23 +2005,26 @@ export class ShellComponent extends BaseAngularComponent {
1832
2005
  if (appRecordMatch) {
1833
2006
  const entityName = decodeURIComponent(appRecordMatch[2]);
1834
2007
  const recordId = decodeURIComponent(appRecordMatch[3]);
1835
- this.openRecordFromUrl(entityName, recordId, queryParams);
1836
- return;
2008
+ return this.openRecordFromUrl(entityName, recordId, queryParams);
1837
2009
  }
1838
2010
  // Check for app-scoped view URL: /app/:appName/view/:viewId
1839
2011
  const appViewMatch = urlPath.match(/^\/app\/([^\/]+)\/view\/([^\/]+)$/);
1840
2012
  if (appViewMatch && appViewMatch[2] !== 'dynamic') {
1841
2013
  const viewId = appViewMatch[2];
1842
2014
  this.navigationService.OpenView(viewId, 'View');
1843
- return;
2015
+ return true;
1844
2016
  }
1845
2017
  // Check for app-scoped dynamic view URL: /app/:appName/view/dynamic/:entityName
1846
2018
  const appDynamicViewMatch = urlPath.match(/^\/app\/([^\/]+)\/view\/dynamic\/(.+)$/);
1847
2019
  if (appDynamicViewMatch) {
1848
2020
  const entityName = decodeURIComponent(appDynamicViewMatch[2]);
1849
- const extraFilter = queryParams.get('ExtraFilter') || undefined;
1850
- this.navigationService.OpenDynamicView(entityName, extraFilter);
1851
- return;
2021
+ if (!this.isKnownEntity(entityName)) {
2022
+ return false;
2023
+ }
2024
+ const extraFilter = queryParams.get('ExtraFilter') || queryParams.get('extraFilter') || undefined;
2025
+ const alternateBaseView = queryParams.get('AlternateBaseView') || queryParams.get('alternateBaseView') || queryParams.get('abv') || undefined;
2026
+ this.navigationService.OpenDynamicView(entityName, extraFilter, alternateBaseView ? { alternateBaseView } : undefined);
2027
+ return true;
1852
2028
  }
1853
2029
  // Check for app-scoped dashboard URL: /app/:appName/dashboard/:dashboardId. The tab goes to
1854
2030
  // the URL's application, where the ResourceResolver also opens it.
@@ -1857,28 +2033,28 @@ export class ShellComponent extends BaseAngularComponent {
1857
2033
  OpenDashboard: (dashboardId, dashboardName, options) => this.navigationService.OpenDashboard(dashboardId, dashboardName, options),
1858
2034
  });
1859
2035
  if (openedDashboard) {
1860
- return;
2036
+ return true;
1861
2037
  }
1862
2038
  // Check for app-scoped artifact URL: /app/:appName/artifact/:artifactId
1863
2039
  const appArtifactMatch = urlPath.match(/^\/app\/([^\/]+)\/artifact\/(.+)$/);
1864
2040
  if (appArtifactMatch) {
1865
2041
  const artifactId = appArtifactMatch[2];
1866
2042
  this.navigationService.OpenArtifact(artifactId, 'Artifact');
1867
- return;
2043
+ return true;
1868
2044
  }
1869
2045
  // Check for app-scoped query URL: /app/:appName/query/:queryId
1870
2046
  const appQueryMatch = urlPath.match(/^\/app\/([^\/]+)\/query\/(.+)$/);
1871
2047
  if (appQueryMatch) {
1872
2048
  const queryId = appQueryMatch[2];
1873
2049
  this.navigationService.OpenQuery(queryId, 'Query');
1874
- return;
2050
+ return true;
1875
2051
  }
1876
2052
  // Check for app-scoped search URL: /app/:appName/search/:searchInput
1877
2053
  const appSearchMatch = urlPath.match(/^\/app\/([^\/]+)\/search\/(.+)$/);
1878
2054
  if (appSearchMatch) {
1879
2055
  const searchInput = decodeURIComponent(appSearchMatch[2]);
1880
2056
  this.navigationService.OpenSearch(searchInput);
1881
- return;
2057
+ return true;
1882
2058
  }
1883
2059
  // Check for app nav item URL: /app/:appName/:navItemName
1884
2060
  const appNavItemMatch = urlPath.match(/^\/app\/([^\/]+)\/([^\/]+)$/);
@@ -1890,18 +2066,18 @@ export class ShellComponent extends BaseAngularComponent {
1890
2066
  // Activate the app and open the nav item
1891
2067
  await this.appManager.SetActiveApp(app.ID);
1892
2068
  const navItems = await app.GetNavItems();
1893
- const navItem = navItems.find(item => item.Label === navItemName);
2069
+ const navItem = navItems.find(item => item.Label?.trim().toLowerCase() === navItemName.trim().toLowerCase());
1894
2070
  if (navItem) {
1895
2071
  // Parse query params to pass as configuration
1896
- const qpObj = {};
1897
- queryParams.forEach((value, key) => { qpObj[key] = value; });
2072
+ const qpObj = this.urlSearchParamsToRecord(queryParams);
1898
2073
  this.navigationService.OpenNavItem(app.ID, navItem, app.GetColor(), Object.keys(qpObj).length > 0 ? { queryParams: qpObj } : undefined);
2074
+ return true;
1899
2075
  }
1900
2076
  else {
1901
2077
  console.warn('handleMissingTabForUrl: nav item not found:', navItemName, 'in app:', appPath);
1902
2078
  }
1903
2079
  }
1904
- return;
2080
+ return false;
1905
2081
  }
1906
2082
  // Check for app-only URL: /app/:appName
1907
2083
  const appOnlyMatch = urlPath.match(/^\/app\/([^\/]+)$/);
@@ -1909,6 +2085,7 @@ export class ShellComponent extends BaseAngularComponent {
1909
2085
  const appPath = decodeURIComponent(appOnlyMatch[1]);
1910
2086
  const app = this.appManager.GetAppByPath(appPath) || this.appManager.GetAppByName(appPath);
1911
2087
  if (app) {
2088
+ const qpObj = this.urlSearchParamsToRecord(queryParams);
1912
2089
  const navItems = await app.GetNavItems();
1913
2090
  // Only auto-create tabs for apps with zero nav items
1914
2091
  // Apps with nav items should have had their tabs preserved
@@ -1917,7 +2094,14 @@ export class ShellComponent extends BaseAngularComponent {
1917
2094
  await this.appManager.SetActiveApp(app.ID);
1918
2095
  const defaultTab = await app.CreateDefaultTab();
1919
2096
  if (defaultTab) {
2097
+ if (Object.keys(qpObj).length > 0) {
2098
+ defaultTab.Configuration = {
2099
+ ...(defaultTab.Configuration || {}),
2100
+ queryParams: qpObj
2101
+ };
2102
+ }
1920
2103
  this.workspaceManager.OpenTab(defaultTab, app.GetColor());
2104
+ return true;
1921
2105
  }
1922
2106
  }
1923
2107
  else {
@@ -1925,11 +2109,12 @@ export class ShellComponent extends BaseAngularComponent {
1925
2109
  await this.appManager.SetActiveApp(app.ID);
1926
2110
  const defaultNavItem = navItems.find(item => item.isDefault) || navItems[0];
1927
2111
  if (defaultNavItem) {
1928
- this.navigationService.OpenNavItem(app.ID, defaultNavItem, app.GetColor());
2112
+ this.navigationService.OpenNavItem(app.ID, defaultNavItem, app.GetColor(), Object.keys(qpObj).length > 0 ? { queryParams: qpObj } : undefined);
2113
+ return true;
1929
2114
  }
1930
2115
  }
1931
2116
  }
1932
- return;
2117
+ return false;
1933
2118
  }
1934
2119
  // Legacy resource URLs (backward compatibility)
1935
2120
  // Check for legacy record URL: /resource/record/:entityName/:recordId
@@ -1937,45 +2122,105 @@ export class ShellComponent extends BaseAngularComponent {
1937
2122
  if (legacyRecordMatch) {
1938
2123
  const entityName = decodeURIComponent(legacyRecordMatch[1]);
1939
2124
  const recordId = decodeURIComponent(legacyRecordMatch[2]);
1940
- this.openRecordFromUrl(entityName, recordId, queryParams);
1941
- return;
2125
+ return this.openRecordFromUrl(entityName, recordId, queryParams);
2126
+ }
2127
+ // Check for legacy dynamic view URL: /resource/view/dynamic/:entityName
2128
+ const legacyDynamicViewMatch = urlPath.match(/^\/resource\/view\/dynamic\/(.+)$/);
2129
+ if (legacyDynamicViewMatch) {
2130
+ const entityName = decodeURIComponent(legacyDynamicViewMatch[1]);
2131
+ if (!this.isKnownEntity(entityName)) {
2132
+ return false;
2133
+ }
2134
+ const extraFilter = queryParams.get('ExtraFilter') || queryParams.get('extraFilter') || undefined;
2135
+ const alternateBaseView = queryParams.get('AlternateBaseView') || queryParams.get('alternateBaseView') || queryParams.get('abv') || undefined;
2136
+ this.navigationService.OpenDynamicView(entityName, extraFilter, alternateBaseView ? { alternateBaseView } : undefined);
2137
+ return true;
1942
2138
  }
1943
2139
  // Check for legacy view URL: /resource/view/:viewId
1944
2140
  const legacyViewMatch = urlPath.match(/^\/resource\/view\/([^\/]+)$/);
1945
2141
  if (legacyViewMatch && legacyViewMatch[1] !== 'dynamic') {
1946
2142
  const viewId = legacyViewMatch[1];
1947
2143
  this.navigationService.OpenView(viewId, 'View');
1948
- return;
2144
+ return true;
1949
2145
  }
1950
2146
  // Check for legacy dashboard URL: /resource/dashboard/:dashboardId
1951
2147
  const legacyDashboardMatch = urlPath.match(/^\/resource\/dashboard\/(.+)$/);
1952
2148
  if (legacyDashboardMatch) {
1953
2149
  const dashboardId = legacyDashboardMatch[1];
1954
2150
  this.navigationService.OpenDashboard(dashboardId, 'Dashboard');
1955
- return;
2151
+ return true;
2152
+ }
2153
+ // Check for legacy artifact URL: /resource/artifact/:artifactId
2154
+ const legacyArtifactMatch = urlPath.match(/^\/resource\/artifact\/(.+)$/);
2155
+ if (legacyArtifactMatch) {
2156
+ this.navigationService.OpenArtifact(legacyArtifactMatch[1], 'Artifact');
2157
+ return true;
2158
+ }
2159
+ // Check for legacy query URL: /resource/query/:queryId
2160
+ const legacyQueryMatch = urlPath.match(/^\/resource\/query\/(.+)$/);
2161
+ if (legacyQueryMatch) {
2162
+ this.navigationService.OpenQuery(legacyQueryMatch[1], 'Query');
2163
+ return true;
1956
2164
  }
2165
+ // Check for legacy search URL: /resource/search/:searchInput
2166
+ const legacySearchMatch = urlPath.match(/^\/resource\/search\/(.+)$/);
2167
+ if (legacySearchMatch) {
2168
+ this.navigationService.OpenSearch(decodeURIComponent(legacySearchMatch[1]));
2169
+ return true;
2170
+ }
2171
+ return false;
1957
2172
  }
1958
2173
  finally {
1959
2174
  this.urlBasedNavigation = false;
1960
2175
  }
1961
2176
  }
2177
+ /**
2178
+ * Checks whether an entity exists in metadata when metadata is loaded.
2179
+ * When metadata is empty (e.g. lightweight unit test harnesses), returns true.
2180
+ */
2181
+ isKnownEntity(entityName) {
2182
+ let md = null;
2183
+ try {
2184
+ md = this.getProviderSafe();
2185
+ }
2186
+ catch {
2187
+ return true;
2188
+ }
2189
+ const entities = md?.Entities;
2190
+ if (!entities || entities.length === 0) {
2191
+ return true;
2192
+ }
2193
+ return !!md?.EntityByName(entityName);
2194
+ }
2195
+ getProviderSafe() {
2196
+ return this.ProviderToUse ?? null;
2197
+ }
1962
2198
  /**
1963
2199
  * Recreate a record tab from a URL. `/new` is OpenNewEntityRecord — feeding
1964
2200
  * the sentinel through OpenEntityRecord produced an empty CompositeKey and
1965
2201
  * another forced tab, which is the loop that killed the browser.
1966
2202
  */
1967
2203
  openRecordFromUrl(entityName, recordId, queryParams) {
2204
+ if (!this.isKnownEntity(entityName)) {
2205
+ LogError(`Entity ${entityName} not found in metadata`);
2206
+ return false;
2207
+ }
2208
+ // Recreating from browser history must keep `?form=standard` (MJ#4755),
2209
+ // or back/forward would reopen the record in the custom form.
2210
+ const formMode = ReadStandardFormQuery({ [FORM_MODE_QUERY_PARAM]: queryParams.get(FORM_MODE_QUERY_PARAM) });
1968
2211
  if (IsNewEntityRecordUrlId(recordId)) {
1969
2212
  const nrv = queryParams.get(NEW_RECORD_VALUES_QUERY_PARAM) ?? undefined;
1970
2213
  this.navigationService.OpenNewEntityRecord(entityName, {
1971
2214
  newRecordValues: nrv,
1972
2215
  recordSource: 'none',
2216
+ formMode,
1973
2217
  });
1974
- return;
2218
+ return true;
1975
2219
  }
1976
2220
  const compositeKey = new CompositeKey();
1977
2221
  compositeKey.SimpleLoadFromURLSegment(recordId);
1978
- this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none' });
2222
+ this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none', formMode });
2223
+ return true;
1979
2224
  }
1980
2225
  recordTabMatchesUrl(tab, entityName, recordId) {
1981
2226
  const tabConfig = tab.configuration || {};
@@ -2030,6 +2275,18 @@ export class ShellComponent extends BaseAngularComponent {
2030
2275
  if (navItems.length === 0) {
2031
2276
  return tabs.find(tab => UUIDsEqual(tab.applicationId, app.ID)) || null;
2032
2277
  }
2278
+ // For apps with nav items, an app-level URL (/app/:appName) resolves to the default nav item's tab
2279
+ const defaultNavItem = navItems.find(item => item.isDefault) || navItems[0];
2280
+ if (defaultNavItem) {
2281
+ return tabs.find(tab => {
2282
+ const tabConfig = tab.configuration || {};
2283
+ const tabAppName = tabConfig['appName'];
2284
+ const tabNavItemName = tabConfig['navItemName'];
2285
+ const appMatches = (tabAppName && tabAppName.toLowerCase() === app.Name.toLowerCase()) ||
2286
+ UUIDsEqual(tab.applicationId, app.ID);
2287
+ return appMatches && tabNavItemName?.toLowerCase() === defaultNavItem.Label?.toLowerCase();
2288
+ }) || null;
2289
+ }
2033
2290
  return null;
2034
2291
  }
2035
2292
  }
@@ -2057,7 +2314,9 @@ export class ShellComponent extends BaseAngularComponent {
2057
2314
  const resourceType = tabConfig['resourceType']?.toLowerCase();
2058
2315
  const tabEntity = (tabConfig['Entity'] || tabConfig['entity']);
2059
2316
  const isDynamic = tabConfig['isDynamic'];
2060
- return resourceType === 'user views' && isDynamic && tabEntity?.toLowerCase() === entityName.toLowerCase();
2317
+ return (resourceType === 'user views' || resourceType === 'mj: user views') &&
2318
+ isDynamic &&
2319
+ tabEntity?.toLowerCase() === entityName.toLowerCase();
2061
2320
  }) || null;
2062
2321
  }
2063
2322
  // Saved view: /app/:appName/view/:viewId
@@ -2068,7 +2327,7 @@ export class ShellComponent extends BaseAngularComponent {
2068
2327
  const tabConfig = tab.configuration || {};
2069
2328
  const resourceType = tabConfig['resourceType']?.toLowerCase();
2070
2329
  const tabViewId = (tabConfig['viewId'] || tabConfig['recordId'] || tab.resourceRecordId);
2071
- return resourceType === 'user views' && tabViewId === viewId;
2330
+ return (resourceType === 'user views' || resourceType === 'mj: user views') && tabViewId === viewId;
2072
2331
  }) || null;
2073
2332
  }
2074
2333
  // Query: /app/:appName/query/:queryId
@@ -2179,6 +2438,14 @@ export class ShellComponent extends BaseAngularComponent {
2179
2438
  const params = new URLSearchParams(all);
2180
2439
  return `${url}${separator}${params.toString()}`;
2181
2440
  };
2441
+ // Extra query params a record tab's URL carries beyond the tab's own
2442
+ // queryParams (which appendQP already merges — that is where the
2443
+ // standard-form `form` param lives, MJ#4755): initial values for an
2444
+ // unsaved record.
2445
+ const recordQueryParams = (isNewRecord) => {
2446
+ const nrv = isNewRecord ? EncodeNewRecordValuesForURL(config['NewRecordValues']) : undefined;
2447
+ return nrv ? { [NEW_RECORD_VALUES_QUERY_PARAM]: nrv } : {};
2448
+ };
2182
2449
  // Helper function to get app path for URL
2183
2450
  const getAppPath = (appIdOrName) => {
2184
2451
  // First try by ID
@@ -2208,7 +2475,7 @@ export class ShellComponent extends BaseAngularComponent {
2208
2475
  // If this is an app's default dashboard (no nav items), use app-level URL
2209
2476
  if (isAppDefault && appName) {
2210
2477
  const appPath = getAppPath(appName) || appName;
2211
- return `/app/${encodeURIComponent(appPath)}`;
2478
+ return appendQP(`/app/${encodeURIComponent(appPath)}`);
2212
2479
  }
2213
2480
  // Fallback: If tab belongs to a non-system app but doesn't have appName/navItemName,
2214
2481
  // try to reconstruct the URL from the ApplicationId and tab title
@@ -2250,7 +2517,7 @@ export class ShellComponent extends BaseAngularComponent {
2250
2517
  if (staticNavItems.length === 0 && (!resourceType || resourceType === 'custom')) {
2251
2518
  // App has zero static nav items AND this is not an orphan resource (no resourceType or custom)
2252
2519
  // Use app-level URL - this handles apps that only have a default dashboard
2253
- return `/app/${encodeURIComponent(appPath)}`;
2520
+ return appendQP(`/app/${encodeURIComponent(appPath)}`);
2254
2521
  }
2255
2522
  // Fall through to orphan resource URL building below
2256
2523
  }
@@ -2289,21 +2556,26 @@ export class ShellComponent extends BaseAngularComponent {
2289
2556
  // here, `syncUrlWithWorkspace`'s `currentUrl !== newUrl` check is permanently true and can
2290
2557
  // drive a re-navigation loop (with onSameUrlNavigation:'reload'). The read side already
2291
2558
  // decodeURIComponent()s this segment, so encoding here keeps both sides consistent.
2292
- const nrv = isNewRecord ? EncodeNewRecordValuesForURL(config['NewRecordValues']) : undefined;
2293
- return appendQP(`/app/${encodeURIComponent(appPath)}/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, nrv ? { [NEW_RECORD_VALUES_QUERY_PARAM]: nrv } : undefined);
2559
+ return appendQP(`/app/${encodeURIComponent(appPath)}/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, recordQueryParams(isNewRecord));
2294
2560
  }
2295
2561
  }
2296
2562
  break;
2297
2563
  case 'user views':
2564
+ case 'mj: user views':
2298
2565
  // Check if it's a dynamic view
2299
2566
  if (isDynamic || recordId === 'dynamic') {
2300
- // /app/:appName/view/dynamic/:entityName?ExtraFilter=...
2567
+ // /app/:appName/view/dynamic/:entityName?ExtraFilter=...&abv=...
2301
2568
  if (entityName) {
2302
- let url = `/app/${encodeURIComponent(appPath)}/view/dynamic/${encodeURIComponent(entityName)}`;
2569
+ const url = `/app/${encodeURIComponent(appPath)}/view/dynamic/${encodeURIComponent(entityName)}`;
2570
+ const extraParams = {};
2303
2571
  if (extraFilter) {
2304
- url += `?ExtraFilter=${encodeURIComponent(extraFilter)}`;
2572
+ extraParams['ExtraFilter'] = extraFilter;
2573
+ }
2574
+ const abv = (config['AlternateBaseView'] || config['alternateBaseView']);
2575
+ if (abv) {
2576
+ extraParams['abv'] = abv;
2305
2577
  }
2306
- return appendQP(url);
2578
+ return appendQP(url, extraParams);
2307
2579
  }
2308
2580
  }
2309
2581
  else if (recordId) {
@@ -2362,8 +2634,7 @@ export class ShellComponent extends BaseAngularComponent {
2362
2634
  if (idSeg) {
2363
2635
  // Encode the CompositeKey segment ('|' → %7C) to match Angular's serialized router.url and
2364
2636
  // the decodeURIComponent() on the read side — see the app-scoped 'records' case above.
2365
- const nrv = isNewRecord ? EncodeNewRecordValuesForURL(config['NewRecordValues']) : undefined;
2366
- return appendQP(`/resource/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, nrv ? { [NEW_RECORD_VALUES_QUERY_PARAM]: nrv } : undefined);
2637
+ return appendQP(`/resource/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, recordQueryParams(isNewRecord));
2367
2638
  }
2368
2639
  }
2369
2640
  break;
@@ -3292,9 +3563,8 @@ export class ShellComponent extends BaseAngularComponent {
3292
3563
  if (compositeKey.KeyValuePairs.length === 0)
3293
3564
  return null;
3294
3565
  const results = await md.GetEntityRecordNames([{ EntityName: entityName, CompositeKey: compositeKey }]);
3295
- if (results.length > 0 && results[0].Success && results[0].RecordName) {
3296
- return results[0].RecordName;
3297
- }
3566
+ const rawName = results.length > 0 && results[0].Success ? results[0].RecordName : null;
3567
+ return FormatEntityRecordDisplayTitle(entityInfo, entityName, compositeKey, rawName);
3298
3568
  }
3299
3569
  catch {
3300
3570
  // Fall through to null
@@ -3491,7 +3761,7 @@ export class ShellComponent extends BaseAngularComponent {
3491
3761
  // Get resource name from active tab
3492
3762
  let resourceName = null;
3493
3763
  if (activeTab) {
3494
- resourceName = activeTab.title || null;
3764
+ resourceName = ResolveRecordTabDisplayTitle(activeTab, this.ProviderToUse);
3495
3765
  }
3496
3766
  // Update title via TitleService
3497
3767
  this.titleService.setContext(appName, resourceName);
@@ -4289,5 +4559,5 @@ export class ShellComponent extends BaseAngularComponent {
4289
4559
  type: HostListener,
4290
4560
  args: ['document:click']
4291
4561
  }] }); })();
4292
- (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber: 61 }); })();
4562
+ (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ShellComponent, { className: "ShellComponent", filePath: "src/lib/shell/shell.component.ts", lineNumber: 62 }); })();
4293
4563
  //# sourceMappingURL=shell.component.js.map