Compare commits

..

5 Commits

Author SHA1 Message Date
Adolfo R. Brandes
789e1db2e8 Merge pull request #362 from raccoongang/fix/fix-responsive-issues
fix: fix sidebar scrolling and adaptation for mobile
2024-04-03 15:28:34 -03:00
Max Sokolski
66bae174dd Merge pull request #371 from DmytroAlipov/fix-import-grades-button-palm
fix: file input handler
2023-11-16 11:40:53 +02:00
alipov_d
facf1c8866 fix: file input handler
hasFile run only once, it will always be null
2023-10-27 18:14:39 +03:00
ihor-romaniuk
8a1ccc708d fix: fix sidebar scrolling and adaptation for mobile
- fix text overload in heading
- fix appearance for sidebar with a short main content
- fix transformation of search and filter button on mobile
- fix extra empty space below main content table
- fix adaptation modal content to mobile view
2023-10-23 15:28:19 +03:00
Bilal Qamar
3644172d94 feat: upgraded to node v18, added .nvmrc and updated workflows (#317)
* Merge branch 'master' of github.com:edx/frontend-app-gradebook

* feat: upgraded to node v18, added .nvmrc and updated workflows

* build: updated frontend-build, frontend-platform, component-footer & component-header packages

* refactor: updated packages

* fix: resolved test case failure window redefine issue

* Merge branch 'master' of github.com:edx/frontend-app-gradebook into bilalqamar95/node-v18-upgrade

* refactor: pinned node to v18.15 in nvmrc
2023-06-09 09:20:39 +02:00
34 changed files with 3853 additions and 27688 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
18.15

30397
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -68,7 +68,7 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/frontend-build": "^12.4.15",
"@edx/frontend-build": "12.8.27",
"@testing-library/react": "^12.1.0",
"axios": "0.21.2",
"axios-mock-adapter": "^1.17.0",

View File

@@ -1,131 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookHeader component render default view shapshot 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
test-course-id
</h2>
</div>
</div>
`;
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
</div>
`;
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
test-course-id
</h2>
<Button
onClick={[MockFunction hooks.handleToggleViewClick]}
variant="tertiary"
>
toggle-view-message
</Button>
</div>
</div>
`;
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;

View File

@@ -29,7 +29,9 @@ exports[`GradebookHeader component snapshots default values (grades frozen, cann
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
<h2
className="text-break"
>
fakeID
</h2>
</div>
@@ -75,7 +77,9 @@ exports[`GradebookHeader component snapshots grades frozen, can view. grades fro
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
<h2
className="text-break"
>
fakeID
</h2>
</div>
@@ -121,7 +125,9 @@ exports[`GradebookHeader component snapshots grades frozen, cannot view unauthor
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
<h2
className="text-break"
>
fakeID
</h2>
</div>
@@ -177,7 +183,9 @@ exports[`GradebookHeader component snapshots show bulk management, active view i
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
<h2
className="text-break"
>
fakeID
</h2>
<Button
@@ -233,7 +241,9 @@ exports[`GradebookHeader component snapshots show bulk management, active view i
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2>
<h2
className="text-break"
>
fakeID
</h2>
<Button

View File

@@ -1,35 +0,0 @@
import { views } from 'data/constants/app';
import { actions, selectors } from 'data/redux/hooks';
import messages from './messages';
export const useGradebookHeaderData = () => {
const activeView = selectors.app.useActiveView();
const courseId = selectors.app.useCourseId();
const areGradesFrozen = selectors.assignmentTypes.useAreGradesFrozen();
const canUserViewGradebook = selectors.roles.useCanUserViewGradebook();
const showBulkManagement = selectors.root.useShowBulkManagement();
const setView = actions.app.useSetView();
const handleToggleViewClick = () => setView(
activeView === views.grades
? views.bulkManagementHistory
: views.grades,
);
const toggleViewMessage = activeView === views.grades
? messages.toActivityLog
: messages.toGradesView;
return {
areGradesFrozen,
canUserViewGradebook,
courseId,
showBulkManagement,
handleToggleViewClick,
toggleViewMessage,
};
};
export default useGradebookHeaderData;

View File

@@ -1,90 +0,0 @@
import { views } from 'data/constants/app';
import { actions, selectors } from 'data/redux/hooks';
import messages from './messages';
import useGradebookHeaderData from './hooks';
jest.mock('data/redux/hooks', () => ({
actions: {
app: {
useSetView: jest.fn(),
},
},
selectors: {
app: {
useActiveView: jest.fn(),
useCourseId: jest.fn(),
},
assignmentTypes: {
useAreGradesFrozen: jest.fn(),
},
roles: {
useCanUserViewGradebook: jest.fn(),
},
root: {
useShowBulkManagement: jest.fn(),
},
},
}));
const activeView = 'test-active-view';
selectors.app.useActiveView.mockReturnValue(activeView);
const courseId = 'test-course-id';
selectors.app.useCourseId.mockReturnValue(courseId);
const areGradesFrozen = 'test-are-grades-frozen';
selectors.assignmentTypes.useAreGradesFrozen.mockReturnValue(areGradesFrozen);
const canUserViewGradebook = 'test-can-user-view-gradebook';
selectors.roles.useCanUserViewGradebook.mockReturnValue(canUserViewGradebook);
const showBulkManagement = 'test-show-bulk-management';
selectors.root.useShowBulkManagement.mockReturnValue(showBulkManagement);
const setView = jest.fn();
actions.app.useSetView.mockReturnValue(setView);
let out;
describe('useGradebookHeaderData hooks', () => {
describe('initialization', () => {
it('initializes redux hooks', () => {
out = useGradebookHeaderData();
expect(selectors.app.useActiveView).toHaveBeenCalled();
expect(selectors.app.useCourseId).toHaveBeenCalled();
expect(selectors.assignmentTypes.useAreGradesFrozen).toHaveBeenCalled();
expect(selectors.roles.useCanUserViewGradebook).toHaveBeenCalled();
expect(selectors.root.useShowBulkManagement).toHaveBeenCalled();
expect(actions.app.useSetView).toHaveBeenCalled();
});
});
describe('output', () => {
test('redux fields', () => {
out = useGradebookHeaderData();
expect(out.areGradesFrozen).toEqual(areGradesFrozen);
expect(out.canUserViewGradebook).toEqual(canUserViewGradebook);
expect(out.courseId).toEqual(courseId);
expect(out.showBulkManagement).toEqual(showBulkManagement);
});
describe('handleToggleViewClick', () => {
it('calls setView with bulkManagemnetHistory message if grades view is active', () => {
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
out = useGradebookHeaderData();
out.handleToggleViewClick();
expect(setView).toHaveBeenCalledWith(views.bulkManagementHistory);
});
it('calls setView with grades view if grades view is not active', () => {
out = useGradebookHeaderData();
out.handleToggleViewClick();
expect(setView).toHaveBeenCalledWith(views.grades);
});
});
describe('toggleViewMessage', () => {
it('returns toActivityLog message if grades view is active', () => {
selectors.app.useActiveView.mockReturnValueOnce(views.grades);
out = useGradebookHeaderData();
expect(out.toggleViewMessage).toEqual(messages.toActivityLog);
});
it('returns toGradesView message if grades view is not active', () => {
out = useGradebookHeaderData();
expect(out.toggleViewMessage).toEqual(messages.toGradesView);
});
});
});
});

View File

@@ -1,50 +1,106 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import useGradebookHeaderData from './hooks';
import { views } from 'data/constants/app';
import actions from 'data/actions';
import selectors from 'data/selectors';
import messages from './messages';
export const GradebookHeader = () => {
const { formatMessage } = useIntl();
const {
areGradesFrozen,
canUserViewGradebook,
courseId,
handleToggleViewClick,
showBulkManagement,
toggleViewMessage,
} = useGradebookHeaderData();
const dashboardUrl = instructorDashboardUrl();
return (
<div className="gradebook-header">
<a href={dashboardUrl} className="mb-3">
<span aria-hidden="true">{'<< '}</span>
{formatMessage(messages.backToDashboard)}
</a>
<h1>{formatMessage(messages.gradebook)}</h1>
<div className="subtitle-row d-flex justify-content-between align-items-center">
<h2>{courseId}</h2>
{showBulkManagement && (
<Button variant="tertiary" onClick={handleToggleViewClick}>
{formatMessage(toggleViewMessage)}
</Button>
export class GradebookHeader extends React.Component {
constructor(props) {
super(props);
this.handleToggleViewClick = this.handleToggleViewClick.bind(this);
}
handleToggleViewClick() {
const newView = this.props.activeView === views.grades ? views.bulkManagementHistory : views.grades;
this.props.setView(newView);
}
get toggleViewMessage() {
return this.props.activeView === views.grades
? messages.toActivityLog
: messages.toGradesView;
}
lmsInstructorDashboardUrl = courseId => (
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
);
render() {
return (
<div className="gradebook-header">
<a
href={this.lmsInstructorDashboardUrl(this.props.courseId)}
className="mb-3"
>
<span aria-hidden="true">{'<< '}</span>
<FormattedMessage {...messages.backToDashboard} />
</a>
<h1>
<FormattedMessage {...messages.gradebook} />
</h1>
<div className="subtitle-row d-flex justify-content-between align-items-center">
<h2 className="text-break">{this.props.courseId}</h2>
{ this.props.showBulkManagement && (
<Button
variant="tertiary"
onClick={this.handleToggleViewClick}
>
<FormattedMessage {...this.toggleViewMessage} />
</Button>
)}
</div>
{this.props.areGradesFrozen
&& (
<div className="alert alert-warning" role="alert">
<FormattedMessage {...messages.frozenWarning} />
</div>
)}
{(this.props.canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
<FormattedMessage {...messages.unauthorizedWarning} />
</div>
)}
</div>
{areGradesFrozen && (
<div className="alert alert-warning" role="alert">
{formatMessage(messages.frozenWarning)}
</div>
)}
{(canUserViewGradebook === false) && (
<div className="alert alert-warning" role="alert">
{formatMessage(messages.unauthorizedWarning)}
</div>
)}
</div>
);
);
}
}
GradebookHeader.defaultProps = {
// redux
courseId: '',
areGradesFrozen: false,
canUserViewGradebook: false,
showBulkManagement: false,
};
export default GradebookHeader;
GradebookHeader.propTypes = {
// redux
activeView: PropTypes.string.isRequired,
courseId: PropTypes.string,
areGradesFrozen: PropTypes.bool,
canUserViewGradebook: PropTypes.bool,
setView: PropTypes.func.isRequired,
showBulkManagement: PropTypes.bool,
};
export const mapStateToProps = (state) => ({
activeView: selectors.app.activeView(state),
courseId: selectors.app.courseId(state),
areGradesFrozen: selectors.assignmentTypes.areGradesFrozen(state),
canUserViewGradebook: selectors.roles.canUserViewGradebook(state),
showBulkManagement: selectors.root.showBulkManagement(state),
});
export const mapDispatchToProps = {
setView: actions.app.setView,
};
export default connect(mapStateToProps, mapDispatchToProps)(GradebookHeader);

View File

@@ -1,77 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@edx/paragon';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import useGradebookHeaderData from './hooks';
import GradebookHeader from '.';
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('data/services/lms/urls', () => ({
instructorDashboardUrl: jest.fn(),
}));
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
const hookProps = {
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'test-course-id',
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
showBulkManagement: false,
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
};
useGradebookHeaderData.mockReturnValue(hookProps);
let el;
describe('GradebookHeader component', () => {
beforeAll(() => {
el = shallow(<GradebookHeader />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookHeaderData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
describe('default view', () => {
test('shapshot', () => {
expect(el).toMatchSnapshot();
});
});
describe('show bulk management', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
el = shallow(<GradebookHeader />);
});
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
expect(el).toMatchSnapshot();
const { onClick, children } = el.find(Button).props();
expect(onClick).toEqual(hookProps.handleToggleViewClick);
expect(children).toEqual(formatMessage(hookProps.toggleViewMessage));
});
});
describe('frozen grades', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
el = shallow(<GradebookHeader />);
});
test('snapshot: show frozen warning', () => {
expect(el).toMatchSnapshot();
});
});
describe('user cannot view gradebook', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
el = shallow(<GradebookHeader />);
});
test('snapshot: show unauthorized warning', () => {
expect(el).toMatchSnapshot();
});
});
});
});

