@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.
- package/README.md +7 -2
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +20 -10
- 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 +6 -3
- package/dist/generated/lazy-feature-config.js.map +1 -1
- package/dist/lib/oauth/oauth-callback.component.d.ts +2 -0
- package/dist/lib/oauth/oauth-callback.component.d.ts.map +1 -1
- package/dist/lib/oauth/oauth-callback.component.js +10 -10
- package/dist/lib/oauth/oauth-callback.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts +17 -3
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js +211 -13
- package/dist/lib/resource-wrappers/chat-conversations-resource.component.js.map +1 -1
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts +12 -3
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.js +18 -3
- package/dist/lib/resource-wrappers/chat-tasks-resource.component.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/search-results-resource.component.d.ts +3 -3
- package/dist/lib/resource-wrappers/search-results-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/search-results-resource.component.js +5 -10
- package/dist/lib/resource-wrappers/search-results-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/server-connectivity/server-connectivity-banner.component.d.ts +16 -3
- package/dist/lib/server-connectivity/server-connectivity-banner.component.d.ts.map +1 -1
- package/dist/lib/server-connectivity/server-connectivity-banner.component.js +17 -3
- package/dist/lib/server-connectivity/server-connectivity-banner.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 +36 -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 +188 -47
- package/dist/lib/shell/components/tabs/tab-container.component.js.map +1 -1
- package/dist/lib/shell/deep-link-workspace.d.ts +25 -0
- package/dist/lib/shell/deep-link-workspace.d.ts.map +1 -0
- package/dist/lib/shell/deep-link-workspace.js +49 -0
- package/dist/lib/shell/deep-link-workspace.js.map +1 -0
- 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 +35 -0
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +348 -78
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/lib/single-record/single-record.component.d.ts +39 -2
- package/dist/lib/single-record/single-record.component.d.ts.map +1 -1
- package/dist/lib/single-record/single-record.component.js +99 -12
- 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 } 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
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
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
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
1542
|
-
|
|
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(
|
|
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,
|
|
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,
|
|
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'] ||
|
|
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
|
-
|
|
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
|
|
1720
|
-
return;
|
|
1811
|
+
if (!config?.tabs) {
|
|
1812
|
+
return false;
|
|
1721
1813
|
}
|
|
1722
|
-
// Find the tab that matches this URL
|
|
1723
|
-
|
|
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
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
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
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
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
|
-
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
2569
|
+
const url = `/app/${encodeURIComponent(appPath)}/view/dynamic/${encodeURIComponent(entityName)}`;
|
|
2570
|
+
const extraParams = {};
|
|
2303
2571
|
if (extraFilter) {
|
|
2304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3296
|
-
|
|
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.
|
|
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:
|
|
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
|