@vertesia/ui 1.3.0-dev.20260620.091720Z → 1.3.0-dev.20260621.091702Z

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vertesia/ui",
3
- "version": "1.3.0-dev.20260620.091720Z",
3
+ "version": "1.3.0-dev.20260621.091702Z",
4
4
  "description": "Vertesia UI components and and hooks",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -90,10 +90,10 @@
90
90
  "vega": "^6.2.0",
91
91
  "vega-embed": "^7.1.0",
92
92
  "vega-lite": "^6.4.3",
93
- "@vertesia/client": "1.3.0-dev.20260620.091720Z",
94
- "@vertesia/common": "1.3.0-dev.20260620.091720Z",
95
- "@vertesia/json": "1.3.0-dev.20260620.091720Z",
96
- "@vertesia/fusion-ux": "1.3.0-dev.20260620.091720Z"
93
+ "@vertesia/client": "1.3.0-dev.20260621.091702Z",
94
+ "@vertesia/fusion-ux": "1.3.0-dev.20260621.091702Z",
95
+ "@vertesia/json": "1.3.0-dev.20260621.091702Z",
96
+ "@vertesia/common": "1.3.0-dev.20260621.091702Z"
97
97
  },
98
98
  "devDependencies": {
99
99
  "@rollup/plugin-commonjs": "^29.0.3",
@@ -185,7 +185,7 @@
185
185
  "url": "https://github.com/vertesia/composableai.git",
186
186
  "directory": "packages/ui"
187
187
  },
188
- "gitHead": "1a76be81111643b66a7b11be9f328d159f1de4fc",
188
+ "gitHead": "0db27f8c9a71f3675a455c167e8b88c4d5f41e11",
189
189
  "scripts": {
190
190
  "build": "rm -rf ./lib ./tsconfig.tsbuildinfo && tsc --build && pnpm exec rollup -c",
191
191
  "lint": "biome lint src && pnpm run check:rtl-classes",
@@ -1,7 +1,7 @@
1
1
  import { createContext, useContext, useEffect } from 'react';
2
2
  import { HistoryNavigator, type LocationChangeEvent, type NavigateOptions } from './HistoryNavigator';
3
3
  import { type PathMatch, PathMatcher } from './PathMatcher';
4
- import { isRootPath, joinPath, type PathMatchParams, stripMountBasename } from './path';
4
+ import { getMountBasename, isRootPath, joinPath, type PathMatchParams, stripMountBasename } from './path';
5
5
 
6
6
  export type RouteComponentProps = PathMatchParams;
7
7
  export type LazyRouteModule = { default: React.ComponentType<Record<string, never>> };
@@ -186,8 +186,12 @@ export function useNavigate() {
186
186
  }
187
187
 
188
188
  export function useRouterBasePath() {
189
- const { matchedRoutePath, router } = useRouterContext();
190
- return router instanceof NestedRouter ? router.basePath : matchedRoutePath;
189
+ const { router } = useRouterContext();
190
+ // For a nested router the base is its mount segment within the parent. For a top-level router the
191
+ // base is the served `<base href>` mount prefix (empty for the origin-served Studio UI) — NOT the
192
+ // matched route, so apps can build mount-correct sibling links (e.g. `${base}/assistant`) and
193
+ // compare them against the full `useLocation().pathname`, which carries the mount.
194
+ return router instanceof NestedRouter ? router.basePath : getMountBasename();
191
195
  }
192
196
 
193
197
  type UseParamsReturn<T> = T extends string ? string : Record<string, string>;
@@ -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
+ }
@@ -88,7 +88,15 @@ function SigninScreenImpl({
88
88
 
89
89
  useEffect(() => {
90
90
  if (!preservePath) {
91
- history.replaceState({}, '', '/');
91
+ // Reset to the app's mount root, not the bare origin. A gateway-mounted app carries a
92
+ // served `<base href>` deep mount; collapsing to '/' drops the app off that mount (the
93
+ // bare origin serves no app) and the address bar can no longer be reloaded. This effect
94
+ // also flashes briefly for already-authenticated users during the loading transition, so
95
+ // a bare '/' here loses the URL even on a normal login. Deriving the root from
96
+ // `document.baseURI` keeps it inside the mount; for the origin-served Studio UI the
97
+ // pathname is '/', so the behavior is unchanged.
98
+ const mountRoot = new URL(document.baseURI).pathname || '/';
99
+ history.replaceState({}, '', mountRoot);
92
100
  }
93
101
  }, [preservePath]);
94
102