Skip to content
8 changes: 0 additions & 8 deletions openaev-front/src/__tests__/utils/hooks/UseTenant.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -232,8 +232,6 @@ describe('useTenant', () => {
expect(mockBuildTenantUrl).toHaveBeenCalledWith(
TENANT_ALPHA.tenant_id,
expect.any(String),
expect.any(String),
expect.any(String),
);
});
expect(window.location.href).toContain(TENANT_ALPHA.tenant_id);
Expand All @@ -253,8 +251,6 @@ describe('useTenant', () => {
expect(mockBuildTenantUrl).toHaveBeenCalledWith(
TENANT_ALPHA.tenant_id,
expect.any(String),
expect.any(String),
expect.any(String),
);
});
expect(window.location.href).toContain(TENANT_ALPHA.tenant_id);
Expand Down Expand Up @@ -311,8 +307,6 @@ describe('useTenant', () => {
expect(mockBuildTenantUrl).toHaveBeenCalledWith(
TENANT_BETA.tenant_id,
expect.any(String),
expect.any(String),
expect.any(String),
);
expect(window.location.href).toContain(TENANT_BETA.tenant_id);
});
Expand Down Expand Up @@ -418,8 +412,6 @@ describe('useTenant', () => {
expect(mockBuildTenantUrl).toHaveBeenCalledWith(
TENANT_GAMMA.tenant_id,
expect.any(String),
expect.any(String),
expect.any(String),
);
expect(window.location.href).toContain(TENANT_GAMMA.tenant_id);
});
Expand Down
22 changes: 22 additions & 0 deletions openaev-front/src/__tests__/utils/url-helper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,28 @@ describe('url-helper', () => {
});
});

// -- stripDetailSegments --

describe('stripDetailSegments', () => {
it.each([
['/admin/scenarios/123e4567-e89b-12d3-a456-426614174000', '/admin/scenarios'],
['/admin/scenarios/123e4567-e89b-12d3-a456-426614174000/injects', '/admin/scenarios'],
['/admin/scenarios/123e4567-e89b-12d3-a456-426614174000/injects/ABCDEF12-0000-1111-2222-333344445555', '/admin/scenarios'],
['/admin/scenarios', '/admin/scenarios'],
['/admin', '/admin'],
['/', '/'],
])('given_%s_should_return_%s', async (input, expected) => {
// Arrange
const { stripDetailSegments } = await importHelper();

// Act
const result = stripDetailSegments(input);

// Assert
expect(result).toBe(expected);
});
});

// -- buildTenantApiPath --

describe('buildTenantApiPath', () => {
Expand Down
7 changes: 5 additions & 2 deletions openaev-front/src/utils/hooks/useTenant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { fetchUserTenants } from '../../actions/user/user-tenant-actions';
import { TENANT_SWITCH_SUCCESS } from '../../constants/ActionTypes';
import { type TenantOutput, type User } from '../api-types';
import { useAppDispatch } from '../hooks';
import { buildTenantUrl, extractTenantFromUrl } from '../url-helper';
import { buildTenantUrl, extractTenantFromUrl, stripDetailSegments } from '../url-helper';

/**
* Internal hook that encapsulates the current-tenant state and
Expand Down Expand Up @@ -57,9 +57,12 @@ const useTenant = (me: User | undefined, logged: unknown, isPlatformRoute: boole
const target = tenants.find(t => t.tenant_id === tenantId);
if (!target) return false;
if (extractTenantFromUrl() !== target.tenant_id) {
// Switching to a different tenant — strip detail segments so we land on
// the list page instead of a detail page for a resource that may not exist.
const safePath = stripDetailSegments(location.pathname);
// Full page navigation — the reload will re-initialise tenant state,
// so we intentionally skip setTenant to avoid a broken intermediate render.
window.location.href = buildTenantUrl(target.tenant_id, location.pathname, location.search, location.hash);
window.location.href = buildTenantUrl(target.tenant_id, safePath);
} else {
setTenant(target);
}
Expand Down
20 changes: 19 additions & 1 deletion openaev-front/src/utils/url-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,25 @@ export const TENANT_URI = '/api/tenants';
*/
export const DEFAULT_TENANT_UUID = '2cffad3a-0001-4078-b0e2-ef74274022c3';

const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
export const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

/**
* Strips entity-specific detail segments from a path so that a tenant switch
* lands on the parent list page rather than a detail page for a resource
* that may not exist in the target tenant.
*
* e.g. "/admin/scenarios/123e4567-e89b-12d3-a456-426614174000"
* → "/admin/scenarios"
* "/admin/scenarios/123e4567-e89b-12d3-a456-426614174000/injects"
* → "/admin/scenarios"
* "/admin/scenarios" → "/admin/scenarios" (unchanged)
*/
export const stripDetailSegments = (pathname: string): string => {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems a bit fragile to me. For example, if you’re on a list page and then switch tenant, but you don’t have access to that list in the new tenant, you’ll get an error as well.

Could we do something similar to what we do after login at first ?

Copy link
Copy Markdown
Contributor Author

@corinnekrych corinnekrych Apr 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean loosing context and forward to home page?
The goal here was to allow end user to switch between tenant keeping context. For ex if a tenant user browse teams in tenantA and wants to check tenantB's teams.
In case the user has no read access to scenario, he will fallback to existing RBAC alert.

const segments = pathname.split('/').filter(Boolean);
const uuidIndex = segments.findIndex(s => UUID_REGEX.test(s));
if (uuidIndex === -1) return pathname;
return '/' + segments.slice(0, uuidIndex).join('/');
};

// ---------------------------------------------------------------------------
// URL helpers
Expand Down
Loading