@vertesia/ui 1.3.0-dev.20260620.061059Z → 1.3.0-dev.20260621.071017Z

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.
@@ -5,7 +5,7 @@ import { jwtDecode } from 'jwt-decode';
5
5
  import { createContext, useContext } from 'react';
6
6
 
7
7
  import { getComposableToken } from './auth/composable';
8
- import { authReturnUrl, shouldRedirectToCentralAuth } from './auth/domainRouting';
8
+ import { authReturnUrl, mountRootUrl, shouldRedirectToCentralAuth } from './auth/domainRouting';
9
9
  import { getFirebaseAuth } from './auth/firebase';
10
10
 
11
11
  import { LastSelectedAccountId_KEY, LastSelectedProjectId_KEY } from './constants';
@@ -168,12 +168,13 @@ class UserSession {
168
168
  this.authToken = undefined;
169
169
  this.setSession = undefined;
170
170
  this.client.withAuthCallback(undefined);
171
- // Navigate to root to avoid React rendering errors when
171
+ // Navigate to the app root to avoid React rendering errors when
172
172
  // unmounting deeply nested route components during logout.
173
- // Only navigate if user was actually logged in to avoid
174
- // infinite reload loop on fresh/incognito sessions.
173
+ // Use the mount root (not bare '/') so a gateway-mounted app stays
174
+ // on its mount. Only navigate if user was actually logged in to
175
+ // avoid an infinite reload loop on fresh/incognito sessions.
175
176
  if (wasLoggedIn) {
176
- location.replace('/');
177
+ location.replace(mountRootUrl().toString());
177
178
  }
178
179
  }
179
180
  }
@@ -188,7 +189,9 @@ class UserSession {
188
189
  }
189
190
  }
190
191
 
191
- window.location.replace(`/?a=${targetAccountId}`);
192
+ const url = mountRootUrl();
193
+ url.searchParams.set('a', targetAccountId);
194
+ window.location.replace(url.toString());
192
195
  }
193
196
 
194
197
  async switchProject(targetProjectId: string) {
@@ -196,7 +199,12 @@ class UserSession {
196
199
  localStorage.setItem(`${LastSelectedProjectId_KEY}-${this.account.id}`, targetProjectId);
197
200
  }
198
201
 
199
- window.location.replace(`/?a=${this.account?.id}&p=${targetProjectId}`);
202
+ const url = mountRootUrl();
203
+ if (this.account?.id) {
204
+ url.searchParams.set('a', this.account.id);
205
+ }
206
+ url.searchParams.set('p', targetProjectId);
207
+ window.location.replace(url.toString());
200
208
  }
201
209
 
202
210
  async fetchAccounts() {
@@ -1,9 +1,15 @@
1
1
  import { afterEach, describe, expect, it } from 'vitest';
2
- import { shouldRedirectToCentralAuth, shouldUseFirebaseAuth } from './domainRouting';
2
+ import { mountRootUrl, shouldRedirectToCentralAuth, shouldUseFirebaseAuth } from './domainRouting';
3
+
4
+ // Stub document.baseURI to simulate the served `<base href>` (deep gateway mount) or its absence.
5
+ function setBaseURI(baseURI: string) {
6
+ (globalThis as { document?: unknown }).document = { baseURI };
7
+ }
3
8
 
4
9
  describe('domainRouting', () => {
5
10
  afterEach(() => {
6
11
  delete (globalThis as { window?: unknown }).window;
12
+ delete (globalThis as { document?: unknown }).document;
7
13
  });
8
14
 
9
15
  it("uses Firebase auth when AUTH_MODE is 'firebase'", () => {
@@ -23,4 +29,25 @@ describe('domainRouting', () => {
23
29
  expect(shouldUseFirebaseAuth()).toBe(false);
24
30
  expect(shouldRedirectToCentralAuth()).toBe(true);
25
31
  });
32
+
33
+ describe('mountRootUrl', () => {
34
+ it('returns the deep gateway mount root (dropping any deep route / query / hash)', () => {
35
+ const mount = 'https://gw.example.com/tenants/05948c_5ed5f4/apps/furniture-catalog/versions/v1/app/';
36
+ setBaseURI(mount);
37
+ expect(mountRootUrl().toString()).toBe(mount);
38
+ });
39
+
40
+ it('strips an existing query and hash so callers control the reload target', () => {
41
+ setBaseURI('https://gw.example.com/tenants/t/apps/a/versions/v/app/');
42
+ const url = mountRootUrl();
43
+ url.searchParams.set('a', 'acct1');
44
+ url.searchParams.set('p', 'proj1');
45
+ expect(url.toString()).toBe('https://gw.example.com/tenants/t/apps/a/versions/v/app/?a=acct1&p=proj1');
46
+ });
47
+
48
+ it('is the origin root for the Studio UI (no <base> element)', () => {
49
+ setBaseURI('https://studio.vertesia.io/');
50
+ expect(mountRootUrl().toString()).toBe('https://studio.vertesia.io/');
51
+ });
52
+ });
26
53
  });
@@ -33,3 +33,19 @@ export function authReturnUrl(): URL {
33
33
  target.hash = '';
34
34
  return target;
35
35
  }
36
+
37
+ /**
38
+ * The app's mount root URL — the served `<base href>` (or the origin root for the Studio UI).
39
+ *
40
+ * Used for full-reload navigations that intentionally reset to the app root (logout, account /
41
+ * project switch). Building these from a bare `/` drops a gateway-mounted app off its mount and
42
+ * lands on a URL that serves no app; resolving against `document.baseURI` keeps the reload inside
43
+ * the mount. For the Studio UI (no `<base>` element) `document.baseURI` is the origin root, so the
44
+ * behavior is unchanged.
45
+ */
46
+ export function mountRootUrl(): URL {
47
+ const url = new URL(document.baseURI);
48
+ url.hash = '';
49
+ url.search = '';
50
+ return url;
51
+ }