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:
Kshitij Sobti
2023-08-07 18:18:51 +05:30
parent e9c0f6cc82
commit fb7caffdd5
20 changed files with 762 additions and 1305 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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",

View File

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

View File

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

View File

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

View File

@@ -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.',