feat: Video selection page created
The SelectImageModal component has been refactored so that it can also be used on the video selection page; and all its child components. Now this component is called SelectionModal and is used both for the image selector and in this new video selection screen. The assets api has been used to get the videos.
This commit is contained in:
@@ -35,6 +35,7 @@ exports[`ImageSettingsModal render snapshot 1`] = `
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="lg"
|
||||
title="Image Settings"
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
Scrollable, SelectableBox, Spinner,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
import GalleryCard from './GalleryCard';
|
||||
|
||||
export const Gallery = ({
|
||||
galleryIsEmpty,
|
||||
searchIsEmpty,
|
||||
displayList,
|
||||
highlighted,
|
||||
onHighlightChange,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
isLoaded,
|
||||
}) => {
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (galleryIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<FormattedMessage {...messages.emptyGalleryLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (searchIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<FormattedMessage {...messages.emptySearchLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height: '375px' }}>
|
||||
<div className="p-4">
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
name="images"
|
||||
onChange={onHighlightChange}
|
||||
type="radio"
|
||||
value={highlighted}
|
||||
>
|
||||
{displayList.map(img => <GalleryCard key={img.id} img={img} />)}
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
);
|
||||
};
|
||||
|
||||
Gallery.defaultProps = {
|
||||
highlighted: '',
|
||||
};
|
||||
Gallery.propTypes = {
|
||||
galleryIsEmpty: PropTypes.bool.isRequired,
|
||||
searchIsEmpty: PropTypes.bool.isRequired,
|
||||
displayList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
highlighted: PropTypes.string,
|
||||
onHighlightChange: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const requestKey = RequestKeys.fetchAssets;
|
||||
export const mapStateToProps = (state) => ({
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(Gallery));
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { Gallery, mapStateToProps, mapDispatchToProps } from './Gallery';
|
||||
|
||||
jest.mock('../../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFinished: (state, { requestKey }) => ({ isFinished: { state, requestKey } }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./GalleryCard', () => 'GalleryCard');
|
||||
|
||||
describe('TextEditor Image Gallery component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
galleryIsEmpty: false,
|
||||
searchIsEmpty: false,
|
||||
displayList: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
highlighted: 'props.highlighted',
|
||||
onHighlightChange: jest.fn().mockName('props.onHighlightChange'),
|
||||
intl: { formatMessage },
|
||||
isLoaded: true,
|
||||
};
|
||||
test('snapshot: not loaded, show spinner', () => {
|
||||
expect(shallow(<Gallery {...props} isLoaded={false} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded but no images, show empty gallery', () => {
|
||||
expect(shallow(<Gallery {...props} galleryIsEmpty />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded but search returns no images, show 0 search result gallery', () => {
|
||||
expect(shallow(<Gallery {...props} searchIsEmpty />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded, show gallery', () => {
|
||||
expect(shallow(<Gallery {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'testState' };
|
||||
test('loads isLoaded from requests.isFinished selector for fetchAssets request', () => {
|
||||
expect(mapStateToProps(testState).isLoaded).toEqual(
|
||||
selectors.requests.isFinished(testState, { requestKey: RequestKeys.fetchAssets }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('is empty', () => {
|
||||
expect(mapDispatchToProps).toEqual({});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Image, SelectableBox } from '@edx/paragon';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const GalleryCard = ({
|
||||
img,
|
||||
}) => (
|
||||
<SelectableBox className="card bg-white" key={img.externalUrl} type="radio" value={img.id}>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<Image
|
||||
style={{ width: '100px', height: '100px' }}
|
||||
src={img.externalUrl}
|
||||
/>
|
||||
<div className="img-text p-3">
|
||||
<h3>{img.displayName}</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={img.dateAdded} />,
|
||||
time: <FormattedTime value={img.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
);
|
||||
|
||||
GalleryCard.propTypes = {
|
||||
img: PropTypes.shape({
|
||||
contentType: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
dateAdded: PropTypes.number,
|
||||
locked: PropTypes.bool,
|
||||
portableUrl: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
export default GalleryCard;
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Image } from '@edx/paragon';
|
||||
import { GalleryCard } from './GalleryCard';
|
||||
|
||||
describe('GalleryCard component', () => {
|
||||
const img = {
|
||||
externalUrl: 'props.img.externalUrl',
|
||||
displayName: 'props.img.displayName',
|
||||
dateAdded: 12345,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GalleryCard img={img} />);
|
||||
});
|
||||
test(`snapshot: dateAdded=${img.dateAdded}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('loads Image with src from image external url', () => {
|
||||
expect(el.find(Image).props().src).toEqual(img.externalUrl);
|
||||
});
|
||||
});
|
||||
@@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow, Dropdown, Form, Icon, IconButton,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Search } from '@edx/paragon/icons';
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { sortKeys, sortMessages } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const SearchSort = ({
|
||||
searchString,
|
||||
onSearchChange,
|
||||
clearSearchString,
|
||||
sortBy,
|
||||
onSortClick,
|
||||
// injected
|
||||
intl,
|
||||
}) => (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
<Form.Control
|
||||
autoFocus
|
||||
onChange={onSearchChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
trailingElement={
|
||||
searchString
|
||||
? (
|
||||
<IconButton
|
||||
iconAs={Icon}
|
||||
invertColors
|
||||
isActive
|
||||
onClick={clearSearchString}
|
||||
size="sm"
|
||||
src={Close}
|
||||
/>
|
||||
)
|
||||
: <Icon src={Search} />
|
||||
}
|
||||
value={searchString}
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="img-sort-button" variant="tertiary">
|
||||
<FormattedMessage {...sortMessages[sortBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{Object.keys(sortKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onSortClick(key)}>
|
||||
<FormattedMessage {...sortMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
SearchSort.propTypes = {
|
||||
searchString: PropTypes.string.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
clearSearchString: PropTypes.func.isRequired,
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
onSortClick: PropTypes.func.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchSort);
|
||||
@@ -1,43 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
|
||||
import { sortKeys, sortMessages } from './utils';
|
||||
import { SearchSort } from './SearchSort';
|
||||
|
||||
describe('SearchSort component', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('snapshots', () => {
|
||||
test('with search string (close button)', () => {
|
||||
expect(shallow(<SearchSort {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('without search string (search icon)', () => {
|
||||
expect(shallow(<SearchSort {...props} searchString="" />)).toMatchSnapshot();
|
||||
});
|
||||
test('adds a sort option for each sortKey', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateNewest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.dateOldest} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameAscending} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...sortMessages.nameDescending} />,
|
||||
)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,91 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded but no images, show empty gallery 1`] = `
|
||||
<div
|
||||
className="gallery p-4 bg-gray-100"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No images found in your gallery. Please upload an image using the button below."
|
||||
description="Label for when image gallery is empty."
|
||||
id="authoring.texteditor.selectimagemodal.emptyGalleryLabel"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded but search returns no images, show 0 search result gallery 1`] = `
|
||||
<div
|
||||
className="gallery p-4 bg-gray-100"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No search results."
|
||||
description="Label for when search returns nothing."
|
||||
id="authoring.texteditor.selectimagemodal.emptySearchLabel"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded, show gallery 1`] = `
|
||||
<Scrollable
|
||||
className="gallery bg-gray-100"
|
||||
style={
|
||||
Object {
|
||||
"height": "375px",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="p-4"
|
||||
>
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
name="images"
|
||||
onChange={[MockFunction props.onHighlightChange]}
|
||||
type="radio"
|
||||
value="props.highlighted"
|
||||
>
|
||||
<GalleryCard
|
||||
img={
|
||||
Object {
|
||||
"id": 1,
|
||||
}
|
||||
}
|
||||
key="1"
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
Object {
|
||||
"id": 2,
|
||||
}
|
||||
}
|
||||
key="2"
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
Object {
|
||||
"id": 3,
|
||||
}
|
||||
}
|
||||
key="3"
|
||||
/>
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = `
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
`;
|
||||
@@ -1,47 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
key="props.img.externalUrl"
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
>
|
||||
<Image
|
||||
src="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"width": "100px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="img-text p-3"
|
||||
>
|
||||
<h3>
|
||||
props.img.displayName
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.texteditor.selectimagemodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
value={12345}
|
||||
/>,
|
||||
"time": <FormattedTime
|
||||
value={12345}
|
||||
/>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</SelectableBox>
|
||||
`;
|
||||
@@ -1,152 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchSort component snapshots with search string (close button) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={
|
||||
<IconButton
|
||||
iconAs="Icon"
|
||||
invertColors={true}
|
||||
isActive={true}
|
||||
onClick={[MockFunction props.clearSearchString]}
|
||||
size="sm"
|
||||
src={[MockFunction icons.Close]}
|
||||
/>
|
||||
}
|
||||
value="props.searchString"
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots without search string (search icon) 1`] = `
|
||||
<ActionRow>
|
||||
<Form.Group
|
||||
style={
|
||||
Object {
|
||||
"margin": 0,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Form.Control
|
||||
autoFocus={true}
|
||||
onChange={[MockFunction props.onSearchChange]}
|
||||
placeholder="Search"
|
||||
trailingElement={<Icon />}
|
||||
value=""
|
||||
/>
|
||||
</Form.Group>
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="dateNewest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (newest)"
|
||||
description="Dropdown label for sorting by date (newest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.datenewest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="dateOldest"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By date added (oldest)"
|
||||
description="Dropdown label for sorting by date (oldest)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.dateoldest.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameAscending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (ascending)"
|
||||
description="Dropdown label for sorting by name (ascending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.nameascending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="nameDescending"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="By name (descending)"
|
||||
description="Dropdown label for sorting by name (descending)"
|
||||
id="authoring.texteditor.selectimagemodal.sort.namedescending.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</ActionRow>
|
||||
`;
|
||||
@@ -1,189 +1,94 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectImageModal component snapshot 1`] = `
|
||||
<BaseModal
|
||||
close={[MockFunction props.close]}
|
||||
confirmAction={
|
||||
<Button
|
||||
select="btnProps"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
description="Label for Next button"
|
||||
id="authoring.texteditor.selectimagemodal.next.label"
|
||||
/>
|
||||
</Button>
|
||||
<SelectionModal
|
||||
acceptedFiles={
|
||||
Object {
|
||||
"gif": ".gif",
|
||||
"ico": ".ico",
|
||||
"jpeg": ".jpeg",
|
||||
"jpg": ".jpg",
|
||||
"png": ".png",
|
||||
"tif": ".tif",
|
||||
"tiff": ".tiff",
|
||||
}
|
||||
}
|
||||
footerAction={
|
||||
<Button
|
||||
onClick="imgHooks.fileInput.click"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload a new image (10 MB max)"
|
||||
description="Label for upload button"
|
||||
id="authoring.texteditor.selectimagemodal.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
close={[MockFunction props.close]}
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
galleryError={
|
||||
Object {
|
||||
"dismiss": [MockFunction],
|
||||
"message": Object {
|
||||
"defaultMessage": "Gallery error",
|
||||
"description": "Gallery error",
|
||||
"id": "Gallery error id",
|
||||
},
|
||||
"set": [MockFunction],
|
||||
"show": "ShoWERror gAlLery",
|
||||
}
|
||||
}
|
||||
galleryProps={
|
||||
Object {
|
||||
"gallery": "props",
|
||||
}
|
||||
}
|
||||
inputError={
|
||||
Object {
|
||||
"dismiss": [MockFunction],
|
||||
"message": Object {
|
||||
"defaultMessage": "Input error",
|
||||
"description": "Input error",
|
||||
"id": "Input error id",
|
||||
},
|
||||
"set": [MockFunction],
|
||||
"show": "ShoWERror inPUT",
|
||||
}
|
||||
}
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
modalMessages={
|
||||
Object {
|
||||
"confirmMsg": Object {
|
||||
"defaultMessage": "Next",
|
||||
"description": "Label for Next button",
|
||||
"id": "authoring.texteditor.selectimagemodal.next.label",
|
||||
},
|
||||
"fetchError": Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
},
|
||||
"titleMsg": Object {
|
||||
"defaultMessage": "Add an image",
|
||||
"description": "Title for the select image modal",
|
||||
"id": "authoring.texteditor.selectimagemodal.title.label",
|
||||
},
|
||||
"uploadButtonMsg": Object {
|
||||
"defaultMessage": "Upload a new image (10 MB max)",
|
||||
"description": "Label for upload button",
|
||||
"id": "authoring.texteditor.selectimagemodal.upload.label",
|
||||
},
|
||||
"uploadError": Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
},
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror inPUT"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
|
||||
description=" Message presented to user when file size of image is larger than 10 MB"
|
||||
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror gAlLery"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select an image to continue."
|
||||
description="Message presented to user when clicking Next without selecting an image"
|
||||
id="authoring.texteditor.selectimagemodal.error.selectImageError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<SearchSort
|
||||
search="sortProps"
|
||||
/>
|
||||
<Gallery
|
||||
gallery="props"
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
`;
|
||||
|
||||
exports[`SelectImageModal component snapshot: uploaded image not loaded, show spinner 1`] = `
|
||||
<BaseModal
|
||||
close={[MockFunction props.close]}
|
||||
confirmAction={
|
||||
<Button
|
||||
select="btnProps"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Next"
|
||||
description="Label for Next button"
|
||||
id="authoring.texteditor.selectimagemodal.next.label"
|
||||
/>
|
||||
</Button>
|
||||
}
|
||||
footerAction={
|
||||
<Button
|
||||
onClick="imgHooks.fileInput.click"
|
||||
variant="link"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Upload a new image (10 MB max)"
|
||||
description="Label for upload button"
|
||||
id="authoring.texteditor.selectimagemodal.upload.label"
|
||||
/>
|
||||
</Button>
|
||||
searchSortProps={
|
||||
Object {
|
||||
"search": "sortProps",
|
||||
}
|
||||
}
|
||||
isOpen={true}
|
||||
title="Add an image"
|
||||
>
|
||||
<FetchErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to obtain course images. Please try again.",
|
||||
"description": "Message presented to user when images are not found",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.fetchImagesError",
|
||||
}
|
||||
selectBtnProps={
|
||||
Object {
|
||||
"select": "btnProps",
|
||||
}
|
||||
/>
|
||||
<UploadErrorAlert
|
||||
message={
|
||||
Object {
|
||||
"defaultMessage": "Failed to upload image. Please try again.",
|
||||
"description": "Message presented to user when image fails to upload",
|
||||
"id": "authoring.texteditor.selectimagemodal.error.uploadImageError",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror inPUT"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Images must be 10 MB or less. Please resize image and try again."
|
||||
description=" Message presented to user when file size of image is larger than 10 MB"
|
||||
id="authoring.texteditor.selectimagemodal.error.fileSizeError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<ErrorAlert
|
||||
dismissError={[MockFunction]}
|
||||
hideHeading={true}
|
||||
isError="ShoWERror gAlLery"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Select an image to continue."
|
||||
description="Message presented to user when clicking Next without selecting an image"
|
||||
id="authoring.texteditor.selectimagemodal.error.selectImageError"
|
||||
/>
|
||||
</ErrorAlert>
|
||||
<Stack
|
||||
gap={3}
|
||||
>
|
||||
<SearchSort
|
||||
search="sortProps"
|
||||
/>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
/>
|
||||
<FileInput
|
||||
acceptedFiles=".gif,.jpg,.jpeg,.png,.tif,.tiff,.ico"
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "imgHooks.fileInput.addFile",
|
||||
"click": "imgHooks.fileInput.click",
|
||||
"ref": "imgHooks.fileInput.ref",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useDispatch } from 'react-redux';
|
||||
|
||||
import { thunkActions } from '../../../data/redux';
|
||||
import * as module from './hooks';
|
||||
import { sortFunctions, sortKeys } from './utils';
|
||||
import { sortFunctions, sortKeys, sortMessages } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const state = {
|
||||
highlighted: (val) => React.useState(val),
|
||||
@@ -22,6 +23,8 @@ export const searchAndSortHooks = () => {
|
||||
clearSearchString: () => setSearchString(''),
|
||||
sortBy,
|
||||
onSortClick: (key) => () => setSortBy(key),
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -49,11 +52,13 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
show: showSelectImageError,
|
||||
set: () => setShowSelectImageError(true),
|
||||
dismiss: () => setShowSelectImageError(false),
|
||||
message: messages.selectImageError,
|
||||
},
|
||||
inputError: {
|
||||
show: showSizeError,
|
||||
set: () => setShowSizeError(true),
|
||||
dismiss: () => setShowSizeError(false),
|
||||
message: messages.fileSizeError,
|
||||
},
|
||||
images,
|
||||
galleryProps: {
|
||||
@@ -62,6 +67,7 @@ export const imgListHooks = ({ searchSortProps, setSelection, images }) => {
|
||||
displayList: list,
|
||||
highlighted,
|
||||
onHighlightChange: (e) => setHighlighted(e.target.value),
|
||||
emptyGalleryLabel: messages.emptyGalleryLabel,
|
||||
},
|
||||
// highlight by id
|
||||
selectBtnProps: {
|
||||
|
||||
@@ -1,27 +1,8 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Stack, Spinner } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
|
||||
import hooks from './hooks';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
import SelectionModal from '../../SelectionModal';
|
||||
import messages from './messages';
|
||||
import BaseModal from '../../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from '../../FileInput';
|
||||
import FetchErrorAlert from '../../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../../ErrorAlerts/UploadErrorAlert';
|
||||
import ErrorAlert from '../../ErrorAlerts/ErrorAlert';
|
||||
|
||||
export const SelectImageModal = ({
|
||||
isOpen,
|
||||
@@ -29,10 +10,6 @@ export const SelectImageModal = ({
|
||||
setSelection,
|
||||
clearSelection,
|
||||
images,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
inputIsLoading,
|
||||
}) => {
|
||||
const {
|
||||
galleryError,
|
||||
@@ -43,53 +20,29 @@ export const SelectImageModal = ({
|
||||
selectBtnProps,
|
||||
} = hooks.imgHooks({ setSelection, clearSelection, images });
|
||||
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
confirmAction={(
|
||||
<Button {...selectBtnProps} variant="primary">
|
||||
<FormattedMessage {...messages.nextButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
footerAction={(
|
||||
<Button iconBefore={Add} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...messages.uploadButtonLabel} />
|
||||
</Button>
|
||||
)}
|
||||
title={intl.formatMessage(messages.titleLabel)}
|
||||
>
|
||||
{/* Error Alerts */}
|
||||
<FetchErrorAlert message={messages.fetchImagesError} />
|
||||
<UploadErrorAlert message={messages.uploadImageError} />
|
||||
<ErrorAlert
|
||||
dismissError={inputError.dismiss}
|
||||
hideHeading
|
||||
isError={inputError.show}
|
||||
>
|
||||
<FormattedMessage {...messages.fileSizeError} />
|
||||
</ErrorAlert>
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.nextButtonLabel,
|
||||
titleMsg: messages.titleLabel,
|
||||
uploadButtonMsg: messages.uploadButtonLabel,
|
||||
fetchError: messages.fetchImagesError,
|
||||
uploadError: messages.uploadImageError,
|
||||
};
|
||||
|
||||
{/* User Feedback Alerts */}
|
||||
<ErrorAlert
|
||||
dismissError={galleryError.dismiss}
|
||||
hideHeading
|
||||
isError={galleryError.show}
|
||||
>
|
||||
<FormattedMessage {...messages.selectImageError} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={3}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
{!inputIsLoading ? <Gallery {...galleryProps} /> : (
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText={intl.formatMessage(messages.loading)}
|
||||
/>
|
||||
)}
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedImgKeys).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
return (
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen,
|
||||
close,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -99,16 +52,6 @@ SelectImageModal.propTypes = {
|
||||
setSelection: PropTypes.func.isRequired,
|
||||
clearSelection: PropTypes.func.isRequired,
|
||||
images: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
inputIsLoading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
inputIsLoading: selectors.requests.isPending(state, { requestKey: RequestKeys.uploadAsset }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SelectImageModal));
|
||||
export default SelectImageModal;
|
||||
|
||||
@@ -2,22 +2,11 @@ import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
import BaseModal from '../../BaseModal';
|
||||
import FileInput from '../../FileInput';
|
||||
import Gallery from './Gallery';
|
||||
import SearchSort from './SearchSort';
|
||||
import SelectionModal from '../../SelectionModal';
|
||||
import hooks from './hooks';
|
||||
import { SelectImageModal, mapStateToProps, mapDispatchToProps } from '.';
|
||||
import { SelectImageModal } from '.';
|
||||
|
||||
jest.mock('../../BaseModal', () => 'BaseModal');
|
||||
jest.mock('../../FileInput', () => 'FileInput');
|
||||
jest.mock('./Gallery', () => 'Gallery');
|
||||
jest.mock('./SearchSort', () => 'SearchSort');
|
||||
jest.mock('../../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
|
||||
jest.mock('../../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
|
||||
jest.mock('../..//ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
|
||||
jest.mock('../../SelectionModal', () => 'SelectionModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
imgHooks: jest.fn(() => ({
|
||||
@@ -25,11 +14,21 @@ jest.mock('./hooks', () => ({
|
||||
show: 'ShoWERror gAlLery',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Gallery error id',
|
||||
defaultMessage: 'Gallery error',
|
||||
description: 'Gallery error',
|
||||
},
|
||||
},
|
||||
inputError: {
|
||||
show: 'ShoWERror inPUT',
|
||||
set: jest.fn(),
|
||||
dismiss: jest.fn(),
|
||||
message: {
|
||||
id: 'Input error id',
|
||||
defaultMessage: 'Input error',
|
||||
description: 'Input error',
|
||||
},
|
||||
},
|
||||
fileInput: {
|
||||
addFile: 'imgHooks.fileInput.addFile',
|
||||
@@ -58,7 +57,6 @@ describe('SelectImageModal', () => {
|
||||
setSelection: jest.fn().mockName('props.setSelection'),
|
||||
clearSelection: jest.fn().mockName('props.clearSelection'),
|
||||
intl: { formatMessage },
|
||||
inputIsLoading: false,
|
||||
};
|
||||
let el;
|
||||
const imgHooks = hooks.imgHooks();
|
||||
@@ -68,42 +66,24 @@ describe('SelectImageModal', () => {
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: uploaded image not loaded, show spinner', () => {
|
||||
props.inputIsLoading = true;
|
||||
expect(shallow(<SelectImageModal {...props} />)).toMatchSnapshot();
|
||||
props.inputIsLoading = false;
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(BaseModal).props().confirmAction.props).toEqual(
|
||||
expect.objectContaining({ ...hooks.imgHooks().selectBtnProps, variant: 'primary' }),
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
expect.objectContaining({ ...hooks.imgHooks().selectBtnProps }),
|
||||
);
|
||||
});
|
||||
it('provides file upload button linked to fileInput.click', () => {
|
||||
expect(el.find(BaseModal).props().footerAction.props.onClick).toEqual(
|
||||
expect(el.find(SelectionModal).props().fileInput.click).toEqual(
|
||||
imgHooks.fileInput.click,
|
||||
);
|
||||
});
|
||||
it('provides a SearchSort component with searchSortProps from imgHooks', () => {
|
||||
expect(el.find(SearchSort).props()).toEqual(imgHooks.searchSortProps);
|
||||
expect(el.find(SelectionModal).props().searchSortProps).toEqual(imgHooks.searchSortProps);
|
||||
});
|
||||
it('provides a Gallery component with galleryProps from imgHooks', () => {
|
||||
expect(el.find(Gallery).props()).toEqual(imgHooks.galleryProps);
|
||||
expect(el.find(SelectionModal).props().galleryProps).toEqual(imgHooks.galleryProps);
|
||||
});
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(FileInput).props()).toMatchObject({ fileInput: imgHooks.fileInput });
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'testState' };
|
||||
test('loads inputIsLoading from requests.isPending selector for uploadAsset request', () => {
|
||||
expect(mapStateToProps(testState).inputIsLoading).toEqual(
|
||||
selectors.requests.isPending(testState, { requestKey: RequestKeys.uploadAsset }),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('is empty', () => {
|
||||
expect(mapDispatchToProps).toEqual({});
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(imgHooks.fileInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -17,11 +17,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Add an image',
|
||||
description: 'Title for the select image modal',
|
||||
},
|
||||
searchPlaceholder: {
|
||||
id: 'authoring.texteditor.selectimagemodal.search.placeholder',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Placeholder text for search bar',
|
||||
},
|
||||
|
||||
// Sort Dropdown
|
||||
sortByDateNewest: {
|
||||
@@ -46,27 +41,12 @@ const messages = defineMessages({
|
||||
},
|
||||
|
||||
// Gallery
|
||||
addedDate: {
|
||||
id: 'authoring.texteditor.selectimagemodal.addedDate.label',
|
||||
defaultMessage: 'Added {date} at {time}',
|
||||
description: 'File date-added string',
|
||||
},
|
||||
loading: {
|
||||
id: 'authoring.texteditor.selectimagemodal.spinner.readertext',
|
||||
defaultMessage: 'loading...',
|
||||
description: 'Gallery loading spinner screen-reader text',
|
||||
},
|
||||
emptyGalleryLabel: {
|
||||
id: 'authoring.texteditor.selectimagemodal.emptyGalleryLabel',
|
||||
defaultMessage:
|
||||
'No images found in your gallery. Please upload an image using the button below.',
|
||||
description: 'Label for when image gallery is empty.',
|
||||
},
|
||||
emptySearchLabel: {
|
||||
id: 'authoring.texteditor.selectimagemodal.emptySearchLabel',
|
||||
defaultMessage: 'No search results.',
|
||||
description: 'Label for when search returns nothing.',
|
||||
},
|
||||
|
||||
// Errors
|
||||
uploadImageError: {
|
||||
|
||||
Reference in New Issue
Block a user