Compare commits

...

9 Commits

Author SHA1 Message Date
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
16 changed files with 76 additions and 122 deletions

View File

@@ -4,7 +4,7 @@
<title>Course Authoring | <%= process.env.SITE_NAME %></title>
<meta charset="utf-8">
<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>
<body>
<div id="root"></div>

View File

@@ -41,6 +41,7 @@ import messages from './messages';
import CustomPagesProvider from './CustomPagesProvider';
import EditModal from './EditModal';
import getPageHeadTitle from '../generic/utils';
import { getPagePath } from '../utils';
const CustomPages = ({
courseId,
@@ -121,7 +122,7 @@ const CustomPages = ({
ariaLabel="Custom Page breadcrumbs"
links={[
{ 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>

View File

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

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.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);
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 runInput = screen.getByPlaceholderText(messages.courseRunPlaceholder.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 () => {
userEvent.type(displayNameInput, 'foo course name');
@@ -250,7 +250,7 @@ describe('<CreateOrRerunCourseForm />', () => {
it('shows alert error if postErrors presents', async () => {
render(<RootWrapper {...props} />);
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);
expect(screen.getByText('aaa')).toBeInTheDocument();

View File

@@ -4,9 +4,9 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { convertObjectToSnakeCase } from '../../utils';
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 getOrganizationsUrl = new URL('organizations', getApiBaseUrl()).href;
export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href;
/**
* Get's organizations data.
@@ -14,7 +14,7 @@ export const getOrganizationsUrl = new URL('organizations', getApiBaseUrl()).hre
*/
export async function getOrganizations() {
const { data } = await getAuthenticatedHttpClient().get(
getOrganizationsUrl,
getOrganizationsUrl(),
);
return camelCaseObject(data);
}
@@ -37,7 +37,7 @@ export async function getCourseRerun(courseId) {
*/
export async function createOrRerunCourse(courseData) {
const { data } = await getAuthenticatedHttpClient().post(
getCreateOrRerunCourseUrl,
getCreateOrRerunCourseUrl(),
convertObjectToSnakeCase(courseData, true),
);
return camelCaseObject(data);

View File

@@ -66,10 +66,10 @@ describe('generic api calls', () => {
org: 'edX',
run: 'Demo_Course',
};
axiosMock.onPost(getCreateOrRerunCourseUrl).reply(200, courseRerunData);
axiosMock.onPost(getCreateOrRerunCourseUrl()).reply(200, 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);
});
});

View File

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

View File

@@ -34,7 +34,12 @@ const AssignmentSection = ({
}
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);
@@ -42,7 +47,7 @@ const AssignmentSection = ({
...prevState,
graders: graders.map(grader => {
if (grader.id === assignmentId) {
return { ...grader, [name]: value };
return { ...grader, [name]: inputValue };
}
return grader;
}),
@@ -66,7 +71,7 @@ const AssignmentSection = ({
return (
<div className="assignment-items">
{graders?.map((gradeField) => {
const courseAssignmentUsage = courseAssignmentLists[gradeField.type.toLowerCase()];
const courseAssignmentUsage = courseAssignmentLists[gradeField.type];
const showDefinedCaseAlert = gradeField.minCount !== courseAssignmentUsage?.length
&& Boolean(courseAssignmentUsage?.length);
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']),
},
{
href: `${studioBaseUrl}/group_configurations/course-v1:${courseId}`,
href: `${studioBaseUrl}/group_configurations/${courseId}`,
title: intl.formatMessage(messages['header.links.groupConfigurations']),
},
{

View File

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

View File

@@ -42,7 +42,6 @@ const StudioHome = ({ intl }) => {
studioRequestEmail,
libraryAuthoringMfeUrl,
redirectToLibraryAuthoringMfe,
splitStudioHome,
} = studioHomeData;
function getHeaderButtons() {
@@ -68,26 +67,24 @@ const StudioHome = ({ intl }) => {
);
}
let libraryHref = `${getConfig().STUDIO_BASE_URL}/home#libraries-tab`;
if (splitStudioHome) {
libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
}
let libraryHref = `${getConfig().STUDIO_BASE_URL}/home_library`;
if (redirectToLibraryAuthoringMfe) {
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;
}

View File

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

View File

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

View File

@@ -95,12 +95,31 @@ export function parseArrayOrObjectValues(obj) {
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) {
if (isMfePageEnabled === 'true') {
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}`;
}