Compare commits
19 Commits
bw/compone
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
706d0ba36b | ||
|
|
aa2b1a56bc | ||
|
|
e795163aa6 | ||
|
|
bd25b6b4d0 | ||
|
|
acdad5af00 | ||
|
|
ba9fb375d9 | ||
|
|
f303712830 | ||
|
|
b536180a3e | ||
|
|
295048b4e9 | ||
|
|
1c70458590 | ||
|
|
247e9f3668 | ||
|
|
5e96dbf614 | ||
|
|
44197f673d | ||
|
|
11b62cce1d | ||
|
|
78d521cd95 | ||
|
|
10cac378b1 | ||
|
|
9a92e39b6c | ||
|
|
39bff6e276 | ||
|
|
3be81e02ea |
1
.env
1
.env
@@ -10,6 +10,7 @@ DATA_API_BASE_URL=''
|
||||
SEGMENT_KEY=''
|
||||
FEATURE_FLAGS={}
|
||||
ACCESS_TOKEN_COOKIE_NAME=''
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME=''
|
||||
NEW_RELIC_APP_ID=''
|
||||
NEW_RELIC_LICENSE_KEY=''
|
||||
SITE_NAME=''
|
||||
|
||||
@@ -12,6 +12,7 @@ FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
|
||||
LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference'
|
||||
USER_INFO_COOKIE_NAME='edx-user-info'
|
||||
SITE_NAME=localhost
|
||||
DATA_API_BASE_URL='http://localhost:8000'
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -11,22 +11,18 @@ on:
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [16]
|
||||
npm: [8.5.x]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install npm 8.5.x
|
||||
run: npm install -g npm@${{ matrix.npm }}
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
2
.github/workflows/lockfileversion-check.yml
vendored
2
.github/workflows/lockfileversion-check.yml
vendored
@@ -10,4 +10,4 @@ on:
|
||||
|
||||
jobs:
|
||||
version-check:
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check.yml@master
|
||||
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master
|
||||
|
||||
5
.github/workflows/npm-publish.yml
vendored
5
.github/workflows/npm-publish.yml
vendored
@@ -15,10 +15,13 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
node-version: ${{ env.NODE_VER }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
3
Makefile
3
Makefile
@@ -68,9 +68,10 @@ pull_translations:
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-gradebook/src/i18n/messages:frontend-app-gradebook
|
||||
|
||||
$(intl_imports) frontend-component-header frontend-component-footer frontend-app-gradebook
|
||||
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-gradebook
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
|
||||
30831
package-lock.json
generated
30831
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
27
package.json
27
package.json
@@ -29,10 +29,11 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@edx/brand-openedx@^1.2.0",
|
||||
"@edx/frontend-component-footer": "^12.0.0",
|
||||
"@edx/frontend-component-header": "^4.0.0",
|
||||
"@edx/frontend-platform": "^4.2.0",
|
||||
"@edx/paragon": "^19.25.4",
|
||||
"@edx/frontend-component-footer": "12.2.0",
|
||||
"@edx/frontend-component-header": "4.6.0",
|
||||
"@edx/frontend-platform": "5.5.4",
|
||||
"@edx/paragon": "20.45.0",
|
||||
"@edx/react-unit-test-utils": "1.7.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.25",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.11.2",
|
||||
@@ -49,12 +50,12 @@
|
||||
"history": "4.10.1",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "6.13.0",
|
||||
"react": "16.14.0",
|
||||
"react-dom": "16.14.0",
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-redux": "^7.1.1",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-router": "6.15.0",
|
||||
"react-router-dom": "6.15.0",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux": "4.0.5",
|
||||
"redux-beacon": "^2.1.0",
|
||||
@@ -67,18 +68,18 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@edx/frontend-build": "^12.4.15",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"@edx/frontend-build": "12.9.3",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"axios": "0.21.2",
|
||||
"axios-mock-adapter": "^1.17.0",
|
||||
"enzyme-adapter-react-16": "^1.14.0",
|
||||
"es-check": "^2.3.0",
|
||||
"fetch-mock": "^6.5.2",
|
||||
"husky": "2.7.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "^26.6.3",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^16.10.1",
|
||||
"react-test-renderer": "17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.3",
|
||||
"semantic-release": "^19.0.3"
|
||||
|
||||
30
src/App.jsx
30
src/App.jsx
@@ -1,12 +1,11 @@
|
||||
import React from 'react';
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
import './App.scss';
|
||||
@@ -15,21 +14,18 @@ import Head from './head/Head';
|
||||
const App = () => (
|
||||
<AppProvider store={store}>
|
||||
<Head />
|
||||
<Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
exact
|
||||
path={routePath}
|
||||
component={GradebookPage}
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/:courseId"
|
||||
element={<GradebookPage />}
|
||||
/>
|
||||
</Routes>
|
||||
</main>
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</AppProvider>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import Header from '@edx/frontend-component-header';
|
||||
|
||||
import { routePath } from 'data/constants/app';
|
||||
import store from 'data/store';
|
||||
import GradebookPage from 'containers/GradebookPage';
|
||||
|
||||
@@ -17,14 +16,10 @@ import Head from './head/Head';
|
||||
jest.mock('react-router-dom', () => ({
|
||||
BrowserRouter: () => 'BrowserRouter',
|
||||
Route: () => 'Route',
|
||||
Switch: () => 'Switch',
|
||||
}));
|
||||
jest.mock('@edx/frontend-platform/react', () => ({
|
||||
AppProvider: () => 'AppProvider',
|
||||
}));
|
||||
jest.mock('data/constants/app', () => ({
|
||||
routePath: '/:courseId',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
jest.mock('data/store', () => 'testStore');
|
||||
jest.mock('containers/GradebookPage', () => 'GradebookPage');
|
||||
@@ -32,7 +27,7 @@ jest.mock('@edx/frontend-component-header', () => 'Header');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
let secondChild;
|
||||
|
||||
describe('App router component', () => {
|
||||
test('snapshot', () => {
|
||||
@@ -42,7 +37,7 @@ describe('App router component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App />);
|
||||
router = el.childAt(1);
|
||||
secondChild = el.childAt(1);
|
||||
});
|
||||
describe('AppProvider', () => {
|
||||
test('AppProvider is the parent component, passed the redux store props', () => {
|
||||
@@ -57,24 +52,24 @@ describe('App router component', () => {
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('second child of AppProvider', () => {
|
||||
expect(router.type()).toBe(Router);
|
||||
expect(secondChild.type()).toBe('div');
|
||||
});
|
||||
test('Header is above/outside-of the routing', () => {
|
||||
expect(router.childAt(0).childAt(0).type()).toBe(Header);
|
||||
expect(router.childAt(0).childAt(1).type()).toBe('main');
|
||||
expect(secondChild.childAt(0).type()).toBe(Header);
|
||||
expect(secondChild.childAt(1).type()).toBe('main');
|
||||
});
|
||||
test('Routing - GradebookPage is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
expect(secondChild.find('main')).toEqual(shallow(
|
||||
<main>
|
||||
<Switch>
|
||||
<Route exact path={routePath} component={GradebookPage} />
|
||||
</Switch>
|
||||
<Routes>
|
||||
<Route path="/:courseId" element={<GradebookPage />} />
|
||||
</Routes>
|
||||
</main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
expect(secondChild.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,20 +5,17 @@ exports[`App router component snapshot 1`] = `
|
||||
store="testStore"
|
||||
>
|
||||
<Head />
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Switch>
|
||||
<Route
|
||||
component="GradebookPage"
|
||||
exact={true}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Switch>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<div>
|
||||
<Header />
|
||||
<main>
|
||||
<Component>
|
||||
<Route
|
||||
element={<GradebookPage />}
|
||||
path="/:courseId"
|
||||
/>
|
||||
</Component>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
</AppProvider>
|
||||
`;
|
||||
|
||||
@@ -21,7 +21,9 @@ exports[`GradebookHeader component render default view shapshot 1`] = `
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
<h2
|
||||
className="text-break"
|
||||
>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
@@ -49,7 +51,9 @@ exports[`GradebookHeader component render frozen grades snapshot: show frozen wa
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
<h2
|
||||
className="text-break"
|
||||
>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
@@ -83,7 +87,9 @@ exports[`GradebookHeader component render show bulk management snapshot: show to
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
<h2
|
||||
className="text-break"
|
||||
>
|
||||
test-course-id
|
||||
</h2>
|
||||
<Button
|
||||
@@ -117,7 +123,9 @@ exports[`GradebookHeader component render user cannot view gradebook snapshot: s
|
||||
<div
|
||||
className="subtitle-row d-flex justify-content-between align-items-center"
|
||||
>
|
||||
<h2>
|
||||
<h2
|
||||
className="text-break"
|
||||
>
|
||||
test-course-id
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const GradebookHeader = () => {
|
||||
</a>
|
||||
<h1>{formatMessage(messages.gradebook)}</h1>
|
||||
<div className="subtitle-row d-flex justify-content-between align-items-center">
|
||||
<h2>{courseId}</h2>
|
||||
<h2 className="text-break">{courseId}</h2>
|
||||
{showBulkManagement && (
|
||||
<Button variant="tertiary" onClick={handleToggleViewClick}>
|
||||
{formatMessage(toggleViewMessage)}
|
||||
|
||||
@@ -20,6 +20,6 @@ exports[`OverrideTable component render snapshot 1`] = `
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
itemCount={3}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -9,7 +9,7 @@ const useOverrideTableData = () => {
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const hide = selectors.grades.useHasOverrideErrors();
|
||||
const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults;
|
||||
const gradeOverrides = selectors.grades.useGradeData().gradeOverrideHistoryResults || [];
|
||||
const tableProps = {};
|
||||
if (!hide) {
|
||||
tableProps.columns = [
|
||||
|
||||
@@ -20,18 +20,20 @@ export const OverrideTable = () => {
|
||||
|
||||
if (hide) { return null; }
|
||||
|
||||
const tableData = [
|
||||
...data,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: formatDateForDisplay(new Date()),
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={[
|
||||
...data,
|
||||
{
|
||||
adjustedGrade: <AdjustedGradeInput />,
|
||||
date: formatDateForDisplay(new Date()),
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={data.length}
|
||||
data={tableData}
|
||||
itemCount={tableData.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ exports[`FilterMenuToggle component render snapshot 1`] = `
|
||||
onClick={[MockFunction hooks.toggleFilterMenu]}
|
||||
>
|
||||
<Icon
|
||||
className="mr-1"
|
||||
src="FilterAlt"
|
||||
/>
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ export const FilterMenuToggle = () => {
|
||||
className="btn-primary align-self-start"
|
||||
onClick={toggleFilterMenu}
|
||||
>
|
||||
<Icon src={FilterAlt} /> {formatMessage(messages.editFilters)}
|
||||
<Icon src={FilterAlt} className="mr-1" /> {formatMessage(messages.editFilters)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,7 +11,7 @@ export const useGradeButtonData = ({ entry, subsection }) => {
|
||||
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
|
||||
const { gradeFormat } = selectors.grades.useGradeData();
|
||||
const setModalState = thunkActions.app.useSetModalStateFromTable();
|
||||
const label = transforms.grades.subsectionGrade({ gradeFormat, subsection });
|
||||
const label = transforms.grades.subsectionGrade({ gradeFormat, subsection })();
|
||||
|
||||
const onClick = () => {
|
||||
setModalState({
|
||||
|
||||
@@ -41,7 +41,7 @@ const props = {
|
||||
};
|
||||
const gradeFormat = 'percent';
|
||||
const setModalState = jest.fn();
|
||||
const subsectionGrade = 'test-subsection-grade';
|
||||
const subsectionGrade = () => 'test-subsection-grade';
|
||||
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(false);
|
||||
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
|
||||
thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
|
||||
@@ -73,7 +73,7 @@ describe('GradeButton', () => {
|
||||
expect(out.areGradesFrozen).toEqual(false);
|
||||
});
|
||||
test('label passed from subsection grade redux hook', () => {
|
||||
expect(out.label).toEqual(subsectionGrade);
|
||||
expect(out.label).toEqual(subsectionGrade());
|
||||
});
|
||||
test('onClick sets modal state with user entry and subsection', () => {
|
||||
out.onClick();
|
||||
|
||||
@@ -39,7 +39,7 @@ export const useGradebookTableData = () => {
|
||||
[Headings.username]: (
|
||||
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
|
||||
),
|
||||
[Headings.email]: (<Fields.Email email={entry.email} />),
|
||||
[Headings.email]: (<Fields.Text value={entry.email} />),
|
||||
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
|
||||
...entry.section_breakdown.reduce((acc, subsection) => ({
|
||||
...acc,
|
||||
|
||||
@@ -22,7 +22,7 @@ jest.mock('i18n/utils', () => ({
|
||||
jest.mock('./GradeButton', () => 'GradeButton');
|
||||
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
Username: 'Fields.Username',
|
||||
Email: 'Fields.Email',
|
||||
Text: 'Fields.Text',
|
||||
}));
|
||||
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
|
||||
@@ -158,7 +158,7 @@ describe('useGradebookTableData', () => {
|
||||
test('email field', () => {
|
||||
allGrades.forEach((entry, index) => {
|
||||
expect(out.data[index][Headings.email]).toMatchObject(
|
||||
<Fields.Email email={entry.email} />,
|
||||
<Fields.Text value={entry.email} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
}
|
||||
.grade-history-header{
|
||||
float: left;
|
||||
min-width: 170px;
|
||||
}
|
||||
|
||||
.grade-history-assignment{
|
||||
@@ -65,7 +66,7 @@
|
||||
.gradebook-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
height: 600px;
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
@@ -122,3 +123,34 @@ select#ScoreView.form-control {
|
||||
border-right-color: $black;
|
||||
}
|
||||
}
|
||||
|
||||
#edit-filters-btn {
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.search-container {
|
||||
@include media-breakpoint-down(xs) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.pgn__modal-body-content .pgn__data-table-layout-wrapper {
|
||||
@include media-breakpoint-down(sm) {
|
||||
clear: both;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.page-gradebook {
|
||||
position: relative;
|
||||
|
||||
.sidebar-container {
|
||||
position: relative;
|
||||
}
|
||||
aside.sidebar {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ImportGradesButton component render Form 1`] = `
|
||||
<Form
|
||||
action="test-grade-export-url"
|
||||
method="post"
|
||||
>
|
||||
<Form.Group
|
||||
controlId="csv"
|
||||
>
|
||||
<Form.Control
|
||||
className="d-none"
|
||||
data-testid="file-control"
|
||||
label="Upload Grade CSV"
|
||||
onChange={[MockFunction props.handleFileInputChange]}
|
||||
type="file"
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
`;
|
||||
|
||||
exports[`ImportGradesButton component render snapshot 1`] = `
|
||||
<Fragment>
|
||||
<Form
|
||||
@@ -13,7 +32,7 @@ exports[`ImportGradesButton component render snapshot 1`] = `
|
||||
className="d-none"
|
||||
data-testid="file-control"
|
||||
label="Upload Grade CSV"
|
||||
onChange={[MockFunction]}
|
||||
onChange={[MockFunction props.handleFileInputChange]}
|
||||
type="file"
|
||||
/>
|
||||
</Form.Group>
|
||||
@@ -28,7 +47,7 @@ exports[`ImportGradesButton component render snapshot 1`] = `
|
||||
"id": "gradebook.GradesView.importGradesBtnText",
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction]}
|
||||
onClick={[MockFunction props.handleClickImportGrades]}
|
||||
/>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
@@ -18,8 +18,8 @@ describe('ImportGradesButton component', () => {
|
||||
props = {
|
||||
fileInputRef: { current: null },
|
||||
gradeExportUrl: 'test-grade-export-url',
|
||||
handleClickImportGrades: jest.fn(),
|
||||
handleFileInputChange: jest.fn(),
|
||||
handleClickImportGrades: jest.fn().mockName('props.handleClickImportGrades'),
|
||||
handleFileInputChange: jest.fn().mockName('props.handleFileInputChange'),
|
||||
};
|
||||
useImportGradesButtonData.mockReturnValue(props);
|
||||
el = shallow(<ImportGradesButton />);
|
||||
@@ -32,14 +32,15 @@ describe('ImportGradesButton component', () => {
|
||||
});
|
||||
describe('render', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('Form', () => {
|
||||
expect(el.find(Form).props().action).toEqual(props.gradeExportUrl);
|
||||
expect(el.find(Form.Control).props().onChange).toEqual(props.handleFileInputChange);
|
||||
expect(el.instance.findByType(Form)[0].snapshot).toMatchSnapshot();
|
||||
expect(el.instance.findByType(Form)[0].props.action).toEqual(props.gradeExportUrl);
|
||||
expect(el.instance.findByType(Form.Control)[0].props.onChange).toEqual(props.handleFileInputChange);
|
||||
});
|
||||
test('import button', () => {
|
||||
expect(el.find(NetworkButton).props().onClick).toEqual(props.handleClickImportGrades);
|
||||
expect(el.instance.findByType(NetworkButton)[0].props.onClick).toEqual(props.handleClickImportGrades);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchControls component render snapshot 1`] = `
|
||||
<div>
|
||||
<div
|
||||
className="search-container"
|
||||
>
|
||||
<SearchField
|
||||
inputLabel="test-input-label"
|
||||
onBlur={[MockFunction hooks.onBlur]}
|
||||
|
||||
@@ -18,7 +18,7 @@ export const SearchControls = () => {
|
||||
} = useSearchControlsData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="search-container">
|
||||
<SearchField
|
||||
onSubmit={onSubmit}
|
||||
inputLabel={inputLabel}
|
||||
|
||||
@@ -10,7 +10,7 @@ exports[`GradesView component render snapshot 1`] = `
|
||||
filter-step-heading
|
||||
</h3>
|
||||
<div
|
||||
className="d-flex justify-content-between"
|
||||
className="d-flex justify-content-between flex-wrap"
|
||||
>
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
|
||||
@@ -34,7 +34,7 @@ export const GradesView = ({ updateQueryParams }) => {
|
||||
{stepHeadings.filter}
|
||||
</h3>
|
||||
|
||||
<div className="d-flex justify-content-between">
|
||||
<div className="d-flex justify-content-between flex-wrap">
|
||||
<FilterMenuToggle />
|
||||
<SearchControls />
|
||||
</div>
|
||||
|
||||
@@ -14,6 +14,8 @@ import GradesView from 'components/GradesView';
|
||||
import GradebookFilters from 'components/GradebookFilters';
|
||||
import BulkManagementHistoryView from 'components/BulkManagementHistoryView';
|
||||
|
||||
import { withParams, withNavigate, withLocation } from '../../utils/hoc';
|
||||
|
||||
/**
|
||||
* <GradebookPage />
|
||||
* Top-level view for the Gradebook MFE.
|
||||
@@ -28,10 +30,11 @@ export class GradebookPage extends React.Component {
|
||||
|
||||
componentDidMount() {
|
||||
const urlQuery = queryString.parse(this.props.location.search);
|
||||
this.props.initializeApp(this.props.match.params.courseId, urlQuery);
|
||||
this.props.initializeApp(this.props.courseId, urlQuery);
|
||||
}
|
||||
|
||||
updateQueryParams(queryParams) {
|
||||
const { pathname } = this.props.location;
|
||||
const parsed = queryString.parse(this.props.location.search);
|
||||
Object.keys(queryParams).forEach((key) => {
|
||||
if (queryParams[key]) {
|
||||
@@ -40,7 +43,7 @@ export class GradebookPage extends React.Component {
|
||||
delete parsed[key];
|
||||
}
|
||||
});
|
||||
this.props.history.push(`?${queryString.stringify(parsed)}`);
|
||||
this.props.navigate({ pathname, search: `?${queryString.stringify(parsed)}` });
|
||||
}
|
||||
|
||||
render() {
|
||||
@@ -60,18 +63,12 @@ export class GradebookPage extends React.Component {
|
||||
}
|
||||
}
|
||||
GradebookPage.defaultProps = {
|
||||
location: { search: '' },
|
||||
location: { pathname: '/', search: '' },
|
||||
};
|
||||
GradebookPage.propTypes = {
|
||||
history: PropTypes.shape({
|
||||
push: PropTypes.func,
|
||||
}).isRequired,
|
||||
location: PropTypes.shape({ search: PropTypes.string }),
|
||||
match: PropTypes.shape({
|
||||
params: PropTypes.shape({
|
||||
courseId: PropTypes.string,
|
||||
}),
|
||||
}).isRequired,
|
||||
navigate: PropTypes.func.isRequired,
|
||||
location: PropTypes.shape({ pathname: PropTypes.string, search: PropTypes.string }),
|
||||
courseId: PropTypes.string.isRequired,
|
||||
// redux
|
||||
activeView: PropTypes.string.isRequired,
|
||||
initializeApp: PropTypes.func.isRequired,
|
||||
@@ -85,4 +82,4 @@ export const mapDispatchToProps = {
|
||||
initializeApp: thunkActions.app.initialize,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GradebookPage);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(withParams(withNavigate(withLocation(GradebookPage))));
|
||||
|
||||
@@ -50,14 +50,15 @@ describe('GradebookPage', () => {
|
||||
let el;
|
||||
const props = {
|
||||
location: {
|
||||
pathname: '/',
|
||||
search: 'searchString',
|
||||
},
|
||||
match: { params: { courseId } },
|
||||
courseId,
|
||||
activeView: views.grades,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.initializeApp = jest.fn();
|
||||
props.history = { push: jest.fn() };
|
||||
props.navigate = jest.fn();
|
||||
});
|
||||
test('snapshot - shows BulkManagementHistoryView if activeView === views.bulkManagementHistory', () => {
|
||||
el = shallow(<GradebookPage {...props} activeView={views.bulkManagementHistory} />);
|
||||
@@ -130,7 +131,7 @@ describe('GradebookPage', () => {
|
||||
const val2 = 'VALTWO!!';
|
||||
const args = { [newKey]: val1, [props.location.search]: val2 };
|
||||
el.instance().updateQueryParams(args);
|
||||
expect(props.history.push).toHaveBeenCalledWith(`?${queryString.stringify(args)}`);
|
||||
expect(props.navigate).toHaveBeenCalledWith({ pathname: '/', search: `?${queryString.stringify(args)}` });
|
||||
});
|
||||
it('clears values for non-truthy values', () => {
|
||||
queryString.parse.mockImplementation(key => ({ [key]: key }));
|
||||
@@ -139,8 +140,8 @@ describe('GradebookPage', () => {
|
||||
const val2 = false;
|
||||
const args = { [newKey]: val1, [props.location.search]: val2 };
|
||||
el.instance().updateQueryParams(args);
|
||||
expect(props.history.push).toHaveBeenCalledWith(
|
||||
`?${queryString.stringify({ [newKey]: val1 })}`,
|
||||
expect(props.navigate).toHaveBeenCalledWith(
|
||||
{ pathname: '/', search: `?${queryString.stringify({ [newKey]: val1 })}` },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { StrictDict } from 'utils';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
export const routePath = `${getConfig().PUBLIC_PATH}:courseId`;
|
||||
|
||||
export const views = StrictDict({
|
||||
grades: 'grades',
|
||||
|
||||
@@ -10,7 +10,7 @@ import { fetchGrades } from './grades';
|
||||
import { fetchTracks } from './tracks';
|
||||
import { fetchAssignmentTypes } from './assignmentTypes';
|
||||
|
||||
export const allowedRoles = ['staff', 'instructor', 'support'];
|
||||
export const allowedRoles = ['staff', 'limited_staff', 'instructor', 'support'];
|
||||
|
||||
export const fetchRoles = () => (
|
||||
(dispatch, getState) => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { messages as footerMessages } from '@edx/frontend-component-footer';
|
||||
import { messages as headerMessages } from '@edx/frontend-component-header';
|
||||
import { messages as paragonMessages } from '@edx/paragon';
|
||||
|
||||
import arMessages from './messages/ar.json';
|
||||
import deMessages from './messages/de.json';
|
||||
@@ -31,6 +32,7 @@ const appMessages = {
|
||||
};
|
||||
|
||||
export default [
|
||||
paragonMessages,
|
||||
footerMessages,
|
||||
headerMessages,
|
||||
appMessages,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable import/no-extraneous-dependencies */
|
||||
|
||||
import Enzyme from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import Adapter from '@wojtekmaj/enzyme-adapter-react-17';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
|
||||
24
src/utils/hoc.jsx
Normal file
24
src/utils/hoc.jsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useLocation, useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
export const withParams = WrappedComponent => {
|
||||
const WithParamsComponent = props => <WrappedComponent {...useParams()} {...props} />;
|
||||
return WithParamsComponent;
|
||||
};
|
||||
|
||||
export const withNavigate = WrappedComponent => {
|
||||
const WithNavigateComponent = props => {
|
||||
const navigate = useNavigate();
|
||||
return <WrappedComponent navigate={navigate} {...props} />;
|
||||
};
|
||||
return WithNavigateComponent;
|
||||
};
|
||||
|
||||
export const withLocation = WrappedComponent => {
|
||||
const WithLocationComponent = props => {
|
||||
const location = useLocation();
|
||||
return <WrappedComponent location={location} {...props} />;
|
||||
};
|
||||
return WithLocationComponent;
|
||||
};
|
||||
38
src/utils/hoc.test.jsx
Normal file
38
src/utils/hoc.test.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { mount } from 'enzyme';
|
||||
|
||||
import { withLocation, withNavigate } from './hoc';
|
||||
|
||||
const mockedNavigator = jest.fn();
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedNavigator,
|
||||
useLocation: () => ({
|
||||
pathname: '/current-location',
|
||||
}),
|
||||
}));
|
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const MockComponent = ({ navigate, location }) => (
|
||||
// eslint-disable-next-line react/button-has-type, react/prop-types
|
||||
<button id="btn" onClick={() => navigate('/some-route')}>{location.pathname}</button>
|
||||
);
|
||||
const WrappedComponent = withNavigate(withLocation(MockComponent));
|
||||
|
||||
test('Provide Navigation to Component', () => {
|
||||
const wrapper = mount(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
const btn = wrapper.find('#btn');
|
||||
btn.simulate('click');
|
||||
|
||||
expect(mockedNavigator).toHaveBeenCalledWith('/some-route');
|
||||
});
|
||||
|
||||
test('Provide Location object to Component', () => {
|
||||
const wrapper = mount(
|
||||
<WrappedComponent />,
|
||||
);
|
||||
|
||||
expect(wrapper.find('#btn').text()).toContain('/current-location');
|
||||
});
|
||||
Reference in New Issue
Block a user