Merge pull request #368 from open-craft/kshitij/fix-video-sort-filter
fix: Video Gallery filters and sorting
This commit is contained in:
@@ -27,7 +27,8 @@ export const handleSaveClicked = ({
|
||||
returnFunction,
|
||||
}) => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const destination = returnFunction ? '' : useSelector(selectors.app.returnUrl);
|
||||
const returnUrl = useSelector(selectors.app.returnUrl);
|
||||
const destination = returnFunction ? '' : returnUrl;
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const analytics = useSelector(selectors.app.analytics);
|
||||
|
||||
@@ -54,10 +55,12 @@ export const handleCancel = ({ onClose, returnFunction }) => {
|
||||
if (onClose) {
|
||||
return onClose;
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const returnUrl = useSelector(selectors.app.returnUrl);
|
||||
return navigateCallback({
|
||||
returnFunction,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
destination: returnFunction ? '' : useSelector(selectors.app.returnUrl),
|
||||
destination: returnFunction ? '' : returnUrl,
|
||||
analyticsEvent: analyticsEvt.editorCancelClick,
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
analytics: useSelector(selectors.app.analytics),
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
|
||||
import * as module from './hooks';
|
||||
import messages from './messages';
|
||||
import * as appHooks from '../../hooks';
|
||||
import { selectors } from '../../data/redux';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
import {
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
sortFunctions,
|
||||
filterKeys, sortFunctions, sortKeys, sortMessages,
|
||||
} from './utils';
|
||||
|
||||
export const {
|
||||
@@ -18,28 +15,19 @@ export const {
|
||||
navigateTo,
|
||||
} = appHooks;
|
||||
|
||||
export const state = {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
highlighted: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
searchString: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
showSelectVideoError: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
showSizeError: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
sortBy: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
filertBy: (val) => React.useState(val),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
hideSelectedVideos: (val) => React.useState(val),
|
||||
};
|
||||
export const useSearchAndSortProps = () => {
|
||||
const [searchString, setSearchString] = React.useState('');
|
||||
const [sortBy, setSortBy] = React.useState(sortKeys.dateNewest);
|
||||
const [filterBy, setFilterBy] = React.useState([]);
|
||||
const [hideSelectedVideos, setHideSelectedVideos] = React.useState(false);
|
||||
|
||||
export const searchAndSortProps = () => {
|
||||
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);
|
||||
const handleFilter = (key) => () => {
|
||||
if (filterBy.includes(key)) {
|
||||
setFilterBy(filterBy.filter(item => item !== key));
|
||||
} else {
|
||||
setFilterBy([...filterBy, key]);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
searchString,
|
||||
@@ -50,9 +38,7 @@ export const searchAndSortProps = () => {
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterBy,
|
||||
onFilterClick: (key) => () => setFilterBy(key),
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
onFilterClick: handleFilter,
|
||||
showSwitch: false,
|
||||
hideSelectedVideos,
|
||||
switchMessage: messages.hideSelectedCourseVideosSwitchLabel,
|
||||
@@ -60,15 +46,23 @@ export const searchAndSortProps = () => {
|
||||
};
|
||||
};
|
||||
|
||||
export const filterListBySearch = ({ searchString, videoList }) => (
|
||||
videoList.filter(({ displayName }) => displayName.toLowerCase().includes(searchString.toLowerCase()))
|
||||
export const filterListBySearch = ({
|
||||
searchString,
|
||||
videoList,
|
||||
}) => (
|
||||
videoList.filter(({ displayName }) => displayName.toLowerCase()
|
||||
.includes(searchString.toLowerCase()))
|
||||
);
|
||||
|
||||
export const filterListByStatus = ({ statusFilter, videoList }) => {
|
||||
if (statusFilter === filterKeys.videoStatus) {
|
||||
export const filterListByStatus = ({
|
||||
statusFilter,
|
||||
videoList,
|
||||
}) => {
|
||||
if (statusFilter.length === 0) {
|
||||
return videoList;
|
||||
}
|
||||
return videoList.filter(({ status }) => status === statusFilter);
|
||||
return videoList.filter(({ status }) => statusFilter.map(key => filterKeys[key])
|
||||
.includes(status));
|
||||
};
|
||||
|
||||
export const filterListByHideSelectedCourse = ({ videoList }) => (
|
||||
@@ -96,20 +90,24 @@ export const filterList = ({
|
||||
return filteredList.sort(sortFunctions[sortBy in sortKeys ? sortKeys[sortBy] : sortKeys.dateNewest]);
|
||||
};
|
||||
|
||||
export const videoListProps = ({ searchSortProps, videos }) => {
|
||||
const [highlighted, setHighlighted] = module.state.highlighted(null);
|
||||
export const useVideoListProps = ({
|
||||
searchSortProps,
|
||||
videos,
|
||||
}) => {
|
||||
const [highlighted, setHighlighted] = React.useState(null);
|
||||
const [
|
||||
showSelectVideoError,
|
||||
setShowSelectVideoError,
|
||||
] = module.state.showSelectVideoError(false);
|
||||
] = React.useState(false);
|
||||
const [
|
||||
showSizeError,
|
||||
setShowSizeError,
|
||||
] = module.state.showSizeError(false);
|
||||
const filteredList = module.filterList({ ...searchSortProps, videos });
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
] = React.useState(false);
|
||||
const filteredList = module.filterList({
|
||||
...searchSortProps,
|
||||
videos,
|
||||
});
|
||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const blockId = useSelector(selectors.app.blockId);
|
||||
return {
|
||||
galleryError: {
|
||||
@@ -147,26 +145,15 @@ export const videoListProps = ({ searchSortProps, videos }) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const fileInputProps = () => {
|
||||
const click = module.handleVideoUpload();
|
||||
return {
|
||||
click,
|
||||
};
|
||||
};
|
||||
|
||||
export const handleVideoUpload = () => {
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
export const useVideoUploadHandler = () => {
|
||||
const learningContextId = useSelector(selectors.app.learningContextId);
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
const blockId = useSelector(selectors.app.blockId);
|
||||
return () => navigateTo(`/course/${learningContextId}/editor/video_upload/${blockId}`);
|
||||
};
|
||||
|
||||
export const handleCancel = () => (
|
||||
export const useCancelHandler = () => (
|
||||
navigateCallback({
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
destination: useSelector(selectors.app.returnUrl),
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks
|
||||
analytics: useSelector(selectors.app.analytics),
|
||||
analyticsEvent: analyticsEvt.videoGalleryCancelClick,
|
||||
})
|
||||
@@ -180,7 +167,7 @@ export const buildVideos = ({ rawVideos }) => {
|
||||
id: video.edx_video_id,
|
||||
displayName: video.client_video_id,
|
||||
externalUrl: video.course_video_image_url,
|
||||
dateAdded: video.created,
|
||||
dateAdded: new Date(video.created),
|
||||
locked: false,
|
||||
thumbnail: video.course_video_image_url,
|
||||
status: video.status,
|
||||
@@ -204,16 +191,19 @@ export const getstatusBadgeVariant = ({ status }) => {
|
||||
}
|
||||
};
|
||||
|
||||
export const videoProps = ({ videos }) => {
|
||||
const searchSortProps = module.searchAndSortProps();
|
||||
const videoList = module.videoListProps({ searchSortProps, videos });
|
||||
export const useVideoProps = ({ videos }) => {
|
||||
const searchSortProps = useSearchAndSortProps();
|
||||
const videoList = useVideoListProps({
|
||||
searchSortProps,
|
||||
videos,
|
||||
});
|
||||
const {
|
||||
galleryError,
|
||||
galleryProps,
|
||||
inputError,
|
||||
selectBtnProps,
|
||||
} = videoList;
|
||||
const fileInput = module.fileInputProps();
|
||||
const fileInput = { click: useVideoUploadHandler() };
|
||||
|
||||
return {
|
||||
galleryError,
|
||||
@@ -226,8 +216,8 @@ export const videoProps = ({ videos }) => {
|
||||
};
|
||||
|
||||
export default {
|
||||
videoProps,
|
||||
useVideoProps,
|
||||
buildVideos,
|
||||
handleCancel,
|
||||
handleVideoUpload,
|
||||
useCancelHandler,
|
||||
useVideoUploadHandler,
|
||||
};
|
||||
|
||||
@@ -1,312 +0,0 @@
|
||||
import * as reactRedux from 'react-redux';
|
||||
import * as hooks from './hooks';
|
||||
import { filterKeys, sortKeys } from './utils';
|
||||
import { MockUseState } from '../../../testUtils';
|
||||
import { keyStore } from '../../utils';
|
||||
import * as appHooks from '../../hooks';
|
||||
import { selectors } from '../../data/redux';
|
||||
import analyticsEvt from '../../data/constants/analyticsEvt';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn(val => ({ current: val })),
|
||||
useEffect: jest.fn(),
|
||||
useCallback: (cb, prereqs) => ({ cb, prereqs }),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => {
|
||||
const dispatchFn = jest.fn();
|
||||
return {
|
||||
...jest.requireActual('react-redux'),
|
||||
dispatch: dispatchFn,
|
||||
useDispatch: jest.fn(() => dispatchFn),
|
||||
useSelector: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
returnUrl: 'returnUrl',
|
||||
analytics: 'analytics',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
navigateCallback: jest.fn((args) => ({ navigateCallback: args })),
|
||||
navigateTo: jest.fn((args) => ({ navigateTo: args })),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
let hook;
|
||||
const testValue = 'testVALUEVALID';
|
||||
|
||||
describe('VideoGallery hooks', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('state hooks', () => {
|
||||
state.testGetter(state.keys.highlighted);
|
||||
state.testGetter(state.keys.searchString);
|
||||
state.testGetter(state.keys.showSelectVideoError);
|
||||
state.testGetter(state.keys.showSizeError);
|
||||
state.testGetter(state.keys.sortBy);
|
||||
state.testGetter(state.keys.filertBy);
|
||||
state.testGetter(state.keys.hideSelectedVideos);
|
||||
});
|
||||
describe('using state', () => {
|
||||
beforeEach(() => { state.mock(); });
|
||||
afterEach(() => { state.restore(); });
|
||||
|
||||
describe('searchAndSortProps', () => {
|
||||
beforeEach(() => {
|
||||
hook = hooks.searchAndSortProps();
|
||||
});
|
||||
it('returns searchString value, initialized to an empty string', () => {
|
||||
expect(state.stateVals.searchString).toEqual(hook.searchString);
|
||||
expect(state.stateVals.searchString).toEqual('');
|
||||
});
|
||||
it('returns highlighted value, initialized to dateNewest', () => {
|
||||
expect(state.stateVals.sortBy).toEqual(hook.sortBy);
|
||||
expect(state.stateVals.sortBy).toEqual(sortKeys.dateNewest);
|
||||
});
|
||||
test('onSearchChange sets searchString with event target value', () => {
|
||||
hook.onSearchChange({ target: { value: testValue } });
|
||||
expect(state.setState.searchString).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
test('clearSearchString sets search string to empty string', () => {
|
||||
hook.clearSearchString();
|
||||
expect(state.setState.searchString).toHaveBeenCalledWith('');
|
||||
});
|
||||
test('onSortClick takes a key and returns callback to set sortBY to that key', () => {
|
||||
hook.onSortClick(testValue);
|
||||
expect(state.setState.sortBy).not.toHaveBeenCalled();
|
||||
hook.onSortClick(testValue)();
|
||||
expect(state.setState.sortBy).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
});
|
||||
describe('filterListBySearch', () => {
|
||||
const matching = [
|
||||
'test',
|
||||
'TEst',
|
||||
'eeees',
|
||||
'essSSSS',
|
||||
];
|
||||
const notMatching = ['bad', 'other', 'bad stuff'];
|
||||
const searchString = 'eS';
|
||||
test('returns list filtered lowercase by displayName', () => {
|
||||
const filter = jest.fn(cb => ({ filter: cb }));
|
||||
hook = hooks.filterListBySearch({ searchString, videoList: { filter } });
|
||||
expect(filter).toHaveBeenCalled();
|
||||
const [[filterCb]] = filter.mock.calls;
|
||||
matching.forEach(val => expect(filterCb({ displayName: val })).toEqual(true));
|
||||
notMatching.forEach(val => expect(filterCb({ displayName: val })).toEqual(false));
|
||||
});
|
||||
});
|
||||
describe('buildVideos', () => {
|
||||
const rawVideos = [
|
||||
{
|
||||
edx_video_id: 'id_1',
|
||||
client_video_id: 'client_id_1',
|
||||
course_video_image_url: 'course_video_image_url_1',
|
||||
created: 'created_1',
|
||||
status: 'status_1',
|
||||
duration: 1,
|
||||
transcripts: [],
|
||||
},
|
||||
{
|
||||
edx_video_id: 'id_2',
|
||||
client_video_id: 'client_id_2',
|
||||
course_video_image_url: 'course_video_image_url_2',
|
||||
created: 'created_2',
|
||||
status: 'status_2',
|
||||
duration: 2,
|
||||
transcripts: [],
|
||||
},
|
||||
];
|
||||
const expectedValues = [
|
||||
{
|
||||
id: 'id_1',
|
||||
displayName: 'client_id_1',
|
||||
externalUrl: 'course_video_image_url_1',
|
||||
dateAdded: 'created_1',
|
||||
locked: false,
|
||||
thumbnail: 'course_video_image_url_1',
|
||||
status: 'status_1',
|
||||
statusBadgeVariant: null,
|
||||
duration: 1,
|
||||
transcripts: [],
|
||||
},
|
||||
{
|
||||
id: 'id_2',
|
||||
displayName: 'client_id_2',
|
||||
externalUrl: 'course_video_image_url_2',
|
||||
dateAdded: 'created_2',
|
||||
locked: false,
|
||||
thumbnail: 'course_video_image_url_2',
|
||||
status: 'status_2',
|
||||
statusBadgeVariant: null,
|
||||
duration: 2,
|
||||
transcripts: [],
|
||||
},
|
||||
];
|
||||
test('return the expected values', () => {
|
||||
const values = hooks.buildVideos({ rawVideos });
|
||||
expect(values).toEqual(expectedValues);
|
||||
});
|
||||
});
|
||||
describe('getstatusBadgeVariant', () => {
|
||||
test('return the expected values', () => {
|
||||
let value = hooks.getstatusBadgeVariant({ status: filterKeys.failed });
|
||||
expect(value).toEqual('danger');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.uploading });
|
||||
expect(value).toEqual('light');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.processing });
|
||||
expect(value).toEqual('light');
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.videoStatus });
|
||||
expect(value).toBeNull();
|
||||
value = hooks.getstatusBadgeVariant({ status: filterKeys.ready });
|
||||
expect(value).toBeNull();
|
||||
});
|
||||
});
|
||||
describe('videoListProps outputs', () => {
|
||||
const props = {
|
||||
searchSortProps: {
|
||||
searchString: 'Es',
|
||||
sortBy: sortKeys.dateNewest,
|
||||
filterBy: filterKeys.videoStatus,
|
||||
},
|
||||
videos: [
|
||||
{
|
||||
displayName: 'sOmEuiMAge',
|
||||
staTICUrl: '/assets/sOmEuiMAge',
|
||||
id: 'sOmEuiMAgeURl',
|
||||
},
|
||||
],
|
||||
};
|
||||
const filterList = (args) => ({ filterList: args });
|
||||
const load = () => {
|
||||
jest.spyOn(hooks, hookKeys.filterList).mockImplementationOnce(filterList);
|
||||
hook = hooks.videoListProps(props);
|
||||
};
|
||||
beforeEach(() => {
|
||||
load();
|
||||
});
|
||||
describe('selectBtnProps', () => {
|
||||
test('on click, if sets selection', () => {
|
||||
const highlighted = 'videoId';
|
||||
state.mockVal(state.keys.highlighted, highlighted);
|
||||
load();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
hook.selectBtnProps.onClick();
|
||||
expect(appHooks.navigateTo).toHaveBeenCalled();
|
||||
});
|
||||
test('on click, sets showSelectVideoError to true if nothing is highlighted', () => {
|
||||
state.mockVal(state.keys.highlighted, null);
|
||||
load();
|
||||
hook.selectBtnProps.onClick();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
describe('galleryProps', () => {
|
||||
it('returns highlighted value, initialized to null', () => {
|
||||
expect(hook.galleryProps.highlighted).toEqual(state.stateVals.highlighted);
|
||||
expect(state.stateVals.highlighted).toEqual(null);
|
||||
});
|
||||
test('onHighlightChange sets highlighted with event target value', () => {
|
||||
hook.galleryProps.onHighlightChange({ target: { value: testValue } });
|
||||
expect(state.setState.highlighted).toHaveBeenCalledWith(testValue);
|
||||
});
|
||||
test('displayList returns filterListhook called with searchSortProps and videos', () => {
|
||||
expect(hook.galleryProps.displayList).toEqual(filterList({
|
||||
...props.searchSortProps,
|
||||
videos: props.videos,
|
||||
}));
|
||||
});
|
||||
});
|
||||
describe('galleryError', () => {
|
||||
test('show is initialized to false and returns properly', () => {
|
||||
const show = 'sHOWSelectiRROr';
|
||||
expect(hook.galleryError.show).toEqual(false);
|
||||
state.mockVal(state.keys.showSelectVideoError, show);
|
||||
hook = hooks.videoListProps(props);
|
||||
expect(hook.galleryError.show).toEqual(show);
|
||||
});
|
||||
test('set sets showSelectVideoError to true', () => {
|
||||
hook.galleryError.set();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(true);
|
||||
});
|
||||
test('dismiss sets showSelectVideoError to false', () => {
|
||||
hook.galleryError.dismiss();
|
||||
expect(state.setState.showSelectVideoError).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('fileInputHooks', () => {
|
||||
test('click calls current.click on the ref', () => {
|
||||
jest.spyOn(hooks, hookKeys.handleVideoUpload).mockImplementationOnce();
|
||||
expect(hooks.handleVideoUpload).not.toHaveBeenCalled();
|
||||
hook = hooks.fileInputProps();
|
||||
expect(hooks.handleVideoUpload).toHaveBeenCalled();
|
||||
expect(appHooks.navigateTo).not.toHaveBeenCalled();
|
||||
hook.click();
|
||||
expect(appHooks.navigateTo).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('videoProps', () => {
|
||||
const videoList = {
|
||||
galleryProps: 'some gallery props',
|
||||
selectBtnProps: 'some select btn props',
|
||||
};
|
||||
const searchAndSortProps = { search: 'props' };
|
||||
const fileInput = { file: 'input hooks' };
|
||||
const videos = { video: { staTICUrl: '/assets/sOmEuiMAge' } };
|
||||
const spies = {};
|
||||
beforeEach(() => {
|
||||
spies.videoList = jest.spyOn(hooks, hookKeys.videoListProps)
|
||||
.mockReturnValueOnce(videoList);
|
||||
spies.search = jest.spyOn(hooks, hookKeys.searchAndSortProps)
|
||||
.mockReturnValueOnce(searchAndSortProps);
|
||||
spies.file = jest.spyOn(hooks, hookKeys.fileInputProps)
|
||||
.mockReturnValueOnce(fileInput);
|
||||
hook = hooks.videoProps({ videos });
|
||||
});
|
||||
it('forwards fileInput as fileInput', () => {
|
||||
expect(hook.fileInput).toEqual(fileInput);
|
||||
expect(spies.file.mock.calls.length).toEqual(1);
|
||||
expect(spies.file).toHaveBeenCalled();
|
||||
});
|
||||
it('initializes videoList', () => {
|
||||
expect(spies.videoList.mock.calls.length).toEqual(1);
|
||||
expect(spies.videoList).toHaveBeenCalledWith({
|
||||
searchSortProps: searchAndSortProps,
|
||||
videos,
|
||||
});
|
||||
});
|
||||
it('forwards searchAndSortHooks as searchSortProps', () => {
|
||||
expect(hook.searchSortProps).toEqual(searchAndSortProps);
|
||||
expect(spies.search.mock.calls.length).toEqual(1);
|
||||
expect(spies.search).toHaveBeenCalled();
|
||||
});
|
||||
it('forwards galleryProps and selectBtnProps from the video list hooks', () => {
|
||||
expect(hook.galleryProps).toEqual(videoList.galleryProps);
|
||||
expect(hook.selectBtnProps).toEqual(videoList.selectBtnProps);
|
||||
});
|
||||
});
|
||||
describe('handleCancel', () => {
|
||||
it('calls navigateCallback', () => {
|
||||
expect(hooks.handleCancel()).toEqual(
|
||||
appHooks.navigateCallback({
|
||||
destination: reactRedux.useSelector(selectors.app.returnUrl),
|
||||
analyticsEvent: analyticsEvt.videoGalleryCancelClick,
|
||||
analytics: reactRedux.useSelector(selectors.app.analytics),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { selectors } from '../../data/redux';
|
||||
import hooks from './hooks';
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
@@ -8,15 +7,19 @@ import { acceptedImgKeys } from './utils';
|
||||
import messages from './messages';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
|
||||
export const VideoGallery = ({
|
||||
// redux
|
||||
rawVideos,
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
}) => {
|
||||
export const VideoGallery = () => {
|
||||
const rawVideos = useSelector(selectors.app.videos);
|
||||
const isLoaded = useSelector(
|
||||
(state) => selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
);
|
||||
const isFetchError = useSelector(
|
||||
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
);
|
||||
const isUploadError = useSelector(
|
||||
(state) => selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
|
||||
);
|
||||
const videos = hooks.buildVideos({ rawVideos });
|
||||
const handleVideoUpload = hooks.handleVideoUpload();
|
||||
const handleVideoUpload = hooks.useVideoUploadHandler();
|
||||
|
||||
useEffect(() => {
|
||||
// If no videos exists redirects to the video upload screen
|
||||
@@ -31,8 +34,8 @@ export const VideoGallery = ({
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
} = hooks.videoProps({ videos });
|
||||
const handleCancel = hooks.handleCancel();
|
||||
} = hooks.useVideoProps({ videos });
|
||||
const handleCancel = hooks.useCancelHandler();
|
||||
|
||||
const modalMessages = {
|
||||
confirmMsg: messages.selectVideoButtonlabel,
|
||||
@@ -43,44 +46,28 @@ export const VideoGallery = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen: true,
|
||||
close: handleCancel,
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
isLoaded,
|
||||
isUploadError,
|
||||
isFetchError,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<SelectionModal
|
||||
{...{
|
||||
isOpen: true,
|
||||
close: handleCancel,
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError,
|
||||
inputError,
|
||||
fileInput,
|
||||
galleryProps,
|
||||
searchSortProps,
|
||||
selectBtnProps,
|
||||
acceptedFiles: acceptedImgKeys,
|
||||
modalMessages,
|
||||
isLoaded,
|
||||
isUploadError,
|
||||
isFetchError,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
VideoGallery.propTypes = {
|
||||
rawVideos: PropTypes.shape({}).isRequired,
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
};
|
||||
VideoGallery.propTypes = {};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
rawVideos: selectors.app.videos(state),
|
||||
isLoaded: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
isFetchError: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchVideos }),
|
||||
isUploadError: selectors.requests.isFailed(state, { requestKey: RequestKeys.uploadVideo }),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(VideoGallery);
|
||||
export default VideoGallery;
|
||||
|
||||
@@ -1,104 +1,192 @@
|
||||
import { initializeMockApp } from '@edx/frontend-platform';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
import React from 'react';
|
||||
import { shallow, mount } from 'enzyme';
|
||||
import {
|
||||
act, fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
|
||||
import SelectionModal from '../../sharedComponents/SelectionModal';
|
||||
import hooks from './hooks';
|
||||
import * as module from '.';
|
||||
import { VideoGallery } from './index';
|
||||
|
||||
jest.mock('../../sharedComponents/SelectionModal', () => 'SelectionModal');
|
||||
jest.unmock('react-redux');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('@edx/paragon/icons');
|
||||
|
||||
const mockHandleVideoUploadHook = jest.fn();
|
||||
|
||||
jest.mock('./hooks', () => ({
|
||||
buildVideos: jest.fn(() => []),
|
||||
videoProps: 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' },
|
||||
})),
|
||||
handleCancel: jest.fn(),
|
||||
handleVideoUpload: () => mockHandleVideoUploadHook,
|
||||
}));
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
selectors: {
|
||||
requests: {
|
||||
isLoaded: (state, { requestKey }) => ({ isLoaded: { state, requestKey } }),
|
||||
isFetchError: (state, { requestKey }) => ({ isFetchError: { state, requestKey } }),
|
||||
isUploadError: (state, { requestKey }) => ({ isUploadError: { state, requestKey } }),
|
||||
},
|
||||
let store;
|
||||
const initialVideos = [
|
||||
{
|
||||
edx_video_id: 'id_1',
|
||||
client_video_id: 'client_id_1',
|
||||
course_video_image_url: 'course_video_image_url_1',
|
||||
created: '2022-09-07T04:56:58.726Z',
|
||||
status: 'Uploading',
|
||||
duration: 3,
|
||||
transcripts: [],
|
||||
},
|
||||
}));
|
||||
{
|
||||
edx_video_id: 'id_2',
|
||||
client_video_id: 'client_id_2',
|
||||
course_video_image_url: 'course_video_image_url_2',
|
||||
created: '2022-11-07T04:56:58.726Z',
|
||||
status: 'In Progress',
|
||||
duration: 2,
|
||||
transcripts: [],
|
||||
}, {
|
||||
edx_video_id: 'id_3',
|
||||
client_video_id: 'client_id_3',
|
||||
course_video_image_url: 'course_video_image_url_3',
|
||||
created: '2022-01-07T04:56:58.726Z',
|
||||
status: 'Ready',
|
||||
duration: 4,
|
||||
transcripts: [],
|
||||
},
|
||||
];
|
||||
|
||||
jest.mock('../../hooks', () => ({
|
||||
...jest.requireActual('../../hooks'),
|
||||
navigateCallback: jest.fn((args) => ({ navigateCallback: args })),
|
||||
}));
|
||||
// We are not using any style-based assertions and this function is very slow with jest-dom
|
||||
window.getComputedStyle = () => ({
|
||||
getPropertyValue: () => undefined,
|
||||
});
|
||||
|
||||
describe('VideoGallery', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
rawVideos: { sOmEaSsET: { staTICUrl: '/video/sOmEaSsET' } },
|
||||
isLoaded: false,
|
||||
isFetchError: false,
|
||||
isUploadError: false,
|
||||
};
|
||||
let el;
|
||||
const videoProps = hooks.videoProps();
|
||||
beforeEach(() => {
|
||||
el = shallow(<module.VideoGallery {...props} />);
|
||||
mockHandleVideoUploadHook.mockReset();
|
||||
let oldLocation;
|
||||
beforeEach(async () => {
|
||||
store = configureStore({
|
||||
reducer: (state, action) => ((action && action.newState) ? action.newState : state),
|
||||
preloadedState: {
|
||||
app: {
|
||||
videos: initialVideos,
|
||||
learningContextId: 'course-v1:test+test+test',
|
||||
blockId: 'some-block-id',
|
||||
},
|
||||
requests: {
|
||||
fetchVideos: { status: 'completed' },
|
||||
uploadVideo: { status: 'inactive' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
initializeMockApp({
|
||||
authenticatedUser: {
|
||||
userId: 3,
|
||||
username: 'test-user',
|
||||
administrator: true,
|
||||
roles: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
it('provides confirm action, forwarding selectBtnProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().selectBtnProps).toEqual(
|
||||
expect.objectContaining({ ...hooks.videoProps().selectBtnProps }),
|
||||
beforeAll(() => {
|
||||
oldLocation = window.location;
|
||||
delete window.location;
|
||||
window.location = { assign: jest.fn() };
|
||||
});
|
||||
afterAll(() => {
|
||||
window.location = oldLocation;
|
||||
});
|
||||
|
||||
function updateState({ videos = initialVideos, fetchVideos = 'completed', uploadVideos = 'inactive' }) {
|
||||
store.dispatch({
|
||||
type: '',
|
||||
newState: {
|
||||
app: {
|
||||
videos,
|
||||
learningContextId: 'course-v1:test+test+test',
|
||||
blockId: 'some-block-id',
|
||||
},
|
||||
requests: {
|
||||
fetchVideos: { status: fetchVideos },
|
||||
uploadVideo: { status: uploadVideos },
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function renderComponent() {
|
||||
return render(
|
||||
<AppProvider store={store}>
|
||||
<VideoGallery />
|
||||
</AppProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
it('displays a list of videos', async () => {
|
||||
await renderComponent();
|
||||
initialVideos.forEach(video => (
|
||||
expect(screen.getByText(video.client_video_id)).toBeInTheDocument()
|
||||
));
|
||||
});
|
||||
it('provides file upload button linked to fileInput.click', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput.click).toEqual(
|
||||
videoProps.fileInput.click,
|
||||
);
|
||||
it('navigates to video upload page when there are no videos', async () => {
|
||||
expect(window.location.assign).not.toHaveBeenCalled();
|
||||
updateState({ videos: [] });
|
||||
await renderComponent();
|
||||
expect(window.location.assign).toHaveBeenCalled();
|
||||
});
|
||||
it('provides a SearchSort component with searchSortProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().searchSortProps).toEqual(videoProps.searchSortProps);
|
||||
it.each([
|
||||
[/by date added \(newest\)/i, [2, 1, 3]],
|
||||
[/by date added \(oldest\)/i, [3, 1, 2]],
|
||||
[/by name \(ascending\)/i, [1, 2, 3]],
|
||||
[/by name \(descending\)/i, [3, 2, 1]],
|
||||
[/by duration \(longest\)/i, [3, 1, 2]],
|
||||
[/by duration \(shortest\)/i, [2, 1, 3]],
|
||||
])('videos can be sorted %s', async (sortBy, order) => {
|
||||
await renderComponent();
|
||||
|
||||
fireEvent.click(screen.getByRole('button', {
|
||||
name: /by date added \(newest\)/i,
|
||||
}));
|
||||
fireEvent.click(screen.getByRole('link', {
|
||||
name: sortBy,
|
||||
}));
|
||||
const videoElements = screen.getAllByRole('button', { name: /client_id/ });
|
||||
order.forEach((clientIdSuffix, idx) => {
|
||||
expect(videoElements[idx]).toHaveTextContent(`client_id_${clientIdSuffix}`);
|
||||
});
|
||||
});
|
||||
it('provides a Gallery component with galleryProps from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().galleryProps).toEqual(videoProps.galleryProps);
|
||||
it.each([
|
||||
['Uploading', 1, [1]],
|
||||
['Processing', 1, [2]],
|
||||
['Ready', 1, [3]],
|
||||
['Failed', 1, [4]],
|
||||
])('videos can be filtered by status %s', async (filterBy, length, items) => {
|
||||
await renderComponent();
|
||||
updateState({
|
||||
videos: [...initialVideos, {
|
||||
edx_video_id: 'id_4',
|
||||
client_video_id: 'client_id_4',
|
||||
course_video_image_url: 'course_video_image_url_4',
|
||||
created: '2022-01-07T04:56:58.726Z',
|
||||
status: 'Failed',
|
||||
duration: 4,
|
||||
transcripts: [],
|
||||
}],
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('button', {
|
||||
name: 'Video status',
|
||||
}));
|
||||
});
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByRole('checkbox', {
|
||||
name: filterBy,
|
||||
}));
|
||||
});
|
||||
|
||||
const videoElements = await screen.findAllByRole('button', { name: /client_id/ });
|
||||
expect(videoElements).toHaveLength(length);
|
||||
items.forEach(clientIdx => (
|
||||
expect(screen.getByText(`client_id_${clientIdx}`)).toBeInTheDocument()
|
||||
));
|
||||
});
|
||||
it('provides a FileInput component with fileInput props from imgHooks', () => {
|
||||
expect(el.find(SelectionModal).props().fileInput).toMatchObject(videoProps.fileInput);
|
||||
});
|
||||
it('handleVideoUpload called if there are no videos', () => {
|
||||
el = mount(<module.VideoGallery {...props} />);
|
||||
expect(mockHandleVideoUploadHook).not.toHaveBeenCalled();
|
||||
el.setProps({ rawVideos: {}, isLoaded: true });
|
||||
el.mount();
|
||||
expect(mockHandleVideoUploadHook).toHaveBeenCalled();
|
||||
|
||||
it('filters videos by search string', async () => {
|
||||
await renderComponent();
|
||||
fireEvent.change(screen.getByRole('textbox'), { target: { value: 'CLIENT_ID_2' } });
|
||||
expect(screen.queryByText('client_id_2')).toBeInTheDocument();
|
||||
expect(screen.queryByText('client_id_1')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('client_id_3')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,15 +22,14 @@ export const sortMessages = StrictDict({
|
||||
});
|
||||
|
||||
export const filterKeys = StrictDict({
|
||||
videoStatus: 'videoStatus',
|
||||
uploading: 'uploading',
|
||||
processing: 'processing',
|
||||
ready: 'ready',
|
||||
failed: 'failed',
|
||||
uploading: 'Uploading',
|
||||
processing: 'In Progress',
|
||||
ready: 'Ready',
|
||||
failed: 'Failed',
|
||||
});
|
||||
|
||||
export const filterMessages = StrictDict({
|
||||
videoStatus: messages[messageKeys.filterByVideoStatusNone],
|
||||
title: messages[messageKeys.filterByVideoStatusNone],
|
||||
uploading: messages[messageKeys.filterByVideoStatusUploading],
|
||||
processing: messages[messageKeys.filterByVideoStatusProcessing],
|
||||
ready: messages[messageKeys.filterByVideoStatusReady],
|
||||
|
||||
@@ -8,6 +8,7 @@ exports[`BaseModal ImageUploadModal template component snapshot 1`] = `
|
||||
isOpen={true}
|
||||
onClose={[MockFunction props.close]}
|
||||
size="lg"
|
||||
title="props.title node"
|
||||
variant="default"
|
||||
>
|
||||
<ModalDialog.Header
|
||||
|
||||
@@ -29,6 +29,7 @@ export const BaseModal = ({
|
||||
hasCloseButton
|
||||
isFullscreenOnMobile
|
||||
isFullscreenScroll={isFullscreenScroll}
|
||||
title={title}
|
||||
>
|
||||
<ModalDialog.Header style={{ zIndex: 1, boxShadow: '2px 2px 5px rgba(0, 0, 0, 0.3)' }}>
|
||||
<ModalDialog.Title>
|
||||
|
||||
@@ -7,15 +7,13 @@ import {
|
||||
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import GalleryCard from './GalleryCard';
|
||||
|
||||
export const Gallery = ({
|
||||
show,
|
||||
galleryIsEmpty,
|
||||
searchIsEmpty,
|
||||
displayList,
|
||||
@@ -25,12 +23,8 @@ export const Gallery = ({
|
||||
showIdsOnCards,
|
||||
height,
|
||||
isLoaded,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
if (!show) {
|
||||
return null;
|
||||
}
|
||||
const intl = useIntl();
|
||||
if (!isLoaded) {
|
||||
return (
|
||||
<div style={{
|
||||
@@ -96,8 +90,6 @@ Gallery.propTypes = {
|
||||
emptyGalleryLabel: PropTypes.shape({}).isRequired,
|
||||
showIdsOnCards: PropTypes.bool,
|
||||
height: PropTypes.string,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(Gallery);
|
||||
export default Gallery;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { Gallery } from './Gallery';
|
||||
|
||||
jest.mock('../../data/redux', () => ({
|
||||
@@ -18,28 +19,28 @@ describe('TextEditor Image Gallery component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
galleryIsEmpty: false,
|
||||
emptyGalleryLabel: {
|
||||
id: 'emptyGalleryMsg',
|
||||
defaultMessage: 'Empty Gallery',
|
||||
},
|
||||
searchIsEmpty: false,
|
||||
displayList: [{ id: 1 }, { id: 2 }, { id: 3 }],
|
||||
highlighted: 'props.highlighted',
|
||||
onHighlightChange: jest.fn().mockName('props.onHighlightChange'),
|
||||
intl: { formatMessage },
|
||||
isLoaded: true,
|
||||
};
|
||||
const shallowWithIntl = (component) => shallow(<IntlProvider locale="en">{component}</IntlProvider>);
|
||||
test('snapshot: not loaded, show spinner', () => {
|
||||
expect(shallow(<Gallery {...props} isLoaded={false} />)).toMatchSnapshot();
|
||||
expect(shallowWithIntl(<Gallery {...props} isLoaded={false} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded but no images, show empty gallery', () => {
|
||||
expect(shallow(<Gallery {...props} galleryIsEmpty />)).toMatchSnapshot();
|
||||
expect(shallowWithIntl(<Gallery {...props} galleryIsEmpty />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded but search returns no images, show 0 search result gallery', () => {
|
||||
expect(shallow(<Gallery {...props} searchIsEmpty />)).toMatchSnapshot();
|
||||
expect(shallowWithIntl(<Gallery {...props} searchIsEmpty />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: loaded, show gallery', () => {
|
||||
expect(shallow(<Gallery {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: not shot gallery', () => {
|
||||
const wrapper = shallow(<Gallery {...props} show={false} />);
|
||||
expect(wrapper.type()).toBeNull();
|
||||
expect(shallowWithIntl(<Gallery {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,23 +16,18 @@ export const GalleryCard = ({
|
||||
asset,
|
||||
}) => (
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key={asset.externalUrl}
|
||||
type="radio"
|
||||
value={asset.id}
|
||||
style={{
|
||||
padding: '10px 20px',
|
||||
border: 'none',
|
||||
boxShadow: 'none',
|
||||
}}
|
||||
>
|
||||
<div className="card-div d-flex flex-row flex-nowrap">
|
||||
<div style={{
|
||||
position: 'relative',
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
margin: '18px 0 0 0',
|
||||
}}
|
||||
<div className="card-div d-flex flex-row flex-nowrap align-items-center">
|
||||
<div
|
||||
className="position-relative"
|
||||
style={{
|
||||
width: '200px',
|
||||
height: '100px',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
style={{ border: 'none', width: '200px', height: '100px' }}
|
||||
@@ -57,7 +52,7 @@ export const GalleryCard = ({
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="card-text p-3" style={{ marginTop: '10px' }}>
|
||||
<div className="card-text px-3 py-2" style={{ marginTop: '10px' }}>
|
||||
<h3 className="text-primary-500">{asset.displayName}</h3>
|
||||
{ asset.transcripts && (
|
||||
<div style={{ margin: '0 0 5px 0' }}>
|
||||
@@ -86,7 +81,7 @@ GalleryCard.propTypes = {
|
||||
displayName: PropTypes.string,
|
||||
externalUrl: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
dateAdded: PropTypes.number,
|
||||
dateAdded: PropTypes.oneOfType([PropTypes.number, PropTypes.instanceOf(Date)]),
|
||||
locked: PropTypes.bool,
|
||||
portableUrl: PropTypes.string,
|
||||
thumbnail: PropTypes.string,
|
||||
@@ -94,7 +89,7 @@ GalleryCard.propTypes = {
|
||||
duration: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
statusBadgeVariant: PropTypes.string,
|
||||
transcripts: PropTypes.shape([]),
|
||||
transcripts: PropTypes.arrayOf(PropTypes.string),
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Dropdown, DropdownToggle, Form } from '@edx/paragon';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils';
|
||||
|
||||
const MultiSelectFilterDropdown = ({
|
||||
selected, onSelectionChange,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<Dropdown autoClose={false}>
|
||||
<DropdownToggle variant="outline" id="gallery-filter">
|
||||
{intl.formatMessage(filterMessages.title)}
|
||||
</DropdownToggle>
|
||||
<Dropdown.Menu renderOnMount className="p-2">
|
||||
{Object.keys(filterKeys).map(key => (
|
||||
<Dropdown.Item
|
||||
key={key}
|
||||
as={Form.Checkbox}
|
||||
checked={selected.includes(key)}
|
||||
onChange={onSelectionChange(key)}
|
||||
>
|
||||
<span className="p-1">{intl.formatMessage(filterMessages[key])}</span>
|
||||
</Dropdown.Item>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
};
|
||||
|
||||
MultiSelectFilterDropdown.propTypes = {
|
||||
selected: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
onSelectionChange: PropTypes.func.isRequired,
|
||||
};
|
||||
export default MultiSelectFilterDropdown;
|
||||
@@ -2,16 +2,17 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
ActionRow, Dropdown, Form, Icon, IconButton,
|
||||
ActionRow, Form, Icon, IconButton, SelectMenu, MenuItem,
|
||||
} from '@edx/paragon';
|
||||
import { Close, Search } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import messages from './messages';
|
||||
import MultiSelectFilterDropdown from './MultiSelectFilterDropdown';
|
||||
import { sortKeys, sortMessages } from '../../containers/VideoGallery/utils';
|
||||
|
||||
export const SearchSort = ({
|
||||
searchString,
|
||||
@@ -19,28 +20,25 @@ export const SearchSort = ({
|
||||
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={
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<ActionRow>
|
||||
<Form.Group style={{ margin: 0 }}>
|
||||
<Form.Control
|
||||
autoFocus
|
||||
onChange={onSearchChange}
|
||||
placeholder={intl.formatMessage(messages.searchPlaceholder)}
|
||||
trailingElement={
|
||||
searchString
|
||||
? (
|
||||
<IconButton
|
||||
alt={intl.formatMessage(messages.clearSearch)}
|
||||
iconAs={Icon}
|
||||
invertColors
|
||||
isActive
|
||||
@@ -51,62 +49,43 @@ export const SearchSort = ({
|
||||
)
|
||||
: <Icon src={Search} />
|
||||
}
|
||||
value={searchString}
|
||||
/>
|
||||
</Form.Group>
|
||||
value={searchString}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
{ !showSwitch && <ActionRow.Spacer /> }
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle className="text-gray-700" id="gallery-sort-button" variant="tertiary">
|
||||
<FormattedMessage {...sortMessages[sortBy]} />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
{ !showSwitch && <ActionRow.Spacer /> }
|
||||
<SelectMenu variant="link">
|
||||
{Object.keys(sortKeys).map(key => (
|
||||
<Dropdown.Item key={key} onClick={onSortClick(key)}>
|
||||
<MenuItem key={key} onClick={onSortClick(key)} defaultSelected={key === sortBy}>
|
||||
<FormattedMessage {...sortMessages[key]} />
|
||||
</Dropdown.Item>
|
||||
</MenuItem>
|
||||
))}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</SelectMenu>
|
||||
|
||||
{ filterKeys && filterMessages && (
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle className="text-gray-700" 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>
|
||||
)}
|
||||
{onFilterClick && <MultiSelectFilterDropdown selected={filterBy} onSelectionChange={onFilterClick} />}
|
||||
|
||||
{ showSwitch && (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
<Form.SwitchSet
|
||||
name="switch"
|
||||
onChange={onSwitchClick}
|
||||
isInline
|
||||
>
|
||||
<Form.Switch className="text-gray-700" value="switch-value" floatLabelLeft>
|
||||
<FormattedMessage {...switchMessage} />
|
||||
</Form.Switch>
|
||||
</Form.SwitchSet>
|
||||
</>
|
||||
)}
|
||||
{ showSwitch && (
|
||||
<>
|
||||
<ActionRow.Spacer />
|
||||
<Form.SwitchSet
|
||||
name="switch"
|
||||
onChange={onSwitchClick}
|
||||
isInline
|
||||
>
|
||||
<Form.Switch className="text-gray-700" value="switch-value" floatLabelLeft>
|
||||
<FormattedMessage {...switchMessage} />
|
||||
</Form.Switch>
|
||||
</Form.SwitchSet>
|
||||
</>
|
||||
)}
|
||||
|
||||
</ActionRow>
|
||||
);
|
||||
</ActionRow>
|
||||
);
|
||||
};
|
||||
|
||||
SearchSort.defaultProps = {
|
||||
filterBy: '',
|
||||
onFilterClick: null,
|
||||
filterKeys: null,
|
||||
filterMessages: null,
|
||||
showSwitch: false,
|
||||
onSwitchClick: null,
|
||||
};
|
||||
@@ -117,17 +96,11 @@ SearchSort.propTypes = {
|
||||
clearSearchString: PropTypes.func.isRequired,
|
||||
sortBy: PropTypes.string.isRequired,
|
||||
onSortClick: PropTypes.func.isRequired,
|
||||
sortKeys: PropTypes.shape({}).isRequired,
|
||||
sortMessages: PropTypes.shape({}).isRequired,
|
||||
filterBy: PropTypes.string,
|
||||
filterBy: PropTypes.arrayOf(PropTypes.string),
|
||||
onFilterClick: PropTypes.func,
|
||||
filterKeys: PropTypes.shape({}),
|
||||
filterMessages: PropTypes.shape({}),
|
||||
showSwitch: PropTypes.bool,
|
||||
switchMessage: PropTypes.shape({}).isRequired,
|
||||
onSwitchClick: PropTypes.func,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SearchSort);
|
||||
export default SearchSort;
|
||||
|
||||
@@ -1,101 +1,89 @@
|
||||
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 '@testing-library/jest-dom';
|
||||
import {
|
||||
act, fireEvent, render, screen,
|
||||
} from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { sortKeys, sortMessages } from '../ImageUploadModal/SelectImageModal/utils';
|
||||
import { filterKeys, filterMessages } from '../../containers/VideoGallery/utils';
|
||||
import { filterMessages } from '../../containers/VideoGallery/utils';
|
||||
import { SearchSort } from './SearchSort';
|
||||
import messages from './messages';
|
||||
|
||||
jest.unmock('react-redux');
|
||||
jest.unmock('@edx/frontend-platform/i18n');
|
||||
jest.unmock('@edx/paragon');
|
||||
jest.unmock('@edx/paragon/icons');
|
||||
|
||||
describe('SearchSort component', () => {
|
||||
describe('snapshots without filterKeys', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
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 },
|
||||
};
|
||||
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);
|
||||
const props = {
|
||||
searchString: '',
|
||||
onSearchChange: jest.fn()
|
||||
.mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn()
|
||||
.mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
onSortClick: jest.fn()
|
||||
.mockName('props.onSortClick'),
|
||||
switchMessage: {
|
||||
id: 'test.id',
|
||||
defaultMessage: 'test message',
|
||||
},
|
||||
onFilterClick: jest.fn(),
|
||||
showSwitch: true,
|
||||
};
|
||||
|
||||
function getComponent(overrideProps = {}) {
|
||||
return render(
|
||||
<IntlProvider locale="en">
|
||||
<SearchSort {...props} {...overrideProps} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
test('adds a sort option for each sortKey', async () => {
|
||||
const { getByRole } = getComponent();
|
||||
await act(() => {
|
||||
fireEvent.click(screen.getByRole('button', {
|
||||
name: /by date added \(oldest\)/i,
|
||||
}));
|
||||
});
|
||||
Object.values(sortMessages)
|
||||
.forEach(({ defaultMessage }) => {
|
||||
expect(getByRole('link', { name: defaultMessage }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
describe('snapshots with filterKeys', () => {
|
||||
const props = {
|
||||
searchString: 'props.searchString',
|
||||
onSearchChange: jest.fn().mockName('props.onSearchChange'),
|
||||
clearSearchString: jest.fn().mockName('props.clearSearchString'),
|
||||
sortBy: sortKeys.dateOldest,
|
||||
sortKeys,
|
||||
sortMessages,
|
||||
filterKeys,
|
||||
filterMessages,
|
||||
showSwitch: true,
|
||||
onSortClick: jest.fn().mockName('props.onSortClick'),
|
||||
onFilterClick: jest.fn().mockName('props.onFilterClick'),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
test('with search string (close button)', () => {
|
||||
expect(shallow(<SearchSort {...props} />)).toMatchSnapshot();
|
||||
test('adds a sort option for each sortKey', async () => {
|
||||
const { getByRole } = getComponent();
|
||||
await act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /by date added \(oldest\)/i }));
|
||||
});
|
||||
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);
|
||||
});
|
||||
test('adds a filter option for each filterKet', () => {
|
||||
const el = shallow(<SearchSort {...props} />);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.videoStatus} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.uploading} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.processing} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.ready} />,
|
||||
)).toEqual(true);
|
||||
expect(el.find(Dropdown).containsMatchingElement(
|
||||
<FormattedMessage {...filterMessages.failed} />,
|
||||
)).toEqual(true);
|
||||
Object.values(sortMessages)
|
||||
.forEach(({ defaultMessage }) => {
|
||||
expect(getByRole('link', { name: defaultMessage }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('adds a filter option for each filterKet', async () => {
|
||||
const { getByRole } = getComponent();
|
||||
await act(() => {
|
||||
fireEvent.click(screen.getByRole('button', { name: /video status/i }));
|
||||
});
|
||||
Object.keys(filterMessages)
|
||||
.forEach((key) => {
|
||||
if (key !== 'title') {
|
||||
expect(getByRole('checkbox', { name: filterMessages[key].defaultMessage }))
|
||||
.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
test('searchbox should show clear message button when not empty', async () => {
|
||||
const { queryByRole } = getComponent({ searchString: 'some string' });
|
||||
expect(queryByRole('button', { name: messages.clearSearch.defaultMessage }))
|
||||
.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,104 +1,297 @@
|
||||
// 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-light-400"
|
||||
style={
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage />
|
||||
</div>
|
||||
<Gallery
|
||||
displayList={
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
},
|
||||
Object {
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
emptyGalleryLabel={
|
||||
Object {
|
||||
"defaultMessage": "Empty Gallery",
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
galleryIsEmpty={true}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
isLoaded={true}
|
||||
onHighlightChange={[MockFunction props.onHighlightChange]}
|
||||
searchIsEmpty={false}
|
||||
show={true}
|
||||
showIdsOnCards={false}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
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-light-400"
|
||||
style={
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="No search results."
|
||||
description="Label for when search returns nothing."
|
||||
id="authoring.selectionmodal.emptySearchLabel"
|
||||
<Gallery
|
||||
displayList={
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
},
|
||||
Object {
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
emptyGalleryLabel={
|
||||
Object {
|
||||
"defaultMessage": "Empty Gallery",
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
isLoaded={true}
|
||||
onHighlightChange={[MockFunction props.onHighlightChange]}
|
||||
searchIsEmpty={true}
|
||||
show={true}
|
||||
showIdsOnCards={false}
|
||||
/>
|
||||
</div>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: loaded, show gallery 1`] = `
|
||||
<Scrollable
|
||||
className="gallery bg-light-400"
|
||||
style={
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"height": "375px",
|
||||
"margin": "0 -1.5rem",
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="p-4"
|
||||
>
|
||||
<SelectableBox.Set
|
||||
columns={1}
|
||||
name="images"
|
||||
onChange={[MockFunction props.onHighlightChange]}
|
||||
type="radio"
|
||||
value="props.highlighted"
|
||||
>
|
||||
<GalleryCard
|
||||
asset={
|
||||
Object {
|
||||
"id": 1,
|
||||
}
|
||||
}
|
||||
key="1"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
asset={
|
||||
Object {
|
||||
"id": 2,
|
||||
}
|
||||
}
|
||||
key="2"
|
||||
showId={false}
|
||||
/>
|
||||
<GalleryCard
|
||||
asset={
|
||||
Object {
|
||||
"id": 3,
|
||||
}
|
||||
}
|
||||
key="3"
|
||||
showId={false}
|
||||
/>
|
||||
</SelectableBox.Set>
|
||||
</div>
|
||||
</Scrollable>
|
||||
<Gallery
|
||||
displayList={
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
},
|
||||
Object {
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
emptyGalleryLabel={
|
||||
Object {
|
||||
"defaultMessage": "Empty Gallery",
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
isLoaded={true}
|
||||
onHighlightChange={[MockFunction props.onHighlightChange]}
|
||||
searchIsEmpty={false}
|
||||
show={true}
|
||||
showIdsOnCards={false}
|
||||
/>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
exports[`TextEditor Image Gallery component component snapshot: not loaded, show spinner 1`] = `
|
||||
<div
|
||||
style={
|
||||
<ContextProvider
|
||||
value={
|
||||
Object {
|
||||
"left": "50%",
|
||||
"position": "absolute",
|
||||
"top": "50%",
|
||||
"transform": "translate(-50%, -50%)",
|
||||
"$t": [Function],
|
||||
"defaultFormats": Object {},
|
||||
"defaultLocale": "en",
|
||||
"defaultRichTextElements": undefined,
|
||||
"fallbackOnEmptyString": true,
|
||||
"formatDate": [Function],
|
||||
"formatDateTimeRange": [Function],
|
||||
"formatDateToParts": [Function],
|
||||
"formatDisplayName": [Function],
|
||||
"formatList": [Function],
|
||||
"formatListToParts": [Function],
|
||||
"formatMessage": [Function],
|
||||
"formatNumber": [Function],
|
||||
"formatNumberToParts": [Function],
|
||||
"formatPlural": [Function],
|
||||
"formatRelativeTime": [Function],
|
||||
"formatTime": [Function],
|
||||
"formatTimeToParts": [Function],
|
||||
"formats": Object {},
|
||||
"formatters": Object {
|
||||
"getDateTimeFormat": [Function],
|
||||
"getDisplayNames": [Function],
|
||||
"getListFormat": [Function],
|
||||
"getMessageFormat": [Function],
|
||||
"getNumberFormat": [Function],
|
||||
"getPluralRules": [Function],
|
||||
"getRelativeTimeFormat": [Function],
|
||||
},
|
||||
"locale": "en",
|
||||
"messages": Object {},
|
||||
"onError": [Function],
|
||||
"onWarn": [Function],
|
||||
"textComponent": Symbol(react.fragment),
|
||||
"timeZone": undefined,
|
||||
"wrapRichTextChunksInFragment": undefined,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="mie-3"
|
||||
screenReaderText="loading..."
|
||||
<Gallery
|
||||
displayList={
|
||||
Array [
|
||||
Object {
|
||||
"id": 1,
|
||||
},
|
||||
Object {
|
||||
"id": 2,
|
||||
},
|
||||
Object {
|
||||
"id": 3,
|
||||
},
|
||||
]
|
||||
}
|
||||
emptyGalleryLabel={
|
||||
Object {
|
||||
"defaultMessage": "Empty Gallery",
|
||||
"id": "emptyGalleryMsg",
|
||||
}
|
||||
}
|
||||
galleryIsEmpty={false}
|
||||
height="375px"
|
||||
highlighted="props.highlighted"
|
||||
isLoaded={false}
|
||||
onHighlightChange={[MockFunction props.onHighlightChange]}
|
||||
searchIsEmpty={false}
|
||||
show={true}
|
||||
showIdsOnCards={false}
|
||||
/>
|
||||
</div>
|
||||
</ContextProvider>
|
||||
`;
|
||||
|
||||
@@ -2,26 +2,18 @@
|
||||
|
||||
exports[`GalleryCard component snapshot with duration badge 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
@@ -51,7 +43,7 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
@@ -94,26 +86,18 @@ exports[`GalleryCard component snapshot with duration badge 1`] = `
|
||||
|
||||
exports[`GalleryCard component snapshot with duration transcripts 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
@@ -130,7 +114,7 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
@@ -188,26 +172,18 @@ exports[`GalleryCard component snapshot with duration transcripts 1`] = `
|
||||
|
||||
exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
@@ -236,7 +212,7 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
</Component>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
@@ -279,26 +255,18 @@ exports[`GalleryCard component snapshot with status badge 1`] = `
|
||||
|
||||
exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
<SelectableBox
|
||||
className="card bg-white"
|
||||
className="card bg-white shadow-none border-0 py-0"
|
||||
key="props.img.externalUrl"
|
||||
style={
|
||||
Object {
|
||||
"border": "none",
|
||||
"boxShadow": "none",
|
||||
"padding": "10px 20px",
|
||||
}
|
||||
}
|
||||
type="radio"
|
||||
>
|
||||
<div
|
||||
className="card-div d-flex flex-row flex-nowrap"
|
||||
className="card-div d-flex flex-row flex-nowrap align-items-center"
|
||||
>
|
||||
<div
|
||||
className="position-relative"
|
||||
style={
|
||||
Object {
|
||||
"height": "100px",
|
||||
"margin": "18px 0 0 0",
|
||||
"position": "relative",
|
||||
"width": "200px",
|
||||
}
|
||||
}
|
||||
@@ -315,7 +283,7 @@ exports[`GalleryCard component snapshot: dateAdded=12345 1`] = `
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="card-text p-3"
|
||||
className="card-text px-3 py-2"
|
||||
style={
|
||||
Object {
|
||||
"marginTop": "10px",
|
||||
|
||||
@@ -1,445 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SearchSort component snapshots with filterKeys 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>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-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>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="videoStatus"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Video status"
|
||||
description="Dropdown label for filter by video status (none)"
|
||||
id="authoring.selectvideomodal.filter.videostatusnone.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="uploading"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Uploading"
|
||||
description="Dropdown label for filter by video status (uploading)"
|
||||
id="authoring.selectvideomodal.filter.videostatusuploading.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="processing"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Processing"
|
||||
description="Dropdown label for filter by video status (processing)"
|
||||
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="ready"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ready"
|
||||
description="Dropdown label for filter by video status (ready)"
|
||||
id="authoring.selectvideomodal.filter.videostatusready.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="failed"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed"
|
||||
description="Dropdown label for filter by video status (failed)"
|
||||
id="authoring.selectvideomodal.filter.videostatusfailed.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ActionRow.Spacer />
|
||||
<Component
|
||||
isInline={true}
|
||||
name="switch"
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
className="text-gray-700"
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Component>
|
||||
</Component>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots with filterKeys 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>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-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>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
className="text-gray-700"
|
||||
id="gallery-filter-button"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item
|
||||
key="videoStatus"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Video status"
|
||||
description="Dropdown label for filter by video status (none)"
|
||||
id="authoring.selectvideomodal.filter.videostatusnone.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="uploading"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Uploading"
|
||||
description="Dropdown label for filter by video status (uploading)"
|
||||
id="authoring.selectvideomodal.filter.videostatusuploading.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="processing"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Processing"
|
||||
description="Dropdown label for filter by video status (processing)"
|
||||
id="authoring.selectvideomodal.filter.videostatusprocessing.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="ready"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ready"
|
||||
description="Dropdown label for filter by video status (ready)"
|
||||
id="authoring.selectvideomodal.filter.videostatusready.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="failed"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Failed"
|
||||
description="Dropdown label for filter by video status (failed)"
|
||||
id="authoring.selectvideomodal.filter.videostatusfailed.label"
|
||||
/>
|
||||
</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<ActionRow.Spacer />
|
||||
<Component
|
||||
isInline={true}
|
||||
name="switch"
|
||||
onChange={null}
|
||||
>
|
||||
<Component
|
||||
className="text-gray-700"
|
||||
floatLabelLeft={true}
|
||||
value="switch-value"
|
||||
>
|
||||
<FormattedMessage />
|
||||
</Component>
|
||||
</Component>
|
||||
</ActionRow>
|
||||
`;
|
||||
|
||||
exports[`SearchSort component snapshots without filterKeys 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
|
||||
className="text-gray-700"
|
||||
id="gallery-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 filterKeys 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
|
||||
className="text-gray-700"
|
||||
id="gallery-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>
|
||||
`;
|
||||
@@ -5,8 +5,7 @@ import { Button, Stack } from '@edx/paragon';
|
||||
import { Add } from '@edx/paragon/icons';
|
||||
import {
|
||||
FormattedMessage,
|
||||
injectIntl,
|
||||
intlShape,
|
||||
useIntl,
|
||||
} from '@edx/frontend-platform/i18n';
|
||||
|
||||
import BaseModal from '../BaseModal';
|
||||
@@ -33,9 +32,8 @@ export const SelectionModal = ({
|
||||
isLoaded,
|
||||
isFetchError,
|
||||
isUploadError,
|
||||
// injected
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
confirmMsg,
|
||||
uploadButtonMsg,
|
||||
@@ -54,7 +52,6 @@ export const SelectionModal = ({
|
||||
|
||||
const galleryPropsValues = {
|
||||
isLoaded,
|
||||
show: showGallery,
|
||||
...galleryProps,
|
||||
};
|
||||
return (
|
||||
@@ -109,7 +106,7 @@ export const SelectionModal = ({
|
||||
<FormattedMessage {...galleryError.message} />
|
||||
</ErrorAlert>
|
||||
<Stack gap={2}>
|
||||
<Gallery {...galleryPropsValues} />
|
||||
{showGallery && <Gallery {...galleryPropsValues} />}
|
||||
<FileInput fileInput={fileInput} acceptedFiles={Object.values(acceptedFiles).join()} />
|
||||
</Stack>
|
||||
</BaseModal>
|
||||
@@ -155,8 +152,6 @@ SelectionModal.propTypes = {
|
||||
isLoaded: PropTypes.bool.isRequired,
|
||||
isFetchError: PropTypes.bool.isRequired,
|
||||
isUploadError: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SelectionModal);
|
||||
export default SelectionModal;
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { formatMessage } from '../../../testUtils';
|
||||
|
||||
import SelectionModal from '.';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
const props = {
|
||||
isOpen: jest.fn(),
|
||||
isClose: jest.fn(),
|
||||
isOpen: true,
|
||||
close: jest.fn(),
|
||||
size: 'fullscreen',
|
||||
isFullscreenScroll: false,
|
||||
galleryError: {
|
||||
@@ -35,7 +36,13 @@ const props = {
|
||||
click: 'imgHooks.fileInput.click',
|
||||
ref: 'imgHooks.fileInput.ref',
|
||||
},
|
||||
galleryProps: { gallery: 'props' },
|
||||
galleryProps: {
|
||||
gallery: 'props',
|
||||
emptyGalleryLabel: {
|
||||
id: 'emptyGalleryMsg',
|
||||
defaultMessage: 'Empty Gallery',
|
||||
},
|
||||
},
|
||||
searchSortProps: { search: 'sortProps' },
|
||||
selectBtnProps: { select: 'btnProps' },
|
||||
acceptedFiles: { png: '.png' },
|
||||
@@ -69,7 +76,6 @@ const props = {
|
||||
isLoaded: true,
|
||||
isFetchError: false,
|
||||
isUploadError: false,
|
||||
intl: { formatMessage },
|
||||
};
|
||||
|
||||
const mockGalleryFn = jest.fn();
|
||||
@@ -105,7 +111,7 @@ describe('Selection Modal', () => {
|
||||
});
|
||||
test('rendering correctly with expected Input', async () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<IntlProvider locale="en">
|
||||
<SelectionModal {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
@@ -118,7 +124,6 @@ describe('Selection Modal', () => {
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: props.isLoaded,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
expect(mockFetchErrorAlertFn).toHaveBeenCalledWith(
|
||||
@@ -142,11 +147,11 @@ describe('Selection Modal', () => {
|
||||
});
|
||||
test('rendering correctly with errors', () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<IntlProvider locale="en">
|
||||
<SelectionModal {...props} isFetchError />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText('Gallery')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Gallery')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('FileInput')).toBeInTheDocument();
|
||||
expect(screen.getByText('FetchErrorAlert')).toBeInTheDocument();
|
||||
expect(screen.getByText('UploadErrorAlert')).toBeInTheDocument();
|
||||
@@ -157,17 +162,10 @@ describe('Selection Modal', () => {
|
||||
message: props.modalMessages.fetchError,
|
||||
}),
|
||||
);
|
||||
expect(mockGalleryFn).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: props.isLoaded,
|
||||
show: false,
|
||||
}),
|
||||
);
|
||||
});
|
||||
test('rendering correctly with loading', () => {
|
||||
render(
|
||||
<IntlProvider>
|
||||
<IntlProvider locale="en">
|
||||
<SelectionModal {...props} isLoaded={false} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
@@ -180,7 +178,6 @@ describe('Selection Modal', () => {
|
||||
expect.objectContaining({
|
||||
...props.galleryProps,
|
||||
isLoaded: false,
|
||||
show: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -4,6 +4,11 @@ export const messages = {
|
||||
defaultMessage: 'Search',
|
||||
description: 'Placeholder text for search bar',
|
||||
},
|
||||
clearSearch: {
|
||||
id: 'authoring.selectionmodal.search.clearSearchButton',
|
||||
defaultMessage: 'Clear search query',
|
||||
description: 'Button to clear search query',
|
||||
},
|
||||
emptySearchLabel: {
|
||||
id: 'authoring.selectionmodal.emptySearchLabel',
|
||||
defaultMessage: 'No search results.',
|
||||
|
||||
Reference in New Issue
Block a user