@memberjunction/ng-explorer-core 6.1.0-edge.2 → 6.1.0-edge.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +180 -4
- package/README.md +1 -1
- package/dist/app-routing.module.d.ts.map +1 -1
- package/dist/app-routing.module.js +33 -7
- package/dist/app-routing.module.js.map +1 -1
- package/dist/lib/identity-claims/claim-redeem.component.d.ts +64 -0
- package/dist/lib/identity-claims/claim-redeem.component.d.ts.map +1 -0
- package/dist/lib/identity-claims/claim-redeem.component.js +276 -0
- package/dist/lib/identity-claims/claim-redeem.component.js.map +1 -0
- package/dist/lib/resource-wrappers/record-resource.component.d.ts.map +1 -1
- package/dist/lib/resource-wrappers/record-resource.component.js +4 -1
- package/dist/lib/resource-wrappers/record-resource.component.js.map +1 -1
- package/dist/lib/shell/shell.component.d.ts +7 -0
- package/dist/lib/shell/shell.component.d.ts.map +1 -1
- package/dist/lib/shell/shell.component.js +60 -42
- package/dist/lib/shell/shell.component.js.map +1 -1
- package/dist/module.d.ts +59 -58
- package/dist/module.d.ts.map +1 -1
- package/dist/module.js +3 -0
- package/dist/module.js.map +1 -1
- package/package.json +53 -53
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Identity Claim Redemption Page
|
|
3
|
+
*
|
|
4
|
+
* Landing page for the links in identity-claim emails
|
|
5
|
+
* (`${PORTAL_BASE_URL}/claims/redeem?id=<claimId>&token=<rawToken>`). When a recipient
|
|
6
|
+
* clicks through, this component:
|
|
7
|
+
* 1. Waits for the user's session to be restored (same posture as the OAuth callback —
|
|
8
|
+
* no AuthGuard; the component handles the not-yet-authenticated case itself)
|
|
9
|
+
* 2. Extracts the claim id and optional verification token from the query params
|
|
10
|
+
* 3. Calls the `RedeemIdentityClaim` GraphQL mutation, which runs the server engine's full
|
|
11
|
+
* gate (email match subject to verification rules, or timing-safe token match, atomic CAS)
|
|
12
|
+
* 4. Shows the outcome and offers navigation back into the app
|
|
13
|
+
*
|
|
14
|
+
* Like the OAuth callback, this is a deliberate exception to the NavigationService-only rule:
|
|
15
|
+
* it is registered directly in the routing module and reads its own query params.
|
|
16
|
+
*
|
|
17
|
+
* @module IdentityClaims
|
|
18
|
+
*/
|
|
19
|
+
import { Component } from '@angular/core';
|
|
20
|
+
import { BaseAngularComponent } from '@memberjunction/ng-base-types';
|
|
21
|
+
import * as i0 from "@angular/core";
|
|
22
|
+
import * as i1 from "@angular/router";
|
|
23
|
+
import * as i2 from "@memberjunction/ng-shared-generic";
|
|
24
|
+
import * as i3 from "@memberjunction/ng-ui-components";
|
|
25
|
+
function ClaimRedeemComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
26
|
+
i0.ɵɵelementStart(0, "div", 2);
|
|
27
|
+
i0.ɵɵelement(1, "mj-loading", 5);
|
|
28
|
+
i0.ɵɵelementEnd();
|
|
29
|
+
} if (rf & 2) {
|
|
30
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
31
|
+
i0.ɵɵadvance();
|
|
32
|
+
i0.ɵɵproperty("text", i0.ɵɵinterpolate(ctx_r0.StatusMessage));
|
|
33
|
+
} }
|
|
34
|
+
function ClaimRedeemComponent_Conditional_3_Conditional_8_Template(rf, ctx) { if (rf & 1) {
|
|
35
|
+
const _r3 = i0.ɵɵgetCurrentView();
|
|
36
|
+
i0.ɵɵelementStart(0, "button", 13);
|
|
37
|
+
i0.ɵɵlistener("click", function ClaimRedeemComponent_Conditional_3_Conditional_8_Template_button_click_0_listener() { i0.ɵɵrestoreView(_r3); const ctx_r0 = i0.ɵɵnextContext(2); return i0.ɵɵresetView(ctx_r0.onRetry()); });
|
|
38
|
+
i0.ɵɵelement(1, "i", 14);
|
|
39
|
+
i0.ɵɵtext(2, " Try Again ");
|
|
40
|
+
i0.ɵɵelementEnd();
|
|
41
|
+
} }
|
|
42
|
+
function ClaimRedeemComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
43
|
+
const _r2 = i0.ɵɵgetCurrentView();
|
|
44
|
+
i0.ɵɵelementStart(0, "div", 3)(1, "div", 6);
|
|
45
|
+
i0.ɵɵelement(2, "i", 7);
|
|
46
|
+
i0.ɵɵelementEnd();
|
|
47
|
+
i0.ɵɵelementStart(3, "h2");
|
|
48
|
+
i0.ɵɵtext(4);
|
|
49
|
+
i0.ɵɵelementEnd();
|
|
50
|
+
i0.ɵɵelementStart(5, "p", 8);
|
|
51
|
+
i0.ɵɵtext(6);
|
|
52
|
+
i0.ɵɵelementEnd();
|
|
53
|
+
i0.ɵɵelementStart(7, "div", 9);
|
|
54
|
+
i0.ɵɵconditionalCreate(8, ClaimRedeemComponent_Conditional_3_Conditional_8_Template, 3, 0, "button", 10);
|
|
55
|
+
i0.ɵɵelementStart(9, "button", 11);
|
|
56
|
+
i0.ɵɵlistener("click", function ClaimRedeemComponent_Conditional_3_Template_button_click_9_listener() { i0.ɵɵrestoreView(_r2); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onGoToApp()); });
|
|
57
|
+
i0.ɵɵelement(10, "i", 12);
|
|
58
|
+
i0.ɵɵtext(11);
|
|
59
|
+
i0.ɵɵelementEnd()()();
|
|
60
|
+
} if (rf & 2) {
|
|
61
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
62
|
+
i0.ɵɵadvance(4);
|
|
63
|
+
i0.ɵɵtextInterpolate(ctx_r0.NeedsSignIn ? "Sign In Required" : "Claim Not Redeemed");
|
|
64
|
+
i0.ɵɵadvance(2);
|
|
65
|
+
i0.ɵɵtextInterpolate(ctx_r0.ErrorMessage);
|
|
66
|
+
i0.ɵɵadvance(2);
|
|
67
|
+
i0.ɵɵconditional(!ctx_r0.NeedsSignIn ? 8 : -1);
|
|
68
|
+
i0.ɵɵadvance();
|
|
69
|
+
i0.ɵɵproperty("variant", ctx_r0.NeedsSignIn ? "primary" : "outline");
|
|
70
|
+
i0.ɵɵadvance(2);
|
|
71
|
+
i0.ɵɵtextInterpolate1(" ", ctx_r0.NeedsSignIn ? "Go to Sign In" : "Open the App", " ");
|
|
72
|
+
} }
|
|
73
|
+
function ClaimRedeemComponent_Conditional_4_Template(rf, ctx) { if (rf & 1) {
|
|
74
|
+
const _r4 = i0.ɵɵgetCurrentView();
|
|
75
|
+
i0.ɵɵelementStart(0, "div", 4)(1, "div", 15);
|
|
76
|
+
i0.ɵɵelement(2, "i", 16);
|
|
77
|
+
i0.ɵɵelementEnd();
|
|
78
|
+
i0.ɵɵelementStart(3, "h2");
|
|
79
|
+
i0.ɵɵtext(4, "Claim Redeemed");
|
|
80
|
+
i0.ɵɵelementEnd();
|
|
81
|
+
i0.ɵɵelementStart(5, "p");
|
|
82
|
+
i0.ɵɵtext(6);
|
|
83
|
+
i0.ɵɵelementEnd();
|
|
84
|
+
i0.ɵɵelementStart(7, "div", 9)(8, "button", 17);
|
|
85
|
+
i0.ɵɵlistener("click", function ClaimRedeemComponent_Conditional_4_Template_button_click_8_listener() { i0.ɵɵrestoreView(_r4); const ctx_r0 = i0.ɵɵnextContext(); return i0.ɵɵresetView(ctx_r0.onGoToApp()); });
|
|
86
|
+
i0.ɵɵelement(9, "i", 18);
|
|
87
|
+
i0.ɵɵtext(10, " Open the App ");
|
|
88
|
+
i0.ɵɵelementEnd()()();
|
|
89
|
+
} if (rf & 2) {
|
|
90
|
+
const ctx_r0 = i0.ɵɵnextContext();
|
|
91
|
+
i0.ɵɵadvance(6);
|
|
92
|
+
i0.ɵɵtextInterpolate(ctx_r0.StatusMessage);
|
|
93
|
+
} }
|
|
94
|
+
/** Maximum time to wait for session restoration (30 seconds) */
|
|
95
|
+
const MAX_WAIT_TIME_MS = 30000;
|
|
96
|
+
/** Polling interval for checking provider readiness (500ms) */
|
|
97
|
+
const POLL_INTERVAL_MS = 500;
|
|
98
|
+
const REDEEM_MUTATION = `
|
|
99
|
+
mutation RedeemIdentityClaim($ClaimID: String!, $Token: String) {
|
|
100
|
+
RedeemIdentityClaim(ClaimID: $ClaimID, Token: $Token) {
|
|
101
|
+
Success
|
|
102
|
+
ErrorMessage
|
|
103
|
+
ClaimID
|
|
104
|
+
Data
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
`;
|
|
108
|
+
export class ClaimRedeemComponent extends BaseAngularComponent {
|
|
109
|
+
route;
|
|
110
|
+
router;
|
|
111
|
+
cdr;
|
|
112
|
+
/** Loading state while restoring the session / redeeming */
|
|
113
|
+
IsLoading = true;
|
|
114
|
+
/** Terminal error state */
|
|
115
|
+
HasError = false;
|
|
116
|
+
/** Error message to display */
|
|
117
|
+
ErrorMessage = '';
|
|
118
|
+
/** True when the wait for authentication timed out (user likely not signed in) */
|
|
119
|
+
NeedsSignIn = false;
|
|
120
|
+
/** Status message shown during processing */
|
|
121
|
+
StatusMessage = 'Restoring session...';
|
|
122
|
+
/** Timer for polling provider readiness */
|
|
123
|
+
pollTimer = null;
|
|
124
|
+
/** Flag to prevent double processing */
|
|
125
|
+
isProcessing = false;
|
|
126
|
+
constructor(route, router, cdr) {
|
|
127
|
+
super();
|
|
128
|
+
this.route = route;
|
|
129
|
+
this.router = router;
|
|
130
|
+
this.cdr = cdr;
|
|
131
|
+
}
|
|
132
|
+
async ngOnInit() {
|
|
133
|
+
await this.waitForProviderAndRedeem();
|
|
134
|
+
}
|
|
135
|
+
ngOnDestroy() {
|
|
136
|
+
if (this.pollTimer) {
|
|
137
|
+
clearInterval(this.pollTimer);
|
|
138
|
+
this.pollTimer = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Waits for the GraphQL provider to be initialized (indicating auth is complete),
|
|
143
|
+
* then redeems the claim from the query params.
|
|
144
|
+
*/
|
|
145
|
+
async waitForProviderAndRedeem() {
|
|
146
|
+
const startTime = Date.now();
|
|
147
|
+
if (this.isProviderReady()) {
|
|
148
|
+
await this.safeRedeem();
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
this.StatusMessage = 'Waiting for sign-in...';
|
|
152
|
+
this.cdr.detectChanges();
|
|
153
|
+
return new Promise((resolve) => {
|
|
154
|
+
this.pollTimer = setInterval(async () => {
|
|
155
|
+
const elapsed = Date.now() - startTime;
|
|
156
|
+
if (this.isProviderReady()) {
|
|
157
|
+
if (this.pollTimer) {
|
|
158
|
+
clearInterval(this.pollTimer);
|
|
159
|
+
this.pollTimer = null;
|
|
160
|
+
}
|
|
161
|
+
await this.safeRedeem();
|
|
162
|
+
resolve();
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
if (elapsed >= MAX_WAIT_TIME_MS) {
|
|
166
|
+
if (this.pollTimer) {
|
|
167
|
+
clearInterval(this.pollTimer);
|
|
168
|
+
this.pollTimer = null;
|
|
169
|
+
}
|
|
170
|
+
this.NeedsSignIn = true;
|
|
171
|
+
this.showError('Please sign in first, then open the link from your email again — the claim will attach to your signed-in account.');
|
|
172
|
+
resolve();
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
const secondsRemaining = Math.ceil((MAX_WAIT_TIME_MS - elapsed) / 1000);
|
|
176
|
+
this.StatusMessage = `Waiting for sign-in... (${secondsRemaining}s)`;
|
|
177
|
+
this.cdr.detectChanges();
|
|
178
|
+
}, POLL_INTERVAL_MS);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
/** Checks if the GraphQL provider is initialized with a valid token */
|
|
182
|
+
isProviderReady() {
|
|
183
|
+
try {
|
|
184
|
+
const provider = this.ProviderToUse;
|
|
185
|
+
if (!provider) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const configData = provider.ConfigData;
|
|
189
|
+
return Boolean(configData && configData.Token && configData.URL);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
/** Redeems exactly once, converting any throw into the error state */
|
|
196
|
+
async safeRedeem() {
|
|
197
|
+
if (this.isProcessing) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.isProcessing = true;
|
|
201
|
+
try {
|
|
202
|
+
await this.redeemClaim();
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
console.error('[Claim Redeem] Error redeeming claim:', error);
|
|
206
|
+
this.showError(error instanceof Error ? error.message : String(error));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
/** Reads the query params and calls the redemption mutation */
|
|
210
|
+
async redeemClaim() {
|
|
211
|
+
const params = this.route.snapshot.queryParams;
|
|
212
|
+
const claimId = params['id'];
|
|
213
|
+
const token = params['token'];
|
|
214
|
+
if (!claimId) {
|
|
215
|
+
this.showError('This link is missing its claim id. Please use the exact link from your email.');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.StatusMessage = 'Redeeming your claim...';
|
|
219
|
+
this.cdr.detectChanges();
|
|
220
|
+
const provider = this.ProviderToUse;
|
|
221
|
+
const result = (await provider.ExecuteGQL(REDEEM_MUTATION, {
|
|
222
|
+
ClaimID: claimId,
|
|
223
|
+
Token: token ?? null
|
|
224
|
+
}));
|
|
225
|
+
const outcome = result?.RedeemIdentityClaim;
|
|
226
|
+
if (outcome?.Success) {
|
|
227
|
+
this.IsLoading = false;
|
|
228
|
+
this.StatusMessage = 'Your claim has been redeemed and attached to your account.';
|
|
229
|
+
this.cdr.detectChanges();
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
this.showError(outcome?.ErrorMessage || 'The claim could not be redeemed.');
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
/** Shows the terminal error state */
|
|
236
|
+
showError(message) {
|
|
237
|
+
this.IsLoading = false;
|
|
238
|
+
this.HasError = true;
|
|
239
|
+
this.ErrorMessage = message;
|
|
240
|
+
this.cdr.detectChanges();
|
|
241
|
+
}
|
|
242
|
+
/** Navigates into the app shell (which handles sign-in when needed) */
|
|
243
|
+
onGoToApp() {
|
|
244
|
+
this.router.navigateByUrl('/');
|
|
245
|
+
}
|
|
246
|
+
/** Retries redemption from scratch (fresh component state) */
|
|
247
|
+
onRetry() {
|
|
248
|
+
this.IsLoading = true;
|
|
249
|
+
this.HasError = false;
|
|
250
|
+
this.NeedsSignIn = false;
|
|
251
|
+
this.ErrorMessage = '';
|
|
252
|
+
this.isProcessing = false;
|
|
253
|
+
void this.waitForProviderAndRedeem();
|
|
254
|
+
}
|
|
255
|
+
static ɵfac = function ClaimRedeemComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || ClaimRedeemComponent)(i0.ɵɵdirectiveInject(i1.ActivatedRoute), i0.ɵɵdirectiveInject(i1.Router), i0.ɵɵdirectiveInject(i0.ChangeDetectorRef)); };
|
|
256
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: ClaimRedeemComponent, selectors: [["mj-claim-redeem"]], standalone: false, features: [i0.ɵɵInheritDefinitionFeature], decls: 5, vars: 3, consts: [[1, "claim-redeem-container"], [1, "claim-redeem-card"], [1, "claim-status", "loading"], [1, "claim-status", "error"], [1, "claim-status", "success"], [3, "text"], [1, "icon-container", "error-icon"], [1, "fa-solid", "fa-circle-exclamation"], [1, "error-message"], [1, "button-group"], ["mjButton", "", "variant", "secondary"], ["mjButton", "", 3, "click", "variant"], [1, "fa-solid", "fa-arrow-right-to-bracket"], ["mjButton", "", "variant", "secondary", 3, "click"], [1, "fa-solid", "fa-rotate-right"], [1, "icon-container", "success-icon"], [1, "fa-solid", "fa-circle-check"], ["mjButton", "", "variant", "primary", 3, "click"], [1, "fa-solid", "fa-house"]], template: function ClaimRedeemComponent_Template(rf, ctx) { if (rf & 1) {
|
|
257
|
+
i0.ɵɵelementStart(0, "div", 0)(1, "div", 1);
|
|
258
|
+
i0.ɵɵconditionalCreate(2, ClaimRedeemComponent_Conditional_2_Template, 2, 2, "div", 2);
|
|
259
|
+
i0.ɵɵconditionalCreate(3, ClaimRedeemComponent_Conditional_3_Template, 12, 5, "div", 3);
|
|
260
|
+
i0.ɵɵconditionalCreate(4, ClaimRedeemComponent_Conditional_4_Template, 11, 1, "div", 4);
|
|
261
|
+
i0.ɵɵelementEnd()();
|
|
262
|
+
} if (rf & 2) {
|
|
263
|
+
i0.ɵɵadvance(2);
|
|
264
|
+
i0.ɵɵconditional(ctx.IsLoading ? 2 : -1);
|
|
265
|
+
i0.ɵɵadvance();
|
|
266
|
+
i0.ɵɵconditional(ctx.HasError ? 3 : -1);
|
|
267
|
+
i0.ɵɵadvance();
|
|
268
|
+
i0.ɵɵconditional(!ctx.IsLoading && !ctx.HasError ? 4 : -1);
|
|
269
|
+
} }, dependencies: [i2.LoadingComponent, i3.MJButtonDirective], styles: [".claim-redeem-container[_ngcontent-%COMP%] {\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n background: var(--mj-brand-primary);\n padding: 1rem;\n}\n\n.claim-redeem-card[_ngcontent-%COMP%] {\n background: var(--mj-bg-surface);\n padding: 3rem;\n border-radius: 1rem;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n text-align: center;\n max-width: 450px;\n width: 100%;\n}\n\n.claim-status[_ngcontent-%COMP%] {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.claim-status[_ngcontent-%COMP%] h2[_ngcontent-%COMP%] {\n margin: 0;\n font-size: 1.5rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.claim-status[_ngcontent-%COMP%] p[_ngcontent-%COMP%] {\n margin: 0;\n color: var(--mj-text-muted);\n line-height: 1.6;\n}\n\n.icon-container[_ngcontent-%COMP%] {\n font-size: 3rem;\n}\n\n.success-icon[_ngcontent-%COMP%] {\n color: var(--mj-status-success);\n}\n\n.error-icon[_ngcontent-%COMP%] {\n color: var(--mj-status-error);\n}\n\n.error-message[_ngcontent-%COMP%] {\n color: var(--mj-text-secondary);\n}\n\n.button-group[_ngcontent-%COMP%] {\n display: flex;\n gap: 0.75rem;\n margin-top: 0.5rem;\n flex-wrap: wrap;\n justify-content: center;\n}"] });
|
|
270
|
+
}
|
|
271
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(ClaimRedeemComponent, [{
|
|
272
|
+
type: Component,
|
|
273
|
+
args: [{ standalone: false, selector: 'mj-claim-redeem', template: "<div class=\"claim-redeem-container\">\n <div class=\"claim-redeem-card\">\n <!-- Loading State -->\n @if (IsLoading) {\n <div class=\"claim-status loading\">\n <mj-loading text=\"{{ StatusMessage }}\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (HasError) {\n <div class=\"claim-status error\">\n <div class=\"icon-container error-icon\">\n <i class=\"fa-solid fa-circle-exclamation\"></i>\n </div>\n <h2>{{ NeedsSignIn ? 'Sign In Required' : 'Claim Not Redeemed' }}</h2>\n <p class=\"error-message\">{{ ErrorMessage }}</p>\n <div class=\"button-group\">\n @if (!NeedsSignIn) {\n <button mjButton variant=\"secondary\" (click)=\"onRetry()\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n Try Again\n </button>\n }\n <button mjButton [variant]=\"NeedsSignIn ? 'primary' : 'outline'\" (click)=\"onGoToApp()\">\n <i class=\"fa-solid fa-arrow-right-to-bracket\"></i>\n {{ NeedsSignIn ? 'Go to Sign In' : 'Open the App' }}\n </button>\n </div>\n </div>\n }\n\n <!-- Success State -->\n @if (!IsLoading && !HasError) {\n <div class=\"claim-status success\">\n <div class=\"icon-container success-icon\">\n <i class=\"fa-solid fa-circle-check\"></i>\n </div>\n <h2>Claim Redeemed</h2>\n <p>{{ StatusMessage }}</p>\n <div class=\"button-group\">\n <button mjButton variant=\"primary\" (click)=\"onGoToApp()\">\n <i class=\"fa-solid fa-house\"></i>\n Open the App\n </button>\n </div>\n </div>\n }\n </div>\n</div>\n", styles: [".claim-redeem-container {\n display: flex;\n justify-content: center;\n align-items: center;\n min-height: 100vh;\n background: var(--mj-brand-primary);\n padding: 1rem;\n}\n\n.claim-redeem-card {\n background: var(--mj-bg-surface);\n padding: 3rem;\n border-radius: 1rem;\n box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);\n text-align: center;\n max-width: 450px;\n width: 100%;\n}\n\n.claim-status {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 1rem;\n}\n\n.claim-status h2 {\n margin: 0;\n font-size: 1.5rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.claim-status p {\n margin: 0;\n color: var(--mj-text-muted);\n line-height: 1.6;\n}\n\n.icon-container {\n font-size: 3rem;\n}\n\n.success-icon {\n color: var(--mj-status-success);\n}\n\n.error-icon {\n color: var(--mj-status-error);\n}\n\n.error-message {\n color: var(--mj-text-secondary);\n}\n\n.button-group {\n display: flex;\n gap: 0.75rem;\n margin-top: 0.5rem;\n flex-wrap: wrap;\n justify-content: center;\n}\n"] }]
|
|
274
|
+
}], () => [{ type: i1.ActivatedRoute }, { type: i1.Router }, { type: i0.ChangeDetectorRef }], null); })();
|
|
275
|
+
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassDebugInfo(ClaimRedeemComponent, { className: "ClaimRedeemComponent", filePath: "src/lib/identity-claims/claim-redeem.component.ts", lineNumber: 56 }); })();
|
|
276
|
+
//# sourceMappingURL=claim-redeem.component.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claim-redeem.component.js","sourceRoot":"","sources":["../../../src/lib/identity-claims/claim-redeem.component.ts","../../../src/lib/identity-claims/claim-redeem.component.html"],"names":[],"mappings":"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","sourcesContent":["/**\n * @fileoverview Identity Claim Redemption Page\n *\n * Landing page for the links in identity-claim emails\n * (`${PORTAL_BASE_URL}/claims/redeem?id=<claimId>&token=<rawToken>`). When a recipient\n * clicks through, this component:\n * 1. Waits for the user's session to be restored (same posture as the OAuth callback —\n * no AuthGuard; the component handles the not-yet-authenticated case itself)\n * 2. Extracts the claim id and optional verification token from the query params\n * 3. Calls the `RedeemIdentityClaim` GraphQL mutation, which runs the server engine's full\n * gate (email match subject to verification rules, or timing-safe token match, atomic CAS)\n * 4. Shows the outcome and offers navigation back into the app\n *\n * Like the OAuth callback, this is a deliberate exception to the NavigationService-only rule:\n * it is registered directly in the routing module and reads its own query params.\n *\n * @module IdentityClaims\n */\n\nimport { Component, OnDestroy, OnInit, ChangeDetectorRef } from '@angular/core';\nimport { ActivatedRoute, Router } from '@angular/router';\nimport { GraphQLDataProvider } from '@memberjunction/graphql-dataprovider';\nimport { BaseAngularComponent } from '@memberjunction/ng-base-types';\n\ninterface RedeemIdentityClaimResponse {\n RedeemIdentityClaim?: {\n Success: boolean;\n ErrorMessage?: string;\n ClaimID?: string;\n Data?: string;\n };\n}\n\n/** Maximum time to wait for session restoration (30 seconds) */\nconst MAX_WAIT_TIME_MS = 30000;\n/** Polling interval for checking provider readiness (500ms) */\nconst POLL_INTERVAL_MS = 500;\n\nconst REDEEM_MUTATION = `\n mutation RedeemIdentityClaim($ClaimID: String!, $Token: String) {\n RedeemIdentityClaim(ClaimID: $ClaimID, Token: $Token) {\n Success\n ErrorMessage\n ClaimID\n Data\n }\n }\n`;\n\n@Component({\n standalone: false,\n selector: 'mj-claim-redeem',\n templateUrl: './claim-redeem.component.html',\n styleUrls: ['./claim-redeem.component.css']\n})\nexport class ClaimRedeemComponent extends BaseAngularComponent implements OnInit, OnDestroy {\n /** Loading state while restoring the session / redeeming */\n public IsLoading = true;\n\n /** Terminal error state */\n public HasError = false;\n\n /** Error message to display */\n public ErrorMessage = '';\n\n /** True when the wait for authentication timed out (user likely not signed in) */\n public NeedsSignIn = false;\n\n /** Status message shown during processing */\n public StatusMessage = 'Restoring session...';\n\n /** Timer for polling provider readiness */\n private pollTimer: ReturnType<typeof setInterval> | null = null;\n\n /** Flag to prevent double processing */\n private isProcessing = false;\n\n constructor(\n private route: ActivatedRoute,\n private router: Router,\n private cdr: ChangeDetectorRef\n ) {\n super();\n }\n\n async ngOnInit(): Promise<void> {\n await this.waitForProviderAndRedeem();\n }\n\n ngOnDestroy(): void {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n }\n\n /**\n * Waits for the GraphQL provider to be initialized (indicating auth is complete),\n * then redeems the claim from the query params.\n */\n private async waitForProviderAndRedeem(): Promise<void> {\n const startTime = Date.now();\n\n if (this.isProviderReady()) {\n await this.safeRedeem();\n return;\n }\n\n this.StatusMessage = 'Waiting for sign-in...';\n this.cdr.detectChanges();\n\n return new Promise<void>((resolve) => {\n this.pollTimer = setInterval(async () => {\n const elapsed = Date.now() - startTime;\n\n if (this.isProviderReady()) {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n await this.safeRedeem();\n resolve();\n return;\n }\n\n if (elapsed >= MAX_WAIT_TIME_MS) {\n if (this.pollTimer) {\n clearInterval(this.pollTimer);\n this.pollTimer = null;\n }\n this.NeedsSignIn = true;\n this.showError('Please sign in first, then open the link from your email again — the claim will attach to your signed-in account.');\n resolve();\n return;\n }\n\n const secondsRemaining = Math.ceil((MAX_WAIT_TIME_MS - elapsed) / 1000);\n this.StatusMessage = `Waiting for sign-in... (${secondsRemaining}s)`;\n this.cdr.detectChanges();\n }, POLL_INTERVAL_MS);\n });\n }\n\n /** Checks if the GraphQL provider is initialized with a valid token */\n private isProviderReady(): boolean {\n try {\n const provider = this.ProviderToUse as GraphQLDataProvider;\n if (!provider) {\n return false;\n }\n const configData = provider.ConfigData;\n return Boolean(configData && configData.Token && configData.URL);\n } catch {\n return false;\n }\n }\n\n /** Redeems exactly once, converting any throw into the error state */\n private async safeRedeem(): Promise<void> {\n if (this.isProcessing) {\n return;\n }\n this.isProcessing = true;\n\n try {\n await this.redeemClaim();\n } catch (error) {\n console.error('[Claim Redeem] Error redeeming claim:', error);\n this.showError(error instanceof Error ? error.message : String(error));\n }\n }\n\n /** Reads the query params and calls the redemption mutation */\n private async redeemClaim(): Promise<void> {\n const params = this.route.snapshot.queryParams;\n const claimId = params['id'] as string | undefined;\n const token = params['token'] as string | undefined;\n\n if (!claimId) {\n this.showError('This link is missing its claim id. Please use the exact link from your email.');\n return;\n }\n\n this.StatusMessage = 'Redeeming your claim...';\n this.cdr.detectChanges();\n\n const provider = this.ProviderToUse as GraphQLDataProvider;\n const result = (await provider.ExecuteGQL(REDEEM_MUTATION, {\n ClaimID: claimId,\n Token: token ?? null\n })) as RedeemIdentityClaimResponse;\n\n const outcome = result?.RedeemIdentityClaim;\n if (outcome?.Success) {\n this.IsLoading = false;\n this.StatusMessage = 'Your claim has been redeemed and attached to your account.';\n this.cdr.detectChanges();\n } else {\n this.showError(outcome?.ErrorMessage || 'The claim could not be redeemed.');\n }\n }\n\n /** Shows the terminal error state */\n private showError(message: string): void {\n this.IsLoading = false;\n this.HasError = true;\n this.ErrorMessage = message;\n this.cdr.detectChanges();\n }\n\n /** Navigates into the app shell (which handles sign-in when needed) */\n public onGoToApp(): void {\n this.router.navigateByUrl('/');\n }\n\n /** Retries redemption from scratch (fresh component state) */\n public onRetry(): void {\n this.IsLoading = true;\n this.HasError = false;\n this.NeedsSignIn = false;\n this.ErrorMessage = '';\n this.isProcessing = false;\n void this.waitForProviderAndRedeem();\n }\n}\n","<div class=\"claim-redeem-container\">\n <div class=\"claim-redeem-card\">\n <!-- Loading State -->\n @if (IsLoading) {\n <div class=\"claim-status loading\">\n <mj-loading text=\"{{ StatusMessage }}\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (HasError) {\n <div class=\"claim-status error\">\n <div class=\"icon-container error-icon\">\n <i class=\"fa-solid fa-circle-exclamation\"></i>\n </div>\n <h2>{{ NeedsSignIn ? 'Sign In Required' : 'Claim Not Redeemed' }}</h2>\n <p class=\"error-message\">{{ ErrorMessage }}</p>\n <div class=\"button-group\">\n @if (!NeedsSignIn) {\n <button mjButton variant=\"secondary\" (click)=\"onRetry()\">\n <i class=\"fa-solid fa-rotate-right\"></i>\n Try Again\n </button>\n }\n <button mjButton [variant]=\"NeedsSignIn ? 'primary' : 'outline'\" (click)=\"onGoToApp()\">\n <i class=\"fa-solid fa-arrow-right-to-bracket\"></i>\n {{ NeedsSignIn ? 'Go to Sign In' : 'Open the App' }}\n </button>\n </div>\n </div>\n }\n\n <!-- Success State -->\n @if (!IsLoading && !HasError) {\n <div class=\"claim-status success\">\n <div class=\"icon-container success-icon\">\n <i class=\"fa-solid fa-circle-check\"></i>\n </div>\n <h2>Claim Redeemed</h2>\n <p>{{ StatusMessage }}</p>\n <div class=\"button-group\">\n <button mjButton variant=\"primary\" (click)=\"onGoToApp()\">\n <i class=\"fa-solid fa-house\"></i>\n Open the App\n </button>\n </div>\n </div>\n }\n </div>\n</div>\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-resource.component.d.ts","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/record-resource.component.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,EAAY,YAAY,EAAc,iBAAiB,
|
|
1
|
+
{"version":3,"file":"record-resource.component.d.ts","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/record-resource.component.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,YAAY,EAAE,MAAM,+BAA+B,CAAC;AAE7D,OAAO,EAAY,YAAY,EAAc,iBAAiB,EAA0B,MAAM,sBAAsB,CAAC;;AACrH,qBAOa,oBAAqB,SAAQ,qBAAqB;IAC3D,IAAW,UAAU,IAAI,YAAY,CAEpC;WAEa,aAAa,CAAC,IAAI,EAAE,YAAY,EAAE,QAAQ,CAAC,EAAE,iBAAiB,GAAG,YAAY;IA4BrF,sBAAsB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;IA2B3D,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,CAAC;yCA5DtD,oBAAoB;2CAApB,oBAAoB;CAyEhC"}
|
|
@@ -8,7 +8,7 @@ var EntityRecordResource_1;
|
|
|
8
8
|
import { Component } from '@angular/core';
|
|
9
9
|
import { BaseResourceComponent } from '@memberjunction/ng-shared';
|
|
10
10
|
import { RegisterClass } from '@memberjunction/global';
|
|
11
|
-
import { Metadata, CompositeKey } from '@memberjunction/core';
|
|
11
|
+
import { Metadata, CompositeKey, IsNewEntityRecordUrlId } from '@memberjunction/core';
|
|
12
12
|
import * as i0 from "@angular/core";
|
|
13
13
|
import * as i1 from "../single-record/single-record.component";
|
|
14
14
|
let EntityRecordResource = class EntityRecordResource extends BaseResourceComponent {
|
|
@@ -33,6 +33,9 @@ let EntityRecordResource = class EntityRecordResource extends BaseResourceCompon
|
|
|
33
33
|
if (!e) {
|
|
34
34
|
throw new Error(`Entity ${data.Configuration.Entity} not found in metadata (tried Name and DisplayName)`);
|
|
35
35
|
}
|
|
36
|
+
if (IsNewEntityRecordUrlId(data.ResourceRecordID)) {
|
|
37
|
+
return new CompositeKey();
|
|
38
|
+
}
|
|
36
39
|
let compositeKey = new CompositeKey();
|
|
37
40
|
compositeKey.LoadFromURLSegment(e, data.ResourceRecordID);
|
|
38
41
|
return compositeKey;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"record-resource.component.js","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/record-resource.component.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAElE,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAiC,MAAM,sBAAsB,CAAC;;;
|
|
1
|
+
{"version":3,"file":"record-resource.component.js","sourceRoot":"","sources":["../../../src/lib/resource-wrappers/record-resource.component.ts"],"names":[],"mappings":"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","sourcesContent":["import { Component } from '@angular/core';\nimport { BaseResourceComponent } from '@memberjunction/ng-shared';\nimport { ResourceData } from '@memberjunction/core-entities';\nimport { RegisterClass } from '@memberjunction/global';\nimport { Metadata, CompositeKey, EntityInfo, IMetadataProvider, IsNewEntityRecordUrlId } from '@memberjunction/core';\n@RegisterClass(BaseResourceComponent, 'RecordResource')\n@Component({\n standalone: false,\n selector: 'mj-record-resource',\n styles: [`:host { display: block; height: 100%; width: 100%; }`],\n template: `<mj-single-record [PrimaryKey]=\"this.PrimaryKey\" [entityName]=\"Data.Configuration.Entity\" [newRecordValues]=\"Data.Configuration.NewRecordValues\" (loadComplete)=\"NotifyLoadComplete()\" (recordSaved)=\"ResourceRecordSaved($event)\" (recordDismissed)=\"NotifyCloseRequested()\"></mj-single-record>`\n})\nexport class EntityRecordResource extends BaseResourceComponent {\n public get PrimaryKey(): CompositeKey {\n return EntityRecordResource.GetPrimaryKey(this.Data, this.ProviderToUse);\n }\n\n public static GetPrimaryKey(data: ResourceData, provider?: IMetadataProvider): CompositeKey {\n // global-provider-ok: static helper has no component instance scope; falls back to default provider\n const md = (provider ?? Metadata.Provider) as IMetadataProvider;\n const requested = (data.Configuration.Entity ?? '').trim();\n // Prefer EntityByName (project rule: never use Entities.find for\n // single-entity lookups — case/whitespace tolerant + O(1) map). Fall\n // back to DisplayName match for callers (AI agents, hand-typed URLs,\n // external CTAs) that pass the human-readable display name instead\n // of the canonical Name (e.g. \"Users\" for \"MJ: Users\").\n let e: EntityInfo | undefined = md.EntityByName(requested);\n if (!e) {\n const lowered = requested.toLowerCase();\n e = md.Entities.find((x: EntityInfo) =>\n (x.DisplayName ?? '').trim().toLowerCase() === lowered);\n }\n if (!e){\n throw new Error(`Entity ${data.Configuration.Entity} not found in metadata (tried Name and DisplayName)`);\n }\n\n if (IsNewEntityRecordUrlId(data.ResourceRecordID)) {\n return new CompositeKey();\n }\n\n let compositeKey: CompositeKey = new CompositeKey();\n compositeKey.LoadFromURLSegment(e, data.ResourceRecordID);\n return compositeKey;\n }\n\n async GetResourceDisplayName(data: ResourceData): Promise<string> {\n if (!data.Configuration.Entity) {\n return '';\n }\n\n const md = this.ProviderToUse;\n let e = md.EntityByName(data.Configuration.Entity);\n if (!e) {\n // Same DisplayName fallback as GetPrimaryKey — keeps the\n // breadcrumb/title in sync when a CTA used the display name.\n const lowered = (data.Configuration.Entity ?? '').trim().toLowerCase();\n e = md.Entities.find((x: EntityInfo) =>\n (x.DisplayName ?? '').trim().toLowerCase() === lowered);\n }\n if (!e) {\n return '';\n }\n\n const pk: CompositeKey = EntityRecordResource.GetPrimaryKey(data, this.ProviderToUse);\n if (pk.HasValue) {\n const name = await md.GetEntityRecordName(data.Configuration.Entity, pk);\n return name ? name : e.DisplayNameOrName;\n } else {\n return `New ${e.DisplayNameOrName} Record`;\n }\n }\n\n async GetResourceIconClass(data: ResourceData): Promise<string> {\n if (!data.Configuration.Entity){\n return ''\n }\n else {\n const md = this.ProviderToUse;\n const e = md.Entities.find(e => e.Name.trim().toLowerCase() === data.Configuration.Entity.trim().toLowerCase());\n if (e)\n return e?.Icon;\n else\n return '';\n }\n }\n}\n"]}
|
|
@@ -246,6 +246,13 @@ export declare class ShellComponent extends BaseAngularComponent implements OnIn
|
|
|
246
246
|
* Creates new tabs for resources when the original tab was replaced.
|
|
247
247
|
*/
|
|
248
248
|
private handleMissingTabForUrl;
|
|
249
|
+
/**
|
|
250
|
+
* Recreate a record tab from a URL. `/new` is OpenNewEntityRecord — feeding
|
|
251
|
+
* the sentinel through OpenEntityRecord produced an empty CompositeKey and
|
|
252
|
+
* another forced tab, which is the loop that killed the browser.
|
|
253
|
+
*/
|
|
254
|
+
private openRecordFromUrl;
|
|
255
|
+
private recordTabMatchesUrl;
|
|
249
256
|
/**
|
|
250
257
|
* Find the tab that matches a given URL
|
|
251
258
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell.component.d.ts","sourceRoot":"","sources":["../../../src/lib/shell/shell.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAa,UAAU,EAAgB,MAAM,eAAe,CAAC;AACtJ,OAAO,EAAE,cAAc,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AAGxE,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,UAAU,EAIV,OAAO,EACR,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAY,UAAU,
|
|
1
|
+
{"version":3,"file":"shell.component.d.ts","sourceRoot":"","sources":["../../../src/lib/shell/shell.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAa,MAAM,EAAE,SAAS,EAAE,aAAa,EAAE,iBAAiB,EAAE,gBAAgB,EAAa,UAAU,EAAgB,MAAM,eAAe,CAAC;AACtJ,OAAO,EAAE,cAAc,EAAE,MAAM,EAAiB,MAAM,iBAAiB,CAAC;AAGxE,OAAO,EACL,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,EACnB,eAAe,EACf,UAAU,EAIV,OAAO,EACR,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAY,UAAU,EAAgN,MAAM,sBAAsB,CAAC;AAE1Q,OAAO,EAAc,iBAAiB,EAAgC,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,iBAAiB,EAAE,eAAe,EAAE,YAAY,EAAsF,MAAM,2BAA2B,CAAC;AAChS,OAAO,EAAE,wBAAwB,EAAE,MAAM,wCAAwC,CAAC;AAClF,OAAO,EAAE,YAAY,EAAE,MAAM,mCAAmC,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,uCAAuC,CAAC;AAC1E,OAAO,EAAE,UAAU,EAAE,MAAM,kCAAkC,CAAC;AAE9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC;AACnE,OAAO,EAAE,8BAA8B,EAAE,MAAM,+CAA+C,CAAC;AAC/F,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAC;AACrE,OAAO,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAIzE,OAAO,EAAE,wBAAwB,EAAyB,qBAAqB,EAAE,MAAM,kDAAkD,CAAC;AAC1I,OAAO,EAAE,qBAAqB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,EAAgB,eAAe,EAAE,YAAY,EAA0D,MAAM,cAAc,CAAC;AAEnI,OAAO,EAAE,qBAAqB,EAAE,MAAM,4CAA4C,CAAC;AACnF,OAAO,EAAE,eAAe,EAAE,MAAM,iCAAiC,CAAC;AAClE,OAAO,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,6BAA6B,CAAC;AAGrF,OAAO,EAAE,oBAAoB,EAAE,MAAM,+BAA+B,CAAC;AACrE,OAAO,EAAE,gBAAgB,EAAE,MAAM,4CAA4C,CAAC;;AAyB9E,qBAMa,cAAe,SAAQ,oBAAqB,YAAW,MAAM,EAAE,SAAS,EAAE,aAAa;IAgThG,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,aAAa;IACrB,OAAO,CAAC,UAAU;IAClB,OAAO,CAAC,iBAAiB;IACzB,OAAO,CAAC,KAAK;IACb,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,QAAQ;IAChB,OAAO,CAAC,GAAG;IACX,OAAO,CAAC,iBAAiB;IACzB,OAAO,CAAC,8BAA8B;IACtC,OAAO,CAAC,kBAAkB;IAC1B,OAAO,CAAC,oBAAoB;IAC5B,OAAO,CAAC,gBAAgB;IACxB,OAAO,CAAC,YAAY;IACb,oBAAoB,EAAE,oBAAoB;IACjD,OAAO,CAAC,wBAAwB;IAChC,OAAO,CAAC,qBAAqB;IAC7B,OAAO,CAAC,YAAY;IACpB,OAAO,CAAC,cAAc;IACtB,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,qBAAqB;IAC7B,OAAO,CAAC,eAAe;IACvB,OAAO,CAAC,eAAe;IAtUzB,OAAO,CAAC,aAAa,CAAsB;IAC3C,OAAO,CAAC,kBAAkB,CAAS;IACnC,0GAA0G;IAC1G,OAAO,CAAC,kBAAkB,CAAuC;IACjE,OAAO,CAAC,yBAAyB,CAAS;IAE1C,SAAS,EAAE,eAAe,GAAG,IAAI,CAAQ;IACzC,OAAO,UAAQ;IACf,WAAW,UAAS;IACpB,OAAO,CAAC,uBAAuB,CAAS;IACxC,aAAa,UAAQ;IAErB;;;;;;OAMG;IACH,IAAW,eAAe,IAAI,OAAO,CAEpC;IACD,eAAe,UAAS;IACxB,aAAa,UAAS;IACtB,uBAAuB,SAAK;IAG5B,aAAa,EAAE,YAAY,EAAE,CAAM;IACnC,oBAAoB,SAAK;IACzB,YAAY,UAAS;IACrB,kBAAkB,UAAS;IAC3B,YAAY,EAAE,MAAM,GAAG,IAAI,CAAQ;IAGnC,OAAO,CAAC,sBAAsB,CAA+C;IAC7E,OAAO,CAAC,mBAAmB,CAAK;IAChC,OAAO,CAAC,kBAAkB,CAAgB;IAC1C,OAAO,CAAC,mBAAmB,CAAgB;IAC3C,OAAO,CAAC,iBAAiB,CAAK;IAC9B,OAAO,CAAC,WAAW,CAAe;IAClC,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAQ;IAC1C,OAAO,CAAC,QAAQ,CAAC,yBAAyB,CAAK;IAE/C,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAqE;IAEvG,OAAO,CAAC,iBAAiB,CAAuB;IAChD,OAAO,CAAC,qBAAqB,CAAK;IAClC,OAAO,CAAC,wBAAwB,CAA8C;IAE9E,eAAe,UAAS;IAExB,sFAAsF;IACtF,SAAgB,SAAS,EAAE,MAAM,CAAmB;IACpD,OAAO,CAAC,mBAAmB,CAA8C;IACzE,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAU;IAC9C,kBAAkB,EAAE,MAAM,CAAC;IAC3B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,uBAAuB,EAAE,MAAM,CAAC;IAChC,sBAAsB,EAAE,YAAY,GAAG,IAAI,CAAC;IAC5C,uBAAuB,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,YAAY,CAAW;IAG9E,YAAY,SAAM;IAClB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAQ;IACpC,QAAQ,SAAM;IACd,SAAS,SAAM;IACf,OAAO,CAAC,UAAU,CAA6B;IAG/C,OAAO,CAAC,QAAQ,CAA6B;IACtC,gBAAgB,EAAE,eAAe,EAAE,CAAM;IAChD,OAAO,CAAC,QAAQ,CAAuB;IAGhC,kBAAkB,UAAS;IAC3B,eAAe,SAAM;IAG5B,YAAY,UAAS;IACrB,kBAAkB,EAAE,UAAU,EAAE,CAAM;IACtC,cAAc,EAAE,UAAU,GAAG,IAAI,CAAQ;IACf,WAAW,EAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAKrE,gBAAgB,UAAS;IAKzB,OAAO,CAAC,YAAY,CAAiC;IAErD;;;;;OAKG;IACH,OAAO,KAAK,WAAW,GAyBtB;IAED,2EAA2E;IAC3E,OAAO,CAAC,uBAAuB,CAA8B;IAE7D;;;;;;;OAOG;IACH,OAAO,CAAC,sBAAsB;IAqB9B,IAAI,aAAa,IAAI,OAAO,CAE3B;IACD,0FAA0F;IAC1F,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IACD,2FAA2F;IAC3F,IAAI,eAAe,IAAI,OAAO,CAE7B;IACD,uFAAuF;IACvF,IAAI,gBAAgB,IAAI,gBAAgB,CAEvC;IACD,yFAAyF;IACzF,IAAI,UAAU,IAAI,OAAO,CAExB;IACD,IAAI,iBAAiB,IAAI,OAAO,CAE/B;IACD;;;;;;;;;OASG;IACH,IAAI,UAAU,IAAI,OAAO,CAExB;IAED,gFAAgF;IAChF,IAAI,oBAAoB,IAAI,MAAM,CAEjC;IAED;;;;OAIG;IACH,IAAI,gBAAgB,IAAI,OAAO,CAQ9B;IAED,sDAAsD;IACtD,QAAQ,CAAC,gBAAgB,+GAA0G;IAEnI;;;;;OAKG;IACH,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAe3C,mFAAmF;IACnF,uBAAuB,IAAI,IAAI;IAI/B,kFAAkF;IAClF,0BAA0B,IAAI,IAAI;IAOL,cAAc,CAAC,EAAE;QAAE,IAAI,CAAC,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE,CAAC;IAEpF;kEAC8D;IAC3B,oBAAoB,EAAE;QACvD,KAAK,CAAC,IAAI,IAAI,CAAC;QACf,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC7B,GAAG,SAAS,CAAC;IAEd,kDAAkD;IAClD,WAAW,CAAC,YAAY,SAAK,GAAG,IAAI;IAKF,eAAe,EAAG,qBAAqB,CAAC;IAG5C,eAAe,EAAG,wBAAwB,CAAC;IACzE,OAAO,CAAC,cAAc,CAAuB;IAE7C;;;;;;OAMG;IACI,kBAAkB,EAAE,eAAe,EAAE,CAAM;IAElD;;;;;OAKG;IACI,kBAAkB,EAAE,eAAe,EAAE,CAAM;IAElD;;;;OAIG;IACH,OAAO,CAAC,mBAAmB;gBAQjB,UAAU,EAAE,kBAAkB,EAC9B,gBAAgB,EAAE,qBAAqB,EACvC,aAAa,EAAE,mBAAmB,EAClC,UAAU,EAAE,UAAU,EACtB,iBAAiB,EAAE,iBAAiB,EACpC,KAAK,EAAE,cAAc,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,UAAU,EACpB,GAAG,EAAE,iBAAiB,EACtB,iBAAiB,EAAE,iBAAiB,EACpC,8BAA8B,EAAE,8BAA8B,EAC9D,kBAAkB,EAAE,kBAAkB,EACtC,oBAAoB,EAAE,oBAAoB,EAC1C,gBAAgB,EAAE,gBAAgB,EAClC,YAAY,EAAE,YAAY,EAC3B,oBAAoB,EAAE,oBAAoB,EACzC,wBAAwB,EAAE,wBAAwB,EAClD,qBAAqB,EAAE,qBAAqB,EAC5C,YAAY,EAAE,YAAY,EAC1B,cAAc,EAAE,iBAAiB,EACjC,eAAe,EAAE,eAAe,EAChC,qBAAqB,EAAE,qBAAqB,EAC5C,eAAe,EAAE,eAAe,EAChC,eAAe,EAAE,eAAe;IA2CpC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IA0BzB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC;IAyTtC;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAiBtB;;OAEG;YACW,iBAAiB;IA4B/B;;;OAGG;YACW,oBAAoB;IA+DlC;;OAEG;YACW,oBAAoB;IAqClC;;;OAGG;YACW,oBAAoB;IAiClC;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAWjC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAUxB;;;OAGG;YACW,sBAAsB;IA2JpC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAczB,OAAO,CAAC,mBAAmB;IAO3B;;OAEG;YACW,aAAa;IAkO3B;;;OAGG;YACW,gBAAgB;IA8Q9B,eAAe,IAAI,IAAI;IAIvB;;;;;;OAMG;IACH;;;;;OAKG;IACH,IAAW,kBAAkB,IAAI,OAAO,CAEvC;IAED,2BAA2B,IAAI,IAAI;IAOnC;;;;;;;;OAQG;IACH,OAAO,CAAC,qBAAqB;IA8B7B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IAc5B;;;;OAIG;IACH,OAAO,CAAC,sBAAsB;IAU9B;;OAEG;IACH,OAAO,CAAC,uBAAuB;IAQ/B;;;;;OAKG;IACG,gBAAgB,IAAI,OAAO,CAAC,IAAI,CAAC;IAwCvC;;;;OAIG;IACH,OAAO,CAAC,wBAAwB;IAwBhC;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IA8B1B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;OAGG;IACH,OAAO,CAAC,4BAA4B;IA+BpC;;OAEG;IACH,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAoBjC;;OAEG;IACH,OAAO,CAAC,yBAAyB;IAgBjC,WAAW,IAAI,IAAI;IAWnB;;OAEG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiF/C;;OAEG;IACH,OAAO,CAAC,wBAAwB;IA0BhC;;OAEG;IACH,gBAAgB,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;IAS/D;;OAEG;IACH,mBAAmB,CAAC,GAAG,EAAE,eAAe,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI;IAKlE;;OAEG;YACW,qBAAqB;IAcnC;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAmB9C;;OAEG;IACH,gBAAgB,CAAC,IAAI,EAAE,OAAO,GAAG,IAAI;IAcrC;;OAEG;IACH,eAAe,IAAI,IAAI;IAIvB;;OAEG;IACH,cAAc,IAAI,IAAI;IAItB;;OAEG;IACH,cAAc,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAkBlC;;OAEG;YACW,kBAAkB;IA0EhC;;OAEG;IACH,OAAO,CAAC,mBAAmB;IAM3B;;OAEG;IACG,mBAAmB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAqFxD;;OAEG;IACH,IAAI,UAAU,IAAI,OAAO,CAGxB;IAED;;OAEG;IACH,kBAAkB;IAIlB;;OAEG;IACH,kBAAkB;IAIlB;;OAEG;IACH,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO;IAIhD;;;OAGG;IACH,UAAU,CAAC,OAAO,EAAE,eAAe,GAAG,YAAY;IAKlD;;OAEG;YACW,eAAe;IA4D7B;;OAEG;YACW,yBAAyB;IAevC;;OAEG;YACW,wBAAwB;IA2BtC,OAAO,CAAC,eAAe;IAMvB,OAAO,CAAC,eAAe;IAMvB;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAW9B;;OAEG;IACH,WAAW,IAAI,IAAI;IAOnB;;OAEG;IACG,aAAa,IAAI,OAAO,CAAC,IAAI,CAAC;IAqDpC;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAO/B;;;OAGG;IAEH,6BAA6B,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IA0BzD;;OAEG;YACW,cAAc;IAgC5B;;OAEG;YACW,0BAA0B;IAiBxC;;;OAGG;IACH,OAAO,CAAC,eAAe;IAevB;;;OAGG;IACH,OAAO,CAAC,kBAAkB;IAqB1B;;OAEG;YACW,sBAAsB;IAQpC;;OAEG;IACH,YAAY,IAAI,IAAI;IAiBpB;;OAEG;IACH,WAAW,IAAI,IAAI;IAInB;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI;IAkC5B,eAAe,CAAC,KAAK,EAAE,aAAa,GAAG,IAAI;IAoB3C;;;8CAG0C;IAC1C,mBAAmB,IAAI,IAAI;IAc3B,sBAAsB,CAAC,MAAM,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI;IAmBzH,mFAAmF;IACnF,uBAAuB,CAAC,KAAK,EAAE;QAAE,MAAM,EAAE;YAAE,UAAU,EAAE,MAAM,CAAC;YAAC,QAAQ,EAAE,MAAM,CAAC;YAAC,UAAU,CAAC,EAAE,MAAM,CAAC;YAAC,WAAW,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,IAAI;IAKrI;2DACuD;IACvD,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAKpC;kDAC8C;IAC9C,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAWtC,sEAAsE;IACtE,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAQnC;;OAEG;IACH,yCAAyC;IACzC,cAAc,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAMvC,kDAAkD;IAClD,qBAAqB,IAAI,IAAI;IAI7B,mEAAmE;IAEnE,4BAA4B,IAAI,IAAI;IAOpC,iBAAiB,IAAI,IAAI;IAsBzB;;;OAGG;IACG,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;IAoBzC;;OAEG;YACW,kCAAkC;IA2BhD;;;;;;OAMG;YACW,oBAAoB;IA+BlC;;OAEG;IACH,OAAO,CAAC,6BAA6B;IA6CrC;;OAEG;YACW,qBAAqB;IAenC;;OAEG;IACH,iBAAiB,IAAI,IAAI;IAgBzB;;OAEG;IACG,uBAAuB,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAwB3E;;OAEG;YACW,uBAAuB;IA2BrC;;OAEG;YACW,sBAAsB;IAkBpC;;;OAGG;YACW,qBAAqB;IA4BnC;;OAEG;YACW,aAAa;IAe3B,8EAA8E;IACvE,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO;IAIjD;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAQzB;;;;;;;OAOG;YACW,cAAc;IA0B5B;;;OAGG;YACW,qBAAqB;yCAlwGxB,cAAc;2CAAd,cAAc;CAixG1B"}
|
|
@@ -2,7 +2,7 @@ import { Component, ViewChild, HostListener } from '@angular/core';
|
|
|
2
2
|
import { NavigationEnd } from '@angular/router';
|
|
3
3
|
import { combineLatest, Subject } from 'rxjs';
|
|
4
4
|
import { filter, takeUntil } from 'rxjs/operators';
|
|
5
|
-
import { LogStatus, LogError, StartupManager, CompositeKey } from '@memberjunction/core';
|
|
5
|
+
import { LogStatus, LogError, StartupManager, CompositeKey, EncodeNewRecordValuesForURL, IsNewEntityRecordUrlId, NEW_ENTITY_RECORD_URL_ID, NEW_RECORD_VALUES_QUERY_PARAM, RecordUrlMatchesTab, ResourceUrlsEquivalent } from '@memberjunction/core';
|
|
6
6
|
import { MJEventType, MJGlobal, uuidv4, UUIDsEqual } from '@memberjunction/global';
|
|
7
7
|
import { EventCodes, SharedService, SYSTEM_APP_ID, SetRecordOpenStyle, IsRecordsRegionTab, IsRecordsTabConfiguration } from '@memberjunction/ng-shared';
|
|
8
8
|
import { MJNotificationService } from '@memberjunction/ng-notifications';
|
|
@@ -1292,8 +1292,11 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1292
1292
|
// Compare full URLs including query params to detect changes
|
|
1293
1293
|
const currentUrl = this.router.url;
|
|
1294
1294
|
const newUrl = resourceUrl;
|
|
1295
|
-
//
|
|
1296
|
-
|
|
1295
|
+
// Decode before compare. encodeURIComponent writes `%3A` for the
|
|
1296
|
+
// colon in `MJ_BizApps_Orders: Order Headers`; Angular's serializer
|
|
1297
|
+
// leaves `:`. A raw !== is permanently true and reloads the route
|
|
1298
|
+
// until the tab dies (Person → Orders → New).
|
|
1299
|
+
if (!ResourceUrlsEquivalent(currentUrl, newUrl)) {
|
|
1297
1300
|
// Suppress ResourceResolver for this navigation - we're just syncing the URL
|
|
1298
1301
|
// to reflect the current active tab, not requesting a new tab to be opened
|
|
1299
1302
|
this.tabService.SuppressNextResolve();
|
|
@@ -1380,12 +1383,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1380
1383
|
if (appRecordMatch) {
|
|
1381
1384
|
const entityName = decodeURIComponent(appRecordMatch[2]);
|
|
1382
1385
|
const recordId = decodeURIComponent(appRecordMatch[3]);
|
|
1383
|
-
|
|
1384
|
-
compositeKey.SimpleLoadFromURLSegment(recordId);
|
|
1385
|
-
// Recreating a closed tab from browser history — the user's CURRENT
|
|
1386
|
-
// page is not where this record came from, so don't stamp it as the
|
|
1387
|
-
// origin (recordSource 'none' = no crumb rather than a false one).
|
|
1388
|
-
this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none' });
|
|
1386
|
+
this.openRecordFromUrl(entityName, recordId, queryParams);
|
|
1389
1387
|
return;
|
|
1390
1388
|
}
|
|
1391
1389
|
// Check for app-scoped view URL: /app/:appName/view/:viewId
|
|
@@ -1488,11 +1486,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1488
1486
|
if (legacyRecordMatch) {
|
|
1489
1487
|
const entityName = decodeURIComponent(legacyRecordMatch[1]);
|
|
1490
1488
|
const recordId = decodeURIComponent(legacyRecordMatch[2]);
|
|
1491
|
-
|
|
1492
|
-
compositeKey.SimpleLoadFromURLSegment(recordId);
|
|
1493
|
-
// Same as the app-scoped branch above: history recreation has no
|
|
1494
|
-
// truthful origin — suppress the crumb instead of inventing one.
|
|
1495
|
-
this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none' });
|
|
1489
|
+
this.openRecordFromUrl(entityName, recordId, queryParams);
|
|
1496
1490
|
return;
|
|
1497
1491
|
}
|
|
1498
1492
|
// Check for legacy view URL: /resource/view/:viewId
|
|
@@ -1514,6 +1508,30 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1514
1508
|
this.urlBasedNavigation = false;
|
|
1515
1509
|
}
|
|
1516
1510
|
}
|
|
1511
|
+
/**
|
|
1512
|
+
* Recreate a record tab from a URL. `/new` is OpenNewEntityRecord — feeding
|
|
1513
|
+
* the sentinel through OpenEntityRecord produced an empty CompositeKey and
|
|
1514
|
+
* another forced tab, which is the loop that killed the browser.
|
|
1515
|
+
*/
|
|
1516
|
+
openRecordFromUrl(entityName, recordId, queryParams) {
|
|
1517
|
+
if (IsNewEntityRecordUrlId(recordId)) {
|
|
1518
|
+
const nrv = queryParams.get(NEW_RECORD_VALUES_QUERY_PARAM) ?? undefined;
|
|
1519
|
+
this.navigationService.OpenNewEntityRecord(entityName, {
|
|
1520
|
+
newRecordValues: nrv,
|
|
1521
|
+
recordSource: 'none',
|
|
1522
|
+
});
|
|
1523
|
+
return;
|
|
1524
|
+
}
|
|
1525
|
+
const compositeKey = new CompositeKey();
|
|
1526
|
+
compositeKey.SimpleLoadFromURLSegment(recordId);
|
|
1527
|
+
this.navigationService.OpenEntityRecord(entityName, compositeKey, { recordSource: 'none' });
|
|
1528
|
+
}
|
|
1529
|
+
recordTabMatchesUrl(tab, entityName, recordId) {
|
|
1530
|
+
const tabConfig = tab.configuration || {};
|
|
1531
|
+
const tabEntity = (tabConfig['Entity'] || tabConfig['entity']);
|
|
1532
|
+
const tabRecordId = (tabConfig['recordId'] ?? tab.resourceRecordId);
|
|
1533
|
+
return RecordUrlMatchesTab(entityName, recordId, tabEntity, tabRecordId);
|
|
1534
|
+
}
|
|
1517
1535
|
/**
|
|
1518
1536
|
* Find the tab that matches a given URL
|
|
1519
1537
|
*/
|
|
@@ -1582,13 +1600,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1582
1600
|
if (appRecordMatch) {
|
|
1583
1601
|
const entityName = decodeURIComponent(appRecordMatch[2]);
|
|
1584
1602
|
const recordId = decodeURIComponent(appRecordMatch[3]);
|
|
1585
|
-
return tabs.find(tab =>
|
|
1586
|
-
const tabConfig = tab.configuration || {};
|
|
1587
|
-
const tabEntity = (tabConfig['Entity'] || tabConfig['entity']);
|
|
1588
|
-
const tabRecordId = (tabConfig['recordId'] || tab.resourceRecordId);
|
|
1589
|
-
return tabEntity?.toLowerCase() === entityName.toLowerCase() &&
|
|
1590
|
-
tabRecordId === recordId;
|
|
1591
|
-
}) || null;
|
|
1603
|
+
return tabs.find(tab => this.recordTabMatchesUrl(tab, entityName, recordId)) || null;
|
|
1592
1604
|
}
|
|
1593
1605
|
// Dynamic view: /app/:appName/view/dynamic/:entityName
|
|
1594
1606
|
const appDynamicViewMatch = urlPath.match(/^\/app\/([^\/]+)\/view\/dynamic\/(.+)$/);
|
|
@@ -1652,13 +1664,7 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1652
1664
|
if (recordMatch) {
|
|
1653
1665
|
const entityName = decodeURIComponent(recordMatch[1]);
|
|
1654
1666
|
const recordId = decodeURIComponent(recordMatch[2]);
|
|
1655
|
-
return tabs.find(tab =>
|
|
1656
|
-
const tabConfig = tab.configuration || {};
|
|
1657
|
-
const tabEntity = (tabConfig['Entity'] || tabConfig['entity']);
|
|
1658
|
-
const tabRecordId = (tabConfig['recordId'] || tab.resourceRecordId);
|
|
1659
|
-
return tabEntity?.toLowerCase() === entityName.toLowerCase() &&
|
|
1660
|
-
tabRecordId === recordId;
|
|
1661
|
-
}) || null;
|
|
1667
|
+
return tabs.find(tab => this.recordTabMatchesUrl(tab, entityName, recordId)) || null;
|
|
1662
1668
|
}
|
|
1663
1669
|
// Check for view URL: /resource/view/:viewId
|
|
1664
1670
|
const viewMatch = urlPath.match(/^\/resource\/view\/([^\/]+)$/);
|
|
@@ -1725,11 +1731,12 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1725
1731
|
const isAppDefault = config['isAppDefault'];
|
|
1726
1732
|
const tabAppId = tab.applicationId;
|
|
1727
1733
|
// Helper to append query params to a URL, preserving any existing params
|
|
1728
|
-
const appendQP = (url) => {
|
|
1729
|
-
|
|
1734
|
+
const appendQP = (url, extra) => {
|
|
1735
|
+
const all = { ...(queryParams ?? {}), ...(extra ?? {}) };
|
|
1736
|
+
if (Object.keys(all).length === 0)
|
|
1730
1737
|
return url;
|
|
1731
1738
|
const separator = url.includes('?') ? '&' : '?';
|
|
1732
|
-
const params = new URLSearchParams(
|
|
1739
|
+
const params = new URLSearchParams(all);
|
|
1733
1740
|
return `${url}${separator}${params.toString()}`;
|
|
1734
1741
|
};
|
|
1735
1742
|
// Helper function to get app path for URL
|
|
@@ -1832,13 +1839,19 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1832
1839
|
switch (resourceType) {
|
|
1833
1840
|
case 'records':
|
|
1834
1841
|
// /app/:appName/record/:entityName/:recordId
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
+
// Unsaved records use the `new` sentinel so they can deeplink with NewRecordValues.
|
|
1843
|
+
if (entityName) {
|
|
1844
|
+
const isNewRecord = config['isNew'] === true || IsNewEntityRecordUrlId(recordId);
|
|
1845
|
+
const idSeg = isNewRecord ? NEW_ENTITY_RECORD_URL_ID : recordId;
|
|
1846
|
+
if (idSeg) {
|
|
1847
|
+
// recordId is a CompositeKey URL segment ("ID|<value>") — the '|' MUST be encoded.
|
|
1848
|
+
// Angular's UrlSerializer percent-encodes '|' to %7C in router.url, so if we embed it raw
|
|
1849
|
+
// here, `syncUrlWithWorkspace`'s `currentUrl !== newUrl` check is permanently true and can
|
|
1850
|
+
// drive a re-navigation loop (with onSameUrlNavigation:'reload'). The read side already
|
|
1851
|
+
// decodeURIComponent()s this segment, so encoding here keeps both sides consistent.
|
|
1852
|
+
const nrv = isNewRecord ? EncodeNewRecordValuesForURL(config['NewRecordValues']) : undefined;
|
|
1853
|
+
return appendQP(`/app/${encodeURIComponent(appPath)}/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, nrv ? { [NEW_RECORD_VALUES_QUERY_PARAM]: nrv } : undefined);
|
|
1854
|
+
}
|
|
1842
1855
|
}
|
|
1843
1856
|
break;
|
|
1844
1857
|
case 'user views':
|
|
@@ -1903,10 +1916,15 @@ export class ShellComponent extends BaseAngularComponent {
|
|
|
1903
1916
|
// Fallback to legacy routes (for backward compatibility during transition)
|
|
1904
1917
|
switch (resourceType) {
|
|
1905
1918
|
case 'records':
|
|
1906
|
-
if (entityName
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1919
|
+
if (entityName) {
|
|
1920
|
+
const isNewRecord = config['isNew'] === true || IsNewEntityRecordUrlId(recordId);
|
|
1921
|
+
const idSeg = isNewRecord ? NEW_ENTITY_RECORD_URL_ID : recordId;
|
|
1922
|
+
if (idSeg) {
|
|
1923
|
+
// Encode the CompositeKey segment ('|' → %7C) to match Angular's serialized router.url and
|
|
1924
|
+
// the decodeURIComponent() on the read side — see the app-scoped 'records' case above.
|
|
1925
|
+
const nrv = isNewRecord ? EncodeNewRecordValuesForURL(config['NewRecordValues']) : undefined;
|
|
1926
|
+
return appendQP(`/resource/record/${encodeURIComponent(entityName)}/${encodeURIComponent(idSeg)}`, nrv ? { [NEW_RECORD_VALUES_QUERY_PARAM]: nrv } : undefined);
|
|
1927
|
+
}
|
|
1910
1928
|
}
|
|
1911
1929
|
break;
|
|
1912
1930
|
case 'user views':
|