@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/lib/router/Router.d.ts.map +1 -1
- package/lib/router/Router.js +7 -3
- package/lib/router/Router.js.map +1 -1
- package/lib/session/UserSession.d.ts.map +1 -1
- package/lib/session/UserSession.js +15 -7
- package/lib/session/UserSession.js.map +1 -1
- package/lib/session/auth/domainRouting.d.ts +10 -0
- package/lib/session/auth/domainRouting.d.ts.map +1 -1
- package/lib/session/auth/domainRouting.js +15 -0
- package/lib/session/auth/domainRouting.js.map +1 -1
- package/lib/shell/login/SigninScreen.js +9 -1
- package/lib/shell/login/SigninScreen.js.map +1 -1
- package/lib/vertesia-ui-router.js +1 -1
- package/lib/vertesia-ui-router.js.map +1 -1
- package/lib/vertesia-ui-session.js +1 -1
- package/lib/vertesia-ui-session.js.map +1 -1
- package/lib/vertesia-ui-shell.js +1 -1
- package/lib/vertesia-ui-shell.js.map +1 -1
- package/package.json +6 -6
- package/src/router/Router.tsx +7 -3
- package/src/session/UserSession.ts +15 -7
- package/src/session/auth/domainRouting.test.ts +28 -1
- package/src/session/auth/domainRouting.ts +16 -0
- package/src/shell/login/SigninScreen.tsx +9 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vertesia/ui",
|
|
3
|
-
"version": "1.3.0-dev.
|
|
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.
|
|
94
|
-
"@vertesia/
|
|
95
|
-
"@vertesia/json": "1.3.0-dev.
|
|
96
|
-
"@vertesia/
|
|
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": "
|
|
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",
|
package/src/router/Router.tsx
CHANGED
|
@@ -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 {
|
|
190
|
-
|
|
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
|
-
//
|
|
174
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|