feat: create Studio Home Page MFE (#589)

This commit is contained in:
ruzniaievdm
2023-09-19 17:04:43 +03:00
committed by GitHub
parent 6f238bdbe0
commit e099243437
107 changed files with 5976 additions and 309 deletions

View File

@@ -0,0 +1,57 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import initializeStore from '../../store';
import { studioHomeMock } from '../__mocks__';
import messages from './messages';
import ProcessingCourses from '.';
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useSelector: jest.fn(),
}));
let store;
const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<ProcessingCourses />
</IntlProvider>
</AppProvider>
);
describe('<ProcessingCourses />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
useSelector.mockReturnValue(studioHomeMock);
});
it('renders successfully processing courses', () => {
const studioHomeInitial = {
...studioHomeMock,
inProcessCourseActions: [{ a: '1' }, { b: '2' }],
};
useSelector.mockReturnValue(studioHomeInitial);
const { getByText, queryAllByTestId } = render(<RootWrapper />);
expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument();
expect(queryAllByTestId('course-item')).toHaveLength(2);
});
it('renders successfully empty list', () => {
const { getByText, queryAllByTestId } = render(<RootWrapper />);
expect(getByText(messages.processingTitle.defaultMessage)).toBeInTheDocument();
expect(queryAllByTestId('course-item')).toHaveLength(0);
});
});

View File

@@ -0,0 +1,66 @@
import React from 'react';
import { render } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import initializeStore from '../../../store';
import { executeThunk } from '../../../utils';
import { handleDeleteNotificationQuery } from '../../data/thunks';
import { getCourseNotificationUrl } from '../../data/api';
import messages from './messages';
import CourseItem from '.';
let store;
let axiosMock;
const RootWrapper = (props) => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<CourseItem {...props} />
</IntlProvider>
</AppProvider>
);
const course = {
displayName: 'course-name-fake',
courseKey: 'course-key-fake',
org: 'course-org-fake',
number: 'course-number-fake',
run: 'course-run-fake',
isInProgress: true,
isFailed: true,
dismissLink: 'course-dismiss-link-fake',
};
const props = { course };
describe('<CourseItem />', async () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
axiosMock.onDelete(getCourseNotificationUrl(course.dismissLink)).reply(200);
await executeThunk(handleDeleteNotificationQuery(course.dismissLink), store.dispatch);
});
it('renders successfully', () => {
const { getByText, getAllByText } = render(<RootWrapper {...props} />);
const subtitle = `${course.org} / ${course.number} / ${course.run}`;
expect(getAllByText(course.displayName)).toHaveLength(2);
expect(getAllByText(subtitle)).toHaveLength(2);
expect(getByText(messages.itemInProgressActionText.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.itemIsFailedActionText.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.itemFailedFooterText.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.itemFailedFooterButton.defaultMessage)).toBeInTheDocument();
});
});

View File

