Compare commits

...

11 Commits

Author SHA1 Message Date
Ihor Romaniuk
292931f2cb fix: info icon shrinking on advanced settings page (#1030) 2024-05-23 09:09:10 -04:00
Ihor Romaniuk
a580b8980a fix: export page timestamp (#785) (#1026)
* fix: export page timestamp

* fix: tests

Co-authored-by: Jesper Hodge <19345795+jesperhodge@users.noreply.github.com>
2024-05-22 12:03:16 -04:00
Dmytro
4592c0d06a fix: incorrect redirect link to Pages&Resources from Custom Pages (#915)
Co-authored-by: Dima Alipov <dimaalipov@MacBook-Pro-Dima.local>
2024-04-08 09:38:51 -04:00
Eugene Dyudyunov
91922874fc fix: wrong min count alert showing (#730) (#911)
Co-authored-by: Kristin Aoki <42981026+KristinAoki@users.noreply.github.com>
2024-03-21 08:59:56 -04:00
Stanislav
0020597d97 fix: use styles from frontend-content-header (#853) 2024-03-20 16:48:11 -04:00
Eugene Dyudyunov
74ce163bec fix: correct internal routes
The Content dropdown items have incorrect URLs for the
internal routing when MFEs are deployed using the common
domain and the PUBLIC_PATH.

Introduce the new function to check the path correctness (
it already exists in master) and wrap the navigational links with it.
2024-03-06 10:01:21 -03:00
Stanislav Lunyachek
2b8edfd761 fix: Paragon stepper fix inside discussions settings 2024-03-05 12:12:07 -03:00
Maria Grimaldi
d0c4765698 fix: remove unnecessary course-v1 from courseId string (#687) 2024-02-29 15:21:08 -03:00
Kristin Aoki
d48d6ffa5d feat: add new libray button (#710) 2024-02-28 12:50:22 -03:00
Adolfo R. Brandes
14f035435c fix: Fix data API URL handling
All configuration calls must handled asynchronously, otherwise they risk
failure in runtime configuration scenarios.
2023-11-21 16:57:44 -03:00
Stanislav
fa25150e3d fix: Missed favicon in Safari (#635)
Co-authored-by: Stanislav Lunyachek <lunyachek@MacBook-Pro-M1.local>
2023-11-01 16:38:39 -04:00
19 changed files with 254 additions and 138 deletions

View File

@@ -4,7 +4,7 @@
<title>Course Authoring | <%= process.env.SITE_NAME %></title> <title>Course Authoring | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="shortcut icon" href="<%= process.env.FAVICON_URL %>" type="image/x-icon" /> <link rel="shortcut icon" href="<%=htmlWebpackPlugin.options.FAVICON_URL%>" type="image/x-icon" />
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -71,7 +71,7 @@ const SettingCard = ({
iconAs={Icon} iconAs={Icon}
alt={intl.formatMessage(messages.helpButtonText)} alt={intl.formatMessage(messages.helpButtonText)}
variant="primary" variant="primary"
className=" ml-1 mr-2" className="flex-shrink-0 ml-1 mr-2"
/> />
<ModalPopup <ModalPopup
hasArrow hasArrow

View File

@@ -41,6 +41,7 @@ import messages from './messages';
import CustomPagesProvider from './CustomPagesProvider'; import CustomPagesProvider from './CustomPagesProvider';
import EditModal from './EditModal'; import EditModal from './EditModal';
import getPageHeadTitle from '../generic/utils'; import getPageHeadTitle from '../generic/utils';
import { getPagePath } from '../utils';
const CustomPages = ({ const CustomPages = ({
courseId, courseId,
@@ -121,7 +122,7 @@ const CustomPages = ({
ariaLabel="Custom Page breadcrumbs" ariaLabel="Custom Page breadcrumbs"
links={[ links={[
{ label: 'Content', href: `${config.STUDIO_BASE_URL}/course/${courseId}` }, { label: 'Content', href: `${config.STUDIO_BASE_URL}/course/${courseId}` },
{ label: 'Pages and Resources', href: `/course/${courseId}/pages-and-resources` }, { label: 'Pages and Resources', href: getPagePath(courseId, 'true', 'tabs') },
]} ]}
/> />
</div> </div>

View File

@@ -1,4 +1,3 @@
@import "./export-stepper/ExportStepper";
@import "./export-footer/ExportFooter"; @import "./export-footer/ExportFooter";
.export { .export {

View File

@@ -22,6 +22,21 @@ import {
updateSavingStatus, updateSavingStatus,
} from './slice'; } from './slice';
function setExportDate({
date, exportStatus, exportOutput, dispatch,
}) {
// If there is no cookie for the last export date, set it now.
const cookies = new Cookies();
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
if (!cookieData?.completed) {
setExportCookie(date, exportStatus === EXPORT_STAGES.SUCCESS);
}
// If we don't have export date set yet via cookie, set success date to current date.
if (exportOutput && !cookieData?.completed) {
dispatch(updateSuccessDate(date));
}
}
export function startExportingCourse(courseId) { export function startExportingCourse(courseId) {
return async (dispatch) => { return async (dispatch) => {
dispatch(updateSavingStatus({ status: RequestStatus.PENDING })); dispatch(updateSavingStatus({ status: RequestStatus.PENDING }));
@@ -45,29 +60,30 @@ export function fetchExportStatus(courseId) {
return async (dispatch) => { return async (dispatch) => {
dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS })); dispatch(updateLoadingStatus({ status: RequestStatus.IN_PROGRESS }));
try { try {
const { exportStatus, exportOutput, exportError } = await getExportStatus(courseId); const {
exportStatus, exportOutput, exportError,
} = await getExportStatus(courseId);
dispatch(updateCurrentStage(Math.abs(exportStatus))); dispatch(updateCurrentStage(Math.abs(exportStatus)));
const date = moment().valueOf();
setExportDate({
date, exportStatus, exportOutput, dispatch,
});
if (exportError) {
const errorMessage = exportError.rawErrorMsg || exportError;
const errorUnitUrl = exportError.editUnitUrl || null;
dispatch(updateError({ msg: errorMessage, unitUrl: errorUnitUrl }));
dispatch(updateIsErrorModalOpen(true));
}
if (exportOutput) { if (exportOutput) {
if (exportOutput.startsWith('/')) { if (exportOutput.startsWith('/')) {
dispatch(updateDownloadPath(`${getConfig().STUDIO_BASE_URL}${exportOutput}`)); dispatch(updateDownloadPath(`${getConfig().STUDIO_BASE_URL}${exportOutput}`));
} else { } else {
dispatch(updateDownloadPath(exportOutput)); dispatch(updateDownloadPath(exportOutput));
} }
dispatch(updateSuccessDate(moment().valueOf()));
}
const cookies = new Cookies();
const cookieData = cookies.get(LAST_EXPORT_COOKIE_NAME);
if (!cookieData?.completed) {
setExportCookie(moment().valueOf(), exportStatus === EXPORT_STAGES.SUCCESS);
}
if (exportError) {
const errorMessage = exportError.rawErrorMsg || exportError;
const errorUnitUrl = exportError.editUnitUrl || null;
dispatch(updateError({ msg: errorMessage, unitUrl: errorUnitUrl }));
dispatch(updateIsErrorModalOpen(true));
} }
dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL })); dispatch(updateLoadingStatus({ status: RequestStatus.SUCCESSFUL }));

View File

@@ -0,0 +1,146 @@
import Cookies from 'universal-cookie';
import { fetchExportStatus } from './thunks';
import * as api from './api';
import { EXPORT_STAGES } from './constants';
jest.mock('universal-cookie', () => jest.fn().mockImplementation(() => ({
get: jest.fn().mockImplementation(() => ({ completed: false })),
})));
jest.mock('../utils', () => ({
setExportCookie: jest.fn(),
}));
describe('fetchExportStatus thunk', () => {
const dispatch = jest.fn();
const getState = jest.fn();
const courseId = 'course-123';
const exportStatus = EXPORT_STAGES.COMPRESSING;
const exportOutput = 'export output';
const exportError = 'export error';
let mockGetExportStatus;
beforeEach(() => {
jest.clearAllMocks();
mockGetExportStatus = jest.spyOn(api, 'getExportStatus').mockResolvedValue({
exportStatus,
exportOutput,
exportError,
});
});
it('should dispatch updateCurrentStage with export status', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus,
exportOutput,
exportError,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: exportStatus,
type: 'exportPage/updateCurrentStage',
});
});
it('should dispatch updateError on export error', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus,
exportOutput,
exportError,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: {
msg: exportError,
unitUrl: null,
},
type: 'exportPage/updateError',
});
});
it('should dispatch updateIsErrorModalOpen with true if export error', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus,
exportOutput,
exportError,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: true,
type: 'exportPage/updateIsErrorModalOpen',
});
});
it('should not dispatch updateIsErrorModalOpen if no export error', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus,
exportOutput,
exportError: null,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).not.toHaveBeenCalledWith({
payload: false,
type: 'exportPage/updateIsErrorModalOpen',
});
});
it("should dispatch updateDownloadPath if there's export output", async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus,
exportOutput,
exportError,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: exportOutput,
type: 'exportPage/updateDownloadPath',
});
});
it('should dispatch updateSuccessDate with current date if export status is success', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus:
EXPORT_STAGES.SUCCESS,
exportOutput,
exportError,
});
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).toHaveBeenCalledWith({
payload: expect.any(Number),
type: 'exportPage/updateSuccessDate',
});
});
it('should not dispatch updateSuccessDate with current date if last-export cookie is already set', async () => {
mockGetExportStatus.mockResolvedValue({
exportStatus:
EXPORT_STAGES.SUCCESS,
exportOutput,
exportError,
});
Cookies.mockImplementation(() => ({
get: jest.fn().mockReturnValueOnce({ completed: true }),
}));
await fetchExportStatus(courseId)(dispatch, getState);
expect(dispatch).not.toHaveBeenCalledWith({
payload: expect.any,
type: 'exportPage/updateSuccessDate',
});
});
});

View File

@@ -1,3 +0,0 @@
.pgn__stepper-header-step-list {
flex-direction: column;
}

View File

@@ -153,7 +153,7 @@ describe('<CreateOrRerunCourseForm />', () => {
userEvent.type(runInput, '1'); userEvent.type(runInput, '1');
userEvent.click(createBtn); userEvent.click(createBtn);
}); });
await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url }); await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { url });
await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch);
expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}`); expect(window.location.assign).toHaveBeenCalledWith(`${process.env.STUDIO_BASE_URL}${url}`);
@@ -168,7 +168,7 @@ describe('<CreateOrRerunCourseForm />', () => {
const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage); const numberInput = screen.getByPlaceholderText(messages.courseNumberPlaceholder.defaultMessage);
const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage); const runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.defaultMessage);
const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage }); const createBtn = screen.getByRole('button', { name: messages.createButton.defaultMessage });
await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { url, destinationCourseKey }); await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { url, destinationCourseKey });
await act(async () => { await act(async () => {
userEvent.type(displayNameInput, 'foo course name'); userEvent.type(displayNameInput, 'foo course name');
@@ -250,7 +250,7 @@ describe('<CreateOrRerunCourseForm />', () => {
it('shows alert error if postErrors presents', async () => { it('shows alert error if postErrors presents', async () => {
render(<RootWrapper {...props} />); render(<RootWrapper {...props} />);
await mockStore(); await mockStore();
await axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, { errMsg: 'aaa' }); await axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, { errMsg: 'aaa' });
await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch); await executeThunk(updateCreateOrRerunCourseQuery({ org: 'testX', run: 'some' }), store.dispatch);
expect(screen.getByText('aaa')).toBeInTheDocument(); expect(screen.getByText('aaa')).toBeInTheDocument();

View File

@@ -4,9 +4,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertObjectToSnakeCase } from '../../utils'; import { convertObjectToSnakeCase } from '../../utils';
export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getCreateOrRerunCourseUrl = new URL('course/', getApiBaseUrl()).href; export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href;
export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href; export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href;
export const getOrganizationsUrl = new URL('organizations', getApiBaseUrl()).href; export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href;
/** /**
* Get's organizations data. * Get's organizations data.
@@ -14,7 +14,7 @@ export const getOrganizationsUrl = new URL('organizations', getApiBaseUrl()).hre
*/ */
export async function getOrganizations() { export async function getOrganizations() {
const { data } = await getAuthenticatedHttpClient().get( const { data } = await getAuthenticatedHttpClient().get(
getOrganizationsUrl, getOrganizationsUrl(),
); );
return camelCaseObject(data); return camelCaseObject(data);
} }
@@ -37,7 +37,7 @@ export async function getCourseRerun(courseId) {
*/ */
export async function createOrRerunCourse(courseData) { export async function createOrRerunCourse(courseData) {
const { data } = await getAuthenticatedHttpClient().post( const { data } = await getAuthenticatedHttpClient().post(
getCreateOrRerunCourseUrl, getCreateOrRerunCourseUrl(),
convertObjectToSnakeCase(courseData, true), convertObjectToSnakeCase(courseData, true),
); );
return camelCaseObject(data); return camelCaseObject(data);

View File

@@ -66,10 +66,10 @@ describe('generic api calls', () => {
org: 'edX', org: 'edX',
run: 'Demo_Course', run: 'Demo_Course',
}; };
axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, courseRerunData); axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, courseRerunData);
const result = await createOrRerunCourse(courseRerunData); const result = await createOrRerunCourse(courseRerunData);
expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl); expect(axiosMock.history.post[0].url).toEqual(getCreateOrRerunCourseUrl());
expect(result).toEqual(courseRerunData); expect(result).toEqual(courseRerunData);
}); });
}); });

View File

@@ -62,10 +62,10 @@ describe('<AssignmentSection />', () => {
it('checking correct assignment weight of total grade value', async () => { it('checking correct assignment weight of total grade value', async () => {
const { getByTestId } = render(<RootWrapper setGradingData={setGradingData} />); const { getByTestId } = render(<RootWrapper setGradingData={setGradingData} />);
await waitFor(() => { await waitFor(() => {
const assignmentShortLabelInput = getByTestId('assignment-weight-input'); const assignmentWeightInput = getByTestId('assignment-weight-input');
expect(assignmentShortLabelInput.value).toBe('100'); expect(assignmentWeightInput.value).toBe('100');
fireEvent.change(assignmentShortLabelInput, { target: { value: '123' } }); fireEvent.change(assignmentWeightInput, { target: { value: '123' } });
expect(testObj.graders[0].weight).toBe('123'); expect(testObj.graders[0].weight).toBe(123);
}); });
}); });
it('checking correct assignment total number value', async () => { it('checking correct assignment total number value', async () => {
@@ -74,7 +74,7 @@ describe('<AssignmentSection />', () => {
const assignmentTotalNumberInput = getByTestId('assignment-minCount-input'); const assignmentTotalNumberInput = getByTestId('assignment-minCount-input');
expect(assignmentTotalNumberInput.value).toBe('1'); expect(assignmentTotalNumberInput.value).toBe('1');
fireEvent.change(assignmentTotalNumberInput, { target: { value: '123' } }); fireEvent.change(assignmentTotalNumberInput, { target: { value: '123' } });
expect(testObj.graders[0].minCount).toBe('123'); expect(testObj.graders[0].minCount).toBe(123);
}); });
}); });
it('checking correct assignment number of droppable value', async () => { it('checking correct assignment number of droppable value', async () => {
@@ -83,7 +83,7 @@ describe('<AssignmentSection />', () => {
const assignmentNumberOfDroppableInput = getByTestId('assignment-dropCount-input'); const assignmentNumberOfDroppableInput = getByTestId('assignment-dropCount-input');
expect(assignmentNumberOfDroppableInput.value).toBe('1'); expect(assignmentNumberOfDroppableInput.value).toBe('1');
fireEvent.change(assignmentNumberOfDroppableInput, { target: { value: '2' } }); fireEvent.change(assignmentNumberOfDroppableInput, { target: { value: '2' } });
expect(testObj.graders[0].dropCount).toBe('2'); expect(testObj.graders[0].dropCount).toBe(2);
}); });
}); });
it('checking correct error msg if dropCount have negative number or empty string', async () => { it('checking correct error msg if dropCount have negative number or empty string', async () => {
@@ -100,20 +100,20 @@ describe('<AssignmentSection />', () => {
it('checking correct error msg if minCount have negative number or empty string', async () => { it('checking correct error msg if minCount have negative number or empty string', async () => {
const { getByText, getByTestId } = render(<RootWrapper />); const { getByText, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const assignmentNumberOfDroppableInput = getByTestId('assignment-minCount-input'); const assignmentMinCountInput = getByTestId('assignment-minCount-input');
expect(assignmentNumberOfDroppableInput.value).toBe('1'); expect(assignmentMinCountInput.value).toBe('1');
fireEvent.change(assignmentNumberOfDroppableInput, { target: { value: '-2' } }); fireEvent.change(assignmentMinCountInput, { target: { value: '-2' } });
expect(getByText(messages.totalNumberErrorMessage.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.totalNumberErrorMessage.defaultMessage)).toBeInTheDocument();
fireEvent.change(assignmentNumberOfDroppableInput, { target: { value: '' } }); fireEvent.change(assignmentMinCountInput, { target: { value: '' } });
expect(getByText(messages.totalNumberErrorMessage.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.totalNumberErrorMessage.defaultMessage)).toBeInTheDocument();
}); });
}); });
it('checking correct error msg if total weight have negative number', async () => { it('checking correct error msg if total weight have negative number', async () => {
const { getByText, getByTestId } = render(<RootWrapper />); const { getByText, getByTestId } = render(<RootWrapper />);
await waitFor(() => { await waitFor(() => {
const assignmentNumberOfDroppableInput = getByTestId('assignment-weight-input'); const assignmentWeightInput = getByTestId('assignment-weight-input');
expect(assignmentNumberOfDroppableInput.value).toBe('100'); expect(assignmentWeightInput.value).toBe('100');
fireEvent.change(assignmentNumberOfDroppableInput, { target: { value: '-100' } }); fireEvent.change(assignmentWeightInput, { target: { value: '-100' } });
expect(getByText(messages.weightOfTotalGradeErrorMessage.defaultMessage)).toBeInTheDocument(); expect(getByText(messages.weightOfTotalGradeErrorMessage.defaultMessage)).toBeInTheDocument();
}); });
}); });

View File

@@ -34,7 +34,12 @@ const AssignmentSection = ({
} }
const handleAssignmentChange = (e, assignmentId) => { const handleAssignmentChange = (e, assignmentId) => {
const { name, value } = e.target; const { name, value, type: inputType } = e.target;
let inputValue = value;
if (inputType === 'number') {
inputValue = parseInt(value, 10);
}
setShowSavePrompt(true); setShowSavePrompt(true);
@@ -42,7 +47,7 @@ const AssignmentSection = ({
...prevState, ...prevState,
graders: graders.map(grader => { graders: graders.map(grader => {
if (grader.id === assignmentId) { if (grader.id === assignmentId) {
return { ...grader, [name]: value }; return { ...grader, [name]: inputValue };
} }
return grader; return grader;
}), }),
@@ -66,7 +71,7 @@ const AssignmentSection = ({
return ( return (
<div className="assignment-items"> <div className="assignment-items">
{graders?.map((gradeField) => { {graders?.map((gradeField) => {
const courseAssignmentUsage = courseAssignmentLists[gradeField.type.toLowerCase()]; const courseAssignmentUsage = courseAssignmentLists[gradeField.type];
const showDefinedCaseAlert = gradeField.minCount !== courseAssignmentUsage?.length const showDefinedCaseAlert = gradeField.minCount !== courseAssignmentUsage?.length
&& Boolean(courseAssignmentUsage?.length); && Boolean(courseAssignmentUsage?.length);
const showNotDefinedCaseAlert = !courseAssignmentUsage?.length && Boolean(gradeField.type); const showNotDefinedCaseAlert = !courseAssignmentUsage?.length && Boolean(gradeField.type);

View File

@@ -1,65 +0,0 @@
// This SCSS was partly copied from edx/frontend-app-support-tools/src/support-header/index.scss.
$spacer: 1rem;
$white: #FFFFFF;
.btn-tertiary:hover {
color: white;
background-color: #00262B;
}
.course-title-lockup {
@media only screen and (max-width: 768px) {
padding-left: .5rem;
max-width: 70%;
}
@media only screen and (min-width: 769px) {
padding: .5rem;
padding-right: $spacer;
border-right: 1px solid #E5E5E5;
min-width: 70%;
}
overflow: hidden;
span {
color: #333333;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.375rem;
}
}
.site-header-mobile,
.site-header-desktop {
position: relative;
z-index: 1000;
}
.site-header-mobile {
img {
height: 1.5rem;
}
}
.site-header-desktop {
height: 3.75rem;
box-shadow: 0 1px 0 0 rgb(0 0 0 / .1);
background: $white;
.logo {
display: block;
box-sizing: content-box;
position: relative;
top: -.05em;
height: 1.75rem;
padding: $spacer 0;
margin-right: $spacer;
img {
display: block;
height: 100%;
}
}
}

View File

@@ -44,7 +44,7 @@ export const getSettingMenuItems = ({ studioBaseUrl, courseId, intl }) => ([
title: intl.formatMessage(messages['header.links.courseTeam']), title: intl.formatMessage(messages['header.links.courseTeam']),
}, },
{ {
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`, href: `${studioBaseUrl}/group_configurations/${courseId}`,
title: intl.formatMessage(messages['header.links.groupConfigurations']), title: intl.formatMessage(messages['header.links.groupConfigurations']),
}, },
{ {

View File

@@ -2,7 +2,7 @@
@import "~@edx/brand/paragon/variables"; @import "~@edx/brand/paragon/variables";
@import "~@edx/paragon/scss/core/core"; @import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides"; @import "~@edx/brand/paragon/overrides";
@import "header/header"; @import "~@edx/frontend-component-header/dist/index";
@import "assets/scss/variables"; @import "assets/scss/variables";
@import "assets/scss/form"; @import "assets/scss/form";
@import "assets/scss/utilities"; @import "assets/scss/utilities";

View File

@@ -42,7 +42,6 @@ const StudioHome = ({ intl }) => {
studioRequestEmail, studioRequestEmail,
libraryAuthoringMfeUrl, libraryAuthoringMfeUrl,
redirectToLibraryAuthoringMfe, redirectToLibraryAuthoringMfe,
splitStudioHome,
} = studioHomeData; } = studioHomeData;
function getHeaderButtons() { function getHeaderButtons() {
@@ -68,26 +67,24 @@ const StudioHome = ({ intl }) => {
); );
} }
let libraryHref = `${getConfig().STUDIO_BASE_URL}/home#libraries-tab`; let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
if (splitStudioHome) {
libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
}
if (redirectToLibraryAuthoringMfe) { if (redirectToLibraryAuthoringMfe) {
libraryHref = `${libraryAuthoringMfeUrl}/create`; libraryHref = `${libraryAuthoringMfeUrl}/create`;
headerButtons.push(
<Button
variant="outline-primary"
iconBefore={AddIcon}
size="sm"
disabled={showNewCourseContainer}
href={libraryHref}
data-testid="new-library-button"
>
{intl.formatMessage(messages.addNewLibraryBtnText)}
</Button>,
);
} }
headerButtons.push(
<Button
variant="outline-primary"
iconBefore={AddIcon}
size="sm"
disabled={showNewCourseContainer}
href={libraryHref}
data-testid="new-library-button"
>
{intl.formatMessage(messages.addNewLibraryBtnText)}
</Button>,
);
return headerButtons; return headerButtons;
} }

