feat: File Preview panel (#39)
* feat: update display preview panel * chore: update package * test: update unit testing * test: update test * test: update unit testing * feat: update preview display to use custom renderer * chore: use worker for react-pdf * test: update unit testing * chore: update requested change * chore: update gitignore for sample files * fix: hard-code filetype mappings * fix: make integration test work again * chore: update tests * feat: add FileInfo to preview cards Co-authored-by: Leangseu Kim <lkim@edx.org>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ npm-debug.log
|
||||
coverage
|
||||
|
||||
dist/
|
||||
public/samples/
|
||||
|
||||
### pyenv ###
|
||||
.python-version
|
||||
|
||||
61
package-lock.json
generated
61
package-lock.json
generated
@@ -4138,8 +4138,7 @@
|
||||
"@types/json-schema": {
|
||||
"version": "7.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
|
||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ=="
|
||||
},
|
||||
"@types/json5": {
|
||||
"version": "0.0.29",
|
||||
@@ -4681,8 +4680,7 @@
|
||||
"ajv-keywords": {
|
||||
"version": "3.5.2",
|
||||
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ=="
|
||||
},
|
||||
"alphanum-sort": {
|
||||
"version": "1.0.2",
|
||||
@@ -5541,8 +5539,7 @@
|
||||
"big.js": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="
|
||||
},
|
||||
"bin-build": {
|
||||
"version": "3.0.0",
|
||||
@@ -8171,8 +8168,7 @@
|
||||
"emojis-list": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
|
||||
"dev": true
|
||||
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="
|
||||
},
|
||||
"encodeurl": {
|
||||
"version": "1.0.2",
|
||||
@@ -9891,7 +9887,6 @@
|
||||
"version": "6.2.0",
|
||||
"resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz",
|
||||
"integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loader-utils": "^2.0.0",
|
||||
"schema-utils": "^3.0.0"
|
||||
@@ -15837,7 +15832,6 @@
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz",
|
||||
"integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"minimist": "^1.2.5"
|
||||
}
|
||||
@@ -15994,7 +15988,6 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"big.js": "^5.2.2",
|
||||
"emojis-list": "^3.0.0",
|
||||
@@ -16472,6 +16465,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-cancellable-promise": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-cancellable-promise/-/make-cancellable-promise-1.1.0.tgz",
|
||||
"integrity": "sha512-X5Opjm2xcZsOLuJ+Bnhb4t5yfu4ehlA3OKEYLtqUchgVzL/QaqW373ZUVxVHKwvJ38cmYuR4rAHD2yUvAIkTPA=="
|
||||
},
|
||||
"make-dir": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
|
||||
@@ -16490,6 +16488,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"make-event-props": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/make-event-props/-/make-event-props-1.3.0.tgz",
|
||||
"integrity": "sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q=="
|
||||
},
|
||||
"makeerror": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz",
|
||||
@@ -16648,12 +16651,22 @@
|
||||
"yargs-parser": "^20.2.3"
|
||||
}
|
||||
},
|
||||
"merge-class-names": {
|
||||
"version": "1.4.2",
|
||||
"resolved": "https://registry.npmjs.org/merge-class-names/-/merge-class-names-1.4.2.tgz",
|
||||
"integrity": "sha512-bOl98VzwCGi25Gcn3xKxnR5p/WrhWFQB59MS/aGENcmUc6iSm96yrFDF0XSNurX9qN4LbJm0R9kfvsQ17i8zCw=="
|
||||
},
|
||||
"merge-descriptors": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=",
|
||||
"dev": true
|
||||
},
|
||||
"merge-refs": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-refs/-/merge-refs-1.0.0.tgz",
|
||||
"integrity": "sha512-WZ4S5wqD9FCR9hxkLgvcHJCBxzXzy3VVE6p8W2OzxRzB+hLRlcadGE2bW9xp2KSzk10rvp4y+pwwKO6JQVguMg=="
|
||||
},
|
||||
"merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
@@ -16782,8 +16795,7 @@
|
||||
"minimist": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz",
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw=="
|
||||
},
|
||||
"minimist-options": {
|
||||
"version": "4.1.0",
|
||||
@@ -21091,6 +21103,28 @@
|
||||
"warning": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"react-pdf": {
|
||||
"version": "5.5.0",
|
||||
"resolved": "https://registry.npmjs.org/react-pdf/-/react-pdf-5.5.0.tgz",
|
||||
"integrity": "sha512-0Sk0lFd95py7H7/G0+fPzUrLsGHQ6SzC+3eJChgSnLtEmrRV36uNAjuMgf5mz2ds55MK4vYCJHGuP9CyvaSEOA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.0.0",
|
||||
"file-loader": "^6.0.0",
|
||||
"make-cancellable-promise": "^1.0.0",
|
||||
"make-event-props": "^1.1.0",
|
||||
"merge-class-names": "^1.1.1",
|
||||
"merge-refs": "^1.0.0",
|
||||
"pdfjs-dist": "2.9.359",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"pdfjs-dist": {
|
||||
"version": "2.9.359",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-2.9.359.tgz",
|
||||
"integrity": "sha512-P2nYtkacdlZaNNwrBLw1ZyMm0oE2yY/5S/GDCAmMJ7U4+ciL/D0mrlEC/o4HZZc/LNE3w8lEVzBEyVgEQlPVKQ=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.2.5.tgz",
|
||||
@@ -22060,7 +22094,6 @@
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz",
|
||||
"integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/json-schema": "^7.0.8",
|
||||
"ajv": "^6.12.5",
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"react": "17.0.2",
|
||||
"react-dom": "17.0.2",
|
||||
"react-intl": "^5.20.9",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.4",
|
||||
"react-router": "5.2.0",
|
||||
"react-router-dom": "5.2.0",
|
||||
|
||||
22
public/pdf.worker.min.js
vendored
Normal file
22
public/pdf.worker.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,32 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePopoverContent component snapshot 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>
|
||||
</Fragment>
|
||||
`;
|
||||
34
src/components/FilePopoverContent/index.jsx
Normal file
34
src/components/FilePopoverContent/index.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const FilePopoverContent = ({ file }) => (
|
||||
<>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverNameTitle} /></strong>
|
||||
<br />
|
||||
{file.name}
|
||||
</div>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverDescriptionTitle} /></strong>
|
||||
<br />
|
||||
{file.description}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
FilePopoverContent.defaultProps = {
|
||||
};
|
||||
|
||||
FilePopoverContent.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string,
|
||||
downloadURL: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default FilePopoverContent;
|
||||
30
src/components/FilePopoverContent/index.test.jsx
Normal file
30
src/components/FilePopoverContent/index.test.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import FilePopoverContent from '.';
|
||||
|
||||
describe('FilePopoverContent', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
file: {
|
||||
name: 'some file name',
|
||||
description: 'long descriptive text...',
|
||||
downloadURL: 'this-url-is.working',
|
||||
},
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverContent {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
expect(el.text()).toContain(props.file.name);
|
||||
expect(el.text()).toContain(props.file.description);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
16
src/components/FilePopoverContent/messages.js
Normal file
16
src/components/FilePopoverContent/messages.js
Normal file
@@ -0,0 +1,16 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
filePopoverNameTitle: {
|
||||
id: 'ora-grading.FilePopoverContent.filePopoverNameTitle',
|
||||
defaultMessage: 'File Name',
|
||||
description: 'Popover title for file name',
|
||||
},
|
||||
filePopoverDescriptionTitle: {
|
||||
id: 'ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle',
|
||||
defaultMessage: 'File Description',
|
||||
description: 'Popover title for file description',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
34
src/components/FilePreview/FileCard.jsx
Normal file
34
src/components/FilePreview/FileCard.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Collapsible } from '@edx/paragon';
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
|
||||
import './FileCard.scss';
|
||||
|
||||
/**
|
||||
* <FileCard />
|
||||
*/
|
||||
export const FileCard = ({ file, children }) => (
|
||||
<Card className="file-card" key={file.name}>
|
||||
<Collapsible className="file-collapsible" defaultOpen title={<h3>{file.name}</h3>}>
|
||||
<div className="preview-panel">
|
||||
<FileInfo><FilePopoverContent file={file} /></FileInfo>
|
||||
{children}
|
||||
</div>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
);
|
||||
FileCard.defaultProps = {
|
||||
};
|
||||
FileCard.propTypes = {
|
||||
file: PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
downloadUrl: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
}).isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
export default FileCard;
|
||||
13
src/components/FilePreview/FileCard.scss
Normal file
13
src/components/FilePreview/FileCard.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.preview-panel {
|
||||
.image-renderer {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pdf-renderer {
|
||||
.react-pdf__Page__canvas {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/components/FilePreview/FileCard.test.jsx
Normal file
42
src/components/FilePreview/FileCard.test.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
jest.mock('./FileInfo', () => 'FileInfo');
|
||||
|
||||
describe('File Preview Card component', () => {
|
||||
const props = {
|
||||
file: {
|
||||
name: 'test-file-name.pdf',
|
||||
description: 'test-file description',
|
||||
downloadUrl: 'destination/test-file-name.pdf',
|
||||
},
|
||||
};
|
||||
const children = (<h1>some children</h1>);
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileCard {...props}>{children}</FileCard>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
test('collapsible title is name header', () => {
|
||||
const title = el.find(Collapsible).prop('title');
|
||||
expect(title).toEqual(<h3>{props.file.name}</h3>);
|
||||
});
|
||||
test('forwards children into preview-panel', () => {
|
||||
const previewPanelChildren = el.find('.preview-panel').children();
|
||||
expect(previewPanelChildren.at(0).equals(
|
||||
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>,
|
||||
));
|
||||
expect(previewPanelChildren.at(1).equals(children)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
49
src/components/FilePreview/FileInfo.jsx
Normal file
49
src/components/FilePreview/FileInfo.jsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Button,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <FileInfo />
|
||||
*/
|
||||
export const FileInfo = ({ onClick, children }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="right-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
onClick={onClick}
|
||||
iconAfter={InfoOutline}
|
||||
>
|
||||
<FormattedMessage {...messages.fileInfo} />
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
FileInfo.defaultProps = {
|
||||
onClick: () => {},
|
||||
};
|
||||
FileInfo.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
};
|
||||
|
||||
export default FileInfo;
|
||||
25
src/components/FilePreview/FileInfo.test.jsx
Normal file
25
src/components/FilePreview/FileInfo.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Popover } from '@edx/paragon';
|
||||
|
||||
import FileInfo from './FileInfo';
|
||||
|
||||
describe('File Preview Card component', () => {
|
||||
const children = (<h1>some Children</h1>);
|
||||
const props = { onClick: jest.fn().mockName('this.props.onClick') };
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileInfo {...props}>{children}</FileInfo>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
test('overlay with passed children', () => {
|
||||
const { overlay } = el.at(0).props();
|
||||
expect(overlay.type).toEqual(Popover);
|
||||
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>);
|
||||
});
|
||||
});
|
||||
});
|
||||
17
src/components/FilePreview/ImageRenderer.jsx
Normal file
17
src/components/FilePreview/ImageRenderer.jsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
const ImageRenderer = ({ url, fileName }) => (<img alt={fileName} className="image-renderer" src={url} />);
|
||||
|
||||
ImageRenderer.supportedTypes = ['jpg', 'jpeg', 'png', 'bmp'];
|
||||
|
||||
ImageRenderer.defaultProps = {
|
||||
fileName: '',
|
||||
};
|
||||
|
||||
ImageRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
fileName: PropTypes.string,
|
||||
};
|
||||
|
||||
export default ImageRenderer;
|
||||
25
src/components/FilePreview/ImageRenderer.test.jsx
Normal file
25
src/components/FilePreview/ImageRenderer.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ImageRenderer from './ImageRenderer';
|
||||
|
||||
describe('Image Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.pdf',
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
const supportedTypes = ['jpg', 'jpeg', 'png', 'bmp'];
|
||||
test('static supported types is expected', () => {
|
||||
expect(ImageRenderer.supportedTypes).toEqual(supportedTypes);
|
||||
});
|
||||
});
|
||||
});
|
||||
147
src/components/FilePreview/PDFRenderer.jsx
Normal file
147
src/components/FilePreview/PDFRenderer.jsx
Normal file
@@ -0,0 +1,147 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { pdfjs, Document, Page } from 'react-pdf';
|
||||
import {
|
||||
Icon, Form, ActionRow, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
|
||||
import pdfjsWorker from 'react-pdf/node_modules/pdfjs-dist/build/pdf.worker.entry';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
/**
|
||||
* <PDFRenderer />
|
||||
*/
|
||||
export class PDFRenderer extends React.Component {
|
||||
static supportedTypes = ['pdf'];
|
||||
|
||||
static INITIAL_STATE = {
|
||||
pageNumber: 1,
|
||||
numPages: 1,
|
||||
relativeHeight: 0,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { ...PDFRenderer.INITIAL_STATE };
|
||||
|
||||
this.wrapperRef = React.createRef();
|
||||
this.onDocumentLoadSuccess = this.onDocumentLoadSuccess.bind(this);
|
||||
this.onDocumentLoadError = this.onDocumentLoadError.bind(this);
|
||||
this.onLoadPageSuccess = this.onLoadPageSuccess.bind(this);
|
||||
this.onPrevPageButtonClick = this.onPrevPageButtonClick.bind(this);
|
||||
this.onNextPageButtonClick = this.onNextPageButtonClick.bind(this);
|
||||
this.onInputPageChange = this.onInputPageChange.bind(this);
|
||||
}
|
||||
|
||||
onDocumentLoadSuccess = ({ numPages }) => {
|
||||
this.setState({ numPages });
|
||||
};
|
||||
|
||||
onLoadPageSuccess = (page) => {
|
||||
const pageWidth = page.view[2];
|
||||
const pageHeight = page.view[3];
|
||||
const wrapperHeight = this.wrapperRef.current.getBoundingClientRect().width;
|
||||
const relativeHeight = (wrapperHeight * pageHeight) / pageWidth;
|
||||
if (relativeHeight !== this.state.relativeHeight) {
|
||||
this.setState({ relativeHeight });
|
||||
}
|
||||
};
|
||||
|
||||
onDocumentLoadError = (error) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(error);
|
||||
};
|
||||
|
||||
onInputPageChange = ({ target: { value } }) => {
|
||||
this.setPageNumber(parseInt(value, 10));
|
||||
}
|
||||
|
||||
onPrevPageButtonClick = () => {
|
||||
this.setPageNumber(this.state.pageNumber - 1);
|
||||
}
|
||||
|
||||
onNextPageButtonClick = () => {
|
||||
this.setPageNumber(this.state.pageNumber + 1);
|
||||
}
|
||||
|
||||
setPageNumber(pageNumber) {
|
||||
if (pageNumber > 0 && pageNumber <= this.state.numPages) {
|
||||
this.setState({ pageNumber });
|
||||
}
|
||||
}
|
||||
|
||||
get hasNext() {
|
||||
return this.state.pageNumber < this.state.numPages;
|
||||
}
|
||||
|
||||
get hasPrev() {
|
||||
return this.state.pageNumber > 1;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div ref={this.wrapperRef} className="pdf-renderer">
|
||||
<Document
|
||||
file={this.props.url}
|
||||
onLoadSuccess={this.onDocumentLoadSuccess}
|
||||
onLoadError={this.onDocumentLoadError}
|
||||
>
|
||||
{/* <Outline /> */}
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={{
|
||||
height: this.state.relativeHeight,
|
||||
}}
|
||||
>
|
||||
<Page
|
||||
pageNumber={this.state.pageNumber}
|
||||
onLoadSuccess={this.onLoadPageSuccess}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow className="d-flex justify-content-center m-0">
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="previous pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronLeft}
|
||||
disabled={!this.hasPrev}
|
||||
onClick={this.onPrevPageButtonClick}
|
||||
/>
|
||||
<Form.Group className="d-flex align-items-center m-0">
|
||||
<Form.Label isInline>Page </Form.Label>
|
||||
<Form.Control
|
||||
type="number"
|
||||
min={0}
|
||||
max={this.state.numPages}
|
||||
value={this.state.pageNumber}
|
||||
onChange={this.onInputPageChange}
|
||||
/>
|
||||
<Form.Label isInline> of {this.state.numPages}</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
size="inline"
|
||||
alt="next pdf page"
|
||||
iconAs={Icon}
|
||||
src={ChevronRight}
|
||||
disabled={!this.hasNext}
|
||||
onClick={this.onNextPageButtonClick}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PDFRenderer.defaultProps = {};
|
||||
|
||||
PDFRenderer.propTypes = {
|
||||
url: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PDFRenderer;
|
||||
225
src/components/FilePreview/PDFRenderer.test.jsx
Normal file
225
src/components/FilePreview/PDFRenderer.test.jsx
Normal file
@@ -0,0 +1,225 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { Form, IconButton } from '@edx/paragon';
|
||||
|
||||
import PDFRenderer from './PDFRenderer';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
}));
|
||||
|
||||
describe('PDF Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.pdf',
|
||||
};
|
||||
|
||||
let el;
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<PDFRenderer {...props} />);
|
||||
el.instance().onDocumentLoadSuccess = jest
|
||||
.fn()
|
||||
.mockName('onDocumentLoadSuccess');
|
||||
el.instance().onDocumentLoadError = jest
|
||||
.fn()
|
||||
.mockName('onDocumentLoadError');
|
||||
el.instance().onLoadPageSuccess = jest.fn().mockName('onLoadPageSuccess');
|
||||
});
|
||||
test('snapshot', () => {
|
||||
el.instance().onPrevPageButtonClick = jest
|
||||
.fn()
|
||||
.mockName('onPrevPageButtonClick');
|
||||
el.instance().onNextPageButtonClick = jest
|
||||
.fn()
|
||||
.mockName('onNextPageButtonClick');
|
||||
el.instance().onInputPageChange = jest.fn().mockName('onInputPageChange');
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
const numPages = 99;
|
||||
const pageNumber = 234;
|
||||
const supportedTypes = ['pdf'];
|
||||
beforeEach(() => {
|
||||
el = shallow(<PDFRenderer {...props} />);
|
||||
});
|
||||
describe('render', () => {
|
||||
describe('Top-level document', () => {
|
||||
let documentEl;
|
||||
beforeEach(() => { documentEl = el.find(Document); });
|
||||
it('displays file from props.url', () => {
|
||||
expect(documentEl.props().file).toEqual(props.url);
|
||||
});
|
||||
it('calls this.onDocumentLoadSuccess onLoadSuccess', () => {
|
||||
expect(documentEl.props().onLoadSuccess).toEqual(el.instance().onDocumentLoadSuccess);
|
||||
});
|
||||
it('calls this.onDocumentLoadError onLoadError', () => {
|
||||
expect(documentEl.props().onLoadError).toEqual(el.instance().onDocumentLoadError);
|
||||
});
|
||||
});
|
||||
describe('Page', () => {
|
||||
let pageProps;
|
||||
beforeEach(() => {
|
||||
el.instance().setState({ pageNumber });
|
||||
pageProps = el.find(Page).props();
|
||||
});
|
||||
it('loads pageNumber from state', () => {
|
||||
expect(pageProps.pageNumber).toEqual(pageNumber);
|
||||
});
|
||||
it('calls onLoadPageSuccess onLoadSuccess', () => {
|
||||
expect(pageProps.onLoadSuccess).toEqual(el.instance().onLoadPageSuccess);
|
||||
});
|
||||
});
|
||||
describe('pagination ActionRow', () => {
|
||||
describe('Previous page button', () => {
|
||||
let hasPrev;
|
||||
beforeEach(() => {
|
||||
hasPrev = jest.spyOn(el.instance(), 'hasPrev', 'get').mockReturnValue(false);
|
||||
});
|
||||
const btn = () => shallow(el.instance().render()).find(IconButton).at(0).props();
|
||||
test('disabled iff not this.hasPrev', () => {
|
||||
expect(btn().disabled).toEqual(true);
|
||||
hasPrev.mockReturnValue(true);
|
||||
expect(btn().disabled).toEqual(false);
|
||||
});
|
||||
it('calls onPrevPageButtonClick onClick', () => {
|
||||
expect(btn().onClick).toEqual(el.instance().onPrevPageButtonClick);
|
||||
});
|
||||
});
|
||||
describe('page indicator', () => {
|
||||
const control = () => el.find(Form.Control).at(0).props();
|
||||
const labels = () => {
|
||||
const flat = el.find({ isInline: true });
|
||||
return [0, 1].map(i => flat.at(i).text());
|
||||
};
|
||||
beforeEach(() => { el.instance().setState({ numPages, pageNumber }); });
|
||||
test('labels: Page <state.pageNumber> of <state.numPages>', () => {
|
||||
expect(`${labels()[0]}${control().value}${labels()[1]}`).toEqual(
|
||||
`Page ${pageNumber} of ${numPages}`,
|
||||
);
|
||||
});
|
||||
it('loads max from state.numPages', () => expect(control().max).toEqual(numPages));
|
||||
it('loads value from state.pageNumber', () => {
|
||||
expect(control().value).toEqual(pageNumber);
|
||||
});
|
||||
it('calls onInputPageChange onChange', () => {
|
||||
expect(control().onChange).toEqual(el.instance().onInputPageChange);
|
||||
});
|
||||
});
|
||||
describe('Next page button', () => {
|
||||
let hasNext;
|
||||
beforeEach(() => {
|
||||
hasNext = jest.spyOn(el.instance(), 'hasNext', 'get').mockReturnValue(false);
|
||||
});
|
||||
const btn = () => shallow(el.instance().render()).find(IconButton).at(1).props();
|
||||
test('disabled iff not this.hasNext', () => {
|
||||
expect(btn().disabled).toEqual(true);
|
||||
hasNext.mockReturnValue(true);
|
||||
expect(btn().disabled).toEqual(false);
|
||||
});
|
||||
it('calls onNextPageButtonClick onClick', () => {
|
||||
expect(btn().onClick).toEqual(el.instance().onNextPageButtonClick);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
test('static supported types is expected', () => {
|
||||
expect(PDFRenderer.supportedTypes).toEqual(supportedTypes);
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('initial state', () => {
|
||||
expect(el.instance().state).toEqual(PDFRenderer.INITIAL_STATE);
|
||||
});
|
||||
describe('onDocumentLoadSuccess', () => {
|
||||
test('loads numPages into state', () => {
|
||||
el.instance().onDocumentLoadSuccess({ numPages });
|
||||
expect(el.instance().state.numPages).toEqual(numPages);
|
||||
});
|
||||
});
|
||||
describe('onLoadPageSuccess', () => {
|
||||
const [pageHeight, pageWidth] = [23, 34];
|
||||
const page = { view: [1, 2, pageWidth, pageHeight] };
|
||||
const wrapperWidth = 20;
|
||||
const expected = (wrapperWidth * pageHeight) / pageWidth;
|
||||
beforeEach(() => {
|
||||
el.instance().wrapperRef = {
|
||||
current: {
|
||||
getBoundingClientRect: () => ({ width: wrapperWidth }),
|
||||
},
|
||||
};
|
||||
});
|
||||
it('sets relative height if it has changes', () => {
|
||||
el.instance().onLoadPageSuccess(page);
|
||||
expect(el.instance().state.relativeHeight).toEqual(expected);
|
||||
});
|
||||
it('does not try to set height if has not changes', () => {
|
||||
el.instance().setState({ relativeHeight: expected });
|
||||
el.instance().setState = jest.fn();
|
||||
el.instance().onLoadPageSuccess(page);
|
||||
expect(el.instance().setState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('setPageNumber inheritors', () => {
|
||||
beforeEach(() => {
|
||||
el.instance().setPageNumber = jest.fn();
|
||||
el.instance().setState({ pageNumber });
|
||||
});
|
||||
describe('onInputChange', () => {
|
||||
it('calls setPageNumber with int value of event target value', () => {
|
||||
el.instance().onInputPageChange({ target: { value: '23' } });
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(23);
|
||||
});
|
||||
});
|
||||
describe('onPrevPageButtonClick', () => {
|
||||
it('calls setPageNumber with state.pageNumber - 1', () => {
|
||||
el.instance().onPrevPageButtonClick();
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber - 1);
|
||||
});
|
||||
});
|
||||
describe('onNextPageButtonClick', () => {
|
||||
it('calls setPageNumber with state.pageNumber + 1', () => {
|
||||
el.instance().onNextPageButtonClick();
|
||||
expect(el.instance().setPageNumber).toHaveBeenCalledWith(pageNumber + 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('setPageNumber', () => {
|
||||
it('calls setState with pageNumber iff valid', () => {
|
||||
el.instance().setState({ numPages });
|
||||
const setState = jest.spyOn(el.instance(), 'setState');
|
||||
el.instance().setPageNumber(0);
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
el.instance().setPageNumber(numPages + 1);
|
||||
expect(setState).not.toHaveBeenCalled();
|
||||
el.instance().setPageNumber(2);
|
||||
expect(setState).toHaveBeenCalledWith({ pageNumber: 2 });
|
||||
});
|
||||
});
|
||||
describe('hasNext getter', () => {
|
||||
it('returns true iff state.pageNumber < state.numPages', () => {
|
||||
el.instance().setState({ pageNumber: 1, numPages: 1 });
|
||||
expect(el.instance().hasNext).toEqual(false);
|
||||
el.instance().setState({ pageNumber: 1, numPages: 2 });
|
||||
expect(el.instance().hasNext).toEqual(true);
|
||||
});
|
||||
});
|
||||
describe('hasPrev getter', () => {
|
||||
it('returns true iff state.pageNumber > 1', () => {
|
||||
el.instance().setState({ pageNumber: 1 });
|
||||
expect(el.instance().hasPrev).toEqual(false);
|
||||
el.instance().setState({ pageNumber: 2 });
|
||||
expect(el.instance().hasPrev).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
test('static supported types is expected', () => {
|
||||
expect(PDFRenderer.supportedTypes).toEqual(supportedTypes);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<Card
|
||||
className="file-card"
|
||||
key="test-file-name.pdf"
|
||||
>
|
||||
<Collapsible
|
||||
className="file-collapsible"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<h3>
|
||||
test-file-name.pdf
|
||||
</h3>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="preview-panel"
|
||||
>
|
||||
<FileInfo>
|
||||
<FilePopoverContent
|
||||
file={
|
||||
Object {
|
||||
"description": "test-file description",
|
||||
"downloadUrl": "destination/test-file-name.pdf",
|
||||
"name": "test-file-name.pdf",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FileInfo>
|
||||
<h1>
|
||||
some children
|
||||
</h1>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<h1>
|
||||
some Children
|
||||
</h1>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
}
|
||||
placement="right-end"
|
||||
trigger="focus"
|
||||
>
|
||||
<Button
|
||||
iconAfter={[MockFunction icons.InfoOutline]}
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File info"
|
||||
description="Popover trigger button text for file preview card"
|
||||
id="ora-grading.InfoPopover.fileInfo"
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Image Renderer Component snapshot 1`] = `
|
||||
<img
|
||||
alt=""
|
||||
className="image-renderer"
|
||||
src="some_url.pdf"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,69 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PDF Renderer Component snapshots snapshot 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
Object {
|
||||
"height": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction onLoadPageSuccess]}
|
||||
pageNumber={1}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow
|
||||
className="d-flex justify-content-center m-0"
|
||||
>
|
||||
<IconButton
|
||||
alt="previous pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction onPrevPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronLeft]}
|
||||
/>
|
||||
<Form.Group
|
||||
className="d-flex align-items-center m-0"
|
||||
>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
Page
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
max={1}
|
||||
min={0}
|
||||
onChange={[MockFunction onInputPageChange]}
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
1
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
`;
|
||||
3
src/components/FilePreview/index.jsx
Normal file
3
src/components/FilePreview/index.jsx
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as FileCard } from './FileCard';
|
||||
export { default as ImageRenderer } from './ImageRenderer';
|
||||
export { default as PDFRenderer } from './PDFRenderer';
|
||||
11
src/components/FilePreview/messages.js
Normal file
11
src/components/FilePreview/messages.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
fileInfo: {
|
||||
id: 'ora-grading.InfoPopover.fileInfo',
|
||||
defaultMessage: 'File info',
|
||||
description: 'Popover trigger button text for file preview card',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -21,7 +21,7 @@ exports[`Info Popover Component snapshot 1`] = `
|
||||
alt="Display more info"
|
||||
className="esg-help-icon"
|
||||
iconAs="Icon"
|
||||
onClick={[Function]}
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
src={[MockFunction icons.InfoOutline]}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
|
||||
@@ -15,7 +15,7 @@ import messages from './messages';
|
||||
/**
|
||||
* <InfoPopover />
|
||||
*/
|
||||
export const InfoPopover = ({ children, intl }) => (
|
||||
export const InfoPopover = ({ onClick, children, intl }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="right-end"
|
||||
@@ -31,13 +31,16 @@ export const InfoPopover = ({ children, intl }) => (
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={() => {}}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
InfoPopover.defaultProps = {};
|
||||
InfoPopover.defaultProps = {
|
||||
onClick: () => {},
|
||||
};
|
||||
InfoPopover.propTypes = {
|
||||
onClick: PropTypes.func,
|
||||
children: PropTypes.oneOfType([
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
|
||||
@@ -6,15 +6,15 @@ import { InfoPopover } from '.';
|
||||
|
||||
describe('Info Popover Component', () => {
|
||||
const child = <div>Children component</div>;
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<InfoPopover intl={{ formatMessage }}>{child}</InfoPopover>)).toMatchSnapshot();
|
||||
const onClick = jest.fn().mockName('this.props.onClick');
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<InfoPopover onClick={onClick} intl={{ formatMessage }}>{child}</InfoPopover>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<InfoPopover intl={{ formatMessage }}>{child}</InfoPopover>);
|
||||
});
|
||||
test('Test component render', () => {
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.find('.esg-help-icon').length).toEqual(1);
|
||||
|
||||
69
src/containers/ResponseDisplay/PreviewDisplay.jsx
Normal file
69
src/containers/ResponseDisplay/PreviewDisplay.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
|
||||
import { FileCard, PDFRenderer, ImageRenderer } from 'components/FilePreview';
|
||||
|
||||
import './PreviewDisplay.scss';
|
||||
|
||||
/**
|
||||
* <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,
|
||||
});
|
||||
|
||||
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 (
|
||||
<>
|
||||
{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>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PreviewDisplay.defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
PreviewDisplay.propTypes = {
|
||||
files: PropTypes.arrayOf(PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
})),
|
||||
};
|
||||
|
||||
export default PreviewDisplay;
|
||||
13
src/containers/ResponseDisplay/PreviewDisplay.scss
Normal file
13
src/containers/ResponseDisplay/PreviewDisplay.scss
Normal file
@@ -0,0 +1,13 @@
|
||||
.preview-panel {
|
||||
.image-renderer {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pdf-renderer {
|
||||
.react-pdf__Page__canvas {
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/containers/ResponseDisplay/PreviewDisplay.test.jsx
Normal file
83
src/containers/ResponseDisplay/PreviewDisplay.test.jsx
Normal file
@@ -0,0 +1,83 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FileCard, ImageRenderer, PDFRenderer } from 'components/FilePreview';
|
||||
import { PreviewDisplay } from './PreviewDisplay';
|
||||
|
||||
jest.mock('components/FilePreview', () => ({
|
||||
FileCard: () => 'FileCard',
|
||||
PDFRenderer: () => 'PDFRenderer',
|
||||
ImageRenderer: () => 'ImageRenderer',
|
||||
}));
|
||||
|
||||
describe('PreviewDisplay', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = [
|
||||
FileTypes.pdf,
|
||||
FileTypes.jpg,
|
||||
FileTypes.jpeg,
|
||||
FileTypes.bmp,
|
||||
FileTypes.png,
|
||||
];
|
||||
const props = {
|
||||
files: [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
name: `fake_file_${index}.${fileType}`,
|
||||
description: `file description ${index}`,
|
||||
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
|
||||
})),
|
||||
{
|
||||
name: 'bad_ext_fake_file.other',
|
||||
description: 'bad_ext file description',
|
||||
downloadUrl: 'bad_ext.other',
|
||||
},
|
||||
],
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<PreviewDisplay {...props} />);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('files does not exist', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('files exited for props', () => {
|
||||
el.setProps({ files: [] });
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('only renders compatible files', () => {
|
||||
const cards = el.find(FileCard);
|
||||
expect(cards.length).toEqual(supportedTypes.length);
|
||||
[0, 1, 2, 3, 4].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));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,10 +7,10 @@
|
||||
height: fit-content;
|
||||
|
||||
.submission-files {
|
||||
padding: map-get($spacers, 3);
|
||||
margin-bottom: map-get($spacers, 2);
|
||||
|
||||
.submission-files-title {
|
||||
padding: map-get($spacers, 3);
|
||||
border-radius: calc(0.375rem - 1px);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 100ms ease 150ms;
|
||||
@@ -33,7 +33,8 @@
|
||||
}
|
||||
|
||||
.submission-files-body {
|
||||
padding: map-get($spacers, 3) 0;
|
||||
padding: map-get($spacers, 3);
|
||||
padding-top: 0;
|
||||
|
||||
thead {
|
||||
display: none;
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
<Fragment>
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"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
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 1",
|
||||
"downloadUrl": "/url-path/fake_file_1.jpg",
|
||||
"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
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 2",
|
||||
"downloadUrl": "/url-path/fake_file_2.jpeg",
|
||||
"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
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.bmp",
|
||||
"name": "fake_file_3.bmp",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_3.bmp"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_3.bmp"
|
||||
url="/url-path/fake_file_3.bmp"
|
||||
/>
|
||||
</FileCard>
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.png",
|
||||
"name": "fake_file_4.png",
|
||||
}
|
||||
}
|
||||
key="/url-path/fake_file_4.png"
|
||||
>
|
||||
<ImageRenderer
|
||||
fileName="fake_file_4.png"
|
||||
url="/url-path/fake_file_4.png"
|
||||
/>
|
||||
</FileCard>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files exited for props 1`] = `<React.Fragment />`;
|
||||
@@ -19,6 +19,9 @@ exports[`ResponseDisplay component snapshot file upload disabled without respons
|
||||
<SubmissionFiles
|
||||
files={Array []}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
files={Array []}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -42,6 +45,22 @@ exports[`ResponseDisplay component snapshot file upload enable with valid respon
|
||||
]
|
||||
}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
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>
|
||||
<Card.Body>
|
||||
parsed html (sanitized (some text response here))
|
||||
@@ -57,5 +76,8 @@ exports[`ResponseDisplay component snapshot file upload enable without response
|
||||
<SubmissionFiles
|
||||
files={Array []}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
files={Array []}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import InfoPopover from 'components/InfoPopover';
|
||||
|
||||
import messages from './messages';
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
|
||||
export const FilePopoverCell = ({ row: { original } }) => (
|
||||
<InfoPopover>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverNameTitle} /></strong>
|
||||
<br />
|
||||
{original.name}
|
||||
</div>
|
||||
<div className="help-popover-option">
|
||||
<strong><FormattedMessage {...messages.filePopoverDescriptionTitle} /></strong>
|
||||
<br />
|
||||
{original.description}
|
||||
</div>
|
||||
<FilePopoverContent file={original} />
|
||||
</InfoPopover>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FilePopoverCell from './FilePopoverCell';
|
||||
|
||||
jest.mock('components/InfoPopover', () => 'InfoPopover');
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
|
||||
describe('FilePopoverCell', () => {
|
||||
describe('component', () => {
|
||||
@@ -27,8 +29,8 @@ describe('FilePopoverCell', () => {
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
const { original } = props.row;
|
||||
expect(el.text()).toContain(original.name);
|
||||
expect(el.text()).toContain(original.description);
|
||||
const content = el.find(FilePopoverContent);
|
||||
expect(content.props()).toEqual({ file: original });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,31 +2,14 @@
|
||||
|
||||
exports[`FilePopoverCell component snapshot 1`] = `
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.ResponseDisplay.FilePopoverCell.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.ResponseDisplay.FilePopoverCell.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<FilePopoverContent
|
||||
file={
|
||||
Object {
|
||||
"description": "long descriptive text...",
|
||||
"downloadURL": "this-url-is.working",
|
||||
"name": "some file name",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</InfoPopover>
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,7 @@ import { selectors } from 'data/redux';
|
||||
import { fileUploadResponseOptions } from 'data/services/lms/constants';
|
||||
|
||||
import SubmissionFiles from './SubmissionFiles';
|
||||
import PreviewDisplay from './PreviewDisplay';
|
||||
|
||||
import './ResponseDisplay.scss';
|
||||
|
||||
@@ -42,6 +43,7 @@ export class ResponseDisplay extends React.Component {
|
||||
return (
|
||||
<div className="response-display">
|
||||
{this.allowFileUpload && <SubmissionFiles files={this.submittedFiles} />}
|
||||
{this.allowFileUpload && <PreviewDisplay files={this.submittedFiles} />}
|
||||
{
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
this.textContents.map((textContent, index) => (
|
||||
|
||||
11
src/data/constants/files.js
Normal file
11
src/data/constants/files.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
export const FileTypes = StrictDict({
|
||||
pdf: 'pdf',
|
||||
jpg: 'jpg',
|
||||
jpeg: 'jpeg',
|
||||
png: 'png',
|
||||
bmp: 'bmp',
|
||||
});
|
||||
|
||||
export default FileTypes;
|
||||
@@ -9,11 +9,13 @@ Phasellus porttitor vel magna et auctor. Nulla porttitor convallis aliquam. Done
|
||||
const descriptiveText = (fileName) => `This is some descriptive text for (${fileName}). Phasellus tempor eros aliquam ipsum molestie, vitae varius lectus tempus. Morbi iaculis, libero euismod vehicula rutrum, nisi leo volutpat diam, quis commodo ex nunc ut odio. Pellentesque condimentum feugiat erat ac vulputate. Pellentesque porta rutrum sagittis. Curabitur vulputate tempus accumsan. Fusce bibendum gravida metus a scelerisque. Mauris fringilla orci non lobortis commodo. Quisque iaculis, quam a tincidunt vehicula, erat nisi accumsan quam, eu cursus ligula magna id odio. Nulla porttitor, lorem gravida vehicula tristique, sapien metus tristique ex, id tincidunt sapien justo nec sapien. Maecenas luctus, nisl vestibulum scelerisque pharetra, ligula orci vulputate turpis, in ultrices mauris dolor eu enim. Suspendisse quis nibh nec augue semper maximus. Morbi maximus eleifend magna.`;
|
||||
|
||||
const allFiles = [
|
||||
'presentation.pdf',
|
||||
'example.jpg',
|
||||
'diagram.png',
|
||||
'notes.doc',
|
||||
'recording.wav',
|
||||
'edX_2021_Internal_BrandTMGuidelines_V1.0.9.pdf',
|
||||
'irs_p5563.pdf',
|
||||
'mit_Cohen_GRL16.pdf',
|
||||
'sample.bmp',
|
||||
'sample.jpg',
|
||||
'sample.png',
|
||||
'sample.jpeg',
|
||||
];
|
||||
|
||||
const getFiles = (submissionUUID) => {
|
||||
@@ -23,9 +25,9 @@ const getFiles = (submissionUUID) => {
|
||||
for (let i = 0; i < numFiles; i++) {
|
||||
const fileName = `${submissionUUID}_${allFiles[i]}`;
|
||||
files.push({
|
||||
name: fileName,
|
||||
name: allFiles[i],
|
||||
description: descriptiveText(fileName),
|
||||
downloadURL: `/download/${fileName}/`,
|
||||
downloadUrl: allFiles[i],
|
||||
});
|
||||
}
|
||||
return files;
|
||||
|
||||
@@ -29,6 +29,21 @@ jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getLoginRedirectUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
Document: () => <div>Document</div>,
|
||||
Image: () => <div>Image</div>,
|
||||
Page: () => <div>Page</div>,
|
||||
PDFViewer: jest.fn(() => null),
|
||||
StyleSheet: { create: () => {} },
|
||||
Text: () => <div>Text</div>,
|
||||
View: () => <div>View</div>,
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
}));
|
||||
/*
|
||||
jest.mock('react-pdf/node_modules/pdfjs-dist/build/pdf.worker.entry', () => (
|
||||
jest.requireActual('react-pdf/dist/umd/entry.jest')
|
||||
));
|
||||
*/
|
||||
const configureStore = () => redux.createStore(
|
||||
reducers,
|
||||
redux.compose(redux.applyMiddleware(thunk)),
|
||||
@@ -153,9 +168,8 @@ const checkLoadedResponses = async (currentIndex) => {
|
||||
};
|
||||
|
||||
describe('ESG app integration tests', () => {
|
||||
beforeAll(() => mockApi());
|
||||
|
||||
test('initialState', async () => {
|
||||
mockApi();
|
||||
await renderEl();
|
||||
expect(state.app).toEqual(
|
||||
jest.requireActual('data/redux/app/reducer').initialState,
|
||||
@@ -177,12 +191,10 @@ describe('ESG app integration tests', () => {
|
||||
});
|
||||
|
||||
describe('table selection', () => {
|
||||
beforeAll(async () => {
|
||||
it('loads selected submission ids', async () => {
|
||||
await renderEl();
|
||||
await initialize();
|
||||
await makeTableSelections();
|
||||
});
|
||||
it('loads selected submission ids', () => {
|
||||
expect(state.grading.selected).toEqual(submissionUUIDs);
|
||||
});
|
||||
test('app flags, { showReview: true, isGrading: false, showRubric: false }', () => {
|
||||
|
||||
Reference in New Issue
Block a user