@memberjunction/ng-explorer-core 6.2.0-edge.5 → 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.
- package/README.md +7 -2
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +12 -4
- package/dist/app-routing.module.js.map +1 -1
- package/dist/generated/lazy-feature-config.d.ts +1 -1
- package/dist/generated/lazy-feature-config.d.ts.map +1 -1
- package/dist/generated/lazy-feature-config.js +4 -2
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/resource-wrappers/record-form-mode.d.ts +44 -0
- package/dist/lib/resource-wrappers/record-form-mode.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/record-form-mode.js +47 -0
- package/dist/lib/resource-wrappers/record-form-mode.js.map +1 -0
- package/dist/lib/resource-wrappers/record-resource.component.d.ts +56 -1
- package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.js +124 -9
- package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/standard-form-create.d.ts +4 -0
- package/dist/lib/resource-wrappers/standard-form-create.d.ts.map +1 -0
- package/dist/lib/resource-wrappers/standard-form-create.js +6 -0
- package/dist/lib/resource-wrappers/standard-form-create.js.map +1 -0
- package/dist/lib/resource-wrappers/view-resource.component.d.ts +18 -1
- package/dist/lib/resource-wrappers/view-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/view-resource.component.js +105 -39
- package/dist/lib/resource-wrappers/view-resource.component.js.map +1 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.d.ts.map +1 -1
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js +12 -8
- package/dist/lib/shell/components/record-open/record-origin-crumb.component.js.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts +6 -0
- package/dist/lib/shell/components/tabs/component-cache-manager.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/component-cache-manager.js +18 -3
- package/dist/lib/shell/components/tabs/component-cache-manager.js.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts +2 -0
- package/dist/lib/shell/components/tabs/tab-container.component.d.ts.map +1 -1
- package/dist/lib/shell/components/tabs/tab-container.component.js +38 -10
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/shell-chrome-policy.d.ts +6 -0
- package/dist/lib/shell/shell-chrome-policy.d.ts.map +1 -1
- package/dist/lib/shell/shell-chrome-policy.js +1 -0
- package/dist/lib/shell/shell-chrome-policy.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +23 -0
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +283 -71
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +27 -2
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +54 -8
- package/dist/lib/single-record/single-record.component.js.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.d.ts +10 -2
- package/dist/lib/user-notifications/user-notifications.component.d.ts.map +1 -1
- package/dist/lib/user-notifications/user-notifications.component.js +77 -22
- package/dist/lib/user-notifications/user-notifications.component.js.map +1 -1
- 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, NormalizeRecordIdForMatch } 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';
|
|
@@ -928,6 +928,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
928
928
|
// to 'auto' (compact under a handful of apps, launcher otherwise)
|
|
929
929
|
appSwitcherStyle: rawStyle === 'launcher' || rawStyle === 'compact' ? rawStyle : 'auto',
|
|
930
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),
|
|
931
934
|
// Resolved (and pushed to collaborators) in resolveRecordOpenStyle()
|
|
932
935
|
// during initializeShell — NEVER as a getter side effect; getters
|
|
933
936
|
// run at change detection's whim and startup correctness must not
|
|
@@ -940,6 +943,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
940
943
|
}
|
|
941
944
|
return flags;
|
|
942
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
|
+
}
|
|
943
956
|
/** The policy's answer may have changed: forget the cached flags and repaint. */
|
|
944
957
|
onChromePolicyChanged() {
|
|
945
958
|
this._chromeFlags = null;
|
|
@@ -1259,18 +1272,33 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1259
1272
|
}
|
|
1260
1273
|
async ngOnInit() {
|
|
1261
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
|
+
}));
|
|
1262
1296
|
MJGlobal.Instance.GetEventListener(true).subscribe(async (loginEvent) => {
|
|
1263
1297
|
if (loginEvent.event === MJEventType.LoggedIn) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
else {
|
|
1269
|
-
// Deep link route - wait for NavigationEnd to ensure router URL is correct
|
|
1270
|
-
this.router.events.pipe(filter((event) => event instanceof NavigationEnd), filter(() => !this.initialNavigationComplete)).subscribe(async () => {
|
|
1271
|
-
this.initialNavigationComplete = true;
|
|
1272
|
-
await this.InitializeShell();
|
|
1273
|
-
});
|
|
1298
|
+
loggedIn = true;
|
|
1299
|
+
if (this.authBase.initialPath === "/" || this.initialNavigationComplete) {
|
|
1300
|
+
// Base route or initial router navigation already settled
|
|
1301
|
+
await initShellOnce();
|
|
1274
1302
|
}
|
|
1275
1303
|
}
|
|
1276
1304
|
});
|
|
@@ -1406,23 +1434,31 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1406
1434
|
// already on the scoped app — fall through to normal handling below
|
|
1407
1435
|
}
|
|
1408
1436
|
}
|
|
1409
|
-
// 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)
|
|
1410
1438
|
const currentUrl = this.router.url;
|
|
1411
|
-
const
|
|
1439
|
+
const currentUrlPath = (currentUrl || '').split('#')[0].split('?')[0];
|
|
1440
|
+
const appMatch = currentUrlPath.match(/\/app\/([^\/?#]+)/);
|
|
1412
1441
|
if (appMatch) {
|
|
1413
1442
|
const routeAppPath = decodeURIComponent(appMatch[1]);
|
|
1414
1443
|
// Find the app from the URL by Path (or Name for backwards compatibility)
|
|
1415
|
-
const urlApp = this.appManager.GetAppByPath(routeAppPath);
|
|
1444
|
+
const urlApp = this.appManager.GetAppByPath(routeAppPath) || this.appManager.GetAppByName(routeAppPath);
|
|
1416
1445
|
if (urlApp) {
|
|
1417
1446
|
// Set the app from URL - takes precedence over workspace restoration
|
|
1418
1447
|
await this.appManager.SetActiveApp(urlApp.ID);
|
|
1419
1448
|
// If the URL is just /app/:appName (no nav item), create default tab
|
|
1420
|
-
const hasNavItem =
|
|
1449
|
+
const hasNavItem = currentUrlPath.match(/\/app\/[^\/]+\/[^\/]+/);
|
|
1421
1450
|
if (!hasNavItem) {
|
|
1422
1451
|
const existingTabs = this.workspaceManager.GetAppTabs(urlApp.ID);
|
|
1423
1452
|
if (existingTabs.length === 0) {
|
|
1424
1453
|
const tabRequest = await urlApp.CreateDefaultTab();
|
|
1425
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
|
+
}
|
|
1426
1462
|
this.tabService.OpenTab(tabRequest);
|
|
1427
1463
|
}
|
|
1428
1464
|
}
|
|
@@ -1545,17 +1581,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1545
1581
|
// Only restore workspace state when the URL is bare root (/) or empty.
|
|
1546
1582
|
const initialUrl = this.router.url.split('?')[0]; // ignore query params for this check
|
|
1547
1583
|
const isDeepLink = initialUrl.length > 1 && initialUrl !== '/';
|
|
1548
|
-
|
|
1584
|
+
let deepLinkResolved = false;
|
|
1549
1585
|
if (isDeepLink) {
|
|
1550
1586
|
// User navigated to a specific URL — sync workspace to match the URL,
|
|
1551
1587
|
// not the other way around. The ResourceResolver will have created a tab
|
|
1552
1588
|
// for this URL; we just need to activate it.
|
|
1553
|
-
await this.syncWorkspaceWithUrl(this.router.url);
|
|
1589
|
+
deepLinkResolved = await this.syncWorkspaceWithUrl(this.router.url);
|
|
1554
1590
|
}
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
await this.syncActiveAppWithTab(initConfig);
|
|
1558
|
-
this.syncUrlWithWorkspace(initConfig);
|
|
1591
|
+
if (!deepLinkResolved && !this.pendingAppPath) {
|
|
1592
|
+
await this.restoreOrOpenFallbackTab();
|
|
1559
1593
|
}
|
|
1560
1594
|
// Force change detection to sync Angular's expected values after all async
|
|
1561
1595
|
// state changes (apps loaded, searchableEntities populated, etc.) to prevent
|
|
@@ -1656,10 +1690,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1656
1690
|
this.titleService.reset();
|
|
1657
1691
|
return;
|
|
1658
1692
|
}
|
|
1693
|
+
const resolvedTabTitle = ResolveRecordTabDisplayTitle(activeTab, this.ProviderToUse);
|
|
1659
1694
|
// Get the tab's application ID
|
|
1660
1695
|
const tabAppId = activeTab.applicationId;
|
|
1661
1696
|
if (!tabAppId) {
|
|
1662
|
-
this.titleService.setResourceName(
|
|
1697
|
+
this.titleService.setResourceName(resolvedTabTitle);
|
|
1663
1698
|
return;
|
|
1664
1699
|
}
|
|
1665
1700
|
// Check if this is a system tab (not associated with a registered app)
|
|
@@ -1668,7 +1703,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1668
1703
|
this.cdr.detectChanges();
|
|
1669
1704
|
// Don't try to set active app - SYSTEM_APP_ID has no registered app
|
|
1670
1705
|
// Update browser title with just the tab title (no app context)
|
|
1671
|
-
this.titleService.setContext(null,
|
|
1706
|
+
this.titleService.setContext(null, resolvedTabTitle || 'Explorer');
|
|
1672
1707
|
return;
|
|
1673
1708
|
}
|
|
1674
1709
|
// Not a system tab - clear the flag
|
|
@@ -1682,7 +1717,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1682
1717
|
// stays wherever the user is. Records DOCKED to the workspace fall
|
|
1683
1718
|
// through: they are ordinary main-layout tabs and DO flip app context.
|
|
1684
1719
|
if (this.RecordTabsStyle && IsRecordsRegionTab(activeTab.configuration)) {
|
|
1685
|
-
this.titleService.setContext(this.ActiveApp?.Name || null,
|
|
1720
|
+
this.titleService.setContext(this.ActiveApp?.Name || null, resolvedTabTitle);
|
|
1686
1721
|
return;
|
|
1687
1722
|
}
|
|
1688
1723
|
// Check if active app needs to be updated
|
|
@@ -1696,7 +1731,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1696
1731
|
// (which can be stale when switching between apps in single-resource mode).
|
|
1697
1732
|
const app = this.appManager.GetAppById(tabAppId);
|
|
1698
1733
|
const appName = app?.Name || null;
|
|
1699
|
-
const tabTitle = activeTab.configuration?.['navItemName'] ||
|
|
1734
|
+
const tabTitle = activeTab.configuration?.['navItemName'] || resolvedTabTitle || null;
|
|
1700
1735
|
this.titleService.setContext(appName, tabTitle);
|
|
1701
1736
|
}
|
|
1702
1737
|
/**
|
|
@@ -1774,7 +1809,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1774
1809
|
async syncWorkspaceWithUrl(url, navigatedAt = this.lastNavigationAt) {
|
|
1775
1810
|
const config = this.workspaceManager.GetConfiguration();
|
|
1776
1811
|
if (!config?.tabs) {
|
|
1777
|
-
return;
|
|
1812
|
+
return false;
|
|
1778
1813
|
}
|
|
1779
1814
|
// Find the tab that matches this URL (null when workspace has no tabs yet,
|
|
1780
1815
|
// which falls through to handleMissingTabForUrl below so deep links still open).
|
|
@@ -1821,16 +1856,37 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1821
1856
|
// stays because it says what the comparison relies on, but removing it changes no behaviour and
|
|
1822
1857
|
// breaks no test — do not read it as the thing that handles NaN.
|
|
1823
1858
|
if (Number.isFinite(activatedAt) && activatedAt > navigatedAt && activatedAt <= Date.now()) {
|
|
1824
|
-
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
|
+
}
|
|
1825
1879
|
}
|
|
1826
1880
|
// Activate the matching tab
|
|
1827
1881
|
this.workspaceManager.SetActiveTab(matchingTab.id);
|
|
1882
|
+
return true;
|
|
1828
1883
|
}
|
|
1829
1884
|
else if (matchingTab && matchingTab.id === config.activeTabId) {
|
|
1830
1885
|
// Same tab is already active, but query params may have changed (back/forward within nav item)
|
|
1831
1886
|
const urlParams = this.extractQueryParamsFromUrl(url);
|
|
1887
|
+
const isBareAppOnlyUrl = /^\/app\/[^\/]+$/.test(url.split('?')[0]) && Object.keys(urlParams).length === 0;
|
|
1832
1888
|
const tabParams = (matchingTab.configuration?.['queryParams'] || {});
|
|
1833
|
-
if (!this.queryParamsEqual(urlParams, tabParams)) {
|
|
1889
|
+
if (!isBareAppOnlyUrl && !this.queryParamsEqual(urlParams, tabParams)) {
|
|
1834
1890
|
// URL is source of truth during back/forward — update tab config to match.
|
|
1835
1891
|
this.urlBasedNavigation = true;
|
|
1836
1892
|
try {
|
|
@@ -1843,9 +1899,60 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1843
1899
|
this.urlBasedNavigation = false;
|
|
1844
1900
|
}
|
|
1845
1901
|
}
|
|
1902
|
+
return true;
|
|
1846
1903
|
}
|
|
1847
1904
|
else if (!matchingTab) {
|
|
1848
|
-
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
|
+
}
|
|
1849
1956
|
}
|
|
1850
1957
|
}
|
|
1851
1958
|
/**
|
|
@@ -1858,9 +1965,16 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1858
1965
|
if (queryIndex === -1)
|
|
1859
1966
|
return {};
|
|
1860
1967
|
const params = new URLSearchParams(cleanUrl.substring(queryIndex + 1));
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
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);
|
|
1864
1978
|
}
|
|
1865
1979
|
/**
|
|
1866
1980
|
* Compare two query param records for equality, normalizing encoding differences
|
|
@@ -1877,6 +1991,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1877
1991
|
/**
|
|
1878
1992
|
* Handle the case where no tab matches the URL during back/forward navigation.
|
|
1879
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.
|
|
1880
1995
|
*/
|
|
1881
1996
|
async handleMissingTabForUrl(url) {
|
|
1882
1997
|
const urlPath = url.split('?')[0];
|
|
@@ -1890,23 +2005,26 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1890
2005
|
if (appRecordMatch) {
|
|
1891
2006
|
const entityName = decodeURIComponent(appRecordMatch[2]);
|
|
1892
2007
|
const recordId = decodeURIComponent(appRecordMatch[3]);
|
|
1893
|
-
this.openRecordFromUrl(entityName, recordId, queryParams);
|
|
1894
|
-
return;
|
|
2008
|
+
return this.openRecordFromUrl(entityName, recordId, queryParams);
|
|
1895
2009
|
}
|
|
1896
2010
|
// Check for app-scoped view URL: /app/:appName/view/:viewId
|
|
1897
2011
|
const appViewMatch = urlPath.match(/^\/app\/([^\/]+)\/view\/([^\/]+)$/);
|
|
1898
2012
|
if (appViewMatch && appViewMatch[2] !== 'dynamic') {
|
|
1899
2013
|
const viewId = appViewMatch[2];
|
|
1900
2014
|
this.navigationService.OpenView(viewId, 'View');
|
|
1901
|
-
return;
|
|
2015
|
+
return true;
|
|
1902
2016
|
}
|
|
1903
2017
|
// Check for app-scoped dynamic view URL: /app/:appName/view/dynamic/:entityName
|
|
1904
2018
|
const appDynamicViewMatch = urlPath.match(/^\/app\/([^\/]+)\/view\/dynamic\/(.+)$/);
|
|
1905
2019
|
if (appDynamicViewMatch) {
|
|
1906
2020
|
const entityName = decodeURIComponent(appDynamicViewMatch[2]);
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
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;
|
|
1910
2028
|
}
|
|
1911
2029
|
// Check for app-scoped dashboard URL: /app/:appName/dashboard/:dashboardId. The tab goes to
|
|
1912
2030
|
// the URL's application, where the ResourceResolver also opens it.
|
|
@@ -1915,28 +2033,28 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1915
2033
|
OpenDashboard: (dashboardId, dashboardName, options) => this.navigationService.OpenDashboard(dashboardId, dashboardName, options),
|
|
1916
2034
|
});
|
|
1917
2035
|
if (openedDashboard) {
|
|
1918
|
-
return;
|
|
2036
|
+
return true;
|
|
1919
2037
|
}
|
|
1920
2038
|
// Check for app-scoped artifact URL: /app/:appName/artifact/:artifactId
|
|
1921
2039
|
const appArtifactMatch = urlPath.match(/^\/app\/([^\/]+)\/artifact\/(.+)$/);
|
|
1922
2040
|
if (appArtifactMatch) {
|
|
1923
2041
|
const artifactId = appArtifactMatch[2];
|
|
1924
2042
|
this.navigationService.OpenArtifact(artifactId, 'Artifact');
|
|
1925
|
-
return;
|
|
2043
|
+
return true;
|
|
1926
2044
|
}
|
|
1927
2045
|
// Check for app-scoped query URL: /app/:appName/query/:queryId
|
|
1928
2046
|
const appQueryMatch = urlPath.match(/^\/app\/([^\/]+)\/query\/(.+)$/);
|
|
1929
2047
|
if (appQueryMatch) {
|
|
1930
2048
|
const queryId = appQueryMatch[2];
|
|
1931
2049
|
this.navigationService.OpenQuery(queryId, 'Query');
|
|
1932
|
-
return;
|
|
2050
|
+
return true;
|
|
1933
2051
|
}
|
|
1934
2052
|
// Check for app-scoped search URL: /app/:appName/search/:searchInput
|
|
1935
2053
|
const appSearchMatch = urlPath.match(/^\/app\/([^\/]+)\/search\/(.+)$/);
|
|
1936
2054
|
if (appSearchMatch) {
|
|
1937
2055
|
const searchInput = decodeURIComponent(appSearchMatch[2]);
|
|
1938
2056
|
this.navigationService.OpenSearch(searchInput);
|
|
1939
|
-
return;
|
|
2057
|
+
return true;
|
|
1940
2058
|
}
|
|
1941
2059
|
// Check for app nav item URL: /app/:appName/:navItemName
|
|
1942
2060
|
const appNavItemMatch = urlPath.match(/^\/app\/([^\/]+)\/([^\/]+)$/);
|
|
@@ -1948,18 +2066,18 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1948
2066
|
// Activate the app and open the nav item
|
|
1949
2067
|
await this.appManager.SetActiveApp(app.ID);
|
|
1950
2068
|
const navItems = await app.GetNavItems();
|
|
1951
|
-
const navItem = navItems.find(item => item.Label === navItemName);
|
|
2069
|
+
const navItem = navItems.find(item => item.Label?.trim().toLowerCase() === navItemName.trim().toLowerCase());
|
|
1952
2070
|
if (navItem) {
|
|
1953
2071
|
// Parse query params to pass as configuration
|
|
1954
|
-
const qpObj =
|
|
1955
|
-
queryParams.forEach((value, key) => { qpObj[key] = value; });
|
|
2072
|
+
const qpObj = this.urlSearchParamsToRecord(queryParams);
|
|
1956
2073
|
this.navigationService.OpenNavItem(app.ID, navItem, app.GetColor(), Object.keys(qpObj).length > 0 ? { queryParams: qpObj } : undefined);
|
|
2074
|
+
return true;
|
|
1957
2075
|
}
|
|
1958
2076
|
else {
|
|
1959
2077
|
console.warn('handleMissingTabForUrl: nav item not found:', navItemName, 'in app:', appPath);
|
|
1960
2078
|
}
|
|
1961
2079
|
}
|
|
1962
|
-
return;
|
|
2080
|
+
return false;
|
|
1963
2081
|
}
|
|
1964
2082
|
// Check for app-only URL: /app/:appName
|
|
1965
2083
|
const appOnlyMatch = urlPath.match(/^\/app\/([^\/]+)$/);
|
|
@@ -1967,6 +2085,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1967
2085
|
const appPath = decodeURIComponent(appOnlyMatch[1]);
|
|
1968
2086
|
const app = this.appManager.GetAppByPath(appPath) || this.appManager.GetAppByName(appPath);
|
|
1969
2087
|
if (app) {
|
|
2088
|
+
const qpObj = this.urlSearchParamsToRecord(queryParams);
|
|
1970
2089
|
const navItems = await app.GetNavItems();
|
|
1971
2090
|
// Only auto-create tabs for apps with zero nav items
|
|
1972
2091
|
// Apps with nav items should have had their tabs preserved
|
|
@@ -1975,7 +2094,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1975
2094
|
await this.appManager.SetActiveApp(app.ID);
|
|
1976
2095
|
const defaultTab = await app.CreateDefaultTab();
|
|
1977
2096
|
if (defaultTab) {
|
|
2097
|
+
if (Object.keys(qpObj).length > 0) {
|
|
2098
|
+
defaultTab.Configuration = {
|
|
2099
|
+
...(defaultTab.Configuration || {}),
|
|
2100
|
+
queryParams: qpObj
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
1978
2103
|
this.workspaceManager.OpenTab(defaultTab, app.GetColor());
|
|
2104
|
+
return true;
|
|
1979
2105
|
}
|
|
1980
2106
|
}
|
|
1981
2107
|
else {
|
|
@@ -1983,11 +2109,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1983
2109
|
await this.appManager.SetActiveApp(app.ID);
|
|
1984
2110
|
const defaultNavItem = navItems.find(item => item.isDefault) || navItems[0];
|
|
1985
2111
|
if (defaultNavItem) {
|
|
1986
|
-
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;
|
|
1987
2114
|
}
|
|
1988
2115
|
}
|
|
1989
2116
|
}
|
|
1990
|
-
return;
|
|
2117
|
+
return false;
|
|
1991
2118
|
}
|
|
1992
2119
|
// Legacy resource URLs (backward compatibility)
|
|
1993
2120
|
// Check for legacy record URL: /resource/record/:entityName/:recordId
|
|
@@ -1995,45 +2122,105 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1995
2122
|
if (legacyRecordMatch) {
|
|
1996
2123
|
const entityName = decodeURIComponent(legacyRecordMatch[1]);
|
|
1997
2124
|
const recordId = decodeURIComponent(legacyRecordMatch[2]);
|
|
1998
|
-
this.openRecordFromUrl(entityName, recordId, queryParams);
|
|
1999
|
-
|
|
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;
|
|
2000
2138
|
}
|
|
2001
2139
|
// Check for legacy view URL: /resource/view/:viewId
|
|
2002
2140
|
const legacyViewMatch = urlPath.match(/^\/resource\/view\/([^\/]+)$/);
|
|
2003
2141
|
if (legacyViewMatch && legacyViewMatch[1] !== 'dynamic') {
|
|
2004
2142
|
const viewId = legacyViewMatch[1];
|
|
2005
2143
|
this.navigationService.OpenView(viewId, 'View');
|
|
2006
|
-
return;
|
|
2144
|
+
return true;
|
|
2007
2145
|
}
|
|
2008
2146
|
// Check for legacy dashboard URL: /resource/dashboard/:dashboardId
|
|
2009
2147
|
const legacyDashboardMatch = urlPath.match(/^\/resource\/dashboard\/(.+)$/);
|
|
2010
2148
|
if (legacyDashboardMatch) {
|
|
2011
2149
|
const dashboardId = legacyDashboardMatch[1];
|
|
2012
2150
|
this.navigationService.OpenDashboard(dashboardId, 'Dashboard');
|
|
2013
|
-
return;
|
|
2151
|
+
return true;
|
|
2014
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;
|
|
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;
|
|
2015
2172
|
}
|
|
2016
2173
|
finally {
|
|
2017
2174
|
this.urlBasedNavigation = false;
|
|
2018
2175
|
}
|
|
2019
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
|
+
}
|
|
2020
2198
|
/**
|
|
2021
2199
|
* Recreate a record tab from a URL. `/new` is OpenNewEntityRecord — feeding
|
|
2022
2200
|
* the sentinel through OpenEntityRecord produced an empty CompositeKey and
|
|
2023
2201
|
* another forced tab, which is the loop that killed the browser.
|
|
2024
2202
|
*/
|
|
2025
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) });
|
|
2026
2211
|
if (IsNewEntityRecordUrlId(recordId)) {
|
|
2027
2212
|
const nrv = queryParams.get(NEW_RECORD_VALUES_QUERY_PARAM) ?? undefined;
|
|
2028
2213
|
this.navigationService.OpenNewEntityRecord(entityName, {
|
|
2029
2214
|
newRecordValues: nrv,
|
|
2030
2215
|
recordSource: 'none',
|
|
2216
|
+
formMode,
|
|
2031
2217
|
});
|
|
2032
|
-
return;
|
|
2218
|
+
return true;
|
|
2033
2219
|
}
|
|
2034
2220
|
const compositeKey = new CompositeKey();
|
|
2035
2221
|
compositeKey.SimpleLoadFromURLSegment(recordId);
|
|
2036
|
-
this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none' });
|
|
2222
|
+
this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none', formMode });
|
|
2223
|
+
return true;
|
|
2037
2224
|
}
|
|
2038
2225
|
recordTabMatchesUrl(tab, entityName, recordId) {
|
|
2039
2226
|
const tabConfig = tab.configuration || {};
|
|
@@ -2088,6 +2275,18 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2088
2275
|
if (navItems.length === 0) {
|
|
2089
2276
|
return tabs.find(tab => UUIDsEqual(tab.applicationId, app.ID)) || null;
|
|
2090
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
|
+
}
|
|
2091
2290
|
return null;
|
|
2092
2291
|
}
|
|
2093
2292
|
}
|
|
@@ -2115,7 +2314,9 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2115
2314
|
const resourceType = tabConfig['resourceType']?.toLowerCase();
|
|
2116
2315
|
const tabEntity = (tabConfig['Entity'] || tabConfig['entity']);
|
|
2117
2316
|
const isDynamic = tabConfig['isDynamic'];
|
|
2118
|
-
return resourceType === 'user views'
|
|
2317
|
+
return (resourceType === 'user views' || resourceType === 'mj: user views') &&
|
|
2318
|
+
isDynamic &&
|
|
2319
|
+
tabEntity?.toLowerCase() === entityName.toLowerCase();
|
|
2119
2320
|
}) || null;
|
|
2120
2321
|
}
|
|
2121
2322
|
// Saved view: /app/:appName/view/:viewId
|
|
@@ -2126,7 +2327,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2126
2327
|
const tabConfig = tab.configuration || {};
|
|
2127
2328
|
const resourceType = tabConfig['resourceType']?.toLowerCase();
|
|
2128
2329
|
const tabViewId = (tabConfig['viewId'] || tabConfig['recordId'] || tab.resourceRecordId);
|
|
2129
|
-
return resourceType === 'user views' && tabViewId === viewId;
|
|
2330
|
+
return (resourceType === 'user views' || resourceType === 'mj: user views') && tabViewId === viewId;
|
|
2130
2331
|
}) || null;
|
|
2131
2332
|
}
|
|
2132
2333
|
// Query: /app/:appName/query/:queryId
|
|
@@ -2237,6 +2438,14 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2237
2438
|
const params = new URLSearchParams(all);
|
|
2238
2439
|
return `${url}${separator}${params.toString()}`;
|
|
2239
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
|
+
};
|
|
2240
2449
|
// Helper function to get app path for URL
|
|
2241
2450
|
const getAppPath = (appIdOrName) => {
|
|
2242
2451
|
// First try by ID
|
|
@@ -2266,7 +2475,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2266
2475
|
// If this is an app's default dashboard (no nav items), use app-level URL
|
|
2267
2476
|
if (isAppDefault && appName) {
|
|
2268
2477
|
const appPath = getAppPath(appName) || appName;
|
|
2269
|
-
return `/app/${encodeURIComponent(appPath)}
|
|
2478
|
+
return appendQP(`/app/${encodeURIComponent(appPath)}`);
|
|
2270
2479
|
}
|
|
2271
2480
|
// Fallback: If tab belongs to a non-system app but doesn't have appName/navItemName,
|
|
2272
2481
|
// try to reconstruct the URL from the ApplicationId and tab title
|
|
@@ -2308,7 +2517,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2308
2517
|
if (staticNavItems.length === 0 && (!resourceType || resourceType === 'custom')) {
|
|
2309
2518
|
// App has zero static nav items AND this is not an orphan resource (no resourceType or custom)
|
|
2310
2519
|
// Use app-level URL - this handles apps that only have a default dashboard
|
|
2311
|
-
return `/app/${encodeURIComponent(appPath)}
|
|
2520
|
+
return appendQP(`/app/${encodeURIComponent(appPath)}`);
|
|
2312
2521
|
}
|
|
2313
2522
|
// Fall through to orphan resource URL building below
|
|
2314
2523
|
}
|
|
@@ -2347,21 +2556,26 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2347
2556
|
// here, `syncUrlWithWorkspace`'s `currentUrl !== newUrl` check is permanently true and can
|
|
2348
2557
|
// drive a re-navigation loop (with onSameUrlNavigation:'reload'). The read side already
|
|
2349
2558
|
// decodeURIComponent()s this segment, so encoding here keeps both sides consistent.
|
|
2350
|
-
|
|
2351
|
-
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));
|
|
2352
2560
|
}
|
|
2353
2561
|
}
|
|
2354
2562
|
break;
|
|
2355
2563
|
case 'user views':
|
|
2564
|
+
case 'mj: user views':
|
|
2356
2565
|
// Check if it's a dynamic view
|
|
2357
2566
|
if (isDynamic || recordId === 'dynamic') {
|
|
2358
|
-
// /app/:appName/view/dynamic/:entityName?ExtraFilter=...
|
|
2567
|
+
// /app/:appName/view/dynamic/:entityName?ExtraFilter=...&abv=...
|
|
2359
2568
|
if (entityName) {
|
|
2360
|
-
|
|
2569
|
+
const url = `/app/${encodeURIComponent(appPath)}/view/dynamic/${encodeURIComponent(entityName)}`;
|
|
2570
|
+
const extraParams = {};
|
|
2361
2571
|
if (extraFilter) {
|
|
2362
|
-
|
|
2572
|
+
extraParams['ExtraFilter'] = extraFilter;
|
|
2363
2573
|
}
|
|
2364
|
-
|
|
2574
|
+
const abv = (config['AlternateBaseView'] || config['alternateBaseView']);
|
|
2575
|
+
if (abv) {
|
|
2576
|
+
extraParams['abv'] = abv;
|
|
2577
|
+
}
|
|
2578
|
+
return appendQP(url, extraParams);
|
|
2365
2579
|
}
|
|
2366
2580
|
}
|
|
2367
2581
|
else if (recordId) {
|
|
@@ -2420,8 +2634,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
2420
2634
|
if (idSeg) {
|
|
2421
2635
|
// Encode the CompositeKey segment ('|' → %7C) to match Angular's serialized router.url and
|
|
2422
2636
|
// the decodeURIComponent() on the read side — see the app-scoped 'records' case above.
|
|
2423
|
-
|
|
2424
|
-
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));
|
|
2425
2638
|
}
|
|
2426
2639
|
}
|
|
2427
2640
|
break;
|
|
@@ -3350,9 +3563,8 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3350
3563
|
if (compositeKey.KeyValuePairs.length === 0)
|
|
3351
3564
|
return null;
|
|
3352
3565
|
const results = await md.GetEntityRecordNames([{ EntityName: entityName, CompositeKey: compositeKey }]);
|
|
3353
|
-
|
|
3354
|
-
|
|
3355
|
-
}
|
|
3566
|
+
const rawName = results.length > 0 && results[0].Success ? results[0].RecordName : null;
|
|
3567
|
+
return FormatEntityRecordDisplayTitle(entityInfo, entityName, compositeKey, rawName);
|
|
3356
3568
|
}
|
|
3357
3569
|
catch {
|
|
3358
3570
|
// Fall through to null
|
|
@@ -3549,7 +3761,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
3549
3761
|
// Get resource name from active tab
|
|
3550
3762
|
let resourceName = null;
|
|
3551
3763
|
if (activeTab) {
|
|
3552
|
-
resourceName = activeTab.
|
|
3764
|
+
resourceName = ResolveRecordTabDisplayTitle(activeTab, this.ProviderToUse);
|
|
3553
3765
|
}
|
|
3554
3766
|
// Update title via TitleService
|
|
3555
3767
|
this.titleService.setContext(appName, resourceName);
|