Compare commits
40 Commits
qa/fronten
...
open-relea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c7d1cdecff | ||
|
|
9c564c5781 | ||
|
|
6c9673cc06 | ||
|
|
ea1b1c8673 | ||
|
|
62a6c7b282 | ||
|
|
803dd2f012 | ||
|
|
bf677db59a | ||
|
|
d033fa310d | ||
|
|
f6432c1896 | ||
|
|
876f5d9413 | ||
|
|
587caff980 | ||
|
|
38d40342db | ||
|
|
1595f52ade | ||
|
|
8d4ef50039 | ||
|
|
257849b55f | ||
|
|
e3d1dde3d1 | ||
|
|
0de9807c94 | ||
|
|
e1442f7890 | ||
|
|
8ebd0497cf | ||
|
|
25aae2cec8 | ||
|
|
e656534a51 | ||
|
|
278ac101a7 | ||
|
|
59c57028a0 | ||
|
|
e549cfb598 | ||
|
|
4267206d58 | ||
|
|
054e6acfc6 | ||
|
|
664368fe17 | ||
|
|
6b94ba4c5c | ||
|
|
11a2838799 | ||
|
|
42535c5f80 | ||
|
|
e8550af85d | ||
|
|
c6ed6938af | ||
|
|
81026ae013 | ||
|
|
46ebc899ee | ||
|
|
f7341efc71 | ||
|
|
c5bf0a7d11 | ||
|
|
b2aac6036e | ||
|
|
21dcc972ed | ||
|
|
d629e4495b | ||
|
|
579ff50c99 |
71
.github/workflows/ci.yml
vendored
71
.github/workflows/ci.yml
vendored
@@ -3,50 +3,55 @@ name: Node CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- master
|
||||
pull_request:
|
||||
branches:
|
||||
- "**"
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
tests:
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
matrix:
|
||||
node: [12, 14, 16]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Verify No Uncommitted Changes
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
- name: Verify No Uncommitted Changes
|
||||
run: make validate-no-uncommitted-package-lock-changes
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: Test
|
||||
run: npm run test
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v2
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: Upgrade python requirements workflow in ${{github.repository}} failed! For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
uses: dawidd6/action-send-mail@v3
|
||||
with:
|
||||
server_address: email-smtp.us-east-1.amazonaws.com
|
||||
server_port: 465
|
||||
username: ${{ secrets.EDX_SMTP_USERNAME }}
|
||||
password: ${{ secrets.EDX_SMTP_PASSWORD }}
|
||||
subject: Upgrade python requirements workflow failed in ${{github.repository}}
|
||||
to: masters-grades@edx.org
|
||||
from: github-actions <github-actions@edx.org>
|
||||
body: Upgrade python requirements workflow in ${{github.repository}} failed!
|
||||
For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id
|
||||
}}"
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -19,6 +19,9 @@ public/samples/
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# Local package dependencies
|
||||
module.config.js
|
||||
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[edx-platform.frontend-app-gradebook]
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-ora-grading]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
type = KEYVALUEJSON
|
||||
|
||||
|
||||
4
Makefile
4
Makefile
@@ -2,7 +2,7 @@ npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = ora-enhanced-staff-grader
|
||||
transifex_resource = frontend-app-ora-grading
|
||||
transifex_langs = "ar,fr,es_419,zh_CN"
|
||||
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
@@ -57,7 +57,7 @@ push_translations:
|
||||
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -f --mode reviewed --language=$(transifex_langs)
|
||||
tx pull -f --mode reviewed --languages=$(transifex_langs)
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('babel');
|
||||
@@ -2,6 +2,7 @@ const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
'jest-expect-message',
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
@@ -12,4 +13,5 @@ module.exports = createConfig('jest', {
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
],
|
||||
testTimeout: 120000,
|
||||
});
|
||||
|
||||
38052
package-lock.json
generated
38052
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -37,12 +37,16 @@
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@zip.js/zip.js": "^2.4.6",
|
||||
"axios": "^0.21.4",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.16.2",
|
||||
"dompurify": "^2.3.1",
|
||||
"email-prop-type": "^3.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^8.0.6",
|
||||
"font-awesome": "4.7.0",
|
||||
"history": "5.0.1",
|
||||
"html-react-parser": "^1.3.0",
|
||||
@@ -69,10 +73,9 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/frontend-build": "8.0.4",
|
||||
"@edx/frontend-build": "9.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^12.1.0",
|
||||
"axios": "0.21.1",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"codecov": "^3.8.3",
|
||||
"enzyme-adapter-react-16": "^1.15.6",
|
||||
@@ -81,6 +84,7 @@
|
||||
"husky": "^7.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "27.0.6",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^11.0.4",
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
|
||||
12
src/App.jsx
12
src/App.jsx
@@ -7,19 +7,21 @@ import Footer from '@edx/frontend-component-footer';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import CourseHeader from 'containers/CourseHeader';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import './App.scss';
|
||||
|
||||
import Header from 'containers/CourseHeader';
|
||||
|
||||
export const App = ({ courseMetadata }) => (
|
||||
export const App = ({ courseMetadata, isEnabled }) => (
|
||||
<Router>
|
||||
<div>
|
||||
<Header
|
||||
<CourseHeader
|
||||
courseTitle={courseMetadata.title}
|
||||
courseNumber={courseMetadata.number}
|
||||
courseOrg={courseMetadata.org}
|
||||
/>
|
||||
{!isEnabled && <DemoWarning />}
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
@@ -40,10 +42,12 @@ App.propTypes = {
|
||||
number: PropTypes.string,
|
||||
org: PropTypes.string,
|
||||
}),
|
||||
isEnabled: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
courseMetadata: selectors.app.courseMetadata(state),
|
||||
isEnabled: selectors.app.isEnabled(state),
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps)(App);
|
||||
|
||||
@@ -7,7 +7,18 @@ import ListView from 'containers/ListView';
|
||||
|
||||
import { App } from './App';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
app: {
|
||||
selectors: {
|
||||
courseMetadata: (state) => ({ courseMetadata: state }),
|
||||
isEnabled: (state) => ({ isEnabled: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
jest.mock('containers/CourseHeader', () => 'CourseHeader');
|
||||
|
||||
@@ -22,10 +33,14 @@ describe('App router component', () => {
|
||||
number: 'course-number',
|
||||
title: 'course-title',
|
||||
},
|
||||
isEnabled: true,
|
||||
};
|
||||
test('snapshot', () => {
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: disabled (show demo warning)', () => {
|
||||
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
|
||||
@@ -1,6 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot 1`] = `
|
||||
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<CourseHeader
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<DemoWarning />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<CourseHeader
|
||||
|
||||
23
src/components/DemoAlert/__snapshots__/index.test.jsx.snap
Normal file
23
src/components/DemoAlert/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoAlert component snapshot 1`] = `
|
||||
<AlertModal
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.onClose]}
|
||||
variant="primary"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction props.onClose]}
|
||||
title="Demo submit prevented"
|
||||
>
|
||||
<p>
|
||||
Grade submission is disabled in the Demo mode of the new ORA Staff Grader.
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
39
src/components/DemoAlert/index.jsx
Normal file
39
src/components/DemoAlert/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
AlertModal,
|
||||
Button,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const DemoAlert = ({
|
||||
intl: { formatMessage },
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => (
|
||||
<AlertModal
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>{formatMessage(messages.warningMessage)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
DemoAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DemoAlert);
|
||||
16
src/components/DemoAlert/index.test.jsx
Normal file
16
src/components/DemoAlert/index.test.jsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { DemoAlert } from '.';
|
||||
|
||||
describe('DemoAlert component', () => {
|
||||
test('snapshot', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
21
src/components/DemoAlert/messages.js
Normal file
21
src/components/DemoAlert/messages.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
warningMessage: {
|
||||
id: 'ora-grading.demoAlert.warningMessage',
|
||||
defaultMessage: 'Grade submission is disabled in the Demo mode of the new ORA Staff Grader.',
|
||||
description: 'Submit Grade button text after successful submission',
|
||||
},
|
||||
confirm: {
|
||||
id: 'ora-grading.demoAlert.confirm',
|
||||
defaultMessage: 'Confirm',
|
||||
description: 'Confirm button text',
|
||||
},
|
||||
title: {
|
||||
id: 'ora-grading.demoAlert.title',
|
||||
defaultMessage: 'Demo submit prevented',
|
||||
description: 'Title of alert modal after submit was prevented because in demo mode',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePopoverContent component snapshot 1`] = `
|
||||
exports[`FilePopoverContent component snapshot default 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
@@ -28,5 +28,62 @@ exports[`FilePopoverContent component snapshot 1`] = `
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
filesize(6000)
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FilePopoverContent component snapshot invalid size 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
some file name
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Description"
|
||||
description="Popover title for file description"
|
||||
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
Unknown
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -2,33 +2,39 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import filesize from 'filesize';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const FilePopoverContent = ({ file }) => (
|
||||
export const FilePopoverContent = ({ name, description, size }) => (
|
||||
<>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverNameTitle} /></strong>
|
||||
<br />
|
||||
{file.name}
|
||||
{name}
|
||||
</div>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverDescriptionTitle} /></strong>
|
||||
<br />
|
||||
{file.description}
|
||||
{description}
|
||||
</div>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.fileSizeTitle} /></strong>
|
||||
<br />
|
||||
{typeof (size) === 'number' ? filesize(size) : 'Unknown'}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
FilePopoverContent.defaultProps = {
|
||||
description: '',
|
||||
size: null,
|
||||
};
|
||||
|
||||
FilePopoverContent.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
downloadURL: PropTypes.string,
|
||||
}).isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
size: PropTypes.number,
|
||||
};
|
||||
|
||||
export default FilePopoverContent;
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import filesize from 'filesize';
|
||||
import FilePopoverContent from '.';
|
||||
|
||||
jest.mock('filesize', () => (size) => `filesize(${size})`);
|
||||
|
||||
describe('FilePopoverContent', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
file: {
|
||||
name: 'some file name',
|
||||
description: 'long descriptive text...',
|
||||
downloadURL: 'this-url-is.working',
|
||||
},
|
||||
name: 'some file name',
|
||||
description: 'long descriptive text...',
|
||||
downloadURL: 'this-url-is.working',
|
||||
size: 6000,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverContent {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
describe('snapshot', () => {
|
||||
test('default', () => expect(el).toMatchSnapshot());
|
||||
test('invalid size', () => {
|
||||
el.setProps({
|
||||
size: null,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
expect(el.text()).toContain(props.file.name);
|
||||
expect(el.text()).toContain(props.file.description);
|
||||
expect(el.text()).toContain(props.name);
|
||||
expect(el.text()).toContain(props.description);
|
||||
expect(el.text()).toContain(filesize(props.size));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -11,6 +11,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'File Description',
|
||||
description: 'Popover title for file description',
|
||||
},
|
||||
fileSizeTitle: {
|
||||
id: 'ora-grading.FilePopoverCellContent.fileSizeTitle',
|
||||
defaultMessage: 'File Size',
|
||||
description: 'Popover title for file size',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
44
src/components/FilePreview/Banners/ErrorBanner.jsx
Normal file
44
src/components/FilePreview/Banners/ErrorBanner.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messageShape = PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
defaultMessage: PropTypes.string,
|
||||
});
|
||||
|
||||
export const ErrorBanner = ({ actions, headingMessage, children }) => {
|
||||
const actionButtons = actions.map(action => (
|
||||
<Button key={action.id} onClick={action.onClick} variant="outline-primary">
|
||||
<FormattedMessage {...action.message} />
|
||||
</Button>
|
||||
));
|
||||
return (
|
||||
<Alert variant="danger" icon={Info} actions={actionButtons}>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage {...headingMessage} />
|
||||
</Alert.Heading>
|
||||
{children}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
ErrorBanner.defaultProps = {
|
||||
actions: [],
|
||||
children: null,
|
||||
};
|
||||
ErrorBanner.propTypes = {
|
||||
actions: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
message: messageShape,
|
||||
}),
|
||||
),
|
||||
headingMessage: messageShape.isRequired,
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
export default ErrorBanner;
|
||||
59
src/components/FilePreview/Banners/ErrorBanner.test.jsx
Normal file
59
src/components/FilePreview/Banners/ErrorBanner.test.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ErrorBanner from './ErrorBanner';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
describe('Error Banner component', () => {
|
||||
const children = <p>Abitary Child</p>;
|
||||
|
||||
const props = {
|
||||
actions: [
|
||||
{
|
||||
id: 'action1',
|
||||
onClick: jest.fn().mockName('action1.onClick'),
|
||||
message: messages.retryButton,
|
||||
},
|
||||
{
|
||||
id: 'action2',
|
||||
onClick: jest.fn().mockName('action2.onClick'),
|
||||
message: messages.retryButton,
|
||||
},
|
||||
],
|
||||
headingMessage: messages.unknownError,
|
||||
children,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ErrorBanner {...props} />);
|
||||
});
|
||||
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('children node', () => {
|
||||
expect(el.containsMatchingElement(children)).toEqual(true);
|
||||
});
|
||||
|
||||
test('verify actions', () => {
|
||||
const actions = el.find('Alert').prop('actions');
|
||||
expect(actions).toHaveLength(props.actions.length);
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
expect(action.type).toEqual('Button');
|
||||
expect(action.props.onClick).toEqual(props.actions[index].onClick);
|
||||
// action message
|
||||
expect(action.props.children.props).toEqual(props.actions[index].message);
|
||||
});
|
||||
});
|
||||
|
||||
test('verify heading', () => {
|
||||
const heading = el.find('FormattedMessage');
|
||||
expect(heading.props()).toEqual(props.headingMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
14
src/components/FilePreview/Banners/LoadingBanner.jsx
Normal file
14
src/components/FilePreview/Banners/LoadingBanner.jsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Spinner } from '@edx/paragon';
|
||||
|
||||
export const LoadingBanner = () => (
|
||||
<Alert variant="info">
|
||||
<Spinner animation="border" className="d-flex m-auto" />
|
||||
</Alert>
|
||||
);
|
||||
|
||||
LoadingBanner.defaultProps = {};
|
||||
LoadingBanner.propTypes = {};
|
||||
|
||||
export default LoadingBanner;
|
||||
11
src/components/FilePreview/Banners/LoadingBanner.test.jsx
Normal file
11
src/components/FilePreview/Banners/LoadingBanner.test.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import LoadingBanner from './LoadingBanner';
|
||||
|
||||
describe('Loading Banner component', () => {
|
||||
test('snapshot', () => {
|
||||
const el = shallow(<LoadingBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction action1.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={[MockFunction action2.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
Abitary Child
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loading Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
variant="info"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="d-flex m-auto"
|
||||
/>
|
||||
</Alert>
|
||||
`;
|
||||
2
src/components/FilePreview/Banners/index.jsx
Normal file
2
src/components/FilePreview/Banners/index.jsx
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as ErrorBanner } from './ErrorBanner';
|
||||
export { default as LoadingBanner } from './LoadingBanner';
|
||||
27
src/components/FilePreview/BaseRenderers/ImageRenderer.jsx
Normal file
27
src/components/FilePreview/BaseRenderers/ImageRenderer.jsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImageRenderer = ({
|
||||
url, fileName, onError, onSuccess,
|
||||
}) => (
|
||||
<img
|
||||
alt={fileName}
|
||||
className="image-renderer"
|
||||
src={url}
|
||||
onError={onError}
|
||||
onLoad={onSuccess}
|
||||
/>
|
||||
);
|
||||
|
||||
ImageRenderer.defaultProps = {
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
ImageRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
@@ -8,6 +8,9 @@ describe('Image Renderer Component', () => {
|
||||
url: 'some_url.jpg',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
@@ -37,6 +37,7 @@ export class PDFRenderer extends React.Component {
|
||||
}
|
||||
|
||||
onDocumentLoadSuccess = ({ numPages }) => {
|
||||
this.props.onSuccess();
|
||||
this.setState({ numPages });
|
||||
};
|
||||
|
||||
@@ -51,8 +52,16 @@ export class PDFRenderer extends React.Component {
|
||||
};
|
||||
|
||||
onDocumentLoadError = (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
let status;
|
||||
switch (error.name) {
|
||||
case 'MissingPDFException':
|
||||
status = 404;
|
||||
break;
|
||||
default:
|
||||
status = 500;
|
||||
break;
|
||||
}
|
||||
this.props.onError(status);
|
||||
};
|
||||
|
||||
onInputPageChange = ({ target: { value } }) => {
|
||||
@@ -140,6 +149,8 @@ PDFRenderer.defaultProps = {};
|
||||
|
||||
PDFRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PDFRenderer;
|
||||
@@ -17,6 +17,9 @@ describe('PDF Renderer Component', () => {
|
||||
url: 'some_url.pdf',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
@@ -1,11 +1,16 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { get } from 'data/services/lms/utils';
|
||||
import { get } from 'axios';
|
||||
|
||||
const TXTRenderer = ({ url }) => {
|
||||
const TXTRenderer = ({ url, onError, onSuccess }) => {
|
||||
const [content, setContent] = useState('');
|
||||
useMemo(() => {
|
||||
get(url).then(({ data }) => setContent(data));
|
||||
get(url)
|
||||
.then(({ data }) => {
|
||||
onSuccess();
|
||||
setContent(data);
|
||||
})
|
||||
.catch(({ response }) => onError(response.status));
|
||||
}, [url]);
|
||||
|
||||
return (
|
||||
@@ -19,6 +24,8 @@ TXTRenderer.defaultProps = {};
|
||||
|
||||
TXTRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
onError: PropTypes.func.isRequired,
|
||||
onSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default TXTRenderer;
|
||||
@@ -3,15 +3,18 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('data/services/lms/utils', () => ({
|
||||
jest.mock('axios', () => ({
|
||||
get: jest.fn((...args) => Promise.resolve({ data: `Content of ${args}` })),
|
||||
}));
|
||||
|
||||
describe('Image Renderer Component', () => {
|
||||
describe('TXT Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.txt',
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<TXTRenderer {...props} />);
|
||||
@@ -4,6 +4,8 @@ exports[`Image Renderer Component snapshot 1`] = `
|
||||
<img
|
||||
alt=""
|
||||
className="image-renderer"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onLoad={[MockFunction this.props.onSuccess]}
|
||||
src="some_url.jpg"
|
||||
/>
|
||||
`;
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Image Renderer Component snapshot 1`] = `
|
||||
exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
3
src/components/FilePreview/BaseRenderers/index.jsx
Normal file
3
src/components/FilePreview/BaseRenderers/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as ImageRenderer } from './ImageRenderer';
|
||||
export { default as PDFRenderer } from './PDFRenderer';
|
||||
export { default as TXTRenderer } from './TXTRenderer';
|
||||
@@ -12,9 +12,9 @@ import './FileCard.scss';
|
||||
*/
|
||||
export const FileCard = ({ file, children }) => (
|
||||
<Card className="file-card" key={file.name}>
|
||||
<Collapsible className="file-collapsible" defaultOpen title={<h3>{file.name}</h3>}>
|
||||
<Collapsible className="file-collapsible" defaultOpen title={<h3 className="file-card-title">{file.name}</h3>}>
|
||||
<div className="preview-panel">
|
||||
<FileInfo><FilePopoverContent file={file} /></FileInfo>
|
||||
<FileInfo><FilePopoverContent {...file} /></FileInfo>
|
||||
{children}
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
|
||||
.file-card {
|
||||
margin: map-get($spacers, 1) 0;
|
||||
|
||||
.file-card-title {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.image-renderer {
|
||||
@@ -18,4 +24,10 @@
|
||||
|
||||
.txt-renderer {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.file-card-title {
|
||||
width: map-get($container-max-widths, "sm")/2;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ describe('File Preview Card component', () => {
|
||||
describe('Component', () => {
|
||||
test('collapsible title is name header', () => {
|
||||
const title = el.find(Collapsible).prop('title');
|
||||
expect(title).toEqual(<h3>{props.file.name}</h3>);
|
||||
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
|
||||
});
|
||||
test('forwards children into preview-panel', () => {
|
||||
const previewPanelChildren = el.find('.preview-panel').children();
|
||||
|
||||
130
src/components/FilePreview/FileRenderer.jsx
Normal file
130
src/components/FilePreview/FileRenderer.jsx
Normal file
@@ -0,0 +1,130 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
PDFRenderer,
|
||||
ImageRenderer,
|
||||
TXTRenderer,
|
||||
} from 'components/FilePreview/BaseRenderers';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
import { ErrorBanner, LoadingBanner } from './Banners';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const RENDERERS = StrictDict({
|
||||
[FileTypes.pdf]: PDFRenderer,
|
||||
[FileTypes.jpg]: ImageRenderer,
|
||||
[FileTypes.jpeg]: ImageRenderer,
|
||||
[FileTypes.bmp]: ImageRenderer,
|
||||
[FileTypes.png]: ImageRenderer,
|
||||
[FileTypes.txt]: TXTRenderer,
|
||||
[FileTypes.gif]: ImageRenderer,
|
||||
[FileTypes.jfif]: ImageRenderer,
|
||||
[FileTypes.pjpeg]: ImageRenderer,
|
||||
[FileTypes.pjp]: ImageRenderer,
|
||||
[FileTypes.svg]: ImageRenderer,
|
||||
});
|
||||
|
||||
export const ERROR_STATUSES = {
|
||||
404: {
|
||||
headingMessage: messages.fileNotFoundError,
|
||||
children: <FormattedMessage {...messages.fileNotFoundError} />,
|
||||
},
|
||||
500: {
|
||||
headingMessage: messages.unknownError,
|
||||
children: <FormattedMessage {...messages.unknownError} />,
|
||||
},
|
||||
};
|
||||
|
||||
export const SUPPORTED_TYPES = Object.keys(RENDERERS);
|
||||
|
||||
export const getFileType = (fileName) => fileName.split('.').pop()?.toLowerCase();
|
||||
export const isSupported = (file) => SUPPORTED_TYPES.includes(getFileType(file.name));
|
||||
|
||||
/**
|
||||
* <FileRenderer />
|
||||
*/
|
||||
export class FileRenderer extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
errorStatus: null,
|
||||
isLoading: true,
|
||||
};
|
||||
|
||||
this.onError = this.onError.bind(this);
|
||||
this.onSuccess = this.onSuccess.bind(this);
|
||||
this.resetState = this.resetState.bind(this);
|
||||
}
|
||||
|
||||
onError(status) {
|
||||
this.setState({
|
||||
errorStatus: status,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
onSuccess() {
|
||||
this.setState({
|
||||
errorStatus: null,
|
||||
isLoading: false,
|
||||
});
|
||||
}
|
||||
|
||||
get error() {
|
||||
const status = this.state.errorStatus;
|
||||
return {
|
||||
...ERROR_STATUSES[status] || ERROR_STATUSES[500],
|
||||
actions: [
|
||||
{
|
||||
id: 'retry',
|
||||
onClick: this.resetState,
|
||||
message: messages.retryButton,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
resetState = () => {
|
||||
this.setState({
|
||||
errorStatus: null,
|
||||
isLoading: true,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { file } = this.props;
|
||||
const Renderer = RENDERERS[getFileType(file.name)];
|
||||
return (
|
||||
<FileCard key={file.downloadUrl} file={file}>
|
||||
{this.state.isLoading && <LoadingBanner />}
|
||||
{this.state.errorStatus ? (
|
||||
<ErrorBanner {...this.error} />
|
||||
) : (
|
||||
<Renderer
|
||||
fileName={file.name}
|
||||
url={file.downloadUrl}
|
||||
onError={this.onError}
|
||||
onSuccess={this.onSuccess}
|
||||
/>
|
||||
)}
|
||||
</FileCard>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
FileRenderer.defaultProps = {};
|
||||
FileRenderer.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default FileRenderer;
|
||||
133
src/components/FilePreview/FileRenderer.test.jsx
Normal file
133
src/components/FilePreview/FileRenderer.test.jsx
Normal file
@@ -0,0 +1,133 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import {
|
||||
ImageRenderer,
|
||||
PDFRenderer,
|
||||
TXTRenderer,
|
||||
} from 'components/FilePreview/BaseRenderers';
|
||||
import {
|
||||
FileRenderer,
|
||||
getFileType,
|
||||
ERROR_STATUSES,
|
||||
RENDERERS,
|
||||
} from './FileRenderer';
|
||||
|
||||
jest.mock('./FileCard', () => 'FileCard');
|
||||
|
||||
jest.mock('components/FilePreview/BaseRenderers', () => ({
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
TXTRenderer: () => 'TXTRenderer',
|
||||
}));
|
||||
|
||||
jest.mock('./Banners', () => ({
|
||||
ErrorBanner: () => 'ErrorBanner',
|
||||
LoadingBanner: () => 'LoadingBanner',
|
||||
}));
|
||||
|
||||
describe('FileRenderer', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = Object.keys(RENDERERS);
|
||||
const files = [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
name: `fake_file_${index}.${fileType}`,
|
||||
description: `file description ${index}`,
|
||||
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
|
||||
})),
|
||||
];
|
||||
|
||||
const els = files.map((file) => {
|
||||
const el = shallow(<FileRenderer file={file} />);
|
||||
el.instance().onError = jest.fn().mockName('this.props.onError');
|
||||
el.instance().onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
return el;
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
els.forEach((el) => {
|
||||
const file = el.prop('file');
|
||||
const fileType = getFileType(file.name);
|
||||
|
||||
test(`successful rendering ${fileType}`, () => {
|
||||
el.setState({ isLoading: false });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
Object.keys(ERROR_STATUSES).forEach((status) => {
|
||||
test(`has error ${status}`, () => {
|
||||
const el = shallow(<FileRenderer file={files[0]} />);
|
||||
el.instance().setState({
|
||||
errorStatus: status,
|
||||
isLoading: false,
|
||||
});
|
||||
el.instance().resetState = jest.fn().mockName('this.resetState');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('uses the correct renderers', () => {
|
||||
const checkFile = (index, expectedRenderer) => {
|
||||
const file = files[index];
|
||||
const el = shallow(<FileRenderer file={file} />);
|
||||
const renderer = el.find(expectedRenderer);
|
||||
const { url, fileName } = renderer.props();
|
||||
|
||||
expect(renderer).toBeDefined();
|
||||
expect(url).toEqual(file.downloadUrl);
|
||||
expect(fileName).toEqual(file.name);
|
||||
};
|
||||
/**
|
||||
* The manual process for this is prefer. I want to be more explicit
|
||||
* of which file correspond to which renderer. If I use RENDERERS dicts,
|
||||
* this wouldn't be a test.
|
||||
*/
|
||||
|
||||
test(FileTypes.pdf, () => checkFile(0, PDFRenderer));
|
||||
test(FileTypes.jpg, () => checkFile(1, ImageRenderer));
|
||||
test(FileTypes.jpeg, () => checkFile(2, ImageRenderer));
|
||||
test(FileTypes.bmp, () => checkFile(3, ImageRenderer));
|
||||
test(FileTypes.png, () => checkFile(4, ImageRenderer));
|
||||
test(FileTypes.txt, () => checkFile(5, TXTRenderer));
|
||||
test(FileTypes.gif, () => checkFile(6, ImageRenderer));
|
||||
test(FileTypes.jfif, () => checkFile(7, ImageRenderer));
|
||||
test(FileTypes.pjpeg, () => checkFile(8, ImageRenderer));
|
||||
test(FileTypes.pjp, () => checkFile(9, ImageRenderer));
|
||||
test(FileTypes.svg, () => checkFile(10, ImageRenderer));
|
||||
});
|
||||
|
||||
test('getter for error', () => {
|
||||
const el = els[0];
|
||||
Object.keys(ERROR_STATUSES).forEach((status) => {
|
||||
el.setState({
|
||||
isLoading: false,
|
||||
errorStatus: status,
|
||||
});
|
||||
const { actions, ...expectedError } = el.instance().error;
|
||||
expect(ERROR_STATUSES[status]).toEqual(expectedError);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderer constraints', () => {
|
||||
els.forEach((el) => {
|
||||
const file = el.prop('file');
|
||||
const fileType = getFileType(file.name);
|
||||
const RendererComponent = RENDERERS[fileType];
|
||||
const ActualRendererComponent = jest.requireActual(
|
||||
'components/FilePreview/BaseRenderers',
|
||||
)[RendererComponent.name];
|
||||
|
||||
test(`${fileType} renderer must have onError and onSuccess props`, () => {
|
||||
/* eslint-disable react/forbid-foreign-prop-types */
|
||||
expect(ActualRendererComponent.propTypes.onError).toBeDefined();
|
||||
expect(ActualRendererComponent.propTypes.onSuccess).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImageRenderer = ({ url, fileName }) => (<img alt={fileName} className="image-renderer" src={url} />);
|
||||
|
||||
ImageRenderer.defaultProps = {
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
ImageRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
@@ -9,7 +9,9 @@ exports[`File Preview Card component snapshot 1`] = `
|
||||
className="file-collapsible"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<h3>
|
||||
<h3
|
||||
className="file-card-title"
|
||||
>
|
||||
test-file-name.pdf
|
||||
</h3>
|
||||
}
|
||||
@@ -19,13 +21,9 @@ exports[`File Preview Card component snapshot 1`] = `
|
||||
>
|
||||
<FileInfo>
|
||||
<FilePopoverContent
|
||||
file={
|
||||
Object {
|
||||
"description": "test-file description",
|
||||
"downloadUrl": "destination/test-file-name.pdf",
|
||||
"name": "test-file-name.pdf",
|
||||
}
|
||||
}
|
||||
description="test-file description"
|
||||
downloadUrl="destination/test-file-name.pdf"
|
||||
name="test-file-name.pdf"
|
||||
/>
|
||||
</FileInfo>
|
||||
<h1>
|
||||
|
||||
@@ -0,0 +1,292 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot has error 404 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "File not found",
|
||||
"description": "File not found error message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.fileNotFound",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File not found"
|
||||
description="File not found error message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.fileNotFound"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot has error 500 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ErrorBanner
|
||||
actions={
|
||||
Array [
|
||||
Object {
|
||||
"id": "retry",
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry",
|
||||
"description": "Retry button for error in file renderer",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.retryButton",
|
||||
},
|
||||
"onClick": [MockFunction this.resetState],
|
||||
},
|
||||
]
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Unknown errors",
|
||||
"description": "Unknown errors message",
|
||||
"id": "ora-grading.ResponseDisplay.FileRenderer.unknownError",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</ErrorBanner>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering bmp 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering gif 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 6",
|
||||
"downloadUrl": "/url-path/fake_file_6.gif",
|
||||
"name": "fake_file_6.gif",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_6.gif"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_6.gif"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jfif 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 7",
|
||||
"downloadUrl": "/url-path/fake_file_7.jfif",
|
||||
"name": "fake_file_7.jfif",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_7.jfif"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_7.jfif"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jpeg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 2",
|
||||
"downloadUrl": "/url-path/fake_file_2.jpeg",
|
||||
"name": "fake_file_2.jpeg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_2.jpeg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_2.jpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering jpg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 1",
|
||||
"downloadUrl": "/url-path/fake_file_1.jpg",
|
||||
"name": "fake_file_1.jpg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_1.jpg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_1.jpg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pdf 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
>
|
||||
<PDFRenderer
|
||||
fileName="fake_file_0.pdf"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_0.pdf"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pjp 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 9",
|
||||
"downloadUrl": "/url-path/fake_file_9.pjp",
|
||||
"name": "fake_file_9.pjp",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_9.pjp"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_9.pjp"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering pjpeg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 8",
|
||||
"downloadUrl": "/url-path/fake_file_8.pjpeg",
|
||||
"name": "fake_file_8.pjpeg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_8.pjpeg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_8.pjpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering png 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.png",
|
||||
"name": "fake_file_4.png",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_4.png"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_4.png"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering svg 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 10",
|
||||
"downloadUrl": "/url-path/fake_file_10.svg",
|
||||
"name": "fake_file_10.svg",
|
||||
}
|
||||
}
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_10.svg"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_10.svg"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot successful rendering txt 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
>
|
||||
<TXTRenderer
|
||||
fileName="fake_file_5.txt"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onSuccess={[MockFunction this.props.onSuccess]}
|
||||
url="/url-path/fake_file_5.txt"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
@@ -1,4 +1 @@
|
||||
export { default as FileCard } from './FileCard';
|
||||
export { default as ImageRenderer } from './ImageRenderer';
|
||||
export { default as PDFRenderer } from './PDFRenderer';
|
||||
export { default as TXTRenderer } from './TXTRenderer';
|
||||
export { default as FileRenderer, isSupported } from './FileRenderer';
|
||||
|
||||
@@ -6,6 +6,21 @@ const messages = defineMessages({
|
||||
defaultMessage: 'File info',
|
||||
description: 'Popover trigger button text for file preview card',
|
||||
},
|
||||
retryButton: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.retryButton',
|
||||
defaultMessage: 'Retry',
|
||||
description: 'Retry button for error in file renderer',
|
||||
},
|
||||
fileNotFoundError: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.fileNotFound',
|
||||
defaultMessage: 'File not found',
|
||||
description: 'File not found error message',
|
||||
},
|
||||
unknownError: {
|
||||
id: 'ora-grading.ResponseDisplay.FileRenderer.unknownError',
|
||||
defaultMessage: 'Unknown errors',
|
||||
description: 'Unknown errors message',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -25,6 +25,15 @@ export class CriterionFeedback extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
get commentMessage() {
|
||||
const { config, isGrading } = this.props;
|
||||
let commentMessage = this.translate(isGrading ? messages.addComments : messages.comments);
|
||||
if (config === feedbackRequirement.optional) {
|
||||
commentMessage += ` ${this.translate(messages.optional)}`;
|
||||
}
|
||||
return commentMessage;
|
||||
}
|
||||
|
||||
translate = (msg) => this.props.intl.formatMessage(msg);
|
||||
|
||||
render() {
|
||||
@@ -40,11 +49,9 @@ export class CriterionFeedback extends React.Component {
|
||||
return (
|
||||
<Form.Group isInvalid={this.feedbackIsInvalid}>
|
||||
<Form.Control
|
||||
as="input"
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
floatingLabel={this.translate(
|
||||
isGrading ? messages.addComments : messages.comments,
|
||||
)}
|
||||
floatingLabel={this.commentMessage}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './CriterionFeedback';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -27,7 +28,9 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
})),
|
||||
},
|
||||
validation: {
|
||||
criterionFeedbackIsInvalid: jest.fn((...args) => ({ selectedFeedbackIsInvalid: args })),
|
||||
criterionFeedbackIsInvalid: jest.fn((...args) => ({
|
||||
selectedFeedbackIsInvalid: args,
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -67,11 +70,13 @@ describe('Criterion Feedback', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('is configure to disabled', () => {
|
||||
el.setProps({
|
||||
config: feedbackRequirement.disabled,
|
||||
Object.values(feedbackRequirement).forEach((requirement) => {
|
||||
test(`feedback is configured to ${requirement}`, () => {
|
||||
el.setProps({
|
||||
config: requirement,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -121,6 +126,40 @@ describe('Criterion Feedback', () => {
|
||||
expect(props.setValue).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getter commentMessage', () => {
|
||||
test('is grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: true });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.addComments.defaultMessage,
|
||||
);
|
||||
});
|
||||
|
||||
test('is not grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: false });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.comments.defaultMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
@@ -142,7 +181,10 @@ describe('Criterion Feedback', () => {
|
||||
});
|
||||
test('selector.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionFeedbackIsInvalid(testState, ownProps),
|
||||
selectors.grading.validation.criterionFeedbackIsInvalid(
|
||||
testState,
|
||||
ownProps,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to disabled 1`] = `null`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to optional 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments (Optional)"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to required 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback value is invalid 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="input"
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
@@ -19,12 +47,10 @@ exports[`Criterion Feedback snapshot feedback value is invalid 1`] = `
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is configure to disabled 1`] = `null`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is graded 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="input"
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
@@ -37,7 +63,7 @@ exports[`Criterion Feedback snapshot is graded 1`] = `
|
||||
exports[`Criterion Feedback snapshot is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="input"
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
|
||||
@@ -11,6 +11,11 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Comments',
|
||||
description: 'label for read-only feedback field',
|
||||
},
|
||||
optional: {
|
||||
id: 'ora-grading.CriterionFeedback.optional',
|
||||
defaultMessage: '(Optional)',
|
||||
description: 'addtional label for optional feedback field',
|
||||
},
|
||||
optionPoints: {
|
||||
id: 'ora-grading.RadioCriterion.optionPoints',
|
||||
defaultMessage: '{points} points',
|
||||
|
||||
36
src/containers/DemoWarning/DemoWarning.test.jsx
Normal file
36
src/containers/DemoWarning/DemoWarning.test.jsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { DemoWarning, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: { isEnabled: (args) => ({ isEnabled: args }) },
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
|
||||
describe('DemoWarning component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('does not render if disabled flag is missing', () => {
|
||||
el = shallow(<DemoWarning hide />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: disabled flag is present', () => {
|
||||
el = shallow(<DemoWarning hide={false} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
test('hide is forwarded from app.isEnabled', () => {
|
||||
expect(mapStateToProps(testState).hide).toEqual(
|
||||
selectors.app.isEnabled(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `""`;
|
||||
|
||||
exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`] = `
|
||||
<Alert
|
||||
className="mb-0 rounded-0"
|
||||
variant="warning"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Demo Mode"
|
||||
description="Demo mode heading"
|
||||
id="ora-grading.ReviewModal.demoHeading"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature."
|
||||
description="Demo mode message"
|
||||
id="ora-grading.ReviewModal.demoMessage"
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
39
src/containers/DemoWarning/index.jsx
Normal file
39
src/containers/DemoWarning/index.jsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <DemoWarning />
|
||||
*/
|
||||
export const DemoWarning = ({ hide }) => {
|
||||
if (hide) { return null; }
|
||||
return (
|
||||
<Alert
|
||||
className="mb-0 rounded-0"
|
||||
variant="warning"
|
||||
icon={Info}
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage {...messages.demoModeHeading} />
|
||||
</Alert.Heading>
|
||||
<p><FormattedMessage {...messages.demoModeMessage} /></p>
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
DemoWarning.propTypes = {
|
||||
hide: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
hide: selectors.app.isEnabled(state),
|
||||
});
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DemoWarning);
|
||||
18
src/containers/DemoWarning/messages.js
Normal file
18
src/containers/DemoWarning/messages.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/* eslint-disable quotes */
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
demoModeHeading: {
|
||||
id: 'ora-grading.ReviewModal.demoHeading',
|
||||
defaultMessage: 'Demo Mode',
|
||||
description: 'Demo mode heading',
|
||||
},
|
||||
demoModeMessage: {
|
||||
id: 'ora-grading.ReviewModal.demoMessage',
|
||||
defaultMessage: 'You are using the Demo Mode of the new Enhanced ORA Staff Grader interface. You will be unable to submit grades until you activate the feature.',
|
||||
description: 'Demo mode message',
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
34
src/containers/ListView/EmptySubmission.jsx
Normal file
34
src/containers/ListView/EmptySubmission.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Button } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import emptyStateSVG from './assets/empty-state.svg';
|
||||
import messages from './messages';
|
||||
|
||||
const EmptySubmission = ({ courseId }) => (
|
||||
<div className="empty-submission">
|
||||
<img src={emptyStateSVG} alt="empty state" />
|
||||
<h3>
|
||||
<FormattedMessage {...messages.noResultsFoundTitle} />
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage {...messages.noResultsFoundBody} />
|
||||
</p>
|
||||
<Hyperlink className="py-4" destination={urls.openResponse(courseId)}>
|
||||
<Button variant="outline-primary">
|
||||
<FormattedMessage {...messages.backToResponses} />
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
);
|
||||
|
||||
EmptySubmission.defaultProps = {
|
||||
};
|
||||
EmptySubmission.propTypes = {
|
||||
courseId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default EmptySubmission;
|
||||
33
src/containers/ListView/EmptySubmission.test.jsx
Normal file
33
src/containers/ListView/EmptySubmission.test.jsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
import EmptySubmission from './EmptySubmission';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `openResponseUrl(${courseId})`,
|
||||
}));
|
||||
|
||||
jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg');
|
||||
|
||||
let el;
|
||||
|
||||
describe('EmptySubmission component', () => {
|
||||
describe('component', () => {
|
||||
const props = { courseId: 'test-course-id' };
|
||||
beforeEach(() => {
|
||||
el = shallow(<EmptySubmission {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('openResponse destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(0).props().destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
});
|
||||
});
|
||||
});
|
||||
67
src/containers/ListView/FilterStatusComponent.jsx
Normal file
67
src/containers/ListView/FilterStatusComponent.jsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DataTableContext } from '@edx/paragon';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
export const filterHooks = () => {
|
||||
const { state, setAllFilters, headers } = React.useContext(DataTableContext);
|
||||
if (!setAllFilters || !state.filters) {
|
||||
return {};
|
||||
}
|
||||
const clearFilters = React.useCallback(() => setAllFilters([]), []);
|
||||
const headerMap = headers.reduce(
|
||||
(obj, cur) => ({ ...obj, [cur.id]: cur.Header }),
|
||||
{},
|
||||
);
|
||||
const filterNames = state.filters.map((filter) => headerMap[filter.id]);
|
||||
return { clearFilters, filterNames };
|
||||
};
|
||||
|
||||
export const FilterStatusComponent = ({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
clearFiltersText,
|
||||
buttonClassName,
|
||||
showFilteredFields,
|
||||
}) => {
|
||||
const hookProps = module.filterHooks();
|
||||
if (hookProps.filterNames === undefined) {
|
||||
return null;
|
||||
}
|
||||
const filterTexts = <p>Filtered by {hookProps.filterNames.join(', ')}</p>;
|
||||
return (
|
||||
<div className={className}>
|
||||
{showFilteredFields && filterTexts}
|
||||
<Button
|
||||
className={buttonClassName}
|
||||
variant={variant}
|
||||
size={size}
|
||||
onClick={hookProps.clearFilters}
|
||||
>
|
||||
{clearFiltersText}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
FilterStatusComponent.defaultProps = {
|
||||
className: '',
|
||||
buttonClassName: 'pgn__smart-status-button',
|
||||
variant: 'link',
|
||||
size: 'inline',
|
||||
clearFiltersText: 'Clear Filters',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
|
||||
FilterStatusComponent.propTypes = {
|
||||
className: PropTypes.string,
|
||||
buttonClassName: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
size: PropTypes.string,
|
||||
clearFiltersText: PropTypes.string,
|
||||
showFilteredFields: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default FilterStatusComponent;
|
||||
91
src/containers/ListView/FilterStatusComponent.test.jsx
Normal file
91
src/containers/ListView/FilterStatusComponent.test.jsx
Normal file
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
const fieldIds = [
|
||||
'field-id-0',
|
||||
'field-id-1',
|
||||
'field-id-2',
|
||||
'field-id-3',
|
||||
];
|
||||
const filterOrder = [1, 0, 3, 2];
|
||||
const filters = filterOrder.map(v => ({ id: fieldIds[v] }));
|
||||
const headers = [0, 1, 2, 3].map(v => ({
|
||||
id: fieldIds[v],
|
||||
Header: `HeaDer-${v}`,
|
||||
}));
|
||||
|
||||
describe('FilterStatusComponent hooks', () => {
|
||||
const context = { headers, state: { filters } };
|
||||
const mockTableContext = (newContext) => {
|
||||
React.useContext.mockReturnValueOnce(newContext);
|
||||
};
|
||||
beforeEach(() => {
|
||||
context.setAllFilters = jest.fn();
|
||||
});
|
||||
it('returns empty dict if setAllFilters or state.filters is falsey', () => {
|
||||
mockTableContext({ ...context, setAllFilters: null });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
mockTableContext({ ...context, state: { filters: null } });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
});
|
||||
describe('clearFilters', () => {
|
||||
it('uses React.useCallback to clear filters, only once', () => {
|
||||
mockTableContext(context);
|
||||
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
|
||||
expect(prereqs).toEqual([]);
|
||||
expect(context.setAllFilters).not.toHaveBeenCalled();
|
||||
cb();
|
||||
expect(context.setAllFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
describe('filterNames', () => {
|
||||
it('returns list of Header values by filter order', () => {
|
||||
mockTableContext(context);
|
||||
expect(module.filterHooks().filterNames).toEqual(
|
||||
filterOrder.map(v => headers[v].Header),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('FilterStatusComponent component', () => {
|
||||
const props = {
|
||||
className: 'css-class-name',
|
||||
variant: 'button-variant',
|
||||
size: 'button-size',
|
||||
clearFiltersText: 'clear-filter-text',
|
||||
buttonClassName: 'css-class-name-for-button',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
const hookProps = {
|
||||
clearFilters: jest.fn().mockName('hookProps.clearFilters'),
|
||||
filterNames: ['filter-name-0', 'filter-name-1'],
|
||||
};
|
||||
const { FilterStatusComponent } = module;
|
||||
const mockHooks = (value) => {
|
||||
jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
describe('with filters', () => {
|
||||
test('showFilteredFields', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('showFilteredFields=false - hide filterTexts', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(
|
||||
<FilterStatusComponent {...props} showFilteredFields={false} />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
test('without filters', () => {
|
||||
mockHooks({});
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -24,7 +24,9 @@ export const ListError = ({ courseId, initializeApp }) => (
|
||||
variant="danger"
|
||||
icon={Info}
|
||||
actions={[
|
||||
<Button onClick={initializeApp}>Reload Submissions</Button>,
|
||||
<Button onClick={initializeApp}>
|
||||
<FormattedMessage {...messages.reloadSubmissions} />
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Alert.Heading>
|
||||
|
||||
@@ -1,4 +1,21 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
span.pgn__icon.breadcrumb-arrow {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
};
|
||||
|
||||
.empty-submission {
|
||||
width: map-get($container-max-widths, "sm");
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 75vh;
|
||||
margin: auto;
|
||||
|
||||
> img {
|
||||
padding: map-get($spacers, 5);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,9 +20,9 @@ export const ListViewBreadcrumb = ({ courseId, oraName }) => (
|
||||
<Icon src={ArrowBack} className="d-inline-block mr-3 breadcrumb-arrow" />
|
||||
<FormattedMessage {...messages.backToResponses} />
|
||||
</Hyperlink>
|
||||
<p className="h3 py-4">
|
||||
{oraName}
|
||||
<Hyperlink destination={urls.ora(courseId, locationId)}>
|
||||
<p className="py-4">
|
||||
<span className="h3">{oraName}</span>
|
||||
<Hyperlink className="align-middle" destination={urls.ora(courseId, locationId)}>
|
||||
<Icon src={Launch} className="d-inline-block" />
|
||||
</Hyperlink>
|
||||
</p>
|
||||
|
||||
@@ -23,9 +23,6 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/constants/app', () => ({
|
||||
locationId: 'fake-location-id',
|
||||
}));
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `openResponseUrl(${courseId})`,
|
||||
ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`,
|
||||
|
||||
@@ -15,6 +15,7 @@ import lmsMessages from 'data/services/lms/messages';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
|
||||
import StatusBadge from 'components/StatusBadge';
|
||||
import FilterStatusComponent from './FilterStatusComponent';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -89,6 +90,7 @@ export class SubmissionsTable extends React.Component {
|
||||
return (
|
||||
<DataTable
|
||||
isFilterable
|
||||
FilterStatusComponent={FilterStatusComponent}
|
||||
numBreakoutFilters={2}
|
||||
defaultColumnValues={{ Filter: TextFilter }}
|
||||
isSelectable
|
||||
@@ -137,7 +139,6 @@ export class SubmissionsTable extends React.Component {
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={this.translate(messages.noResultsFound)} />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
);
|
||||
@@ -153,7 +154,7 @@ SubmissionsTable.propTypes = {
|
||||
isIndividual: PropTypes.bool.isRequired,
|
||||
listData: PropTypes.arrayOf(PropTypes.shape({
|
||||
username: PropTypes.string,
|
||||
dateSubmitted: PropTypes.number,
|
||||
dateSubmitted: PropTypes.string,
|
||||
gradingStatus: PropTypes.string,
|
||||
score: PropTypes.shape({
|
||||
pointsEarned: PropTypes.number,
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
mapDispatchToProps,
|
||||
} from './SubmissionsTable';
|
||||
|
||||
jest.mock('./FilterStatusComponent', () => jest.fn().mockName('FilterStatusComponent'));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
@@ -40,10 +42,16 @@ jest.mock('data/redux', () => ({
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const dates = [
|
||||
new Date(16131215154955).toLocaleTimeString(),
|
||||
new Date(16131225154955).toLocaleTimeString(),
|
||||
new Date(16131215250955).toLocaleTimeString(),
|
||||
];
|
||||
|
||||
const individualData = [
|
||||
{
|
||||
username: 'username-1',
|
||||
dateSubmitted: 16131215154955,
|
||||
dateSubmitted: dates[0],
|
||||
gradingStatus: statuses.ungraded,
|
||||
score: {
|
||||
pointsEarned: 1,
|
||||
@@ -52,7 +60,7 @@ const individualData = [
|
||||
},
|
||||
{
|
||||
username: 'username-2',
|
||||
dateSubmitted: 16131225154955,
|
||||
dateSubmitted: dates[1],
|
||||
gradingStatus: statuses.graded,
|
||||
score: {
|
||||
pointsEarned: 2,
|
||||
@@ -61,7 +69,7 @@ const individualData = [
|
||||
},
|
||||
{
|
||||
username: 'username-3',
|
||||
dateSubmitted: 16131215250955,
|
||||
dateSubmitted: dates[2],
|
||||
gradingStatus: statuses.inProgress,
|
||||
score: {
|
||||
pointsEarned: 3,
|
||||
@@ -73,7 +81,7 @@ const individualData = [
|
||||
const teamData = [
|
||||
{
|
||||
teamName: 'teamName-1',
|
||||
dateSubmitted: 16131215154955,
|
||||
dateSubmitted: dates[0],
|
||||
gradingStatus: statuses.ungraded,
|
||||
score: {
|
||||
pointsEarned: 1,
|
||||
@@ -82,7 +90,7 @@ const teamData = [
|
||||
},
|
||||
{
|
||||
teamName: 'teamName-2',
|
||||
dateSubmitted: 16131225154955,
|
||||
dateSubmitted: dates[1],
|
||||
gradingStatus: statuses.graded,
|
||||
score: {
|
||||
pointsEarned: 2,
|
||||
@@ -91,7 +99,7 @@ const teamData = [
|
||||
},
|
||||
{
|
||||
teamName: 'teamName-3',
|
||||
dateSubmitted: 16131215250955,
|
||||
dateSubmitted: dates[2],
|
||||
gradingStatus: statuses.inProgress,
|
||||
score: {
|
||||
pointsEarned: 3,
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptySubmission component component snapshot 1`] = `
|
||||
<div
|
||||
className="empty-submission"
|
||||
>
|
||||
<img
|
||||
alt="empty state"
|
||||
src="./assets/emptyState.svg"
|
||||
/>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Nothing here yet"
|
||||
description="Empty table for the submission table title"
|
||||
id="ora-grading.ListView.noResultsFoundTitle"
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="When learners submit responses, they will appear here"
|
||||
description="Empty table messages"
|
||||
id="ora-grading.ListView.noResultsFoundBody"
|
||||
/>
|
||||
</p>
|
||||
<Hyperlink
|
||||
className="py-4"
|
||||
destination="openResponseUrl(test-course-id)"
|
||||
>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to all open responses"
|
||||
description="Breadcrumbs link text to return to ORA list in LMS"
|
||||
id="ora-grading.ListView.ListViewBreadcrumbs.backToResponses"
|
||||
/>
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterStatusComponent component snapshot with filters showFilteredFields 1`] = `
|
||||
<div
|
||||
className="css-class-name"
|
||||
>
|
||||
<p>
|
||||
Filtered by
|
||||
filter-name-0, filter-name-1
|
||||
</p>
|
||||
<Button
|
||||
className="css-class-name-for-button"
|
||||
onClick={[MockFunction hookProps.clearFilters]}
|
||||
size="button-size"
|
||||
variant="button-variant"
|
||||
>
|
||||
clear-filter-text
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterStatusComponent component snapshot with filters showFilteredFields=false - hide filterTexts 1`] = `
|
||||
<div
|
||||
className="css-class-name"
|
||||
>
|
||||
<Button
|
||||
className="css-class-name-for-button"
|
||||
onClick={[MockFunction hookProps.clearFilters]}
|
||||
size="button-size"
|
||||
variant="button-variant"
|
||||
>
|
||||
clear-filter-text
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterStatusComponent component snapshot without filters 1`] = `""`;
|
||||
@@ -7,7 +7,11 @@ exports[`ListError component component render tests snapshot 1`] = `
|
||||
<Button
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
Reload Submissions
|
||||
<FormattedMessage
|
||||
defaultMessage="Reload submissions"
|
||||
description="Reload button text in case of network failure"
|
||||
id="ora-grading.ListView.reloadSubmissions"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
|
||||
@@ -12,15 +12,20 @@ exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1
|
||||
/>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to all open responses"
|
||||
description="Breadcrumbs link text to return to ORA list in LMS."
|
||||
description="Breadcrumbs link text to return to ORA list in LMS"
|
||||
id="ora-grading.ListView.ListViewBreadcrumbs.backToResponses"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<p
|
||||
className="h3 py-4"
|
||||
className="py-4"
|
||||
>
|
||||
fake-ora-name
|
||||
<span
|
||||
className="h3"
|
||||
>
|
||||
fake-ora-name
|
||||
</span>
|
||||
<Hyperlink
|
||||
className="align-middle"
|
||||
destination="oraUrl(test-course-id, fake-location-id)"
|
||||
>
|
||||
<Icon
|
||||
|
||||
@@ -4,6 +4,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: e
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: happy path 1`] = `
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
[MockFunction this.selectedBulkAction],
|
||||
@@ -57,7 +58,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": 16131215154955,
|
||||
"dateSubmitted": "9:05:54 PM",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
@@ -66,7 +67,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
|
||||
"username": "username-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": 16131225154955,
|
||||
"dateSubmitted": "11:52:34 PM",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
@@ -75,7 +76,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
|
||||
"username": "username-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": 16131215250955,
|
||||
"dateSubmitted": "9:07:30 PM",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
@@ -115,15 +116,13 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: h
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content="No results found"
|
||||
/>
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
`;
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = `
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
Array [
|
||||
[MockFunction this.selectedBulkAction],
|
||||
@@ -177,7 +176,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: t
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"dateSubmitted": 16131215154955,
|
||||
"dateSubmitted": "9:05:54 PM",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": Object {
|
||||
"pointsEarned": 1,
|
||||
@@ -186,7 +185,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: t
|
||||
"teamName": "teamName-1",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": 16131225154955,
|
||||
"dateSubmitted": "11:52:34 PM",
|
||||
"gradingStatus": "graded",
|
||||
"score": Object {
|
||||
"pointsEarned": 2,
|
||||
@@ -195,7 +194,7 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: t
|
||||
"teamName": "teamName-2",
|
||||
},
|
||||
Object {
|
||||
"dateSubmitted": 16131215250955,
|
||||
"dateSubmitted": "9:07:30 PM",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": Object {
|
||||
"pointsEarned": 3,
|
||||
@@ -235,9 +234,6 @@ exports[`SubmissionsTable component component render tests snapshots snapshot: t
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable
|
||||
content="No results found"
|
||||
/>
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
`;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListView component component render tests snapshots snapshot: error 1`] = `
|
||||
exports[`ListView component component snapshots error 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
@@ -9,17 +9,30 @@ exports[`ListView component component render tests snapshots snapshot: error 1`]
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component render tests snapshots snapshot: loaded 1`] = `
|
||||
exports[`ListView component component snapshots loaded has data 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<ListViewBreadcrumb />
|
||||
<SubmissionsTable />
|
||||
<React.Fragment>
|
||||
<ListViewBreadcrumb />
|
||||
<SubmissionsTable />
|
||||
</React.Fragment>
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component render tests snapshots snapshot: loading 1`] = `
|
||||
exports[`ListView component component snapshots loaded with no data 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<EmptySubmission
|
||||
courseId="test-course-id"
|
||||
/>
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component snapshots loading 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
|
||||
44
src/containers/ListView/assets/empty-state.svg
Normal file
44
src/containers/ListView/assets/empty-state.svg
Normal file
@@ -0,0 +1,44 @@
|
||||
<svg width="247" height="206" viewBox="0 0 247 206" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.4664 82.3668C2.4299 103.195 -4.81519 127.522 5.05784 149.841C23.0467 190.506 121.073 179.077 163.76 163.108C283.987 118.132 226.175 0.117809 96.1636 29.2994C59.7626 37.4697 30.503 61.5381 16.4664 82.3668Z" fill="#E1DDDB" fill-opacity="0.3"/>
|
||||
<path d="M96.1307 25.1587C101.602 15.0087 110.432 7.97478 123.536 12.1972C124.691 12.6616 125.903 12.9722 127.139 13.1208C128.395 13.0472 129.646 12.898 130.884 12.6742C136.792 12.187 141.156 17.9116 144.029 23.0881C149.367 32.7408 159.122 45.8444 157.224 57.4459C155.752 66.4388 147.297 72.6709 138.761 75.8174C129.136 79.3674 118.744 80.3162 108.636 78.568C103.561 77.6951 98.313 75.9899 94.659 72.3055C89.584 67.1696 88.569 59.2627 88.9547 52.0055C89.4926 43.8956 91.6749 33.4716 96.1307 25.1587Z" fill="white"/>
|
||||
<path d="M118.491 79.9181C115.16 79.9178 111.836 79.6326 108.554 79.0655C102.149 77.9693 97.5006 75.8784 94.3439 72.671C89.9084 68.2151 87.9393 61.3131 88.4772 52.0563C89.0558 42.4544 91.7557 32.3146 95.7142 24.9457C100.353 16.3182 108.96 7.02078 123.637 11.7507L124.337 11.9943C125.23 12.3406 126.165 12.5626 127.118 12.654C127.848 12.6579 128.577 12.5831 129.29 12.4307C129.778 12.3495 130.305 12.2582 130.803 12.2176C135.665 11.8014 140.252 15.3945 144.394 22.8649C145.348 24.5904 146.424 26.4174 147.591 28.3561C152.778 37.0343 159.233 47.8339 157.639 57.5272C156.371 65.3731 149.377 72.3766 138.892 76.2539C132.364 78.6799 125.455 79.9207 118.491 79.9181ZM96.5972 25.3821C92.6996 32.6394 90.0302 42.6371 89.4922 52.1071C88.9543 61.0594 90.832 67.7584 95.0544 72.0214C98.0994 75.0664 102.576 77.0964 108.777 78.1114C118.8 79.8365 129.102 78.8841 138.639 75.3506C148.789 71.5849 155.579 64.8453 156.807 57.3343C158.34 47.9862 151.986 37.3388 146.88 28.7824C145.723 26.8437 144.627 25.0167 143.663 23.2811C139.714 16.1761 135.441 12.7352 130.965 13.1311C130.488 13.1311 130.021 13.2529 129.534 13.3341C128.718 13.5156 127.884 13.6075 127.047 13.6081C126.003 13.516 124.977 13.2733 124.002 12.8875L123.312 12.654C109.244 8.11698 100.982 17.0896 96.4957 25.4024L96.5972 25.3821Z" fill="#454545"/>
|
||||
<path d="M183.345 169.411C183.345 169.411 179.096 95.8131 168.357 86.059C152.909 72.0926 98.0788 69.1795 86.9443 72.052C67.2533 77.127 53.5508 107.881 53.5508 107.881L86.2135 117.646C86.2845 142.909 88.8119 169.411 88.8119 169.411H183.345Z" fill="#03C7E8"/>
|
||||
<path d="M84.7307 147.72C84.6627 147.715 84.5963 147.696 84.5354 147.665C84.4746 147.635 84.4204 147.592 84.3761 147.54C84.3317 147.488 84.2981 147.428 84.2771 147.363C84.2561 147.298 84.2481 147.23 84.2537 147.162L88.6689 89.3068C88.6741 89.2374 88.6928 89.1697 88.7242 89.1076C88.7556 89.0455 88.799 88.9902 88.8518 88.9449C88.9046 88.8997 88.9659 88.8653 89.032 88.8438C89.0982 88.8222 89.168 88.814 89.2373 88.8195C89.3063 88.8246 89.3736 88.8435 89.4352 88.875C89.4967 88.9065 89.5514 88.95 89.5959 89.003C89.6403 89.056 89.6737 89.1174 89.6941 89.1835C89.7144 89.2496 89.7213 89.3191 89.7144 89.388L85.2991 147.243C85.294 147.312 85.2752 147.379 85.2437 147.441C85.2122 147.502 85.1686 147.557 85.1156 147.601C85.0627 147.646 85.0013 147.679 84.9352 147.7C84.8691 147.72 84.7995 147.727 84.7307 147.72Z" fill="#454545"/>
|
||||
<path d="M114.663 92.3823C118.328 96.9397 123.798 100.188 129.696 100.279C130.459 100.321 131.224 100.218 131.949 99.9745C132.942 99.5501 133.788 98.8444 134.385 97.9445C137.044 94.2905 137.156 89.3678 136.486 84.9018C136.009 81.7654 135.095 78.4768 132.72 76.3758C129.929 73.9093 125.879 73.6759 122.154 73.5744C118.429 73.4729 109.791 72.3868 108.411 76.9746C107.122 81.1666 112.207 89.3272 114.663 92.3823Z" fill="#454545"/>
|
||||
<path d="M129.92 100.726H129.687C124.053 100.634 118.309 97.6198 114.32 92.6666C111.67 89.378 106.646 81.1971 107.976 76.8428C109.305 72.4884 116.4 72.7828 120.623 73.0467L122.176 73.1177C126.286 73.2395 130.204 73.544 133.016 76.0308C135.827 78.5175 136.528 82.1918 136.924 84.8308C137.786 90.5351 137.056 94.9808 134.752 98.1984C134.111 99.177 133.192 99.9409 132.113 100.391C131.408 100.636 130.665 100.749 129.92 100.726ZM117.162 73.8282C113.589 73.8282 109.681 74.3459 108.839 77.1067C107.61 81.0753 112.564 89.0634 115.01 92.1084C118.836 96.8586 124.327 99.7412 129.697 99.8325C130.408 99.8697 131.12 99.7768 131.798 99.5585C132.711 99.1619 133.488 98.5053 134.031 97.6706C136.183 94.7068 136.863 90.4235 136.061 84.9628C135.685 82.4557 134.904 78.8728 132.447 76.7007C129.991 74.5286 126.104 74.1226 122.166 74.0109C121.709 74.0109 121.15 74.0109 120.602 73.9399C119.567 73.8891 118.38 73.8282 117.162 73.8282Z" fill="#454545"/>
|
||||
<path d="M138.374 45.4691C138.749 44.204 139.385 43.0318 140.242 42.0283C140.681 41.5344 141.261 41.1877 141.904 41.0352C142.547 40.8826 143.221 40.9317 143.835 41.1757C144.262 41.4299 144.62 41.7849 144.879 42.2095C145.137 42.6341 145.287 43.1155 145.317 43.6117C145.367 44.6098 145.201 45.607 144.83 46.5349C144.265 48.2993 143.287 49.9035 141.977 51.214C141.327 51.8652 140.55 52.3766 139.695 52.7169C138.84 53.0572 137.924 53.2193 137.004 53.1933" fill="white"/>
|
||||
<path d="M137.39 53.5893H136.974C136.867 53.579 136.768 53.5278 136.698 53.4465C136.628 53.3652 136.593 53.26 136.598 53.1529C136.601 53.099 136.614 53.0462 136.638 52.9977C136.661 52.9493 136.695 52.9061 136.735 52.871C136.776 52.8358 136.824 52.8094 136.875 52.7933C136.927 52.7771 136.981 52.7717 137.035 52.7773C137.903 52.8069 138.768 52.6583 139.576 52.3405C140.384 52.0228 141.119 51.5427 141.734 50.93C142.996 49.6653 143.936 48.1164 144.475 46.4133C144.829 45.5414 144.988 44.6026 144.942 43.6626C144.917 43.2304 144.787 42.8108 144.562 42.4409C144.337 42.071 144.025 41.762 143.652 41.5413C143.115 41.3386 142.528 41.3038 141.97 41.4415C141.411 41.5792 140.908 41.883 140.526 42.3127C139.718 43.2852 139.114 44.4105 138.75 45.6216C138.732 45.6713 138.704 45.717 138.668 45.7559C138.632 45.7947 138.589 45.8259 138.541 45.8477C138.492 45.8695 138.44 45.8813 138.387 45.8826C138.334 45.8838 138.282 45.8745 138.232 45.855C138.132 45.8183 138.05 45.7433 138.004 45.6464C137.958 45.5494 137.953 45.4383 137.989 45.3374C138.386 44.0151 139.057 42.791 139.958 41.7443C140.458 41.1916 141.115 40.8063 141.842 40.6411C142.569 40.4758 143.328 40.5385 144.018 40.8206C144.512 41.1012 144.929 41.5005 145.23 41.9826C145.531 42.4646 145.708 43.0141 145.743 43.5814C145.799 44.6213 145.626 45.6609 145.236 46.6264C144.66 48.4708 143.643 50.1468 142.272 51.5086C140.968 52.7971 139.223 53.541 137.39 53.5893Z" fill="#454545"/>
|
||||
<path d="M131.402 63.0693L132.975 78.7206C132.975 78.7206 132.214 87.6222 122.429 85.8256C119.555 85.2707 116.84 84.0853 114.479 82.3547C112.119 80.6241 110.171 78.3915 108.777 75.8177L109.143 47.7327C109.082 46.5857 111.467 42.6577 110.34 32.731C113.695 32.3879 116.941 31.3497 119.871 29.6825C122.802 28.0153 125.353 25.7557 127.362 23.0479C127.362 23.0479 131.899 28.6507 133.807 30.3863C135.339 31.6684 136.703 33.1382 137.867 34.761C140.912 39.3285 140.78 46.7785 140.049 52.0159C139.339 56.3195 136.842 63.181 131.402 63.0693Z" fill="white"/>
|
||||
<path d="M124.875 86.5565C124.027 86.5508 123.181 86.4693 122.347 86.3129C119.393 85.7364 116.602 84.5138 114.176 82.7328C111.749 80.9518 109.746 78.6563 108.31 76.0106C108.295 75.9438 108.295 75.8745 108.31 75.8076C108.31 75.8076 108.675 47.9053 108.675 47.7327C108.718 47.2308 108.823 46.7363 108.99 46.261C109.558 44.3122 110.726 40.2522 109.883 32.7818C109.877 32.722 109.882 32.6615 109.9 32.6039C109.917 32.5464 109.946 32.4929 109.985 32.4468C110.019 32.3972 110.065 32.3559 110.117 32.326C110.17 32.2962 110.229 32.2785 110.289 32.2743C113.572 31.929 116.748 30.9087 119.618 29.2772C122.488 27.6458 124.99 25.4386 126.966 22.7942C127.007 22.7348 127.062 22.6857 127.125 22.6505C127.188 22.6153 127.259 22.595 127.331 22.5912C127.404 22.5884 127.477 22.6025 127.544 22.6324C127.611 22.6623 127.67 22.7073 127.717 22.7637C127.717 22.7637 132.254 28.3564 134.121 30.0413C135.658 31.353 137.022 32.8535 138.181 34.5073C141.693 39.8259 140.922 48.5447 140.435 52.0465C139.846 56.2384 137.39 63.2115 131.939 63.5261L133.452 78.6801C133.209 81.1139 132.021 83.3552 130.143 84.9223C128.621 86.0456 126.765 86.6215 124.875 86.5565ZM109.193 75.7061C110.572 78.1968 112.48 80.3553 114.782 82.0299C117.085 83.7045 119.726 84.8546 122.52 85.3994C125.443 85.9272 127.808 85.5212 129.554 84.1915C131.198 82.7922 132.253 80.8238 132.508 78.6801L130.944 63.1201C130.939 63.0541 130.947 62.9877 130.968 62.9249C130.989 62.862 131.022 62.8041 131.066 62.7547C131.111 62.7091 131.165 62.6729 131.224 62.6485C131.284 62.6241 131.347 62.6119 131.411 62.6126H131.533C136.608 62.6126 138.933 55.9035 139.491 51.9145C139.978 48.5143 140.729 40.0796 137.39 35.0249C136.257 33.432 134.931 31.9863 133.441 30.7213C131.297 28.5208 129.264 26.2138 127.351 23.8092C123.266 28.9595 117.328 32.3074 110.807 33.137C111.568 40.4958 110.401 44.5456 109.792 46.5147C109.659 46.9033 109.567 47.3047 109.518 47.7124C109.518 47.7124 109.558 47.7429 109.193 75.7061Z" fill="#454545"/>
|
||||
<path d="M131.938 68.2154C129.368 68.6651 126.723 68.3837 124.305 67.4034C118.733 65.4444 118.408 61.6382 118.408 61.6382C118.408 61.6382 123.097 64.5817 131.4 63.0389L131.938 68.2154Z" fill="#454545"/>
|
||||
<path d="M129.676 68.8751C127.792 68.8432 125.927 68.5002 124.155 67.8601C118.379 65.8301 117.983 61.8817 117.963 61.7701C117.955 61.6862 117.971 61.6017 118.008 61.5263C118.046 61.451 118.105 61.3879 118.177 61.3444C118.249 61.3009 118.332 61.2787 118.416 61.2804C118.501 61.282 118.583 61.3076 118.653 61.3539C118.653 61.3539 123.312 64.1858 131.32 62.6937C131.445 62.6727 131.573 62.7019 131.676 62.7749C131.726 62.8112 131.768 62.8583 131.798 62.9127C131.827 62.9671 131.845 63.0276 131.848 63.0896L132.376 68.1646C132.398 68.2488 132.395 68.3375 132.368 68.4202C132.341 68.5029 132.291 68.5762 132.224 68.6315C132.18 68.6716 132.129 68.7022 132.073 68.7215C132.017 68.7407 131.958 68.7481 131.899 68.7431C131.164 68.8583 130.42 68.9025 129.676 68.8751ZM119.09 62.5212C119.536 63.6884 120.815 65.7286 124.459 67.0075C126.686 67.8572 129.089 68.1397 131.452 67.8296L131.016 63.5971C124.906 64.6222 120.815 63.3433 119.09 62.5212Z" fill="#454545"/>
|
||||
<path d="M124.561 37.7349C124.561 37.7349 130.346 44.7485 129.28 46.1797C128.215 47.6108 124.784 47.0729 124.784 47.0729" fill="white"/>
|
||||
<path d="M126.214 47.6208C125.711 47.6202 125.21 47.5863 124.712 47.5193C124.606 47.4869 124.517 47.4177 124.458 47.3243C124.4 47.2309 124.378 47.1197 124.395 47.011C124.412 46.9024 124.468 46.8036 124.552 46.7329C124.637 46.6622 124.744 46.6243 124.854 46.6261C125.737 46.7682 128.183 46.9002 128.914 45.9055C129.37 45.2965 127.168 41.612 124.194 38.0189C124.157 37.9736 124.128 37.9214 124.111 37.8652C124.094 37.8091 124.088 37.7501 124.094 37.6916C124.1 37.6332 124.117 37.5764 124.144 37.5246C124.172 37.4728 124.209 37.4269 124.255 37.3896C124.349 37.316 124.468 37.2813 124.587 37.2926C124.706 37.304 124.816 37.3605 124.894 37.4505C126.701 39.6429 130.781 44.9006 129.634 46.4434C128.914 47.4178 127.381 47.6208 126.214 47.6208Z" fill="#454545"/>
|
||||
<path d="M131.802 39.3453C132.274 39.2821 132.56 38.5057 132.441 37.6111C132.321 36.7166 131.841 36.0426 131.369 36.1058C130.897 36.169 130.611 36.9454 130.73 37.8399C130.85 38.7345 131.33 39.4084 131.802 39.3453Z" fill="#454545"/>
|
||||
<path d="M117.567 39.3654C118.043 39.3598 118.421 38.6237 118.411 37.7213C118.4 36.8188 118.005 36.0918 117.529 36.0973C117.052 36.1029 116.675 36.839 116.685 37.7415C116.696 38.6439 117.09 39.371 117.567 39.3654Z" fill="#454545"/>
|
||||
<path d="M112.298 43.5303C111.949 42.1904 111.353 40.9273 110.542 39.8052C110.075 39.1684 109.464 38.652 108.758 38.2989C108.051 37.9458 107.271 37.7663 106.482 37.7752C105.994 37.7942 105.516 37.9238 105.086 38.1541C104.655 38.3844 104.282 38.7095 103.995 39.1049C103.653 39.6745 103.438 40.3117 103.366 40.9725C102.848 44.4539 104.015 48.991 107.294 50.7875C107.847 51.1353 108.488 51.3199 109.141 51.3199C109.795 51.3199 110.435 51.1353 110.988 50.7875" fill="white"/>
|
||||
<path d="M109.365 51.7216C108.572 51.7034 107.796 51.4945 107.101 51.1126C103.671 49.2247 102.432 44.5455 102.97 40.8814C103.047 40.1619 103.283 39.4685 103.661 38.8514C103.981 38.404 104.398 38.0353 104.882 37.7732C105.365 37.5112 105.902 37.3627 106.452 37.339C107.31 37.3329 108.157 37.5313 108.923 37.9179C109.689 38.3044 110.352 38.868 110.857 39.5619C111.703 40.7214 112.323 42.0299 112.684 43.4189C112.712 43.5211 112.7 43.6304 112.648 43.7234C112.597 43.8164 112.512 43.8856 112.41 43.9162C112.36 43.9333 112.307 43.94 112.254 43.9359C112.202 43.9317 112.151 43.9168 112.104 43.8921C112.057 43.8673 112.016 43.8333 111.983 43.792C111.95 43.7508 111.926 43.7032 111.913 43.6523C111.577 42.361 111.006 41.1428 110.228 40.0592C109.806 39.4648 109.247 38.981 108.599 38.6488C107.95 38.3165 107.231 38.1458 106.503 38.151C106.078 38.1668 105.662 38.2782 105.286 38.4767C104.91 38.6753 104.583 38.956 104.33 39.298C104.02 39.8176 103.829 40.4002 103.772 41.0032C103.265 44.3831 104.381 48.6969 107.487 50.4021C107.966 50.7209 108.525 50.8977 109.099 50.912C109.674 50.9264 110.241 50.7778 110.735 50.4833C110.821 50.4201 110.928 50.3922 111.034 50.4054C111.139 50.4186 111.236 50.4719 111.304 50.5543C111.369 50.6392 111.399 50.7467 111.385 50.8532C111.372 50.9597 111.317 51.0566 111.232 51.1227C110.693 51.5228 110.036 51.7334 109.365 51.7216Z" fill="#454545"/>
|
||||
<path d="M166.529 163.625C166.406 163.625 166.286 163.581 166.191 163.501C166.097 163.421 166.033 163.311 166.012 163.189C162.53 141.458 155.862 100.036 155.557 98.777C155.523 98.6424 155.543 98.4999 155.614 98.3802C155.684 98.2605 155.799 98.1733 155.933 98.1375C156.068 98.1065 156.211 98.1294 156.33 98.2013C156.45 98.2733 156.536 98.3887 156.572 98.5232C156.968 100.137 166.631 160.438 167.047 163.016C167.067 163.153 167.033 163.293 166.951 163.405C166.87 163.517 166.747 163.593 166.61 163.615L166.529 163.625Z" fill="#454545"/>
|
||||
<path d="M103.612 186.504L103.477 186.316H103.247H55.5401C50.5527 186.316 46.4305 181.8 46.4305 176.115V118.078C46.4305 112.399 50.5532 107.869 55.5401 107.869H181.516C186.503 107.869 190.634 112.4 190.665 118.08V176.115C190.665 181.8 186.537 186.316 181.549 186.316H132.429H132.226L132.092 186.468L116.369 204.271L103.612 186.504Z" fill="white" stroke="#454545" stroke-width="0.9"/>
|
||||
<path d="M126.986 56.6951C119.485 56.6951 117.09 46.7989 116.988 46.3726C116.964 46.2556 116.986 46.1337 117.051 46.0332C117.115 45.9326 117.217 45.8613 117.333 45.8346C117.39 45.8211 117.45 45.819 117.508 45.8285C117.566 45.8381 117.622 45.859 117.672 45.8901C117.722 45.9213 117.765 45.962 117.799 46.0099C117.834 46.0579 117.858 46.1121 117.871 46.1696C117.871 46.2609 120.195 55.8019 127.006 55.8019C127.712 55.8292 128.416 55.7005 129.067 55.4251C129.717 55.1497 130.3 54.7342 130.772 54.2084C133.116 51.5389 132.487 46.3624 132.477 46.3117C132.468 46.2529 132.472 46.1931 132.486 46.1356C132.501 46.0782 132.527 46.0242 132.563 45.977C132.599 45.9297 132.644 45.8901 132.696 45.8604C132.747 45.8308 132.804 45.8116 132.863 45.8042C132.98 45.7922 133.098 45.8253 133.192 45.8969C133.287 45.9685 133.35 46.0732 133.37 46.1899C133.37 46.4233 134.07 51.8231 131.452 54.7971C130.895 55.4246 130.205 55.9204 129.432 56.2486C128.66 56.5769 127.824 56.7294 126.986 56.6951Z" fill="#454545"/>
|
||||
<path d="M70.6776 104.836C74.2642 103.038 78.6204 101.574 82.1648 103.478C83.846 104.527 85.2636 105.948 86.3083 107.632C88.5674 110.797 90.6087 114.112 92.418 117.554C92.8918 118.509 93.2959 119.679 92.5586 120.366C91.6605 121.211 90.2382 120.299 89.4487 119.361C88.0241 117.689 86.83 115.834 85.8986 113.844C87.781 116.597 89.274 119.597 90.3353 122.758C90.5182 123.131 90.5822 123.552 90.5186 123.962C90.3727 124.327 90.0885 124.62 89.7279 124.777C89.3672 124.934 88.9592 124.942 88.5926 124.799C87.8502 124.518 87.205 124.028 86.7352 123.388C84.6643 120.9 82.8746 118.191 81.3988 115.309C82.8833 118.317 84.38 121.406 84.7435 124.739C84.8093 125.014 84.8167 125.3 84.7653 125.578C84.714 125.856 84.605 126.121 84.4454 126.355C84.207 126.565 83.9067 126.692 83.5899 126.716C83.2731 126.74 82.957 126.66 82.6896 126.488C82.1798 126.125 81.7573 125.653 81.4525 125.106L76.1787 117.438C77.5146 120.386 78.4798 123.487 79.0523 126.672C79.1239 126.935 79.1234 127.211 79.051 127.473C78.7363 128.374 77.3347 128.002 76.6667 127.323C75.6111 126.128 74.791 124.745 74.2494 123.245C72.5648 119.395 70.9978 115.373 69.1542 111.557C67.6298 108.421 66.9766 106.693 70.6776 104.836Z" fill="white"/>
|
||||
<path d="M78.4585 128.426L78.5689 128.409C78.7763 128.365 78.9686 128.267 79.1271 128.126C79.2855 127.985 79.4047 127.805 79.4731 127.604C79.5618 127.268 79.568 126.914 79.491 126.575C79.1526 124.713 78.6844 122.876 78.0899 121.08L81.0101 125.358C81.365 125.97 81.8554 126.492 82.4437 126.885C82.801 127.114 83.2249 127.215 83.6469 127.173C84.0689 127.131 84.4645 126.948 84.77 126.654C84.9771 126.373 85.1215 126.05 85.1934 125.708C85.2653 125.366 85.2631 125.012 85.1868 124.671C85.0278 123.454 84.7525 122.254 84.3645 121.09C84.9989 122.02 85.6581 122.844 86.359 123.671C86.8575 124.388 87.556 124.942 88.3675 125.265C88.6458 125.352 88.9387 125.382 89.2289 125.354C89.5191 125.325 89.8007 125.239 90.057 125.1C90.2502 124.998 90.421 124.858 90.5592 124.689C90.6975 124.52 90.8004 124.325 90.8619 124.115C90.9579 123.619 90.9048 123.106 90.7092 122.64C90.4758 121.915 90.2122 121.196 89.9301 120.489C90.4037 120.875 90.9787 121.115 91.5859 121.182C91.8172 121.199 92.0496 121.167 92.2677 121.088C92.4858 121.009 92.6845 120.884 92.8506 120.722C93.3715 120.232 93.786 119.245 92.7997 117.352C90.9861 113.893 88.9389 110.562 86.6723 107.381C85.5842 105.636 84.1097 104.164 82.3624 103.078C79.3577 101.482 75.4715 101.92 70.4546 104.439C66.4007 106.473 67.1575 108.544 68.7142 111.757C69.9402 114.281 71.0526 116.935 72.1235 119.452C72.6824 120.763 73.2313 122.076 73.7485 123.383C74.316 124.933 75.1673 126.363 76.259 127.601C76.5397 127.895 76.8834 128.122 77.2643 128.265C77.6452 128.408 78.0534 128.463 78.4585 128.426ZM76.1087 116.966C76.0541 116.965 75.9997 116.973 75.9481 116.991C75.8474 117.048 75.7715 117.14 75.7351 117.25C75.6987 117.36 75.7044 117.48 75.7512 117.586C77.0749 120.493 78.0274 123.556 78.5863 126.702C78.6398 126.885 78.6479 127.077 78.6101 127.263C78.5933 127.318 78.5615 127.367 78.5181 127.404C78.4748 127.442 78.4218 127.466 78.3651 127.475C78.1118 127.488 77.8586 127.447 77.6219 127.356C77.3852 127.264 77.1702 127.124 76.9908 126.945C75.9815 125.79 75.1941 124.458 74.6682 123.017C74.1008 121.718 73.5519 120.405 73.0447 119.096C71.9646 116.519 70.8392 113.847 69.6001 111.304C68.1018 108.206 67.6232 106.882 70.9325 105.218C75.6437 102.848 79.2613 102.4 82.0163 103.839C83.6364 104.858 85.0014 106.234 86.0064 107.863C88.2538 111.008 90.2821 114.305 92.0768 117.729C92.3968 118.348 92.855 119.469 92.3085 119.994C92.2309 120.067 92.1386 120.123 92.0377 120.158C91.9368 120.193 91.8296 120.206 91.7233 120.196C90.9809 120.042 90.3192 119.625 89.8607 119.021C89.4618 118.558 89.0859 118.112 88.7264 117.572C88.0184 116.179 87.2236 114.833 86.3468 113.54C86.2791 113.445 86.179 113.378 86.0653 113.351C85.9515 113.325 85.8321 113.342 85.7297 113.397C85.6289 113.457 85.554 113.551 85.5197 113.663C85.4853 113.775 85.4941 113.895 85.5443 114.001C86.2116 115.422 87.0093 116.778 87.9269 118.053C88.7095 119.612 89.3773 121.226 89.9248 122.882C90.0577 123.178 90.1074 123.504 90.0683 123.825C90.008 124.005 89.8794 124.154 89.7104 124.239C89.5619 124.331 89.3966 124.391 89.2242 124.418C89.0518 124.444 88.8759 124.435 88.7069 124.392C88.0709 124.114 87.5256 123.664 87.1332 123.091C85.0892 120.626 83.3212 117.945 81.8608 115.095C81.7982 114.999 81.7026 114.929 81.5921 114.899C81.4815 114.869 81.3638 114.881 81.2612 114.932C81.1587 114.983 81.0785 115.07 81.036 115.177C80.9935 115.283 80.9915 115.401 81.0305 115.509C82.4557 118.464 83.9263 121.516 84.3223 124.792C84.3778 125.002 84.3905 125.22 84.3597 125.435C84.3289 125.65 84.2551 125.856 84.1428 126.041C83.976 126.17 83.7734 126.243 83.5631 126.251C83.3528 126.259 83.1452 126.202 82.969 126.087C82.5067 125.762 82.1204 125.341 81.8366 124.852L76.5643 117.195C76.5208 117.115 76.4538 117.05 76.3723 117.009C76.2908 116.968 76.1989 116.953 76.1087 116.966Z" fill="#454545"/>
|
||||
<path d="M207.872 101.393C207.93 101.389 207.987 101.373 208.039 101.348C215.715 95.9007 220.521 89.8717 222.347 83.3964C229.522 78.8199 233.484 72.3143 233.106 65.5406C232.476 53.473 218.185 44.4319 201.334 45.3343C184.481 46.2688 171.309 56.8277 171.95 68.8926C172.59 80.9575 186.868 89.9915 203.684 89.0774C206.008 88.951 208.317 88.6334 210.588 88.1279C210.423 92.5024 209.28 96.7843 207.243 100.659C207.191 100.746 207.166 100.847 207.171 100.948C207.177 101.049 207.213 101.147 207.274 101.227C207.336 101.308 207.42 101.369 207.516 101.401C207.612 101.433 207.716 101.436 207.813 101.409L207.872 101.393ZM201.422 46.3618C217.7 45.4776 231.456 54.0954 232.09 65.5941C232.452 72.0358 228.642 78.248 221.638 82.6415C221.534 82.7122 221.454 82.8146 221.412 82.9338C219.833 88.7643 215.704 94.2636 209.123 99.2893C210.763 95.5729 211.629 91.5615 211.669 87.4994C211.669 87.4226 211.652 87.3468 211.618 87.2777C211.584 87.2086 211.536 87.1481 211.475 87.1006C211.415 87.0531 211.344 87.02 211.269 87.0037C211.194 86.9874 211.116 86.9883 211.042 87.0065C208.63 87.5807 206.173 87.9383 203.697 88.0751C187.403 88.9744 173.625 80.3521 172.99 68.8533C172.356 57.3546 185.1 47.2789 201.393 46.3698L201.422 46.3618Z" fill="#D3CECB"/>
|
||||
<path d="M76.4248 171.812C76.1912 171.897 76.0706 172.155 76.1553 172.389C76.2401 172.622 76.4982 172.743 76.7319 172.658L76.4248 171.812ZM106.194 148.02L105.825 147.763L106.194 148.02ZM109.015 127.397L109.049 127.845L109.015 127.397ZM96.9601 136.594L97.3292 136.851L96.9601 136.594ZM151.542 129.685L151.94 129.894L151.542 129.685ZM150.212 115.549L150.245 115.998L150.212 115.549ZM134.132 132.961L133.702 132.831L133.702 132.831L134.132 132.961ZM134.117 133.012L134.547 133.143L134.547 133.143L134.117 133.012ZM157.554 149.539C157.791 149.463 157.921 149.209 157.845 148.973C157.768 148.736 157.515 148.606 157.278 148.682L157.554 149.539ZM133.702 132.831L133.686 132.882L134.547 133.143L134.563 133.092L133.702 132.831ZM137.028 149.604C142.741 143.932 148.172 137.055 151.94 129.894L151.143 129.475C147.427 136.539 142.057 143.343 136.393 148.966L137.028 149.604ZM151.94 129.894C152.58 128.678 153.847 124.968 154.158 121.61C154.314 119.937 154.24 118.275 153.672 117.043C153.384 116.417 152.961 115.889 152.366 115.538C151.771 115.187 151.042 115.036 150.178 115.101L150.245 115.998C150.965 115.944 151.503 116.074 151.909 116.313C152.315 116.552 152.626 116.924 152.855 117.42C153.321 118.431 153.413 119.897 153.262 121.527C152.961 124.775 151.725 128.369 151.143 129.475L151.94 129.894ZM150.178 115.101C146.034 115.41 142.438 118.135 139.639 121.588C136.833 125.049 134.771 129.307 133.702 132.831L134.563 133.092C135.602 129.666 137.614 125.515 140.338 122.155C143.068 118.787 146.459 116.281 150.245 115.998L150.178 115.101ZM133.686 132.882C132.948 135.315 132.251 138.66 132.42 141.81C132.59 144.956 133.633 148.005 136.483 149.673L136.938 148.897C134.464 147.449 133.481 144.769 133.319 141.762C133.157 138.758 133.825 135.525 134.547 133.143L133.686 132.882ZM96.591 136.336C94.4783 139.364 92.052 144.368 91.1804 149.236C90.3122 154.086 90.9654 158.991 95.2866 161.521L95.7412 160.744C91.9248 158.51 91.2184 154.131 92.0663 149.395C92.911 144.677 95.2766 139.793 97.3292 136.851L96.591 136.336ZM108.981 126.948C106.21 127.155 103.804 128.52 101.756 130.308C99.7081 132.095 97.9895 134.332 96.591 136.336L97.3292 136.851C98.7148 134.865 100.384 132.7 102.348 130.986C104.311 129.272 106.54 128.033 109.049 127.845L108.981 126.948ZM106.563 148.278C107.794 146.509 110.781 141.144 112.329 136.29C113.099 133.877 113.542 131.506 113.167 129.752C112.977 128.859 112.567 128.092 111.849 127.58C111.135 127.07 110.178 126.858 108.981 126.948L109.049 127.845C110.111 127.766 110.837 127.963 111.327 128.313C111.813 128.66 112.13 129.202 112.287 129.94C112.609 131.447 112.235 133.622 111.472 136.016C109.954 140.775 107.011 146.059 105.825 147.763L106.563 148.278ZM95.8309 161.452C99.8824 157.43 103.324 152.931 106.563 148.278L105.825 147.763C102.599 152.398 99.1929 156.846 95.1969 160.813L95.8309 161.452ZM76.7319 172.658C84.0488 170.003 90.1767 167.064 95.8309 161.452L95.1969 160.813C89.6766 166.293 83.6888 169.176 76.4248 171.812L76.7319 172.658ZM95.2866 161.521C101.788 165.327 109.986 164.563 117.624 161.778C125.273 158.988 132.461 154.137 137.028 149.604L136.393 148.966C131.916 153.41 124.838 158.189 117.316 160.932C109.782 163.68 101.903 164.351 95.7412 160.744L95.2866 161.521ZM136.483 149.673C139.571 151.481 143.311 151.934 147.02 151.7C150.733 151.464 154.458 150.537 157.554 149.539L157.278 148.682C154.22 149.668 150.574 150.573 146.963 150.801C143.348 151.03 139.812 150.579 136.938 148.897L136.483 149.673Z" fill="#D7D3D1"/>
|
||||
<path d="M34.6208 3.33385L34.6208 3.33385C40.4029 2.59168 45.8366 3.58675 49.9422 5.7744C54.0488 7.96265 56.7929 11.3207 57.3045 15.3069C57.8161 19.2932 56.0083 23.2362 52.5884 26.3917C49.1694 29.5462 44.1661 31.8823 38.3913 32.625L38.3912 32.625C36.6449 32.85 34.8819 32.9172 33.1236 32.826L32.5474 32.7961L32.6587 33.3622C33.1982 36.1046 34.2686 38.7088 35.8035 41.0305C29.7428 37.8826 26.8184 34.2909 25.4143 31.3739L25.3379 31.2152L25.1742 31.1505C19.8949 29.0654 16.3095 25.2779 15.715 20.6524L15.7149 20.6521C15.1999 16.6653 17.0008 12.7219 20.4169 9.56645C23.8321 6.41183 28.8352 4.07573 34.6208 3.33385Z" stroke="#D3CECB" stroke-width="0.9"/>
|
||||
<path d="M61.5231 137.52L48.805 135.525L49.7502 148.075L57.5239 150.9L66.2092 154.058L63.8431 145.715L61.5231 137.52Z" fill="white"/>
|
||||
<path d="M66.5579 154.119C66.5648 154.069 66.5613 154.018 66.5477 153.97L61.8616 137.431C61.8421 137.367 61.805 137.31 61.7544 137.265C61.7038 137.221 61.6417 137.191 61.575 137.178L48.8569 135.183C48.8039 135.178 48.7503 135.183 48.6995 135.198C48.6487 135.213 48.6018 135.239 48.5616 135.273C48.5239 135.308 48.4944 135.351 48.4751 135.398C48.4558 135.446 48.4471 135.497 48.4496 135.548L49.4053 148.079C49.4097 148.145 49.4336 148.208 49.4742 148.261C49.5148 148.314 49.5702 148.355 49.6337 148.377L66.0927 154.361C66.1534 154.385 66.2197 154.392 66.2838 154.38C66.348 154.369 66.4075 154.34 66.4554 154.297C66.5052 154.248 66.5406 154.186 66.5579 154.119ZM61.2765 137.828L65.7501 153.517L50.1494 147.842L49.2496 135.945L61.2765 137.828Z" fill="#454545"/>
|
||||
<path d="M55.8166 139.155L51.8945 142.674L15.9608 104.412L19.8828 100.892L55.8166 139.155Z" fill="white"/>
|
||||
<path d="M56.1719 139.213L56.1801 139.165C56.1838 139.12 56.1774 139.074 56.1612 139.031C56.145 138.989 56.1195 138.95 56.0863 138.918L20.1493 100.653C20.1177 100.62 20.0797 100.592 20.0375 100.573C19.9953 100.554 19.9496 100.543 19.9032 100.541C19.8104 100.539 19.7205 100.571 19.6509 100.631L15.7269 104.161C15.6925 104.19 15.6645 104.226 15.6448 104.267C15.6251 104.307 15.6141 104.351 15.6124 104.396C15.6107 104.441 15.6183 104.486 15.6349 104.528C15.6515 104.57 15.6766 104.609 15.7087 104.641L51.6388 142.904C51.7014 142.972 51.7888 143.014 51.882 143.019C51.9752 143.025 52.0665 142.994 52.1361 142.934L56.06 139.404C56.1186 139.355 56.1581 139.287 56.1719 139.213ZM19.8681 101.369L55.311 139.127L51.9142 142.181L16.4632 104.428L19.8681 101.369Z" fill="#454545"/>
|
||||
<path d="M15.965 104.405L13.8205 109.812L49.7506 148.075L51.9021 142.669L15.965 104.405Z" fill="#454545"/>
|
||||
<path d="M52.2505 142.73C52.2591 142.677 52.2552 142.623 52.2392 142.571C52.2231 142.52 52.1954 142.473 52.158 142.433L16.228 104.17C16.1862 104.128 16.1344 104.096 16.0772 104.078C16.02 104.06 15.9592 104.056 15.9004 104.067C15.8415 104.078 15.7864 104.103 15.7401 104.14C15.6938 104.176 15.6578 104.224 15.6352 104.278L13.4907 109.685C13.466 109.746 13.4591 109.811 13.4709 109.875C13.4827 109.939 13.5127 109.999 13.5575 110.047L49.4945 148.312C49.5356 148.354 49.5869 148.386 49.6438 148.404C49.7006 148.422 49.761 148.426 49.8195 148.415C49.8779 148.404 49.9325 148.379 49.9781 148.342C50.0237 148.304 50.0589 148.257 50.0803 148.202L52.2318 142.796L52.2505 142.73ZM16.0906 105.051L51.494 142.746L49.6238 147.437L14.2226 109.769L16.0906 105.051Z" fill="#454545"/>
|
||||
<path d="M25.5873 99.2556L19.8945 100.89L55.8245 139.153L61.5174 137.519L25.5873 99.2556Z" fill="#454545"/>
|
||||
<path d="M61.867 137.607C61.8741 137.554 61.8695 137.5 61.8535 137.449C61.8376 137.398 61.8107 137.351 61.7745 137.311L25.8492 99.0205C25.8025 98.9743 25.7443 98.941 25.6805 98.9238C25.6166 98.9067 25.5494 98.9063 25.4856 98.9228L19.7858 100.556C19.7279 100.571 19.6755 100.601 19.6341 100.643C19.5927 100.686 19.5639 100.738 19.5507 100.795C19.5349 100.851 19.534 100.91 19.5482 100.967C19.5623 101.023 19.591 101.075 19.6315 101.118L55.5616 139.381C55.6107 139.425 55.6717 139.454 55.7371 139.466C55.8026 139.477 55.8699 139.47 55.931 139.445L61.6296 137.819C61.6865 137.801 61.7379 137.77 61.779 137.728C61.8201 137.686 61.8496 137.635 61.8647 137.579L61.867 137.607ZM25.51 99.656L60.9135 137.352L55.9693 138.764L20.5345 101.042L25.51 99.656Z" fill="#454545"/>
|
||||
<path d="M63.8443 145.715L61.3645 145.779L57.8153 148.967L57.5251 150.901L66.2104 154.059L63.8443 145.715Z" fill="#03C7E8"/>
|
||||
<path d="M66.5577 154.119C66.5646 154.069 66.5611 154.018 66.5475 153.97L64.1825 145.619C64.1602 145.546 64.1136 145.481 64.0499 145.436C63.9863 145.391 63.9092 145.367 63.8308 145.37L61.3499 145.44C61.2667 145.441 61.187 145.472 61.1267 145.528L57.5786 148.709C57.5218 148.765 57.4823 148.836 57.4644 148.913L57.1801 150.855C57.1674 150.932 57.1828 151.011 57.2237 151.079C57.2646 151.146 57.3284 151.197 57.4037 151.223L66.089 154.381C66.1497 154.405 66.216 154.412 66.2801 154.401C66.3443 154.389 66.4038 154.361 66.4517 154.318C66.5061 154.263 66.543 154.194 66.5577 154.119ZM63.5822 146.063L65.6872 153.506L57.9136 150.68L58.1438 149.136L61.5336 146.123L63.5822 146.063Z" fill="#454545"/>
|
||||
<path d="M74.3776 60.9354L69.0595 60.0361L65.308 63.907L64.5142 58.5749L59.6738 56.2074L64.5001 53.8048L65.2589 48.4727L69.0384 52.3154L74.3425 51.3881L71.8485 56.1723L74.3776 60.9354Z" fill="#D3CECB"/>
|
||||
<path d="M32.1856 199.443L25.6642 198.341L25.4049 198.297L25.2218 198.486L20.6199 203.234L19.646 196.693L19.6073 196.433L19.3712 196.318L13.4346 193.414L19.3544 190.467L19.5896 190.35L19.6266 190.09L20.5574 183.549L25.1946 188.263L25.3786 188.45L25.6372 188.405L32.1422 187.268L29.0826 193.137L28.961 193.37L29.0844 193.603L32.1856 199.443Z" stroke="#D3CECB"/>
|
||||
<path d="M246.299 175.29L238.776 174.018L233.47 179.494L232.347 171.951L225.5 168.603L232.327 165.204L233.4 157.662L238.746 163.097L246.249 161.786L242.721 168.553L246.299 175.29Z" fill="#D7D3D1"/>
|
||||
<path d="M221.424 157.471L219.888 155.003L219.749 154.78L219.486 154.768L216.581 154.635L218.452 152.409L218.621 152.208L218.551 151.955L217.779 149.154L220.473 150.245L220.717 150.344L220.936 150.199L223.362 148.6L223.16 151.502L223.142 151.763L223.347 151.927L225.616 153.738L222.791 154.44L222.536 154.503L222.443 154.75L221.424 157.471Z" stroke="#D3CECB"/>
|
||||
<path d="M46.8063 96.1241L42.3463 92.377L42.165 92.2247L41.9369 92.2878L36.3235 93.8402L38.5027 88.4386L38.5912 88.2192L38.4606 88.0219L35.2482 83.1681L41.0584 83.5717L41.2943 83.5881L41.4416 83.4031L45.0671 78.8512L46.4847 84.5006L46.5422 84.7297L46.7635 84.8125L52.2106 86.8514L47.2684 89.9431L47.0677 90.0686L47.0575 90.3052L46.8063 96.1241Z" stroke="#D3CECB" stroke-width="0.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 31 KiB |
@@ -2,10 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
Container,
|
||||
Spinner,
|
||||
} from '@edx/paragon';
|
||||
import { Container, Spinner } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
@@ -16,6 +13,7 @@ import ReviewModal from 'containers/ReviewModal';
|
||||
import ListError from './ListError';
|
||||
import ListViewBreadcrumb from './ListViewBreadcrumb';
|
||||
import SubmissionsTable from './SubmissionsTable';
|
||||
import EmptySubmission from './EmptySubmission';
|
||||
import messages from './messages';
|
||||
import './ListView.scss';
|
||||
|
||||
@@ -29,16 +27,27 @@ export class ListView extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { isLoaded, hasError } = this.props;
|
||||
const {
|
||||
isLoaded, hasError, courseId, isEmptySubmissionData,
|
||||
} = this.props;
|
||||
return (
|
||||
<Container className="py-4">
|
||||
{ isLoaded && <ListViewBreadcrumb /> }
|
||||
{ isLoaded && <SubmissionsTable /> }
|
||||
{ hasError && <ListError /> }
|
||||
{ (!isLoaded && !hasError) && (
|
||||
{isLoaded
|
||||
&& (isEmptySubmissionData ? (
|
||||
<EmptySubmission courseId={courseId} />
|
||||
) : (
|
||||
<>
|
||||
<ListViewBreadcrumb />
|
||||
<SubmissionsTable />
|
||||
</>
|
||||
))}
|
||||
{hasError && <ListError />}
|
||||
{!isLoaded && !hasError && (
|
||||
<div className="w-100 h-100 text-center">
|
||||
<Spinner animation="border" variant="primary" />
|
||||
<h4><FormattedMessage {...messages.loadingResponses} /></h4>
|
||||
<h4>
|
||||
<FormattedMessage {...messages.loadingResponses} />
|
||||
</h4>
|
||||
</div>
|
||||
)}
|
||||
<ReviewModal />
|
||||
@@ -46,22 +55,25 @@ export class ListView extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
ListView.defaultProps = {
|
||||
};
|
||||
ListView.defaultProps = {};
|
||||
ListView.propTypes = {
|
||||
// redux
|
||||
courseId: PropTypes.string.isRequired,
|
||||
initializeApp: PropTypes.func.isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isPending: PropTypes.bool.isRequired,
|
||||
hasError: PropTypes.bool.isRequired,
|
||||
isEmptySubmissionData: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
courseId: selectors.app.courseId(state),
|
||||
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.initialize }),
|
||||
isPending: selectors.requests.isPending(state, { requestKey: RequestKeys.initialize }),
|
||||
hasError: selectors.requests.isFailed(state, { requestKey: RequestKeys.initialize }),
|
||||
isLoaded: selectors.requests.isCompleted(state, {
|
||||
requestKey: RequestKeys.initialize,
|
||||
}),
|
||||
hasError: selectors.requests.isFailed(state, {
|
||||
requestKey: RequestKeys.initialize,
|
||||
}),
|
||||
isEmptySubmissionData: selectors.submissions.isEmptySubmissionData(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
|
||||
@@ -5,17 +5,14 @@ import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
ListView,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from '.';
|
||||
import { ListView, mapStateToProps, mapDispatchToProps } from '.';
|
||||
|
||||
jest.mock('components/StatusBadge', () => 'StatusBadge');
|
||||
jest.mock('containers/ReviewModal', () => 'ReviewModal');
|
||||
jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb');
|
||||
jest.mock('./ListError', () => 'ListError');
|
||||
jest.mock('./SubmissionsTable', () => 'SubmissionsTable');
|
||||
jest.mock('./EmptySubmission', () => 'EmptySubmission');
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -24,11 +21,10 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
isPending: (...args) => ({ isPending: args }),
|
||||
isFailed: (...args) => ({ isFailed: args }),
|
||||
},
|
||||
submissions: {
|
||||
listData: (...args) => ({ listData: args }),
|
||||
isEmptySubmissionData: (...args) => ({ isEmptySubmissionData: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
@@ -51,29 +47,32 @@ describe('ListView component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
isLoaded: false,
|
||||
isPending: false,
|
||||
hasError: false,
|
||||
isEmptySubmissionData: false,
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.initializeApp = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
});
|
||||
describe('render tests', () => {
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListView {...props} />);
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('snapshot: loading', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded', () => {
|
||||
el.setProps({ isLoaded: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: error', () => {
|
||||
el.setProps({ hasError: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('loading', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('loaded has data', () => {
|
||||
el.setProps({ isLoaded: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('loaded with no data', () => {
|
||||
el.setProps({ isLoaded: true, isEmptySubmissionData: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
test('error', () => {
|
||||
el.setProps({ hasError: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
@@ -94,18 +93,26 @@ describe('ListView component', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
test('isLoaded loads from requests.isCompleted', () => {
|
||||
expect(mapped.isLoaded).toEqual(selectors.requests.isCompleted(testState, { requestKey }));
|
||||
});
|
||||
test('isPending loads from requests.isPending', () => {
|
||||
expect(mapped.isPending).toEqual(selectors.requests.isPending(testState, { requestKey }));
|
||||
expect(mapped.isLoaded).toEqual(
|
||||
selectors.requests.isCompleted(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
test('hasError loads from requests.isFailed', () => {
|
||||
expect(mapped.hasError).toEqual(selectors.requests.isFailed(testState, { requestKey }));
|
||||
expect(mapped.hasError).toEqual(
|
||||
selectors.requests.isFailed(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
test('isEmptySubmissionData loads from submissions.isEmptySubmissionData', () => {
|
||||
expect(mapped.isEmptySubmissionData).toEqual(
|
||||
selectors.submissions.isEmptySubmissionData(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(
|
||||
thunkActions.app.initialize,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,12 +4,17 @@ const messages = defineMessages({
|
||||
backToResponses: {
|
||||
id: 'ora-grading.ListView.ListViewBreadcrumbs.backToResponses',
|
||||
defaultMessage: 'Back to all open responses',
|
||||
description: 'Breadcrumbs link text to return to ORA list in LMS.',
|
||||
description: 'Breadcrumbs link text to return to ORA list in LMS',
|
||||
},
|
||||
noResultsFound: {
|
||||
id: 'ora-grading.ListView.noResultsFound',
|
||||
defaultMessage: 'No results found',
|
||||
description: 'Empty table content for submissions list',
|
||||
noResultsFoundTitle: {
|
||||
id: 'ora-grading.ListView.noResultsFoundTitle',
|
||||
defaultMessage: 'Nothing here yet',
|
||||
description: 'Empty table for the submission table title',
|
||||
},
|
||||
noResultsFoundBody: {
|
||||
id: 'ora-grading.ListView.noResultsFoundBody',
|
||||
defaultMessage: 'When learners submit responses, they will appear here',
|
||||
description: 'Empty table messages',
|
||||
},
|
||||
viewAllResponses: {
|
||||
id: 'ora-grading.ListView.viewAllResponses',
|
||||
|
||||
58
src/containers/ResponseDisplay/FileDownload.jsx
Normal file
58
src/containers/ResponseDisplay/FileDownload.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
StatefulButton,
|
||||
Icon,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { RequestKeys, RequestStates } from 'data/constants/requests';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
export const statusMapping = {
|
||||
[RequestStates.inactive]: 'default',
|
||||
[RequestStates.pending]: 'pending',
|
||||
[RequestStates.completed]: 'completed',
|
||||
[RequestStates.failed]: 'failed',
|
||||
};
|
||||
/**
|
||||
* <FileDownload />
|
||||
*/
|
||||
export const FileDownload = ({ requestStatus, downloadFiles }) => (
|
||||
<StatefulButton
|
||||
state={statusMapping[requestStatus.status]}
|
||||
onClick={downloadFiles}
|
||||
icons={{
|
||||
default: <Icon className="fa fa-download" />,
|
||||
pending: <Icon className="fa fa-spinner fa-spin" />,
|
||||
complete: <Icon className="fa fa-check" />,
|
||||
failed: <Icon className="fa fa-refresh" />,
|
||||
}}
|
||||
labels={{
|
||||
default: <FormattedMessage {...messages.downloadFiles} />,
|
||||
pending: <FormattedMessage {...messages.downloading} />,
|
||||
complete: <FormattedMessage {...messages.downloaded} />,
|
||||
failed: <FormattedMessage {...messages.retryDownload} />,
|
||||
}}
|
||||
disabledStates={['pending', 'complete']}
|
||||
/>
|
||||
);
|
||||
|
||||
FileDownload.defaultProps = {
|
||||
};
|
||||
FileDownload.propTypes = {
|
||||
downloadFiles: PropTypes.func.isRequired,
|
||||
requestStatus: PropTypes.shape({ status: PropTypes.string }).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
requestStatus: selectors.requests.requestStatus(state, { requestKey: RequestKeys.downloadFiles }),
|
||||
});
|
||||
export const mapDispatchToProps = {
|
||||
downloadFiles: thunkActions.download.downloadFiles,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(FileDownload);
|
||||
70
src/containers/ResponseDisplay/FileDownload.test.jsx
Normal file
70
src/containers/ResponseDisplay/FileDownload.test.jsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { RequestKeys, RequestStates } from 'data/constants/requests';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import {
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
FileDownload,
|
||||
statusMapping,
|
||||
} from './FileDownload';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
requests: { requestStatus: (...args) => ({ requestStatus: args }) },
|
||||
},
|
||||
thunkActions: {
|
||||
download: { downloadFiles: jest.fn() },
|
||||
},
|
||||
}));
|
||||
|
||||
describe('FileDownload', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
requestStatus: { status: RequestStates.inactive },
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
props.downloadFiles = jest.fn().mockName('this.props.downloadFiles');
|
||||
el = shallow(<FileDownload {...props} />);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('download is inactive', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.at(0).props().state).toEqual(statusMapping[RequestStates.inactive]);
|
||||
});
|
||||
test('download is pending', () => {
|
||||
el.setProps({ requestStatus: { status: RequestStates.pending } });
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.at(0).props().state).toEqual(statusMapping[RequestStates.pending]);
|
||||
});
|
||||
test('download is completed', () => {
|
||||
el.setProps({ requestStatus: { status: RequestStates.completed } });
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.at(0).props().state).toEqual(statusMapping[RequestStates.completed]);
|
||||
});
|
||||
test('download is failed', () => {
|
||||
el.setProps({ requestStatus: { status: RequestStates.failed } });
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.at(0).props().state).toEqual(statusMapping[RequestStates.failed]);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const requestKey = RequestKeys.downloadFiles;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('requestStatus loads from requests.requestStatus(downloadFiles)', () => {
|
||||
expect(mapped.requestStatus).toEqual(selectors.requests.requestStatus(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads downloadFiles from thunkActions.download.downloadFiles', () => {
|
||||
expect(mapDispatchToProps.downloadFiles).toEqual(thunkActions.download.downloadFiles);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,70 +1,29 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
|
||||
import {
|
||||
FileCard, PDFRenderer, ImageRenderer, TXTRenderer,
|
||||
} from 'components/FilePreview';
|
||||
import { FileRenderer, isSupported } from 'components/FilePreview';
|
||||
|
||||
/**
|
||||
* <PreviewDisplay />
|
||||
*/
|
||||
export class PreviewDisplay extends React.Component {
|
||||
static RENDERERS = StrictDict({
|
||||
[FileTypes.pdf]: PDFRenderer,
|
||||
[FileTypes.jpg]: ImageRenderer,
|
||||
[FileTypes.jpeg]: ImageRenderer,
|
||||
[FileTypes.bmp]: ImageRenderer,
|
||||
[FileTypes.png]: ImageRenderer,
|
||||
[FileTypes.txt]: TXTRenderer,
|
||||
});
|
||||
|
||||
static SUPPORTED_TYPES = Object.keys(PreviewDisplay.RENDERERS);
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.isSupported = this.isSupported.bind(this);
|
||||
this.fileType = this.fileType.bind(this);
|
||||
}
|
||||
|
||||
get supportedFiles() {
|
||||
return this.props.files.filter(this.isSupported);
|
||||
}
|
||||
|
||||
isSupported(file) {
|
||||
return PreviewDisplay.SUPPORTED_TYPES.includes(this.fileType(file.name));
|
||||
}
|
||||
|
||||
fileType(fileName) {
|
||||
return fileName.split('.').pop();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="preview-display">
|
||||
{this.supportedFiles.map((file) => {
|
||||
const Renderer = PreviewDisplay.RENDERERS[this.fileType(file.name)];
|
||||
return (
|
||||
<FileCard key={file.downloadUrl} file={file}>
|
||||
<Renderer fileName={file.name} url={file.downloadUrl} />
|
||||
</FileCard>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
export const PreviewDisplay = ({ files }) => (
|
||||
<div className="preview-display">
|
||||
{files.filter(isSupported).map((file) => (
|
||||
<FileRenderer key={file.name} file={file} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
||||
PreviewDisplay.defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
PreviewDisplay.propTypes = {
|
||||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
})),
|
||||
files: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
export default PreviewDisplay;
|
||||
|
||||
@@ -2,24 +2,17 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FileCard, ImageRenderer, PDFRenderer } from 'components/FilePreview';
|
||||
import { FileRenderer } from 'components/FilePreview';
|
||||
import { PreviewDisplay } from './PreviewDisplay';
|
||||
|
||||
jest.mock('components/FilePreview', () => ({
|
||||
FileCard: () => 'FileCard',
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
FileRenderer: () => 'FileRenderer',
|
||||
isSupported: jest.requireActual('components/FilePreview').isSupported,
|
||||
}));
|
||||
|
||||
describe('PreviewDisplay', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = [
|
||||
FileTypes.pdf,
|
||||
FileTypes.jpg,
|
||||
FileTypes.jpeg,
|
||||
FileTypes.bmp,
|
||||
FileTypes.png,
|
||||
];
|
||||
const supportedTypes = Object.values(FileTypes);
|
||||
const props = {
|
||||
files: [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
@@ -40,44 +33,25 @@ describe('PreviewDisplay', () => {
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('files does not exist', () => {
|
||||
test('files render with props', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('files exited for props', () => {
|
||||
test('files does not exist', () => {
|
||||
el.setProps({ files: [] });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('only renders compatible files', () => {
|
||||
const cards = el.find(FileCard);
|
||||
const cards = el.find(FileRenderer);
|
||||
expect(cards.length).toEqual(supportedTypes.length);
|
||||
[0, 1, 2, 3, 4].forEach(index => {
|
||||
cards.forEach((_, index) => {
|
||||
expect(
|
||||
cards.at(index).prop('file'),
|
||||
).toEqual(props.files[index]);
|
||||
});
|
||||
});
|
||||
describe('uses the correct renderers', () => {
|
||||
const loadRenderer = (index) => (
|
||||
el.find(FileCard).at(index).children().at(0)
|
||||
);
|
||||
const checkFile = (index, expectedRenderer) => {
|
||||
const file = props.files[index];
|
||||
const renderer = loadRenderer(index);
|
||||
expect(renderer.type()).toEqual(expectedRenderer);
|
||||
expect(renderer.props()).toEqual({
|
||||
url: file.downloadUrl,
|
||||
fileName: file.name,
|
||||
});
|
||||
};
|
||||
test(FileTypes.pdf, () => checkFile(0, PDFRenderer));
|
||||
test(FileTypes.jpg, () => checkFile(1, ImageRenderer));
|
||||
test(FileTypes.jpeg, () => checkFile(2, ImageRenderer));
|
||||
test(FileTypes.bmp, () => checkFile(3, ImageRenderer));
|
||||
test(FileTypes.png, () => checkFile(4, ImageRenderer));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.response-display {
|
||||
padding: map-get($spacers, 0);
|
||||
max-width: map-get($container-max-widths, "md");
|
||||
width: map-get($container-max-widths, "md");
|
||||
overflow-y: hidden;
|
||||
height: fit-content;
|
||||
|
||||
@@ -43,4 +43,14 @@
|
||||
.preview-display {
|
||||
padding: map-get($spacers, 3) 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
.response-display {
|
||||
width: 100%;
|
||||
|
||||
.preview-display {
|
||||
padding: map-get($spacers, 1) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import FileNameCell from './components/FileNameCell';
|
||||
import FileExtensionCell from './components/FileExtensionCell';
|
||||
import FilePopoverCell from './components/FilePopoverCell';
|
||||
import FileDownload from './FileDownload';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
@@ -26,45 +27,52 @@ export class SubmissionFiles extends React.Component {
|
||||
return (
|
||||
<Card className="submission-files">
|
||||
{files.length ? (
|
||||
<Collapsible.Advanced defaultOpen>
|
||||
<Collapsible.Trigger className="submission-files-title">
|
||||
<h3>{this.title}</h3>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ArrowDropDown} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ArrowDropUp} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="submission-files-body">
|
||||
<div className="submission-files-table">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tableNameHeader),
|
||||
accessor: 'name',
|
||||
Cell: FileNameCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tableExtensionHeader),
|
||||
accessor: 'name',
|
||||
id: 'extension',
|
||||
Cell: FileExtensionCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tablePopoverHeader),
|
||||
accessor: '',
|
||||
Cell: FilePopoverCell,
|
||||
},
|
||||
]}
|
||||
data={files}
|
||||
itemCount={files.length}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<>
|
||||
<Collapsible.Advanced defaultOpen>
|
||||
<Collapsible.Trigger className="submission-files-title">
|
||||
<h3>{this.title}</h3>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ArrowDropDown} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ArrowDropUp} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="submission-files-body">
|
||||
<div className="submission-files-table">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tableNameHeader),
|
||||
accessor: 'name',
|
||||
Cell: FileNameCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(
|
||||
messages.tableExtensionHeader,
|
||||
),
|
||||
accessor: 'name',
|
||||
id: 'extension',
|
||||
Cell: FileExtensionCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tablePopoverHeader),
|
||||
accessor: '',
|
||||
Cell: FilePopoverCell,
|
||||
},
|
||||
]}
|
||||
data={files}
|
||||
itemCount={files.length}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer className="text-right">
|
||||
<FileDownload files={files} />
|
||||
</Card.Footer>
|
||||
</>
|
||||
) : (
|
||||
<div className="submission-files-title no-submissions">
|
||||
<h3>{this.title}</h3>
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileDownload component snapshot download is completed 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
Object {
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files penging state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="completed"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is failed 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
Object {
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files penging state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="failed"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is inactive 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
Object {
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files penging state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="default"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is pending 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
Array [
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
Object {
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
Object {
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files penging state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="pending"
|
||||
/>
|
||||
`;
|
||||
@@ -1,10 +1,16 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files render with props 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
>
|
||||
<FileCard
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
@@ -12,14 +18,9 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_0.pdf"
|
||||
>
|
||||
<PDFRenderer
|
||||
fileName="fake_file_0.pdf"
|
||||
url="/url-path/fake_file_0.pdf"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_0.pdf"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 1",
|
||||
@@ -27,14 +28,9 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_1.jpg",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_1.jpg"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_1.jpg"
|
||||
url="/url-path/fake_file_1.jpg"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_1.jpg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 2",
|
||||
@@ -42,48 +38,87 @@ exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
"name": "fake_file_2.jpeg",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_2.jpeg"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_2.jpeg"
|
||||
url="/url-path/fake_file_2.jpeg"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_2.jpeg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
"downloadUrl": "/url-path/fake_file_3.png",
|
||||
"name": "fake_file_3.png",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_3.bmp"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
key="fake_file_3.png"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.png",
|
||||
"name": "fake_file_4.png",
|
||||
"downloadUrl": "/url-path/fake_file_4.bmp",
|
||||
"name": "fake_file_4.bmp",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_4.png"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_4.png"
|
||||
url="/url-path/fake_file_4.png"
|
||||
/>
|
||||
</FileCard>
|
||||
key="fake_file_4.bmp"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
key="fake_file_5.txt"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 6",
|
||||
"downloadUrl": "/url-path/fake_file_6.gif",
|
||||
"name": "fake_file_6.gif",
|
||||
}
|
||||
}
|
||||
key="fake_file_6.gif"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 7",
|
||||
"downloadUrl": "/url-path/fake_file_7.jfif",
|
||||
"name": "fake_file_7.jfif",
|
||||
}
|
||||
}
|
||||
key="fake_file_7.jfif"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 8",
|
||||
"downloadUrl": "/url-path/fake_file_8.pjpeg",
|
||||
"name": "fake_file_8.pjpeg",
|
||||
}
|
||||
}
|
||||
key="fake_file_8.pjpeg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 9",
|
||||
"downloadUrl": "/url-path/fake_file_9.pjp",
|
||||
"name": "fake_file_9.pjp",
|
||||
}
|
||||
}
|
||||
key="fake_file_9.pjp"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 10",
|
||||
"downloadUrl": "/url-path/fake_file_10.svg",
|
||||
"name": "fake_file_10.svg",
|
||||
}
|
||||
}
|
||||
key="fake_file_10.svg"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files exited for props 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -90,5 +90,25 @@ exports[`SubmissionFiles component snapshot files exited for props 1`] = `
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer
|
||||
className="text-right"
|
||||
>
|
||||
<Connect(FileDownload)
|
||||
files={
|
||||
Array [
|
||||
Object {
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
},
|
||||
Object {
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
@@ -6,7 +6,7 @@ import FilePopoverContent from 'components/FilePopoverContent';
|
||||
|
||||
export const FilePopoverCell = ({ row: { original } }) => (
|
||||
<InfoPopover>
|
||||
<FilePopoverContent file={original} />
|
||||
<FilePopoverContent {...original} />
|
||||
</InfoPopover>
|
||||
);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('FilePopoverCell', () => {
|
||||
test('content', () => {
|
||||
const { original } = props.row;
|
||||
const content = el.find(FilePopoverContent);
|
||||
expect(content.props()).toEqual({ file: original });
|
||||
expect(content.props()).toEqual({ ...original });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
exports[`FilePopoverCell component snapshot 1`] = `
|
||||
<InfoPopover>
|
||||
<FilePopoverContent
|
||||
file={
|
||||
Object {
|
||||
"description": "long descriptive text...",
|
||||
"downloadURL": "this-url-is.working",
|
||||
"name": "some file name",
|
||||
}
|
||||
}
|
||||
description="long descriptive text..."
|
||||
downloadURL="this-url-is.working"
|
||||
name="some file name"
|
||||
/>
|
||||
</InfoPopover>
|
||||
`;
|
||||
|
||||
@@ -16,6 +16,26 @@ const messages = defineMessages({
|
||||
defaultMessage: 'File Metadata',
|
||||
description: 'Table header for popover file metadata',
|
||||
},
|
||||
downloadFiles: {
|
||||
id: 'ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles',
|
||||
defaultMessage: 'Download files',
|
||||
description: 'Download files inactive state label',
|
||||
},
|
||||
downloading: {
|
||||
id: 'ora-grading.ResponseDisplay.SubmissionFiles.downloading',
|
||||
defaultMessage: 'Downloading',
|
||||
description: 'Download files penging state label',
|
||||
},
|
||||
downloaded: {
|
||||
id: 'ora-grading.ResponseDisplay.SubmissionFiles.downloaded',
|
||||
defaultMessage: 'Downloaded!',
|
||||
description: 'Download files completed state label',
|
||||
},
|
||||
retryDownload: {
|
||||
id: 'ora-grading.ResponseDisplay.SubmissionFiles.retryDownload',
|
||||
defaultMessage: 'Retry download',
|
||||
description: 'Download files failed state label',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
|
||||
@@ -38,7 +38,7 @@ exports[`ReviewActions component component snapshot: do not show rubric 1`] = `
|
||||
>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.toggleShowRubric]}
|
||||
variant="outline-primary"
|
||||
variant="outline-primary mr-2"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Show Rubric"
|
||||
@@ -121,7 +121,7 @@ exports[`ReviewActions component component snapshot: show rubric, no score 1`] =
|
||||
>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.toggleShowRubric]}
|
||||
variant="outline-primary"
|
||||
variant="outline-primary mr-2"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Hide Rubric"
|
||||
|
||||
@@ -42,7 +42,7 @@ export const ReviewActions = ({
|
||||
<div className="review-actions-group">
|
||||
{isLoaded && (
|
||||
<>
|
||||
<Button variant="outline-primary" onClick={toggleShowRubric}>
|
||||
<Button variant="outline-primary mr-2" onClick={toggleShowRubric}>
|
||||
<FormattedMessage {...(showRubric ? messages.hideRubric : messages.showRubric)} />
|
||||
</Button>
|
||||
<StartGradingButton />
|
||||
|
||||
@@ -15,15 +15,19 @@ import ReviewErrors from './ReviewErrors';
|
||||
* <ReviewContent />
|
||||
*/
|
||||
export const ReviewContent = ({ isFailed, isLoaded, showRubric }) => (isLoaded || isFailed) && (
|
||||
<div className="content-block">
|
||||
<div className="content-block">
|
||||
<div className="content-wrapper">
|
||||
<ReviewErrors />
|
||||
{ isLoaded && (
|
||||
<Row className="flex-nowrap">
|
||||
<Col><ResponseDisplay /></Col>
|
||||
{ showRubric && <Rubric /> }
|
||||
</Row>
|
||||
{isLoaded && (
|
||||
<Row className="flex-nowrap m-0">
|
||||
<Col className="p-0">
|
||||
<ResponseDisplay />
|
||||
</Col>
|
||||
{showRubric && <Rubric />}
|
||||
</Row>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
ReviewContent.defaultProps = {
|
||||
isFailed: false,
|
||||
@@ -37,8 +41,12 @@ ReviewContent.propTypes = {
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
isLoaded: selectors.requests.isCompleted(state, { requestKey: RequestKeys.fetchSubmission }),
|
||||
isFailed: selectors.requests.isFailed(state, {
|
||||
requestKey: RequestKeys.fetchSubmission,
|
||||
}),
|
||||
isLoaded: selectors.requests.isCompleted(state, {
|
||||
requestKey: RequestKeys.fetchSubmission,
|
||||
}),
|
||||
showRubric: selectors.app.showRubric(state),
|
||||
});
|
||||
|
||||
|
||||
59
src/containers/ReviewModal/ReviewErrors/DownloadErrors.jsx
Normal file
59
src/containers/ReviewModal/ReviewErrors/DownloadErrors.jsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
/**
|
||||
* <DownloadErrors />
|
||||
*/
|
||||
export class DownloadErrors extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.cancelAction = this.cancelAction.bind(this);
|
||||
}
|
||||
|
||||
cancelAction() {
|
||||
this.props.clearState({ requestKey: RequestKeys.downloadFiles });
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.isFailed) { return null; }
|
||||
return (
|
||||
<ReviewError
|
||||
key="lockFailed"
|
||||
headingMessage={messages.downloadFailedHeading}
|
||||
actions={{
|
||||
cancel: { onClick: this.cancelAction, message: messages.dismiss },
|
||||
confirm: { onClick: this.props.downloadFiles, message: messages.retryDownload },
|
||||
}}
|
||||
>
|
||||
<FormattedMessage {...messages.downloadFailedContent} />
|
||||
</ReviewError>
|
||||
);
|
||||
}
|
||||
}
|
||||
DownloadErrors.propTypes = {
|
||||
// redux
|
||||
clearState: PropTypes.func.isRequired,
|
||||
isFailed: PropTypes.bool.isRequired,
|
||||
downloadFiles: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
isFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.downloadFiles }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
clearState: actions.requests.clearRequest,
|
||||
downloadFiles: thunkActions.download.downloadFiles,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(DownloadErrors);
|
||||
@@ -0,0 +1,80 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors, actions, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import {
|
||||
DownloadErrors,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './DownloadErrors';
|
||||
|
||||
let el;
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
requests: { isFailed: (...args) => ({ isFailed: args }) },
|
||||
},
|
||||
actions: {
|
||||
requests: { clearRequest: jest.fn() },
|
||||
},
|
||||
thunkActions: {
|
||||
download: { downloadFiles: jest.fn() },
|
||||
},
|
||||
}));
|
||||
jest.mock('./ReviewError', () => 'ReviewError');
|
||||
|
||||
describe('DownloadErrors component', () => {
|
||||
const props = {
|
||||
isFailed: false,
|
||||
};
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
props.clearState = jest.fn();
|
||||
props.downloadFiles = jest.fn().mockName('this.props.downloadFiles');
|
||||
el = shallow(<DownloadErrors {...props} />);
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().cancelAction = jest.fn().mockName('this.cancelAction');
|
||||
});
|
||||
test('failed: show error', () => {
|
||||
el.setProps({ isFailed: true });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
});
|
||||
test('not failed: hide error', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('clearState', () => {
|
||||
it('calls props.clearState with requestKey: downladFiles', () => {
|
||||
el.instance().cancelAction();
|
||||
expect(props.clearState).toHaveBeenCalledWith({ requestKey: RequestKeys.downloadFiles });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('isFailed loads from requests.isFailed(downloadFiles)', () => {
|
||||
const requestKey = RequestKeys.downloadFiles;
|
||||
expect(mapped.isFailed).toEqual(selectors.requests.isFailed(testState, { requestKey }));
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads clearState from actions.requests.clearRequest', () => {
|
||||
expect(mapDispatchToProps.clearState).toEqual(actions.requests.clearRequest);
|
||||
});
|
||||
it('loads downloadFiles from thunkActions.download.downloadFiles', () => {
|
||||
expect(mapDispatchToProps.downloadFiles).toEqual(thunkActions.download.downloadFiles);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,7 +7,7 @@ import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import messages from '../messages';
|
||||
import messages from './messages';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ const ReviewError = ({
|
||||
cancel,
|
||||
confirm,
|
||||
},
|
||||
className,
|
||||
headingMessage,
|
||||
variant,
|
||||
children,
|
||||
}) => {
|
||||
const actions = [];
|
||||
@@ -35,8 +37,9 @@ const ReviewError = ({
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
variant="danger"
|
||||
variant={variant}
|
||||
icon={Info}
|
||||
className={className}
|
||||
actions={actions}
|
||||
>
|
||||
<Alert.Heading><FormattedMessage {...headingMessage} /></Alert.Heading>
|
||||
@@ -46,6 +49,8 @@ const ReviewError = ({
|
||||
};
|
||||
ReviewError.defaultProps = {
|
||||
actions: {},
|
||||
className: '',
|
||||
variant: 'danger',
|
||||
};
|
||||
ReviewError.propTypes = {
|
||||
actions: PropTypes.shape({
|
||||
@@ -58,7 +63,9 @@ ReviewError.propTypes = {
|
||||
message: messageShape,
|
||||
}),
|
||||
}),
|
||||
className: PropTypes.string,
|
||||
headingMessage: messageShape.isRequired,
|
||||
variant: PropTypes.string,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
|
||||
82
src/containers/ReviewModal/ReviewErrors/ReviewError.test.jsx
Normal file
82
src/containers/ReviewModal/ReviewErrors/ReviewError.test.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Button } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import ReviewError from './ReviewError';
|
||||
|
||||
let el;
|
||||
const messages = {
|
||||
heading: {
|
||||
id: 'test-header-message',
|
||||
defaultMessage: 'Test Header Message',
|
||||
},
|
||||
cancel: {
|
||||
id: 'test-cancel-message',
|
||||
defaultMessage: 'Test Cancel Message',
|
||||
},
|
||||
confirm: {
|
||||
id: 'test-confirm-message',
|
||||
defaultMessage: 'Test Confirm Message',
|
||||
},
|
||||
};
|
||||
const cancel = {
|
||||
onClick: jest.fn().mockName('this.props.cancel.onClick'),
|
||||
message: messages.cancel,
|
||||
};
|
||||
const confirm = {
|
||||
onClick: jest.fn().mockName('this.props.confirm.onClick'),
|
||||
message: messages.confirm,
|
||||
};
|
||||
|
||||
const confirmBtn = (
|
||||
<Button key="confirm" onClick={confirm.onClick}>
|
||||
<FormattedMessage {...confirm.message} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
const cancelBtn = (
|
||||
<Button key="cancel" variant="outline-primary" onClick={cancel.onClick}>
|
||||
<FormattedMessage {...cancel.message} />
|
||||
</Button>
|
||||
);
|
||||
|
||||
describe('ReviewError component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
headingMessage: messages.heading,
|
||||
};
|
||||
const children = <div>Test Children</div>;
|
||||
describe('snapshots', () => {
|
||||
test('no actions', () => {
|
||||
el = shallow(<ReviewError {...props}>{children}</ReviewError>);
|
||||
expect(el).toMatchSnapshot();
|
||||
const { actions } = el.at(0).props();
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
test('cancel only', () => {
|
||||
el = shallow(<ReviewError {...props} actions={{ cancel }}>{children}</ReviewError>);
|
||||
expect(el).toMatchSnapshot();
|
||||
const { actions } = el.at(0).props();
|
||||
expect(actions.length).toEqual(1);
|
||||
expect(actions[0]).toEqual(cancelBtn);
|
||||
});
|
||||
test('confirm only', () => {
|
||||
el = shallow(<ReviewError {...props} actions={{ confirm }}>{children}</ReviewError>);
|
||||
expect(el).toMatchSnapshot();
|
||||
const { actions } = el.at(0).props();
|
||||
expect(actions.length).toEqual(1);
|
||||
expect(actions[0]).toEqual(confirmBtn);
|
||||
});
|
||||
test('cancel and confirm', () => {
|
||||
el = shallow(<ReviewError {...props} actions={{ cancel, confirm }}>{children}</ReviewError>);
|
||||
expect(el).toMatchSnapshot();
|
||||
const { actions } = el.at(0).props();
|
||||
expect(actions.length).toEqual(2);
|
||||
expect(actions[0]).toEqual(cancelBtn);
|
||||
expect(actions[1]).toEqual(confirmBtn);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DownloadErrors component component snapshots failed: show error 1`] = `
|
||||
<ReviewError
|
||||
actions={
|
||||
Object {
|
||||
"cancel": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Dismiss",
|
||||
"description": "Dismiss error action button text",
|
||||
"id": "ora-grading.ReviewModal.dismiss",
|
||||
},
|
||||
"onClick": [MockFunction this.cancelAction],
|
||||
},
|
||||
"confirm": Object {
|
||||
"message": Object {
|
||||
"defaultMessage": "Retry download",
|
||||
"description": "Failed download retry button text",
|
||||
"id": "ora-grading.ReviewModal.errorRetryDownload",
|
||||
},
|
||||
"onClick": [MockFunction this.props.downloadFiles],
|
||||
},
|
||||
}
|
||||
}
|
||||
headingMessage={
|
||||
Object {
|
||||
"defaultMessage": "Couldn't download files",
|
||||
"id": "ora-grading.ReviewModal.errorDownloadFailed",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="We're sorry, something went wrong when we tried to download these files. Please try again."
|
||||
description="Failed download error content"
|
||||
id="ora-grading.ReviewModal.errorDownloadFailedContent"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
|
||||
exports[`DownloadErrors component component snapshots not failed: hide error 1`] = `""`;
|
||||
@@ -0,0 +1,124 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ReviewError component component snapshots cancel and confirm 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.props.cancel.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Cancel Message"
|
||||
id="test-cancel-message"
|
||||
/>
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={[MockFunction this.props.confirm.onClick]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Confirm Message"
|
||||
id="test-confirm-message"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
className=""
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Header Message"
|
||||
id="test-header-message"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<div>
|
||||
Test Children
|
||||
</div>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`ReviewError component component snapshots cancel only 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.props.cancel.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Cancel Message"
|
||||
id="test-cancel-message"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
className=""
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Header Message"
|
||||
id="test-header-message"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<div>
|
||||
Test Children
|
||||
</div>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`ReviewError component component snapshots confirm only 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<Button
|
||||
onClick={[MockFunction this.props.confirm.onClick]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Confirm Message"
|
||||
id="test-confirm-message"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
className=""
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Header Message"
|
||||
id="test-header-message"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<div>
|
||||
Test Children
|
||||
</div>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
|
||||
exports[`ReviewError component component snapshots no actions 1`] = `
|
||||
<Alert
|
||||
actions={Array []}
|
||||
className=""
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Test Header Message"
|
||||
id="test-header-message"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<div>
|
||||
Test Children
|
||||
</div>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -66,7 +66,7 @@ exports[`SubmitErrors component component snapshots snapshot: with network failu
|
||||
<FormattedMessage
|
||||
defaultMessage="We're sorry, something went wrong when we tried to submit this grade. Please try again."
|
||||
description="Grade submission network error message"
|
||||
id="ora-grading.ReviewModal.gradeNotSubmitted.heading"
|
||||
id="ora-grading.ReviewModal.gradeNotSubmitted.Content"
|
||||
/>
|
||||
</ReviewError>
|
||||
`;
|
||||
|
||||
@@ -5,5 +5,6 @@ exports[`ReviewErrors component component snapshot: no failure 1`] = `
|
||||
<FetchErrors />
|
||||
<SubmitErrors />
|
||||
<LockErrors />
|
||||
<DownloadErrors />
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from 'react';
|
||||
import FetchErrors from './FetchErrors';
|
||||
import LockErrors from './LockErrors';
|
||||
import SubmitErrors from './SubmitErrors';
|
||||
import DownloadErrors from './DownloadErrors';
|
||||
|
||||
/**
|
||||
* <ReviewErrors />
|
||||
@@ -12,6 +13,7 @@ export const ReviewErrors = () => (
|
||||
<FetchErrors />
|
||||
<SubmitErrors />
|
||||
<LockErrors />
|
||||
<DownloadErrors />
|
||||
</>
|
||||
);
|
||||
ReviewErrors.defaultProps = {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user