@@ -0,0 +1,100 @@
import React from 'react';
import { useDispatch } from 'react-redux';
import PropTypes from 'prop-types';
import {
ActionRow,
Card,
Hyperlink,
Button,
Icon,
} from '@edx/paragon';
import {
Close as CloseIcon,
Warning as WarningIcon,
RotateRight as RotateRightIcon,
} from '@edx/paragon/icons';
import { useIntl } from '@edx/frontend-platform/i18n';
import { handleDeleteNotificationQuery } from '../../data/thunks';
import messages from './messages';
const CourseItem = ({ course }) => {
const intl = useIntl();
const dispatch = useDispatch();
const {
displayName, org, number, run, isInProgress, isFailed, dismissLink,
} = course;
const subtitle = `${org} / ${number} / ${run}`;
return (
<div data-testid="course-item">
{isInProgress && (
<Card className="card-item">
<Card.Header
title={<p className="card-item-title">{displayName}</p>}
subtitle={subtitle}
actions={(
<ActionRow>
<Icon src={RotateRightIcon} className="spinner-icon text-gray-300" />
<ActionRow.Spacer />
<span className="small text-gray-300">{intl.formatMessage(messages.itemInProgressActionText)}</span>
</ActionRow>
)}
/>
<Card.Divider />
<Card.Section className="p-3.5 small text-black bg-gray-100">
{intl.formatMessage(messages.itemInProgressFooterText, {
refresh: (
<Hyperlink destination="/home">
{intl.formatMessage(messages.itemInProgressFooterHyperlink)}
</Hyperlink>
),
})}
</Card.Section>
</Card>
)}
{isFailed && (
<Card className="card-item">
<Card.Header
title={<p className="card-item-title">{displayName}</p>}
subtitle={subtitle}
actions={(
<ActionRow>
<Icon src={WarningIcon} className="text-danger-300" />
<span className="small text-danger-300">{intl.formatMessage(messages.itemIsFailedActionText)}</span>
</ActionRow>
)}
/>
<Card.Divider />
<Card.Footer className="p-3.5 small text-white bg-danger-300 align-content-between">
<span className="w-75 mr-auto">{intl.formatMessage(messages.itemFailedFooterText)}</span>
<Button
onClick={() => dispatch(handleDeleteNotificationQuery(dismissLink))}
iconBefore={CloseIcon}
variant="outline-danger"
size="sm"
>
{intl.formatMessage(messages.itemFailedFooterButton)}
</Button>
</Card.Footer>
</Card>
)}
</div>
);
};
CourseItem.propTypes = {
course: PropTypes.shape({
displayName: PropTypes.string.isRequired,
courseKey: PropTypes.string.isRequired,
org: PropTypes.string.isRequired,
number: PropTypes.string.isRequired,
run: PropTypes.string.isRequired,
isFailed: PropTypes.bool.isRequired,
isInProgress: PropTypes.bool.isRequired,
dismissLink: PropTypes.string.isRequired,
}).isRequired,
};
export default CourseItem;

View File

@@ -0,0 +1,30 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
itemInProgressFooterText: {
id: 'course-authoring.studio-home.processing.course-item.footer.in-progress',
defaultMessage: 'The new course will be added to your course list in 5-10 minutes. Return to this page or {refresh} to update the course list. The new course will need some manual configuration.',
},
itemInProgressFooterHyperlink: {
id: 'course-authoring.studio-home.processing.course-item.footer.in-progress.hyperlink',
defaultMessage: 'refresh it',
},
itemInProgressActionText: {
id: 'course-authoring.studio-home.processing.course-item.action.in-progress',
defaultMessage: 'Configuring as re-run',
},
itemIsFailedActionText: {
id: 'course-authoring.studio-home.processing.course-item.action.failed',
defaultMessage: 'Configuration error',
},
itemFailedFooterText: {
id: 'course-authoring.studio-home.processing.course-item.footer.failed',
defaultMessage: 'A system error occurred while your course was being processed. Please go to the original course to try the re-run again, or contact your PM for assistance.',
},
itemFailedFooterButton: {
id: 'course-authoring.studio-home.processing.course-item.footer.failed.button',
defaultMessage: 'Dismiss',
},
});
export default messages;

View File

@@ -0,0 +1,32 @@
import React from 'react';
import { useSelector } from 'react-redux';
import { Stack } from '@edx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getStudioHomeData } from '../data/selectors';
import CourseItem from './course-item';
import messages from './messages';
const ProcessingCourses = () => {
const intl = useIntl();
const { inProcessCourseActions } = useSelector(getStudioHomeData);
return (
<>
<p className="text-gray-300">
{intl.formatMessage(messages.processingTitle)}
</p>
<hr />
<Stack gap={3}>
{inProcessCourseActions.map((course) => (
<CourseItem
course={course}
key={course.courseKey}
/>
))}
</Stack>
</>
);
};
export default ProcessingCourses;

View File

@@ -0,0 +1,10 @@
import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({
processingTitle: {
id: 'course-authoring.studio-home.processing.title',
defaultMessage: 'Courses being processed',
},
});
export default messages;