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:
XnpioChV
2023-03-15 11:57:55 -05:00
parent 2a5f6795d3
commit 14504073e0
37 changed files with 1317 additions and 422 deletions

View File

@@ -35,6 +35,7 @@ exports[`ImageSettingsModal render snapshot 1`] = `
</Button>
}
footerAction={null}
isFullscreenScroll={true}
isOpen={false}
size="lg"
title="Image Settings"

View File

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

View File

@@ -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({});
});
});
});

View File

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

View File

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

View File

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

View File

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

View File

@@ -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..."
/>
`;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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