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:
34
src/editors/Selector.jsx
Normal file
34
src/editors/Selector.jsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import VideoGallery from './containers/VideoGallery';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
export const Selector = ({
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}) => {
|
||||
const dispatch = useDispatch();
|
||||
hooks.initializeApp({
|
||||
dispatch,
|
||||
data: {
|
||||
blockId: '',
|
||||
blockType: 'video',
|
||||
learningContextId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
},
|
||||
});
|
||||
return (
|
||||
<VideoGallery />
|
||||
);
|
||||
};
|
||||
|
||||
Selector.propTypes = {
|
||||
learningContextId: PropTypes.string.isRequired,
|
||||
lmsEndpointUrl: PropTypes.string.isRequired,
|
||||
studioEndpointUrl: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Selector;
|
||||
40
src/editors/Selector.test.jsx
Normal file
40
src/editors/Selector.test.jsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { shallow } from 'enzyme';
|
||||
import * as hooks from './hooks';
|
||||
import Selector from './Selector';
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
initializeApp: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./containers/VideoGallery', () => 'VideoGallery');
|
||||
|
||||
const props = {
|
||||
learningContextId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
};
|
||||
|
||||
const initData = {
|
||||
blockId: '',
|
||||
blockType: 'video',
|
||||
...props,
|
||||
};
|
||||
|
||||
describe('Editor', () => {
|
||||
describe('render', () => {
|
||||
test('rendering correctly with expected Input', () => {
|
||||
expect(shallow(<Selector {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('calls initializeApp hook with dispatch, and passed data', () => {
|
||||
shallow(<Selector {...props} />);
|
||||
expect(hooks.initializeApp).toHaveBeenCalledWith({
|
||||
dispatch: useDispatch(),
|
||||
data: initData,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
38
src/editors/SelectorPage.jsx
Normal file
38
src/editors/SelectorPage.jsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
import { Provider } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
import ErrorBoundary from './sharedComponents/ErrorBoundary';
|
||||
import { Selector } from './Selector';
|
||||
import store from './data/store';
|
||||
|
||||
const SelectorPage = ({
|
||||
courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}) => (
|
||||
<ErrorBoundary>
|
||||
<Provider store={store}>
|
||||
<Selector
|
||||
{...{
|
||||
learningContextId: courseId,
|
||||
lmsEndpointUrl,
|
||||
studioEndpointUrl,
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
SelectorPage.defaultProps = {
|
||||
courseId: null,
|
||||
lmsEndpointUrl: null,
|
||||
studioEndpointUrl: null,
|
||||
};
|
||||
|
||||
SelectorPage.propTypes = {
|
||||
courseId: PropTypes.string,
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
studioEndpointUrl: PropTypes.string,
|
||||
};
|
||||
|
||||
export default SelectorPage;
|
||||
25
src/editors/SelectorPage.test.jsx
Normal file
25
src/editors/SelectorPage.test.jsx
Normal file
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import SelectorPage from './SelectorPage';
|
||||
|
||||
const props = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
lmsEndpointUrl: 'evenfakerurl.com',
|
||||
studioEndpointUrl: 'fakeurl.com',
|
||||
};
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
Provider: 'Provider',
|
||||
}));
|
||||
jest.mock('./Selector', () => 'Selector');
|
||||
|
||||
describe('Selector Page', () => {
|
||||
describe('snapshots', () => {
|
||||
test('rendering correctly with expected Input', () => {
|
||||
expect(shallow(<SelectorPage {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('rendering with props to null', () => {
|
||||
expect(shallow(<SelectorPage />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
3
src/editors/__snapshots__/Selector.test.jsx.snap
Normal file
3
src/editors/__snapshots__/Selector.test.jsx.snap
Normal file
@@ -0,0 +1,3 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Editor render rendering correctly with expected Input 1`] = `<VideoGallery />`;
|
||||
45
src/editors/__snapshots__/SelectorPage.test.jsx.snap
Normal file
45
src/editors/__snapshots__/SelectorPage.test.jsx.snap
Normal file
@@ -0,0 +1,45 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Selector Page snapshots rendering correctly with expected Input 1`] = `
|
||||
<ErrorBoundary>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
learningContextId="course-v1:edX+DemoX+Demo_Course"
|
||||
lmsEndpointUrl="evenfakerurl.com"
|
||||
studioEndpointUrl="fakeurl.com"
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
`;
|
||||
|
||||
exports[`Selector Page snapshots rendering with props to null 1`] = `
|
||||
<ErrorBoundary>
|
||||
<Provider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<Component
|
||||
learningContextId={null}
|
||||
lmsEndpointUrl={null}
|
||||
studioEndpointUrl={null}
|
||||
/>
|
||||
</Provider>
|
||||
</ErrorBoundary>
|
||||
`;
|
||||
@@ -25,6 +25,7 @@ exports[`EditorContainer component render snapshot: initialized. enable save and
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
@@ -103,6 +104,7 @@ exports[`EditorContainer component render snapshot: not initialized. disable sav
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title="Exit the editor?"
|
||||
|
||||
@@ -21,6 +21,7 @@ exports[`SwitchToAdvancedEditorCard snapshot snapshot: SwitchToAdvancedEditorCar
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="md"
|
||||
title={
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`VideoGallery component snapshot 1`] = `
|
||||
<div>
|
||||
<SelectionModal
|
||||
acceptedFiles={
|
||||
Object {
|
||||
"mp4": ".mp4",
|
||||
}
|
||||
}
|
||||
close={[Function]}
|
||||
fileInput={
|
||||
Object {
|
||||
"addFile": "videoHooks.fileInput.addFile",
|
||||
"click": "videoHooks.fileInput.click",
|
||||
"ref": "videoHooks.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",
|
||||
}
|
||||
}
|
||||
isFullscreenScroll={false}
|
||||
isOpen={true}
|
||||
modalMessages={
|
||||
Object {
|
||||
"confirmMsg": Object {
|
||||
"defaultMessage": "Select video",
|
||||
"description": "Label for Select video button",
|
||||
"id": "authoring.selectvideomodal.selectvideo.label",
|
||||
},
|
||||
"fetchError": Object {
|
||||
"defaultMessage": "Failed to obtain course videos. Please try again.",
|
||||
"description": "Message presented to user when videos are not found",
|
||||
"id": "authoring.selectvideomodal.error.fetchVideosError",
|
||||
},
|
||||
"titleMsg": Object {
|
||||
"defaultMessage": "Add video to your course",
|
||||
"description": "Title for the select video modal",
|
||||
"id": "authoring.selectvideomodal.title.label",
|
||||
},
|
||||
"uploadButtonMsg": Object {
|
||||
"defaultMessage": "Upload or embed a new video",
|
||||
"description": "Label for upload button",
|
||||
"id": "authoring.selectvideomodal.upload.label",
|
||||
},
|
||||
"uploadError": Object {
|
||||
"defaultMessage": "Failed to upload video. Please try again.",
|
||||
"description": "Message presented to user when video fails to upload",
|
||||
"id": "authoring.selectvideomodal.error.uploadVideoError",
|
||||
},
|
||||
}
|
||||
}
|
||||
searchSortProps={
|
||||
Object {
|
||||
"search": "sortProps",
|
||||
}
|
||||
}
|
||||
selectBtnProps={
|
||||
Object {
|
||||
"select": "btnProps",
|
||||
}
|
||||
}
|
||||
size="fullscreen"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
134
src/editors/containers/VideoGallery/hooks.js
Normal file
134
src/editors/containers/VideoGallery/hooks.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React from 'react';
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
import {
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
} from './utils';
|
||||
|
||||
export const state = {
|
||||
highlighted: (val) => React.useState(val),
|
||||
searchString: (val) => React.useState(val),
|
||||
showSelectVideoError: (val) => React.useState(val),
|
||||
showSizeError: (val) => React.useState(val),
|
||||
sortBy: (val) => React.useState(val),
|
||||
filertBy: (val) => React.useState(val),
|
||||
hideSelectedVideos: (val) => React.useState(val),
|
||||
};
|
||||
|
||||
export const searchAndSortHooks = () => {
|
||||
const [searchString, setSearchString] = module.state.searchString('');
|
||||
const [sortBy, setSortBy] = module.state.sortBy(sortKeys.dateNewest);
|
||||
const [filterBy, setFilterBy] = module.state.filertBy(filterKeys.videoStatus);
|
||||
const [hideSelectedVideos, setHideSelectedVideos] = module.state.hideSelectedVideos(false);
|
||||
|
||||
return {
|
||||
searchString,
|
||||
onSearchChange: (e) => setSearchString(e.target.value),
|
||||
clearSearchString: () => setSearchString(''),
|
||||
sortBy,
|
||||
onSortClick: (key) => () => setSortBy(key),
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterBy,
|
||||
onFilterClick: (key) => () => setFilterBy(key),
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch: true,
|
||||
hideSelectedVideos,
|
||||
switchMessage: messages.hideSelectedCourseVideosSwitchLabel,
|
||||
onSwitchClick: () => setHideSelectedVideos(!hideSelectedVideos),
|
||||
};
|
||||
};
|
||||
|
||||
export const videoListHooks = ({ videos }) => {
|
||||
const [highlighted, setHighlighted] = module.state.highlighted(null);
|
||||
const [
|
||||
showSelectVideoError,
|
||||
setShowSelectVideoError,
|
||||
] = module.state.showSelectVideoError(false);
|
||||
const [
|
||||
showSizeError,
|
||||
setShowSizeError,
|
||||
] = module.state.showSizeError(false);
|
||||
const filteredList = videos; // TODO missing filters and sort
|
||||
return {
|
||||
galleryError: {
|
||||
show: showSelectVideoError,
|
||||
set: () => setShowSelectVideoError(true),
|
||||
dismiss: () => setShowSelectVideoError(false),
|
||||
message: messages.selectVideoError,
|
||||
},
|
||||
// TODO We need to update this message when implementing the video upload screen
|
||||
inputError: {
|
||||
show: showSizeError,
|
||||
set: () => setShowSizeError(true),
|
||||
dismiss: () => setShowSelectVideoError(false),
|
||||
message: messages.fileSizeError,
|
||||
},
|
||||
galleryProps: {
|
||||
galleryIsEmpty: Object.keys(filteredList).length === 0,
|
||||
searchIsEmpty: filteredList.length === 0,
|
||||
displayList: filteredList,
|
||||
highlighted,
|
||||
onHighlightChange: (e) => setHighlighted(e.target.value),
|
||||
emptyGalleryLabel: messages.emptyGalleryLabel,
|
||||
showIdsOnCards: true,
|
||||
height: '100%',
|
||||
},
|
||||
selectBtnProps: {
|
||||
onclick: () => {
|
||||
// TODO Update this when implementing the selection feature
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const fileInputHooks = () => {
|
||||
// TODO [Update video] Implement this
|
||||
const ref = React.useRef();
|
||||
const click = () => ref.current.click();
|
||||
|
||||
return {
|
||||
click,
|
||||
addFile: () => {},
|
||||
ref,
|
||||
};
|
||||
};
|
||||
|
||||
export const filterAssets = ({ assets }) => {
|
||||
let videos = [];
|
||||
const assetsList = Object.values(assets);
|
||||
if (assetsList.length > 0) {
|
||||
videos = assetsList.filter(asset => asset?.contentType?.startsWith('video/'));
|
||||
}
|
||||
return videos;
|
||||
};
|
||||
|
||||
export const videoHooks = ({ videos }) => {
|
||||
const searchSortProps = module.searchAndSortHooks();
|
||||
const videoList = module.videoListHooks({ videos });
|
||||
const {
|
||||
galleryError,
|
||||
galleryProps,
|
||||
inputError,
|
||||
selectBtnProps,
|
||||
} = videoList;
|
||||
const fileInput = module.fileInputHooks();
|
||||
|
||||
return {
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
};
|
||||
};
|
||||
|
||||
export default {
|
||||
videoHooks,
|
||||
filterAssets,
|
||||
};
|
||||
64
src/editors/containers/VideoGallery/index.jsx
Normal file
64
src/editors/containers/VideoGallery/index.jsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { selectors } from '../../data/redux';
|
||||
import hooks from './hooks';
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import { acceptedImgKeys } from './utils';
|
||||
import messages from './messages';
|
||||
|
||||
export const VideoGallery = ({
|
||||
// redux
|
||||
assets,
|
||||
}) => {
|
||||
const videos = hooks.filterAssets({ assets });
|
||||
const {
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
} = hooks.videoHooks({ videos });
|
||||
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.selectVideoButtonlabel,
|
||||
titleMsg: messages.titleLabel,
|
||||
uploadButtonMsg: messages.uploadButtonLabel,
|
||||
fetchError: messages.fetchVideosError,
|
||||
uploadError: messages.uploadVideoError,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen: true,
|
||||
close: () => { /* TODO */ },
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
VideoGallery.propTypes = {
|
||||
assets: PropTypes.shape({}).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
assets: selectors.app.assets(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoGallery);
|
||||
85
src/editors/containers/VideoGallery/index.test.jsx
Normal file
85
src/editors/containers/VideoGallery/index.test.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import hooks from './hooks';
|
||||
import * as module from '.';
|
||||
|
||||
jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal');
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
filterAssets: jest.fn(() => []),
|
||||
videoHooks: jest.fn(() => ({
|
||||
galleryError: {
|
||||
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: 'videoHooks.fileInput.addFile',
|
||||
click: 'videoHooks.fileInput.click',
|
||||
ref: 'videoHooks.fileInput.ref',
|
||||
},
|
||||
galleryProps: { gallery: 'props' },
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isPending: (state, { requestKey }) => ({ isPending: { state, requestKey } }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('VideoGallery', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
assets: { sOmEaSsET: { staTICUrl: '/assets/sOmEaSsET' } },
|
||||
};
|
||||
let el;
|
||||
const videoHooks = hooks.videoHooks();
|
||||
beforeEach(() => {
|
||||
el = shallow(<module.VideoGallery {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
expect.objectContaining({ ...hooks.videoHooks().selectBtnProps }),
|
||||
);
|
||||
});
|
||||
it('provides file upload button linked to fileInput.click', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput.click).toEqual(
|
||||
videoHooks.fileInput.click,
|
||||
);
|
||||
});
|
||||
it('provides a SearchSort component with searchSortProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoHooks.searchSortProps);
|
||||
});
|
||||
it('provides a Gallery component with galleryProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().galleryProps).toEqual(videoHooks.galleryProps);
|
||||
});
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoHooks.fileInput);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
src/editors/containers/VideoGallery/messages.js
Normal file
117
src/editors/containers/VideoGallery/messages.js
Normal file
@@ -0,0 +1,117 @@
|
||||
export const messages = {
|
||||
// Gallery
|
||||
emptyGalleryLabel: {
|
||||
id: 'authoring.selectvideomodal.emptyGalleryLabel',
|
||||
defaultMessage:
|
||||
'No videos found in your gallery. Please upload a video using the button below.',
|
||||
description: 'Label for when video gallery is empty.',
|
||||
},
|
||||
selectVideoButtonlabel: {
|
||||
id: 'authoring.selectvideomodal.selectvideo.label',
|
||||
defaultMessage: 'Select video',
|
||||
description: 'Label for Select video button',
|
||||
},
|
||||
titleLabel: {
|
||||
id: 'authoring.selectvideomodal.title.label',
|
||||
defaultMessage: 'Add video to your course',
|
||||
description: 'Title for the select video modal',
|
||||
},
|
||||
uploadButtonLabel: {
|
||||
id: 'authoring.selectvideomodal.upload.label',
|
||||
defaultMessage: 'Upload or embed a new video',
|
||||
description: 'Label for upload button',
|
||||
},
|
||||
|
||||
// Sort Dropdown
|
||||
sortByDateNewest: {
|
||||
id: 'authoring.selectvideomodal.sort.datenewest.label',
|
||||
defaultMessage: 'By date added (newest)',
|
||||
description: 'Dropdown label for sorting by date (newest)',
|
||||
},
|
||||
sortByDateOldest: {
|
||||
id: 'authoring.selectvideomodal.sort.dateoldest.label',
|
||||
defaultMessage: 'By date added (oldest)',
|
||||
description: 'Dropdown label for sorting by date (oldest)',
|
||||
},
|
||||
sortByNameAscending: {
|
||||
id: 'authoring.selectvideomodal.sort.nameascending.label',
|
||||
defaultMessage: 'By name (ascending)',
|
||||
description: 'Dropdown label for sorting by name (ascending)',
|
||||
},
|
||||
sortByNameDescending: {
|
||||
id: 'authoring.selectvideomodal.sort.namedescending.label',
|
||||
defaultMessage: 'By name (descending)',
|
||||
description: 'Dropdown label for sorting by name (descending)',
|
||||
},
|
||||
sortByDurationShortest: {
|
||||
id: 'authoring.selectvideomodal.sort.durationshortest.label',
|
||||
defaultMessage: 'By duration (shortest)',
|
||||
description: 'Dropdown label for sorting by duration (shortest)',
|
||||
},
|
||||
sortByDurationLongest: {
|
||||
id: 'authoring.selectvideomodal.sort.durationlongest.label',
|
||||
defaultMessage: 'By duration (longest)',
|
||||
description: 'Dropdown label for sorting by duration (longest)',
|
||||
},
|
||||
|
||||
// Filter Dropdown
|
||||
filterByVideoStatusNone: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusnone.label',
|
||||
defaultMessage: 'Video status',
|
||||
description: 'Dropdown label for filter by video status (none)',
|
||||
},
|
||||
filterByVideoStatusUploading: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusuploading.label',
|
||||
defaultMessage: 'Uploading',
|
||||
description: 'Dropdown label for filter by video status (uploading)',
|
||||
},
|
||||
filterByVideoStatusProcessing: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusprocessing.label',
|
||||
defaultMessage: 'Processing',
|
||||
description: 'Dropdown label for filter by video status (processing)',
|
||||
},
|
||||
filterByVideoStatusReady: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusready.label',
|
||||
defaultMessage: 'Ready',
|
||||
description: 'Dropdown label for filter by video status (ready)',
|
||||
},
|
||||
filterByVideoStatusFailed: {
|
||||
id: 'authoring.selectvideomodal.filter.videostatusfailed.label',
|
||||
defaultMessage: 'Failed',
|
||||
description: 'Dropdown label for filter by video status (failed)',
|
||||
},
|
||||
|
||||
// Hide switch
|
||||
hideSelectedCourseVideosSwitchLabel: {
|
||||
id: 'authoring.selectvideomodal.switch.hideselectedcoursevideos.label',
|
||||
defaultMessage: 'Hide selected course videos',
|
||||
description: 'Switch label for hide selected course videos',
|
||||
},
|
||||
|
||||
// Errors
|
||||
selectVideoError: {
|
||||
id: 'authoring.selectvideomodal.error.selectVideoError',
|
||||
defaultMessage: 'Select a video to continue.',
|
||||
description:
|
||||
'Message presented to user when clicking Next without selecting a video',
|
||||
},
|
||||
fileSizeError: {
|
||||
id: 'authoring.selectvideomodal.error.fileSizeError',
|
||||
defaultMessage:
|
||||
'Video must be 10 MB or less. Please resize image and try again.',
|
||||
description:
|
||||
'Message presented to user when file size of video is larger than 10 MB',
|
||||
},
|
||||
uploadVideoError: {
|
||||
id: 'authoring.selectvideomodal.error.uploadVideoError',
|
||||
defaultMessage: 'Failed to upload video. Please try again.',
|
||||
description: 'Message presented to user when video fails to upload',
|
||||
},
|
||||
fetchVideosError: {
|
||||
id: 'authoring.selectvideomodal.error.fetchVideosError',
|
||||
defaultMessage: 'Failed to obtain course videos. Please try again.',
|
||||
description: 'Message presented to user when videos are not found',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
42
src/editors/containers/VideoGallery/utils.js
Normal file
42
src/editors/containers/VideoGallery/utils.js
Normal file
@@ -0,0 +1,42 @@
|
||||
import { StrictDict, keyStore } from '../../utils';
|
||||
import messages from './messages';
|
||||
|
||||
const messageKeys = keyStore(messages);
|
||||
|
||||
export const sortKeys = StrictDict({
|
||||
dateNewest: 'dateNewest',
|
||||
dateOldest: 'dateOldest',
|
||||
nameAscending: 'nameAscending',
|
||||
nameDescending: 'nameDescending',
|
||||
durationShortest: 'durationShortest',
|
||||
durationLongest: 'durationLongest',
|
||||
});
|
||||
|
||||
export const sortMessages = StrictDict({
|
||||
dateNewest: messages[messageKeys.sortByDateNewest],
|
||||
dateOldest: messages[messageKeys.sortByDateOldest],
|
||||
nameAscending: messages[messageKeys.sortByNameAscending],
|
||||
nameDescending: messages[messageKeys.sortByNameDescending],
|
||||
durationShortest: messages[messageKeys.sortByDurationShortest],
|
||||
durationLongest: messages[messageKeys.sortByDurationLongest],
|
||||
});
|
||||
|
||||
export const filterKeys = StrictDict({
|
||||
videoStatus: 'videoStatus',
|
||||
uploading: 'uploading',
|
||||
processing: 'processing',
|
||||
ready: 'ready',
|
||||
failed: 'failed',
|
||||
});
|
||||
|
||||
export const filterMessages = StrictDict({
|
||||
videoStatus: messages[messageKeys.filterByVideoStatusNone],
|
||||
uploading: messages[messageKeys.filterByVideoStatusUploading],
|
||||
processing: messages[messageKeys.filterByVideoStatusProcessing],
|
||||
ready: messages[messageKeys.filterByVideoStatusReady],
|
||||
failed: messages[messageKeys.filterByVideoStatusFailed],
|
||||
});
|
||||
|
||||
export const acceptedImgKeys = StrictDict({
|
||||
mp4: '.mp4',
|
||||
});
|
||||
@@ -17,6 +17,7 @@ export const BaseModal = ({
|
||||
confirmAction,
|
||||
footerAction,
|
||||
size,
|
||||
isFullscreenScroll,
|
||||
}) => (
|
||||
<ModalDialog
|
||||
isOpen={isOpen}
|
||||
@@ -25,7 +26,7 @@ export const BaseModal = ({
|
||||
variant="default"
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
isFullscreenScroll
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
>
|
||||
<ModalDialog.Header>
|
||||
<ModalDialog.Title>
|
||||
@@ -51,6 +52,7 @@ export const BaseModal = ({
|
||||
BaseModal.defaultProps = {
|
||||
footerAction: null,
|
||||
size: 'lg',
|
||||
isFullscreenScroll: true,
|
||||
};
|
||||
|
||||
BaseModal.propTypes = {
|
||||
@@ -61,6 +63,7 @@ BaseModal.propTypes = {
|
||||
confirmAction: PropTypes.node.isRequired,
|
||||
footerAction: PropTypes.node,
|
||||
size: PropTypes.string,
|
||||
isFullscreenScroll: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default BaseModal;
|
||||
|
||||
@@ -35,6 +35,7 @@ exports[`ImageSettingsModal render snapshot 1`] = `
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="lg"
|
||||
title="Image Settings"
|
||||
|
||||
@@ -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,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: {
|
||||
|
||||
@@ -6,10 +6,15 @@ import {
|
||||
Scrollable, SelectableBox, Spinner,
|
||||
} from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
MessageDescriptor,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from '../../../data/redux';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
import messages from './messages';
|
||||
import GalleryCard from './GalleryCard';
|
||||
@@ -20,6 +25,9 @@ export const Gallery = ({
|
||||
displayList,
|
||||
highlighted,
|
||||
onHighlightChange,
|
||||
emptyGalleryLabel,
|
||||
showIdsOnCards,
|
||||
height,
|
||||
// injected
|
||||
intl,
|
||||
// redux
|
||||
@@ -36,20 +44,20 @@ export const Gallery = ({
|
||||
}
|
||||
if (galleryIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<FormattedMessage {...messages.emptyGalleryLabel} />
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height }}>
|
||||
<FormattedMessage {...emptyGalleryLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (searchIsEmpty) {
|
||||
return (
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height: '375px' }}>
|
||||
<div className="gallery p-4 bg-gray-100" style={{ height }}>
|
||||
<FormattedMessage {...messages.emptySearchLabel} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height: '375px' }}>
|
||||
<Scrollable className="gallery bg-gray-100" style={{ height }}>
|
||||
<div className="p-4">
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
@@ -58,7 +66,7 @@ export const Gallery = ({
|
||||
type="radio"
|
||||
value={highlighted}
|
||||
>
|
||||
{displayList.map(img => <GalleryCard key={img.id} img={img} />)}
|
||||
{ displayList.map(asset => <GalleryCard key={asset.id} asset={asset} showId={showIdsOnCards} />) }
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
@@ -67,6 +75,9 @@ export const Gallery = ({
|
||||
|
||||
Gallery.defaultProps = {
|
||||
highlighted: '',
|
||||
showIdsOnCards: false,
|
||||
height: '375px',
|
||||
emptyGalleryLabel: null,
|
||||
};
|
||||
Gallery.propTypes = {
|
||||
galleryIsEmpty: PropTypes.bool.isRequired,
|
||||
@@ -74,6 +85,9 @@ Gallery.propTypes = {
|
||||
displayList: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
highlighted: PropTypes.string,
|
||||
onHighlightChange: PropTypes.func.isRequired,
|
||||
emptyGalleryLabel: MessageDescriptor,
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
@@ -1,12 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { RequestKeys } from '../../../data/constants/requests';
|
||||
import { selectors } from '../../../data/redux';
|
||||
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', () => ({
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isFinished: (state, { requestKey }) => ({ isFinished: { state, requestKey } }),
|
||||
@@ -1,28 +1,42 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Image, SelectableBox } from '@edx/paragon';
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Image,
|
||||
SelectableBox,
|
||||
} from '@edx/paragon';
|
||||
import { FormattedMessage, FormattedDate, FormattedTime } from '@edx/frontend-platform/i18n';
|
||||
import { Link } from '@edx/paragon/icons';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const GalleryCard = ({
|
||||
img,
|
||||
asset,
|
||||
showId,
|
||||
}) => (
|
||||
<SelectableBox className="card bg-white" key={img.externalUrl} type="radio" value={img.id}>
|
||||
<SelectableBox className="card bg-white" key={asset.externalUrl} type="radio" value={asset.id}>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<Image
|
||||
style={{ width: '100px', height: '100px' }}
|
||||
src={img.externalUrl}
|
||||
src={asset.externalUrl}
|
||||
/>
|
||||
<div className="img-text p-3">
|
||||
<h3>{img.displayName}</h3>
|
||||
<h3>{asset.displayName}</h3>
|
||||
{ showId && (
|
||||
<p>
|
||||
<Button variant="link" size="inline" onClick={() => { /* TODO */ }}>
|
||||
<Icon src={Link} /> {asset.id}
|
||||
</Button>
|
||||
</p>
|
||||
)}
|
||||
<p>
|
||||
<FormattedMessage
|
||||
{...messages.addedDate}
|
||||
values={{
|
||||
date: <FormattedDate value={img.dateAdded} />,
|
||||
time: <FormattedTime value={img.dateAdded} />,
|
||||
date: <FormattedDate value={asset.dateAdded} />,
|
||||
time: <FormattedTime value={asset.dateAdded} />,
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
@@ -31,8 +45,12 @@ export const GalleryCard = ({
|
||||
</SelectableBox>
|
||||
);
|
||||
|
||||
GalleryCard.defaultProps = {
|
||||
showId: false,
|
||||
};
|
||||
|
||||
GalleryCard.propTypes = {
|
||||
img: PropTypes.shape({
|
||||
asset: PropTypes.shape({
|
||||
contentType: PropTypes.string,
|
||||
displayName: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
@@ -43,6 +61,7 @@ GalleryCard.propTypes = {
|
||||
thumbnail: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
}).isRequired,
|
||||
showId: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default GalleryCard;
|
||||
@@ -12,7 +12,7 @@ describe('GalleryCard component', () => {
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GalleryCard img={img} />);
|
||||
el = shallow(<GalleryCard asset={img} />);
|
||||
});
|
||||
test(`snapshot: dateAdded=${img.dateAdded}`, () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
135
src/editors/sharedComponents/SelectionModal/SearchSort.jsx
Normal file
135
src/editors/sharedComponents/SelectionModal/SearchSort.jsx
Normal file
@@ -0,0 +1,135 @@
|
||||
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,
|
||||
MessageDescriptor,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const SearchSort = ({
|
||||
searchString,
|
||||
onSearchChange,
|
||||
clearSearchString,
|
||||
sortBy,
|
||||
onSortClick,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterBy,
|
||||
onFilterClick,
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch,
|
||||
switchMessage,
|
||||
onSwitchClick,
|
||||
// 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>
|
||||
|
||||
{ !showSwitch && <ActionRow.Spacer /> }
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-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>
|
||||
|
||||
{ filterKeys && filterMessages && (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle id="gallery-filter-button" variant="tertiary">
|
||||
<FormattedMessage {...filterMessages[filterBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{Object.keys(filterKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onFilterClick(key)}>
|
||||
<FormattedMessage {...filterMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
{ showSwitch && (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
<Form.SwitchSet
|
||||
name="switch"
|
||||
onChange={onSwitchClick}
|
||||
isInline
|
||||
>
|
||||
<Form.Switch value="switch-value" floatLabelLeft>
|
||||
<FormattedMessage {...switchMessage} />
|
||||
</Form.Switch>
|
||||
</Form.SwitchSet>
|
||||
</>
|
||||
)}
|
||||
|
||||
</ActionRow>
|
||||
);
|
||||
|
||||
SearchSort.defaultProps = {
|
||||
filterBy: '',
|
||||
onFilterClick: null,
|
||||
filterKeys: null,
|
||||
filterMessages: null,
|
||||
showSwitch: false,
|
||||
switchMessage: null,
|
||||
onSwitchClick: null,
|
||||
};
|
||||
|
||||
SearchSort.propTypes = {
|
||||
searchString: PropTypes.string.isRequired,
|
||||
onSearchChange: PropTypes.func.isRequired,
|
||||
clearSearchString: PropTypes.func.isRequired,
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
onSortClick: PropTypes.func.isRequired,
|
||||
sortKeys: PropTypes.shape({}).isRequired,
|
||||
sortMessages: PropTypes.shape({}).isRequired,
|
||||
filterBy: PropTypes.string,
|
||||
onFilterClick: PropTypes.func,
|
||||
filterKeys: PropTypes.shape({}),
|
||||
filterMessages: PropTypes.shape({}),
|
||||
showSwitch: PropTypes.bool,
|
||||
switchMessage: MessageDescriptor,
|
||||
onSwitchClick: PropTypes.func,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchSort);
|
||||
@@ -3,9 +3,9 @@ import { shallow } from 'enzyme';
|
||||
import { Dropdown } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { formatMessage } from '../../../../testUtils';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
|
||||
import { sortKeys, sortMessages } from './utils';
|
||||
import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils';
|
||||
import { SearchSort } from './SearchSort';
|
||||
|
||||
describe('SearchSort component', () => {
|
||||
@@ -14,6 +14,8 @@ describe('SearchSort component', () => {
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
@@ -9,11 +9,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but no im
|
||||
}
|
||||
}
|
||||
>
|
||||
<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"
|
||||
/>
|
||||
<FormattedMessage />
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -29,7 +25,7 @@ exports[`TextEditor Image Gallery component component snapshot: loaded but searc
|
||||
<FormattedMessage
|
||||
defaultMessage="No search results."
|
||||
description="Label for when search returns nothing."
|
||||
id="authoring.texteditor.selectimagemodal.emptySearchLabel"
|
||||
id="authoring.selectionmodal.emptySearchLabel"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -54,28 +50,31 @@ exports[`TextEditor Image Gallery component component snapshot: loaded, show gal
|
||||
value="props.highlighted"
|
||||
>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 1,
|
||||
}
|
||||
}
|
||||
key="1"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 2,
|
||||
}
|
||||
}
|
||||
key="2"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
img={
|
||||
asset={
|
||||
Object {
|
||||
"id": 3,
|
||||
}
|
||||
}
|
||||
key="3"
|
||||
showId={false}
|
||||
/>
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
@@ -28,7 +28,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<FormattedMessage
|
||||
defaultMessage="Added {date} at {time}"
|
||||
description="File date-added string"
|
||||
id="authoring.texteditor.selectimagemodal.addedDate.label"
|
||||
id="authoring.selectionmodal.addedDate.label"
|
||||
values={
|
||||
Object {
|
||||
"date": <FormattedDate
|
||||
@@ -29,7 +29,7 @@ exports[`SearchSort component snapshots with search string (close button) 1`] =
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -100,7 +100,7 @@ exports[`SearchSort component snapshots without search string (search icon) 1`]
|
||||
<ActionRow.Spacer />
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
id="img-sort-button"
|
||||
id="gallery-sort-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -0,0 +1,13 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Selection Modal snapshots rendering correctly with expected Input 1`] = `
|
||||
<ContextConsumer>
|
||||
<Component />
|
||||
</ContextConsumer>
|
||||
`;
|
||||
|
||||
exports[`Selection Modal snapshots rendering with props to null 1`] = `
|
||||
<ContextConsumer>
|
||||
<Component />
|
||||
</ContextConsumer>
|
||||
`;
|
||||
131
src/editors/sharedComponents/SelectionModal/index.jsx
Normal file
131
src/editors/sharedComponents/SelectionModal/index.jsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button, Stack } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
MessageDescriptor,
|
||||
intlShape,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import BaseModal from '../BaseModal';
|
||||
import SearchSort from './SearchSort';
|
||||
import Gallery from './Gallery';
|
||||
import FileInput from '../FileInput';
|
||||
import ErrorAlert from '../ErrorAlerts/ErrorAlert';
|
||||
import FetchErrorAlert from '../ErrorAlerts/FetchErrorAlert';
|
||||
import UploadErrorAlert from '../ErrorAlerts/UploadErrorAlert';
|
||||
|
||||
export const SelectionModal = ({
|
||||
isOpen,
|
||||
close,
|
||||
size,
|
||||
isFullscreenScroll,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles,
|
||||
modalMessages,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const {
|
||||
confirmMsg,
|
||||
uploadButtonMsg,
|
||||
titleMsg,
|
||||
fetchError,
|
||||
uploadError,
|
||||
} = modalMessages;
|
||||
return (
|
||||
<BaseModal
|
||||
close={close}
|
||||
confirmAction={(
|
||||
<Button {...selectBtnProps} variant="primary">
|
||||
<FormattedMessage {...confirmMsg} />
|
||||
</Button>
|
||||
)}
|
||||
isOpen={isOpen}
|
||||
size={size}
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
footerAction={(
|
||||
<Button iconBefore={Add} onClick={fileInput.click} variant="link">
|
||||
<FormattedMessage {...uploadButtonMsg} />
|
||||
</Button>
|
||||
)}
|
||||
title={intl.formatMessage(titleMsg)}
|
||||
>
|
||||
{/* Error Alerts */}
|
||||
<FetchErrorAlert message={fetchError} />
|
||||
<UploadErrorAlert message={uploadError} />
|
||||
<ErrorAlert
|
||||
dismissError={inputError.dismiss}
|
||||
hideHeading
|
||||
isError={inputError.show}
|
||||
>
|
||||
<FormattedMessage {...inputError.message} />
|
||||
</ErrorAlert>
|
||||
|
||||
{/* User Feedback Alerts */}
|
||||
<ErrorAlert
|
||||
dismissError={galleryError.dismiss}
|
||||
hideHeading
|
||||
isError={galleryError.show}
|
||||
>
|
||||
<FormattedMessage {...galleryError.message} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={3}>
|
||||
<SearchSort {...searchSortProps} />
|
||||
<Gallery {...galleryProps} />
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedFiles).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
);
|
||||
};
|
||||
|
||||
SelectionModal.defaultProps = {
|
||||
size: 'lg',
|
||||
isFullscreenScroll: true,
|
||||
};
|
||||
|
||||
SelectionModal.propTypes = {
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
close: PropTypes.func.isRequired,
|
||||
size: PropTypes.string,
|
||||
isFullscreenScroll: PropTypes.bool,
|
||||
galleryError: PropTypes.shape({
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
set: PropTypes.func.isRequired,
|
||||
message: MessageDescriptor,
|
||||
}).isRequired,
|
||||
inputError: PropTypes.shape({
|
||||
dismiss: PropTypes.func.isRequired,
|
||||
show: PropTypes.bool.isRequired,
|
||||
set: PropTypes.func.isRequired,
|
||||
message: MessageDescriptor,
|
||||
}).isRequired,
|
||||
fileInput: PropTypes.shape({
|
||||
click: PropTypes.func.isRequired,
|
||||
addFile: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
galleryProps: PropTypes.shape({}).isRequired,
|
||||
searchSortProps: PropTypes.shape({}).isRequired,
|
||||
selectBtnProps: PropTypes.shape({}).isRequired,
|
||||
acceptedFiles: PropTypes.shape({}).isRequired,
|
||||
modalMessages: PropTypes.shape({
|
||||
confirmMsg: MessageDescriptor,
|
||||
uploadButtonMsg: MessageDescriptor,
|
||||
titleMsg: MessageDescriptor,
|
||||
fetchError: MessageDescriptor,
|
||||
uploadError: MessageDescriptor,
|
||||
}).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SelectionModal);
|
||||
87
src/editors/sharedComponents/SelectionModal/index.test.jsx
Normal file
87
src/editors/sharedComponents/SelectionModal/index.test.jsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
import SelectionModal from '.';
|
||||
|
||||
const props = {
|
||||
isOpen: jest.fn(),
|
||||
isClose: jest.fn(),
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError: {
|
||||
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',
|
||||
click: 'imgHooks.fileInput.click',
|
||||
ref: 'imgHooks.fileInput.ref',
|
||||
},
|
||||
galleryProps: { gallery: 'props' },
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
acceptedFiles: { png: '.png' },
|
||||
modalMessages: {
|
||||
confirmMsg: {
|
||||
id: 'confirmMsg',
|
||||
defaultMessage: 'confirmMsg',
|
||||
description: 'confirmMsg',
|
||||
},
|
||||
uploadButtonMsg: {
|
||||
id: 'uploadButtonMsg',
|
||||
defaultMessage: 'uploadButtonMsg',
|
||||
description: 'uploadButtonMsg',
|
||||
},
|
||||
titleMsg: {
|
||||
id: 'titleMsg',
|
||||
defaultMessage: 'titleMsg',
|
||||
description: 'titleMsg',
|
||||
},
|
||||
fetchError: {
|
||||
id: 'fetchError',
|
||||
defaultMessage: 'fetchError',
|
||||
description: 'fetchError',
|
||||
},
|
||||
uploadError: {
|
||||
id: 'uploadError',
|
||||
defaultMessage: 'uploadError',
|
||||
description: 'uploadError',
|
||||
},
|
||||
},
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
jest.mock('../BaseModal', () => 'BaseModal');
|
||||
jest.mock('./SearchSort', () => 'SearchSort');
|
||||
jest.mock('./Gallery', () => 'Gallery');
|
||||
jest.mock('../FileInput', () => 'FileInput');
|
||||
jest.mock('../ErrorAlerts/ErrorAlert', () => 'ErrorAlert');
|
||||
jest.mock('../ErrorAlerts/FetchErrorAlert', () => 'FetchErrorAlert');
|
||||
jest.mock('../ErrorAlerts/UploadErrorAlert', () => 'UploadErrorAlert');
|
||||
|
||||
describe('Selection Modal', () => {
|
||||
describe('snapshots', () => {
|
||||
test('rendering correctly with expected Input', () => {
|
||||
expect(shallow(<SelectionModal {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('rendering with props to null', () => {
|
||||
expect(shallow(<SelectionModal />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
24
src/editors/sharedComponents/SelectionModal/messages.js
Normal file
24
src/editors/sharedComponents/SelectionModal/messages.js
Normal file
@@ -0,0 +1,24 @@
|
||||
export const messages = {
|
||||
searchPlaceholder: {
|
||||
id: 'authoring.selectionmodal.search.placeholder',
|
||||
defaultMessage: 'Search',
|
||||
description: 'Placeholder text for search bar',
|
||||
},
|
||||
emptySearchLabel: {
|
||||
id: 'authoring.selectionmodal.emptySearchLabel',
|
||||
defaultMessage: 'No search results.',
|
||||
description: 'Label for when search returns nothing.',
|
||||
},
|
||||
loading: {
|
||||
id: 'authoring.selectionmodal.spinner.readertext',
|
||||
defaultMessage: 'loading...',
|
||||
description: 'Gallery loading spinner screen-reader text',
|
||||
},
|
||||
addedDate: {
|
||||
id: 'authoring.selectionmodal.addedDate.label',
|
||||
defaultMessage: 'Added {date} at {time}',
|
||||
description: 'File date-added string',
|
||||
},
|
||||
};
|
||||
|
||||
export default messages;
|
||||
@@ -24,6 +24,7 @@ exports[`SourceCodeModal renders as expected with default behavior 1`] = `
|
||||
</Button>
|
||||
}
|
||||
footerAction={null}
|
||||
isFullscreenScroll={true}
|
||||
isOpen={false}
|
||||
size="xl"
|
||||
title="Edit Source Code"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import Placeholder from './Placeholder';
|
||||
import messages from './i18n/index';
|
||||
import EditorPage from './editors/EditorPage';
|
||||
import SelectorPage from './editors/SelectorPage';
|
||||
|
||||
export { messages, EditorPage };
|
||||
export { messages, EditorPage, SelectorPage };
|
||||
export default Placeholder;
|
||||
|
||||
Reference in New Issue
Block a user