Merge pull request #11476 from strapi/search-ml-cm

v4/ Search ML CM
This commit is contained in:
cyril lopez 2021-11-08 08:22:46 +01:00 committed by GitHub
commit 7f4e24ba39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 339 additions and 26 deletions

View File

@ -480,7 +480,7 @@ describe('<SearchURLQuery />', () => {
expect(clearedUrlSearchQuery).toEqual('?page=1');
});
it.only('should call trackUsage with trackedEvent props when submit', async () => {
it('should call trackUsage with trackedEvent props when submit', async () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history, 'thisEvent'));

View File

@ -0,0 +1,77 @@
import React, { useState, useLayoutEffect, useRef } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import { Searchbar, SearchForm } from '@strapi/design-system/Searchbar';
import { IconButton } from '@strapi/design-system/IconButton';
import SearchIcon from '@strapi/icons/Search';
import getTrad from '../../../../../utils/getTrad';
const SearchAsset = ({ onChangeSearch, queryValue }) => {
const { formatMessage } = useIntl();
const [isOpen, setIsOpen] = useState(!!queryValue);
const [value, setValue] = useState(queryValue || '');
const wrapperRef = useRef(null);
useLayoutEffect(() => {
if (isOpen) {
setTimeout(() => {
wrapperRef.current.querySelector('input').focus();
}, 0);
}
}, [isOpen]);
const handleToggle = () => {
setIsOpen(prev => !prev);
};
const handleClear = () => {
handleToggle();
onChangeSearch(null);
};
const handleSubmit = e => {
e.preventDefault();
e.stopPropagation();
onChangeSearch(value);
};
if (isOpen) {
return (
<div ref={wrapperRef}>
<SearchForm onSubmit={handleSubmit}>
<Searchbar
name="search"
onClear={handleClear}
onChange={e => setValue(e.target.value)}
clearLabel={formatMessage({
id: getTrad('search.clear.label'),
defaultMessage: 'Clear the search',
})}
size="S"
value={value}
placeholder={formatMessage({
id: getTrad('search.placeholder'),
defaultMessage: 'e.g: the first dog on the moon',
})}
>
{formatMessage({ id: getTrad('search.label'), defaultMessage: 'Search for an asset' })}
</Searchbar>
</SearchForm>
</div>
);
}
return <IconButton icon={<SearchIcon />} label="Search" onClick={handleToggle} />;
};
SearchAsset.defaultProps = {
queryValue: null,
};
SearchAsset.propTypes = {
onChangeSearch: PropTypes.func.isRequired,
queryValue: PropTypes.string,
};
export default SearchAsset;

View File

@ -0,0 +1,210 @@
/**
*
* Tests for SearchAsset
*
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import SearchAsset from '../index';
const handleChange = jest.fn();
const makeApp = queryValue => (
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<SearchAsset onChangeSearch={handleChange} queryValue={queryValue} />
</IntlProvider>
</ThemeProvider>
);
describe('<SearchURLQuery />', () => {
it('renders and matches the snapshot', () => {
const { container } = render(makeApp(null));
expect(container).toMatchInlineSnapshot(`
.c2 {
border: 0;
-webkit-clip: rect(0 0 0 0);
clip: rect(0 0 0 0);
height: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
width: 1px;
}
.c0 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
cursor: pointer;
padding: 8px;
border-radius: 4px;
background: #ffffff;
border: 1px solid #dcdce4;
position: relative;
outline: none;
}
.c0 svg {
height: 12px;
width: 12px;
}
.c0 svg > g,
.c0 svg path {
fill: #ffffff;
}
.c0[aria-disabled='true'] {
pointer-events: none;
}
.c0:after {
-webkit-transition-property: all;
transition-property: all;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
border-radius: 8px;
content: '';
position: absolute;
top: -4px;
bottom: -4px;
left: -4px;
right: -4px;
border: 2px solid transparent;
}
.c0:focus-visible {
outline: none;
}
.c0:focus-visible:after {
border-radius: 8px;
content: '';
position: absolute;
top: -5px;
bottom: -5px;
left: -5px;
right: -5px;
border: 2px solid #4945ff;
}
.c1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
-webkit-box-pack: center;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
height: 2rem;
width: 2rem;
}
.c1 svg > g,
.c1 svg path {
fill: #8e8ea9;
}
.c1:hover svg > g,
.c1:hover svg path {
fill: #666687;
}
.c1:active svg > g,
.c1:active svg path {
fill: #a5a5ba;
}
.c1[aria-disabled='true'] {
background-color: #eaeaef;
}
.c1[aria-disabled='true'] svg path {
fill: #666687;
}
<div>
<span>
<button
aria-disabled="false"
aria-labelledby="tooltip-1"
class="c0 c1"
tabindex="0"
type="button"
>
<svg
fill="none"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
clip-rule="evenodd"
d="M23.813 20.163l-5.3-5.367a9.792 9.792 0 001.312-4.867C19.825 4.455 15.375 0 9.913 0 4.45 0 0 4.455 0 9.929c0 5.473 4.45 9.928 9.912 9.928a9.757 9.757 0 005.007-1.4l5.275 5.35a.634.634 0 00.913 0l2.706-2.737a.641.641 0 000-.907zM9.91 3.867c3.338 0 6.05 2.718 6.05 6.061s-2.712 6.061-6.05 6.061c-3.337 0-6.05-2.718-6.05-6.06 0-3.344 2.713-6.062 6.05-6.062z"
fill="#32324D"
fill-rule="evenodd"
/>
</svg>
</button>
</span>
<div
class="c2"
>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-log"
role="log"
/>
<p
aria-live="polite"
aria-relevant="all"
id="live-region-status"
role="status"
/>
<p
aria-live="assertive"
aria-relevant="all"
id="live-region-alert"
role="alert"
/>
</div>
</div>
`);
});
it('should set input value to queryValue if it exists', () => {
const queryValue = 'michka';
const { container } = render(makeApp(queryValue));
const input = container.querySelector('input[name="search"]');
expect(input).toBeInTheDocument();
expect(input.value).toEqual(queryValue);
});
it('should call handleChange when submitting search input', () => {
const { container } = render(makeApp(null));
fireEvent.click(container.querySelector('button[type="button"]'));
const input = container.querySelector('input[name="search"]');
fireEvent.change(input, { target: { value: 'michka' } });
fireEvent.submit(input);
expect(handleChange.mock.calls.length).toBe(1);
});
});

View File

@ -4,18 +4,20 @@ import { useIntl } from 'react-intl';
import { Flex } from '@strapi/design-system/Flex';
import { Stack } from '@strapi/design-system/Stack';
import { BaseCheckbox } from '@strapi/design-system/BaseCheckbox';
import { AssetList } from '../../../AssetList';
import getTrad from '../../../../utils/getTrad';
import { AssetList } from '../../../AssetList';
import SortPicker from '../../../SortPicker';
import getAllowedFiles from '../../utils/getAllowedFiles';
import PaginationFooter from './PaginationFooter';
import PageSize from './PageSize';
import SearchAsset from './SearchAsset';
import getAllowedFiles from '../../utils/getAllowedFiles';
export const BrowseStep = ({
allowedTypes,
assets,
onChangePage,
onChangePageSize,
onChangeSearch,
onChangeSort,
onEditAsset,
onSelectAllAsset,
@ -38,27 +40,30 @@ export const BrowseStep = ({
<>
<Stack size={4}>
{onSelectAllAsset && (
<Stack horizontal size={2}>
<Flex
paddingLeft={2}
paddingRight={2}
background="neutral0"
hasRadius
borderColor="neutral200"
height={`${32 / 16}rem`}
>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
indeterminate={!areAllAssetSelected && hasSomeAssetSelected}
value={areAllAssetSelected}
onChange={onSelectAllAsset}
/>
</Flex>
<SortPicker onChangeSort={onChangeSort} />
</Stack>
<Flex justifyContent="space-between">
<Stack horizontal size={2}>
<Flex
paddingLeft={2}
paddingRight={2}
background="neutral0"
hasRadius
borderColor="neutral200"
height={`${32 / 16}rem`}
>
<BaseCheckbox
aria-label={formatMessage({
id: getTrad('bulk.select.label'),
defaultMessage: 'Select all assets',
})}
indeterminate={!areAllAssetSelected && hasSomeAssetSelected}
value={areAllAssetSelected}
onChange={onSelectAllAsset}
/>
</Flex>
<SortPicker onChangeSort={onChangeSort} />
</Stack>
<SearchAsset onChangeSearch={onChangeSearch} queryValue={queryObject._q || ''} />
</Flex>
)}
<AssetList
@ -93,12 +98,14 @@ BrowseStep.propTypes = {
onChangePage: PropTypes.func.isRequired,
onChangePageSize: PropTypes.func.isRequired,
onChangeSort: PropTypes.func.isRequired,
onChangeSearch: PropTypes.func.isRequired,
onEditAsset: PropTypes.func.isRequired,
onSelectAsset: PropTypes.func.isRequired,
onSelectAllAsset: PropTypes.func,
queryObject: PropTypes.shape({
page: PropTypes.number.isRequired,
pageSize: PropTypes.number.isRequired,
_q: PropTypes.string,
}).isRequired,
pagination: PropTypes.shape({ pageCount: PropTypes.number.isRequired }).isRequired,
selectedAssets: PropTypes.arrayOf(PropTypes.shape({})).isRequired,

View File

@ -33,7 +33,7 @@ export const AssetDialog = ({
const { canRead, canCreate, isLoading: isLoadingPermissions } = useMediaLibraryPermissions();
const [
{ rawQuery, queryObject },
{ onChangePage, onChangePageSize, onChangeSort },
{ onChangePage, onChangePageSize, onChangeSort, onChangeSearch },
] = useModalQueryParams();
const { data, isLoading, error } = useModalAssets({ skipWhen: !canRead, rawQuery });
@ -102,7 +102,7 @@ export const AssetDialog = ({
);
}
if (canRead && assets?.length === 0) {
if (canRead && assets?.length === 0 && !queryObject._q) {
return (
<ModalLayout onClose={onClose} labelledBy="asset-dialog-title">
<DialogTitle />
@ -189,6 +189,7 @@ export const AssetDialog = ({
onChangePage={onChangePage}
onChangePageSize={onChangePageSize}
onChangeSort={onChangeSort}
onChangeSearch={onChangeSearch}
/>
</ModalBody>
</TabPanel>

View File

@ -21,12 +21,29 @@ const useModalQueryParams = () => {
setQueryObject(prev => ({ ...prev, sort }));
};
const handleChangeSearch = _q => {
if (_q) {
setQueryObject(prev => ({ ...prev, _q, page: 1 }));
} else {
const newState = { page: 1 };
Object.keys(queryObject).forEach(key => {
if (!['page', '_q'].includes(key)) {
newState[key] = queryObject[key];
}
});
setQueryObject(newState);
}
};
return [
{ queryObject, rawQuery: stringify(queryObject, { encode: false }) },
{
onChangePage: handeChangePage,
onChangePageSize: handleChangePageSize,
onChangeSort: handleChangeSort,
onChangeSearch: handleChangeSearch,
},
];
};

View File

@ -77,6 +77,7 @@
"plugin.description.long": "Media file management.",
"plugin.description.short": "Media file management.",
"plugin.name": "Media Library",
"search.clear.label": "Clear the search",
"search.label": "Search for an asset",
"search.placeholder": "e.g: the first dog on the moon",
"settings.form.autoOrientation.description": "Automatically rotate image according to EXIF orientation tag",