feat: Allow selecting my multiple filters in video gallery
The sort and filter UI of the video gallery was not working, this fixes that issue, and also adds a new UI for filering videos that allows filtering videos to include more than one status. It also fixes the hooks related to VideoGallery to avoid potential bugs in the future and updates tests to use react testing library instead of enzyme. It also reduces the padding in gallery page.
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