View File

@@ -116,15 +116,16 @@ describe('<StudioHome />', async () => {
}); });
describe('render new library button', () => { describe('render new library button', () => {
it('should not show button', async () => { it('href should include home_library', async () => {
useSelector.mockReturnValue({ useSelector.mockReturnValue({
...studioHomeMock, ...studioHomeMock,
courseCreatorStatus: COURSE_CREATOR_STATES.granted, courseCreatorStatus: COURSE_CREATOR_STATES.granted,
}); });
const studioBaseUrl = 'http://localhost:18010';
const { queryByTestId } = render(<RootWrapper />); const { getByTestId } = render(<RootWrapper />);
const createNewLibraryButton = queryByTestId('new-library-button'); const createNewLibraryButton = getByTestId('new-library-button');
expect(createNewLibraryButton).toBeNull(); expect(createNewLibraryButton.getAttribute('href')).toBe(`${studioBaseUrl}/home_library`);
}); });
it('href should include create', async () => { it('href should include create', async () => {
useSelector.mockReturnValue({ useSelector.mockReturnValue({

View File

@@ -42,7 +42,7 @@ describe('<OrganizationSection />', async () => {
}); });
store = initializeStore(); store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient()); axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onDelete(getOrganizationsUrl).reply(200); axiosMock.onDelete(getOrganizationsUrl()).reply(200);
await executeThunk(fetchOrganizationsQuery(), store.dispatch); await executeThunk(fetchOrganizationsQuery(), store.dispatch);
useSelector.mockReturnValue(['edX', 'org']); useSelector.mockReturnValue(['edX', 'org']);
}); });

View File

@@ -95,12 +95,31 @@ export function parseArrayOrObjectValues(obj) {
return result; return result;
} }
/**
* Create a correct inner path depend on config PUBLIC_PATH.
* @param {string} checkPath - the internal route path that is validated
* @returns {string} - the correct internal route path
*/
export const createCorrectInternalRoute = (checkPath) => {
let basePath = getConfig().PUBLIC_PATH;
if (basePath.endsWith('/')) {
basePath = basePath.slice(0, -1);
}
if (!checkPath.startsWith(basePath)) {
return `${basePath}${checkPath}`;
}
return checkPath;
};
export function getPagePath(courseId, isMfePageEnabled, urlParameter) { export function getPagePath(courseId, isMfePageEnabled, urlParameter) {
if (isMfePageEnabled === 'true') { if (isMfePageEnabled === 'true') {
if (urlParameter === 'tabs') { if (urlParameter === 'tabs') {
return `${getConfig().BASE_URL}/course/${courseId}/pages-and-resources`; return createCorrectInternalRoute(`/course/${courseId}/pages-and-resources`);
} }
return `${getConfig().BASE_URL}/course/${courseId}/${urlParameter}`; return createCorrectInternalRoute(`/course/${courseId}/${urlParameter}`);
} }
return `${getConfig().STUDIO_BASE_URL}/${urlParameter}/${courseId}`; return `${getConfig().STUDIO_BASE_URL}/${urlParameter}/${courseId}`;
} }