View File

@@ -0,0 +1,152 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Button } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import actions from 'data/actions';
import selectors from 'data/selectors';
import { views } from 'data/constants/app';
import messages from './messages';
import { GradebookHeader, mapDispatchToProps, mapStateToProps } from '.';
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
app: { setView: jest.fn() },
},
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
activeView: jest.fn(state => ({ aciveView: state })),
courseId: jest.fn(state => ({ courseId: state })),
},
assignmentTypes: { areGradesFrozen: jest.fn(state => ({ areGradesFrozen: state })) },
roles: { canUserViewGradebook: jest.fn(state => ({ canUserViewGradebook: state })) },
root: { showBulkManagement: jest.fn(state => ({ showBulkManagement: state })) },
},
}));
const courseId = 'fakeID';
describe('GradebookHeader component', () => {
const props = {
activeView: views.grades,
areGradesFrozen: false,
canUserViewGradebook: false,
courseId,
showBulkManagement: false,
};
beforeEach(() => {
props.setView = jest.fn();
});
describe('snapshots', () => {
let el;
beforeEach(() => {
el = shallow(<GradebookHeader {...props} />);
el.instance().handleToggleViewClick = jest.fn().mockName('this.handleToggleViewClick');
});
describe('default values (grades frozen, cannot view).', () => {
test('unauthorized warning, but no grades frozen warning', () => {
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('grades frozen, cannot view', () => {
test('unauthorized warning, and grades frozen warning.', () => {
el.setProps({ areGradesFrozen: true });
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('grades frozen, can view.', () => {
test('grades frozen warning but no unauthorized warning', () => {
el.setProps({ areGradesFrozen: true, canUserViewGradebook: true });
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('show bulk management, active view is grades view', () => {
test('toggle view button to activity log', () => {
el.setProps({ showBulkManagement: true });
expect(el.find(Button).getElement()).toEqual((
<Button
variant="tertiary"
onClick={el.instance().handleToggleViewClick}
>
<FormattedMessage {...messages.toActivityLog} />
</Button>
));
expect(el.instance().render()).toMatchSnapshot();
});
});
describe('show bulk management, active view is bulkManagementHistory view', () => {
test('toggle view button to grades', () => {
el.setProps({ showBulkManagement: true, activeView: views.bulkManagementHistory });
expect(el.find(Button).getElement()).toEqual((
<Button
variant="tertiary"
onClick={el.instance().handleToggleViewClick}
>
<FormattedMessage {...messages.toGradesView} />
</Button>
));
expect(el.instance().render()).toMatchSnapshot();
});
});
});
describe('behavior', () => {
let el;
beforeEach(() => {
el = shallow(<GradebookHeader {...props} />);
});
describe('handleToggleViewClick', () => {
test('calls setView with activity view if activeView is grades', () => {
el.instance().handleToggleViewClick();
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
});
test('calls setView with grades view if activeView is bulkManagementHistory', () => {
el.setProps({ activeView: views.bulkManagementHistory });
el.instance().handleToggleViewClick();
expect(props.setView).toHaveBeenCalledWith(views.grades);
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { a: 'test', example: 'state' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('activeView from app.activeView', () => {
expect(mapped.activeView).toEqual(selectors.app.activeView(testState));
});
test('courseId from app.courseId', () => {
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
});
test('areGradesFrozen from assignmentTypes selector', () => {
expect(
mapped.areGradesFrozen,
).toEqual(selectors.assignmentTypes.areGradesFrozen(testState));
});
test('canUserViewGradebook from roles selector', () => {
expect(
mapped.canUserViewGradebook,
).toEqual(selectors.roles.canUserViewGradebook(testState));
});
test('showBulkManagement from root showBulkManagement selector', () => {
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
});
});
describe('mapDispatchToProps', () => {
test('setView from actions.app.setView', () => {
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
});
});
});

View File

@@ -0,0 +1,71 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { views } from 'data/constants/app';
import actions from 'data/actions';
import selectors from 'data/selectors';
import NetworkButton from 'components/NetworkButton';
import ImportGradesButton from './ImportGradesButton';
import messages from './BulkManagementControls.messages';
/**
* <BulkManagementControls />
* Provides download buttons for Bulk Management and Intervention reports, only if
* showBulkManagement is set in redus.
*/
export class BulkManagementControls extends React.Component {
constructor(props) {
super(props);
this.handleClickExportGrades = this.handleClickExportGrades.bind(this);
this.handleViewActivityLog = this.handleViewActivityLog.bind(this);
}
handleClickExportGrades() {
this.props.downloadBulkGradesReport();
window.location.assign(this.props.gradeExportUrl);
}
handleViewActivityLog() {
this.props.setView(views.bulkManagementHistory);
}
render() {
return this.props.showBulkManagement && (
<div className="d-flex">
<NetworkButton
label={messages.downloadGradesBtn}
onClick={this.handleClickExportGrades}
/>
<ImportGradesButton />
</div>
);
}
}
BulkManagementControls.defaultProps = {
showBulkManagement: false,
};
BulkManagementControls.propTypes = {
// redux
downloadBulkGradesReport: PropTypes.func.isRequired,
gradeExportUrl: PropTypes.string.isRequired,
showBulkManagement: PropTypes.bool,
setView: PropTypes.func.isRequired,
};
export const mapStateToProps = (state) => ({
gradeExportUrl: selectors.root.gradeExportUrl(state),
showBulkManagement: selectors.root.showBulkManagement(state),
});
export const mapDispatchToProps = {
downloadBulkGradesReport: actions.grades.downloadReport.bulkGrades,
setView: actions.app.setView,
};
export default connect(mapStateToProps, mapDispatchToProps)(BulkManagementControls);

View File

@@ -0,0 +1,124 @@
import React from 'react';
import { shallow } from 'enzyme';
import actions from 'data/actions';
import selectors from 'data/selectors';
import { views } from 'data/constants/app';
import {
BulkManagementControls,
mapStateToProps,
mapDispatchToProps,
} from './BulkManagementControls';
jest.mock('./ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
root: {
gradeExportUrl: (state) => ({ gradeExportUrl: state }),
interventionExportUrl: (state) => ({ interventionExportUrl: state }),
showBulkManagement: (state) => ({ showBulkManagement: state }),
},
},
}));
jest.mock('data/actions', () => ({
__esModule: true,
default: {
app: { setView: jest.fn() },
grades: {
downloadReport: {
bulkGrades: jest.fn(),
intervention: jest.fn(),
},
},
},
}));
describe('BulkManagementControls', () => {
describe('component', () => {
let el;
let props = {
gradeExportUrl: 'gradesGoHere',
interventionExportUrl: 'interventionsGoHere',
};
beforeEach(() => {
props = {
...props,
downloadBulkGradesReport: jest.fn(),
downloadInterventionReport: jest.fn(),
setView: jest.fn(),
};
});
test('snapshot - empty if showBulkManagement is not truthy', () => {
expect(shallow(<BulkManagementControls {...props} />)).toEqual({});
});
describe('behavior', () => {
const oldWindowLocation = window.location;
beforeAll(() => {
delete window.location;
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: {
configurable: true,
value: jest.fn(),
},
},
);
});
beforeEach(() => {
window.location.assign.mockReset();
el = shallow(<BulkManagementControls {...props} showBulkManagement />);
});
afterAll(() => {
// restore `window.location` to the `jsdom` `Location` object
window.location = oldWindowLocation;
});
describe('handleViewActivityLog', () => {
it('calls props.setView(views.bulkManagementHistory)', () => {
el.instance().handleViewActivityLog();
expect(props.setView).toHaveBeenCalledWith(views.bulkManagementHistory);
});
});
describe('handleClickExportGrades', () => {
const assertions = [
'calls props.downloadBulkGradesReport',
'sets location to props.gradeExportUrl',
];
it(assertions.join(' and '), () => {
el.instance().handleClickExportGrades();
expect(props.downloadBulkGradesReport).toHaveBeenCalled();
expect(window.location.assign).toHaveBeenCalledWith(props.gradeExportUrl);
});
});
});
});
describe('mapStateToProps', () => {
let mapped;
const testState = { do: 'not', test: 'me' };
beforeEach(() => {
mapped = mapStateToProps(testState);
});
test('gradeExportUrl from root.gradeExportUrl', () => {
expect(mapped.gradeExportUrl).toEqual(selectors.root.gradeExportUrl(testState));
});
test('showBulkManagement from root.showBulkManagement', () => {
expect(mapped.showBulkManagement).toEqual(selectors.root.showBulkManagement(testState));
});
});
describe('mapDispatchToProps', () => {
test('downloadBulkGradesReport from actions.grades.downloadReport.bulkGrades', () => {
expect(
mapDispatchToProps.downloadBulkGradesReport,
).toEqual(actions.grades.downloadReport.bulkGrades);
});
test('setView from actions.app.setView', () => {
expect(mapDispatchToProps.setView).toEqual(actions.app.setView);
});
});
});

View File

@@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
<div
className="d-flex"
>
<NetworkButton
label={
Object {
"defaultMessage": "Download Grades",
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
}
}
onClick={[MockFunction]}
/>
<ImportGradesButton />
</div>
`;

View File

@@ -1,18 +0,0 @@
import { actions, selectors } from 'data/redux/hooks';
export const useBulkManagementControlsData = () => {
const gradeExportUrl = selectors.root.useGradeExportUrl();
const showBulkManagement = selectors.root.useShowBulkManagement();
const downloadBulkGradesReport = actions.grades.downloadReport.useBulkGrades();
const handleClickExportGrades = () => {
downloadBulkGradesReport();
window.location.assign(gradeExportUrl);
};
return {
show: showBulkManagement,
handleClickExportGrades,
};
};
export default useBulkManagementControlsData;

View File

@@ -1,72 +0,0 @@
import { actions, selectors } from 'data/redux/hooks';
import useBulkManagementControlsData from './hooks';
jest.mock('data/redux/hooks', () => ({
actions: {
grades: {
downloadReport: { useBulkGrades: jest.fn() },
},
},
selectors: {
root: {
useGradeExportUrl: jest.fn(),
useShowBulkManagement: jest.fn(),
},
},
}));
const downloadBulkGrades = jest.fn();
actions.grades.downloadReport.useBulkGrades.mockReturnValue(downloadBulkGrades);
const gradeExportUrl = 'test-grade-export-url';
selectors.root.useGradeExportUrl.mockReturnValue(gradeExportUrl);
selectors.root.useShowBulkManagement.mockReturnValue(true);
let hook;
describe('useBulkManagementControlsData', () => {
const oldWindowLocation = window.location;
beforeAll(() => {
delete window.location;
window.location = Object.defineProperties(
{},
{
...Object.getOwnPropertyDescriptors(oldWindowLocation),
assign: { configurable: true, value: jest.fn() },
},
);
});
beforeEach(() => {
window.location.assign.mockReset();
hook = useBulkManagementControlsData();
});
afterAll(() => {
// restore `window.location` to the `jsdom` `Location` object
window.location = oldWindowLocation;
});
describe('initialization', () => {
it('initializes redux hooks', () => {
expect(selectors.root.useGradeExportUrl).toHaveBeenCalledWith();
expect(selectors.root.useShowBulkManagement).toHaveBeenCalledWith();
expect(actions.grades.downloadReport.useBulkGrades).toHaveBeenCalledWith();
});
});
describe('output', () => {
it('forwards show from showBulkManagement', () => {
expect(hook.show).toEqual(true);
selectors.root.useShowBulkManagement.mockReturnValue(false);
hook = useBulkManagementControlsData();
expect(hook.show).toEqual(false);
});
describe('handleClickExportGrades', () => {
beforeEach(() => {
hook.handleClickExportGrades();
});
it('downloads bulk grades report', () => {
expect(downloadBulkGrades).toHaveBeenCalledWith();
});
it('sets window location to grade export url', () => {
expect(window.location.assign).toHaveBeenCalledWith(gradeExportUrl);
});
});
});
});

View File

@@ -1,32 +0,0 @@
/* eslint-disable react/sort-comp, react/button-has-type */
import React from 'react';
import NetworkButton from 'components/NetworkButton';
import ImportGradesButton from '../ImportGradesButton';
import useBulkManagementControlsData from './hooks';
import messages from './messages';
/**
* <BulkManagementControls />
* Provides download buttons for Bulk Management and Intervention reports, only if
* showBulkManagement is set in redus.
*/
export const BulkManagementControls = () => {
const {
show,
handleClickExportGrades,
} = useBulkManagementControlsData();
return show && (
<div className="d-flex">
<NetworkButton
label={messages.downloadGradesBtn}
onClick={handleClickExportGrades}
/>
<ImportGradesButton />
</div>
);
};
export default BulkManagementControls;

View File

@@ -1,32 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import useBulkManagementControlsData from './hooks';
import BulkManagementControls from '.';
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => jest.fn());
const hookProps = {
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
describe('BulkManagementControls', () => {
describe('behavior', () => {
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />)).toMatchSnapshot();
});
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
});
});
});

View File

@@ -19,7 +19,7 @@ export const FilterMenuToggle = ({ toggleFilterDrawer }) => (
className="btn-primary align-self-start"
onClick={toggleFilterDrawer}
>
<Icon className="fa fa-filter" /> <FormattedMessage {...messages.editFilters} />
<Icon className="fa fa-filter mr-1" /> <FormattedMessage {...messages.editFilters} />
</Button>
);

View File

@@ -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;
}
}

View File

@@ -6,11 +6,10 @@ export const useImportButtonData = () => {
const submitImportGradesButtonData = thunkActions.grades.useSubmitImportGradesButtonData();
const fileInputRef = useRef();
const hasFile = fileInputRef.current && fileInputRef.current.files[0];
const handleClickImportGrades = () => hasFile && fileInputRef.current.click();
const handleClickImportGrades = () => fileInputRef.current?.click();
const handleFileInputChange = () => {
if (hasFile) {
if (fileInputRef.current?.files[0]) {
const clearInput = () => {
fileInputRef.current.value = null;
};

View File

@@ -40,7 +40,7 @@ export class SearchControls extends React.Component {
render() {
return (
<div>
<div className="search-container">
<SearchField
onSubmit={this.onSubmit}
inputLabel={<FormattedMessage {...messages.label} />}

View File

@@ -7,7 +7,7 @@ exports[`FilterMenuToggle component snapshots basic snapshot 1`] = `
onClick={[MockFunction this.props.toggleFilterDrawer]}
>
<Icon
className="fa fa-filter"
className="fa fa-filter mr-1"
/>
<FormattedMessage

View File

@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchControls Component Snapshots basic snapshot 1`] = `
<div>
<div
className="search-container"
>
<SearchField
inputLabel={
<FormattedMessage

View File

@@ -14,7 +14,7 @@ exports[`GradesView Component snapshots basic snapshot 1`] = `
/>
</h3>
<div
className="d-flex justify-content-between"
className="d-flex justify-content-between flex-wrap"
>
<FilterMenuToggle />
<SearchControls />

View File

@@ -50,7 +50,7 @@ export class GradesView extends React.Component {
<FormattedMessage {...messages.filterStepHeading} />
</h3>
<div className="d-flex justify-content-between">
<div className="d-flex justify-content-between flex-wrap">
<FilterMenuToggle />
<SearchControls />
</div>

View File

@@ -4,7 +4,6 @@ import { actionHook } from './utils';
const app = StrictDict({
useSetLocalFilter: actionHook(actions.app.setLocalFilter),
useSetView: actionHook(actions.app.setView),
});
const filters = StrictDict({
@@ -17,14 +16,7 @@ const filters = StrictDict({
useUpdateTrack: actionHook(actions.filters.update.track),
});
const grades = StrictDict({
downloadReport: {
useBulkGrades: actionHook(actions.grades.downloadReport.bulkGrades),
},
});
export default StrictDict({
app,
filters,
grades,
});

View File

@@ -7,7 +7,6 @@ import actionHooks from './actions';
jest.mock('data/actions', () => ({
app: {
setLocalFilter: jest.fn(),
setView: jest.fn(),
},
filters: {
update: {
@@ -15,11 +14,6 @@ jest.mock('data/actions', () => ({
assignmentLimits: jest.fn(),
},
},
grades: {
downloadReport: {
bulkGrades: jest.fn(),
},
},
}));
jest.mock('./utils', () => ({
actionHook: (action) => ({ actionHook: action }),
@@ -38,7 +32,6 @@ describe('action hooks', () => {
const hookKeys = keyStore(actionHooks.app);
beforeEach(() => { hooks = actionHooks.app; });
testActionHook(hookKeys.useSetLocalFilter, actions.app.setLocalFilter);
testActionHook(hookKeys.useSetView, actions.app.setView);
});
describe('filters', () => {
const hookKeys = keyStore(actionHooks.filters);
@@ -54,11 +47,4 @@ describe('action hooks', () => {
);
testActionHook(hookKeys.useUpdateTrack, actionGroup.updateTrack);
});
describe('grades', () => {
beforeEach(() => { hooks = actionHooks.grades; });
test('downloadReport.useBulkGrades', () => {
expect(hooks.downloadReport.useBulkGrades)
.toEqual(actionHook(actions.grades.downloadReport.bulkGrades));
});
});
});

View File

@@ -7,20 +7,16 @@ export const root = StrictDict({
useGradeExportUrl: () => useSelector(selectors.root.gradeExportUrl),
useSelectedCohortEntry: () => useSelector(selectors.root.selectedCohortEntry),
useSelectedTrackEntry: () => useSelector(selectors.root.selectedTrackEntry),
useShowBulkManagement: () => useSelector(selectors.root.showBulkManagement),
});
export const app = StrictDict({
useActiveView: () => useSelector(selectors.app.activeView),
useAssignmentGradeLimits: () => useSelector(selectors.app.assignmentGradeLimits),
useAreCourseGradeFiltersValid: () => useSelector(selectors.app.areCourseGradeFiltersValid),
useCourseGradeLimits: () => useSelector(selectors.app.courseGradeLimits),
useCourseId: () => useSelector(selectors.app.courseId),
});
export const assignmentTypes = StrictDict({
useAllAssignmentTypes: () => useSelector(selectors.assignmentTypes.allAssignmentTypes),
useAreGradesFrozen: () => useSelector(selectors.assignmentTypes.areGradesFrozen),
});
export const cohorts = StrictDict({
@@ -37,10 +33,6 @@ export const filters = StrictDict({
useAssignmentType: () => useSelector(selectors.filters.assignmentType),
});
export const roles = StrictDict({
useCanUserViewGradebook: () => useSelector(selectors.roles.canUserViewGradebook),
});
export const tracks = StrictDict({
useAllTracks: () => useSelector(selectors.tracks.allTracks),
// maybe not needed?
@@ -52,7 +44,6 @@ export default StrictDict({
assignmentTypes,
cohorts,
filters,
roles,
tracks,
root,
});

View File

@@ -9,16 +9,11 @@ jest.mock('react-redux', () => ({
jest.mock('data/selectors', () => ({
app: {
activeView: jest.fn(),
assignmentGradeLimits: jest.fn(),
areCourseGradeFiltersValid: jest.fn(),
courseGradelimits: jest.fn(),
courseId: jest.fn(),
},
assignmentTypes: {
allAssignmentTypes: jest.fn(),
areGradesFrozen: jest.fn(),
},
assignmentTypes: { allAssignmentTypes: jest.fn() },
cohorts: {
allCohorts: jest.fn(),
cohortsByName: jest.fn(),
@@ -30,9 +25,6 @@ jest.mock('data/selectors', () => ({
selectedAssignmentLabel: jest.fn(),
assignmentType: jest.fn(),
},
roles: {
canUserViewGradebook: jest.fn(),
},
tracks: {
allTracks: jest.fn(),
tracksByName: jest.fn(),
@@ -41,7 +33,6 @@ jest.mock('data/selectors', () => ({
gradeExportUrl: jest.fn(),
selectedCohortEntry: jest.fn(),
selectedTrackEntry: jest.fn(),
showBulkManagement: jest.fn(),
},
}));
@@ -58,24 +49,20 @@ describe('selector hooks', () => {
testHook(hookKeys.useGradeExportUrl, selectors.root.gradeExportUrl);
testHook(hookKeys.useSelectedCohortEntry, selectors.root.selectedCohortEntry);
testHook(hookKeys.useSelectedTrackEntry, selectors.root.selectedTrackEntry);
testHook(hookKeys.useShowBulkManagement, selectors.root.showBulkManagement);
});
describe('app', () => {
const hookKeys = keyStore(selectorHooks.app);
const selGroup = selectors.app;
beforeEach(() => { hooks = selectorHooks.app; });
testHook(hookKeys.useActiveView, selGroup.activeView);
testHook(hookKeys.useAssignmentGradeLimits, selGroup.assignmentGradeLimits);
testHook(hookKeys.useAreCourseGradeFiltersValid, selGroup.areCourseGradeFiltersValid);
testHook(hookKeys.useCourseGradeLimits, selGroup.courseGradeLimits);
testHook(hookKeys.useCourseId, selGroup.courseId);
});
describe('assignmentTypes', () => {
const hookKeys = keyStore(selectorHooks.assignmentTypes);
const selGroup = selectors.assignmentTypes;
beforeEach(() => { hooks = selectorHooks.assignmentTypes; });
testHook(hookKeys.useAllAssignmentTypes, selGroup.allAssignmentTypes);
testHook(hookKeys.useAreGradesFrozen, selGroup.areGradesFrozen);
});
describe('cohorts', () => {
const hookKeys = keyStore(selectorHooks.cohorts);
@@ -94,12 +81,6 @@ describe('selector hooks', () => {
testHook(hookKeys.useSelectedAssignmentLabel, selGroup.selectedAssignmentLabel);
testHook(hookKeys.useAssignmentType, selGroup.assignmentType);
});
describe('roles', () => {
const hookKeys = keyStore(selectorHooks.roles);
const selGroup = selectors.roles;
beforeEach(() => { hooks = selectorHooks.roles; });
testHook(hookKeys.useCanUserViewGradebook, selGroup.canUserViewGradebook);
});
describe('tracks', () => {
const hookKeys = keyStore(selectorHooks.tracks);
const selGroup = selectors.tracks;

View File

@@ -36,10 +36,6 @@ export const sectionOverrideHistoryUrl = (subsectionId, userId) => stringifyUrl(
{ user_id: userId, history_record_limit: historyRecordLimit },
);
export const instructorDashboardUrl = () => (
`${getConfig().LMS_BASE_URL}/courses/${courseId}/instructor`
);
export default StrictDict({
getUrlPrefix,
getBulkGradesUrl,