Skip to content

Commit 4ff413c

Browse files
arbrandesclaude
andcommitted
feat: Internal dashboard navigation
When the backend returns the LMS dashboard URL as redirect_url after login or registration, replace it with getUrlByRouteRole(dashboardRole) so that RedirectLogistration can use <Navigate> for SPA navigation instead of a full page reload. Also hydrate the authenticated user before SPA navigation so the shell header displays the user's name. This applies the same fetchAuthenticatedUser/hydrateAuthenticatedUser pattern already used for the localNextPath flow to any redirect URL that starts with '/'. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 027f72c commit 4ff413c

7 files changed

Lines changed: 35 additions & 7 deletions

File tree

src/data/utils/dataUtils.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Utility functions
2+
import { getSiteConfig, getUrlByRouteRole } from '@openedx/frontend-base';
23
import * as QueryString from 'query-string';
34

5+
import { dashboardRole } from '../../constants';
46
import { AUTH_PARAMS } from '../constants';
57

68
export const getTpaProvider = (tpaHintProvider, primaryProviders, secondaryProviders) => {
@@ -75,6 +77,19 @@ export const windowScrollTo = (options) => {
7577
return window.scrollTo(options.top, options.left);
7678
};
7779

80+
/**
81+
* Normalize a backend redirect URL: if the backend returns the LMS dashboard
82+
* URL (or nothing), replace it with the role-based dashboard URL so that SPA
83+
* navigation can be used when the dashboard lives in the same shell.
84+
*/
85+
export const normalizeRedirectUrl = (backendUrl) => {
86+
const dashboardUrl = getUrlByRouteRole(dashboardRole);
87+
const lmsDashboardUrl = `${getSiteConfig().lmsBaseUrl}/dashboard`;
88+
return (!backendUrl || backendUrl.startsWith(lmsDashboardUrl))
89+
? dashboardUrl
90+
: backendUrl;
91+
};
92+
7893
export const isHostAvailableInQueryParams = () => {
7994
const queryParams = getAllPossibleQueryParams();
8095
return 'host' in queryParams;

src/data/utils/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export {
55
getActivationStatus,
66
isHostAvailableInQueryParams,
77
updatePathWithQueryParams,
8+
normalizeRedirectUrl,
89
windowScrollTo,
910
} from './dataUtils';
1011
export { default as setCookie } from './cookies';

src/login/LoginPage.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ const LoginPage = ({
7474
// Hydrate in the background — publishes AUTHENTICATED_USER_CHANGED after
7575
// SPA navigation, so the header picks up the full user profile (avatar, etc.)
7676
hydrateAuthenticatedUser();
77+
} else if (data.redirectUrl?.startsWith('/')) {
78+
await fetchAuthenticatedUser({ forceRefresh: true });
79+
setLoginResult({ success: true, redirectUrl: data.redirectUrl });
80+
hydrateAuthenticatedUser();
7781
} else {
7882
setLoginResult({ success: true, redirectUrl: data.redirectUrl || '' });
7983
}

src/login/data/api.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,12 +170,13 @@ describe('login api', () => {
170170
extra_data: { some: 'value' },
171171
};
172172
const mockResponse = { data: mockResponseData };
173+
// normalizeRedirectUrl replaces the LMS dashboard URL with the role-based one
173174
const expectedCamelCaseInput = {
174-
redirectUrl: 'http://localhost:18000/dashboard',
175+
redirectUrl: '/dashboard',
175176
success: true,
176177
};
177178
const expectedResult = {
178-
redirectUrl: 'http://localhost:18000/dashboard',
179+
redirectUrl: '/dashboard',
179180
success: true,
180181
};
181182

src/login/data/api.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { camelCaseObject, getAuthenticatedHttpClient, getSiteConfig, getUrlByRouteRole } from '@openedx/frontend-base';
1+
import { camelCaseObject, getAuthenticatedHttpClient, getSiteConfig } from '@openedx/frontend-base';
22
import * as QueryString from 'query-string';
33

4+
import { normalizeRedirectUrl } from '../../data/utils';
5+
46
const login = async (creds) => {
57
const requestConfig = {
68
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -9,9 +11,8 @@ const login = async (creds) => {
911
const url = `${getSiteConfig().lmsBaseUrl}/api/user/v2/account/login_session/`;
1012
const { data } = await getAuthenticatedHttpClient()
1113
.post(url, QueryString.stringify(creds), requestConfig);
12-
const defaultRedirectUrl = getUrlByRouteRole('org.openedx.frontend.role.dashboard');
1314
return camelCaseObject({
14-
redirectUrl: data.redirect_url || defaultRedirectUrl,
15+
redirectUrl: normalizeRedirectUrl(data.redirect_url || ''),
1516
success: data.success || false,
1617
});
1718
};

src/register/RegistrationPage.jsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ const RegistrationPage = (props) => {
116116
// Hydrate in the background — publishes AUTHENTICATED_USER_CHANGED after
117117
// SPA navigation, so the header picks up the full user profile (avatar, etc.)
118118
hydrateAuthenticatedUser();
119+
} else if (data.redirectUrl?.startsWith('/')) {
120+
await fetchAuthenticatedUser({ forceRefresh: true });
121+
setRegistrationResult({ ...data, redirectUrl: data.redirectUrl });
122+
hydrateAuthenticatedUser();
119123
} else {
120124
setRegistrationResult(data);
121125
}

src/register/data/api.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { getAuthenticatedHttpClient, getHttpClient, getSiteConfig, getUrlByRouteRole } from '@openedx/frontend-base';
1+
import { getAuthenticatedHttpClient, getHttpClient, getSiteConfig } from '@openedx/frontend-base';
22
import * as QueryString from 'query-string';
33

4+
import { normalizeRedirectUrl } from '../../data/utils';
5+
46
const registerNewUserApi = async (registrationInformation) => {
57
const requestConfig = {
68
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
@@ -14,7 +16,7 @@ const registerNewUserApi = async (registrationInformation) => {
1416
});
1517

1618
return {
17-
redirectUrl: data.redirect_url || getUrlByRouteRole('org.openedx.frontend.role.dashboard'),
19+
redirectUrl: normalizeRedirectUrl(data.redirect_url || ''),
1820
success: data.success || false,
1921
authenticatedUser: data.authenticated_user,
2022
};

0 commit comments

Comments
 (0)