merge conflict

This commit is contained in:
ronronscelestes 2021-11-05 13:36:18 +01:00
commit 7f16b2e465
11 changed files with 730 additions and 65 deletions

View File

@ -12,7 +12,7 @@ import { stringify } from 'qs';
import {
NoPermissions,
CheckPermissions,
Search,
SearchURLQuery,
useFocusWhenNavigate,
useQueryParams,
useNotification,
@ -296,7 +296,7 @@ function ListView({
startActions={
<>
{isSearchable && (
<Search
<SearchURLQuery
label={formatMessage(
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
{ target: headerLayoutTitle }

View File

@ -1,7 +1,7 @@
import React, { useState } from 'react';
import {
DynamicTable,
Search,
SearchURLQuery,
SettingsPageTitle,
useRBAC,
useNotification,
@ -129,7 +129,7 @@ const ListPage = () => {
<ActionLayout
startActions={
<>
<Search
<SearchURLQuery
label={formatMessage(
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
{ target: title }

View File

@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useReducer, useState } from 'react';
import {
ConfirmDialog,
LoadingIndicatorPage,
Search,
SearchURLQuery,
SettingsPageTitle,
useNotification,
useQueryParams,
@ -277,7 +277,7 @@ const RoleListPage = () => {
{canRead && (
<ActionLayout
startActions={
<Search
<SearchURLQuery
label={formatMessage(
{ id: 'app.component.search.label', defaultMessage: 'Search for {target}' },
{ target: title }

View File

@ -27,7 +27,7 @@ const HomePage = () => {
const { formatMessage } = useIntl();
const _q = query?._q || ''
const items = [{name: 'Paul', instrument: 'bass'}, {name: 'George', instrument: 'guitar'}]
const sortedList = matchSorter(items, _q, {keys: ['name', 'instrument']})
const itemsList = sortedList?.length ? sortedList : items

View File

@ -0,0 +1,82 @@
<!--- Search.stories.mdx --->
import { ArgsTable, Meta, Canvas, Story } from '@storybook/addon-docs';
import { Stack } from '@strapi/design-system/Stack';
import matchSorter from 'match-sorter';
import useQueryParams from '../../hooks/useQueryParams';
import SearchURLQuery from './index';
<Meta title="components/SearchURLQuery" />
# SearchURLQuery
This component provides and input to search an array
## Imports
```js
import { SearchURLQuery } from '@strapi/helper-plugin';
import { useIntl } from 'react-intl';
```
## Usage
```jsx
import { SearchURLQuery, useQueryParams } from '@strapi/helper-plugin';
import matchSorter from 'match-sorter';
const HomePage = () => {
const [{ query }] = useQueryParams()
const { formatMessage } = useIntl();
const _q = query?._q || ''
const items = [{name: 'Paul', instrument: 'bass'}, {name: 'George', instrument: 'guitar'}]
const sortedList = matchSorter(items, _q, {keys: ['name', 'instrument']})
const itemsList = sortedList?.length ? sortedList : items
return (
<SearchURLQuery
label={formatMessage({
id: 'app.component.search.label',
defaultMessage: 'Search for {target}' },
{ target: 'users' }
)}
// Use this props to send an event
trackedEvent="didSearch"
/>
{itemsList.map(item => (
<div>
<h1>{item.name}</h1>
<p>{item.instrument}</p>
</div>
))}
)
};
```
## Base
<Canvas>
<Story name="base">
{() => {
const [{ query }] = useQueryParams()
const _q = query?._q || ''
const items = [{name: 'Paul', instrument: 'bass'}, {name: 'George', instrument: 'guitar'}]
const sortedList = matchSorter(items, _q, {keys: ['name', 'instrument']})
const itemsList = sortedList?.length ? sortedList : items;
return (
<Stack paddingTop={6} size={4}>
<SearchURLQuery label="Label" />
{itemsList.map((item, i) => (
<div key={i}>
<h1>{item.name}</h1>
<p>{item.instrument}</p>
</div>
))}
</Stack>
);
}}
</Story>
</Canvas>
<ArgsTable of={SearchURLQuery} />

View File

@ -0,0 +1,82 @@
import React, { useLayoutEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useIntl } from 'react-intl';
import SearchIcon from '@strapi/icons/Search';
import { Searchbar, SearchForm } from '@strapi/design-system/Searchbar';
import { IconButton } from '@strapi/design-system/IconButton';
import useQueryParams from '../../hooks/useQueryParams';
import useTracking from '../../hooks/useTracking';
const SearchURLQuery = ({ label, trackedEvent }) => {
const wrapperRef = useRef(null);
const iconButtonRef = useRef(null);
const [{ query }, setQuery] = useQueryParams();
const [value, setValue] = useState(query?._q || '');
const [isOpen, setIsOpen] = useState(!!value);
const { formatMessage } = useIntl();
const { trackUsage } = useTracking();
const handleToggle = () => setIsOpen(prev => !prev);
useLayoutEffect(() => {
if (isOpen) {
setTimeout(() => {
wrapperRef.current.querySelector('input').focus();
}, 0);
}
}, [isOpen]);
const handleClear = () => {
setValue('');
setQuery({ _q: '' }, 'remove');
};
const handleSubmit = e => {
e.preventDefault();
if (value) {
if (trackedEvent) {
trackUsage(trackedEvent);
}
setQuery({ _q: value, page: 1 });
} else {
handleToggle();
setQuery({ _q: '' }, 'remove');
}
};
if (isOpen) {
return (
<div ref={wrapperRef}>
<SearchForm onSubmit={handleSubmit}>
<Searchbar
name="search"
onChange={({ target: { value } }) => setValue(value)}
value={value}
clearLabel={formatMessage({ id: 'clearLabel', defaultMessage: 'Clear' })}
onClear={handleClear}
size="S"
>
{label}
</Searchbar>
</SearchForm>
</div>
);
}
return (
<IconButton ref={iconButtonRef} icon={<SearchIcon />} label="Search" onClick={handleToggle} />
);
};
SearchURLQuery.defaultProps = {
trackedEvent: null,
};
SearchURLQuery.propTypes = {
label: PropTypes.string.isRequired,
trackedEvent: PropTypes.string,
};
export default SearchURLQuery;

View File

@ -0,0 +1,495 @@
/**
*
* Tests for SearchURLQuery
*
*/
import React from 'react';
import { render, fireEvent } from '@testing-library/react';
import { IntlProvider } from 'react-intl';
import { Router } from 'react-router-dom';
import { createMemoryHistory } from 'history';
import { ThemeProvider, lightTheme } from '@strapi/design-system';
import SearchURLQuery from '../index';
const trackUsage = jest.fn();
jest.mock('../../../hooks/useTracking', () => () => ({
trackUsage,
}));
const makeApp = (history, trackedEvent) => (
<Router history={history}>
<ThemeProvider theme={lightTheme}>
<IntlProvider locale="en">
<SearchURLQuery label="Search label" trackedEvent={trackedEvent} />
</IntlProvider>
</ThemeProvider>
</Router>
);
describe('<SearchURLQuery />', () => {
it('renders and matches the snapshot', () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history));
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 toggle searchbar form and searchbar', async () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history));
fireEvent.click(container.querySelector('button[type="button"]'));
expect(container).toMatchInlineSnapshot(`
.c11 {
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;
}
.c2 {
font-weight: 500;
font-size: 0.75rem;
line-height: 1.33;
color: #32324d;
}
.c6 {
padding-right: 8px;
padding-left: 12px;
}
.c4 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-box-pack: justify;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c8 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
}
.c10 {
border: none;
border-radius: 4px;
padding-left: 0;
padding-right: 16px;
color: #32324d;
font-weight: 400;
font-size: 0.875rem;
display: block;
width: 100%;
}
.c10::-webkit-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c10::-moz-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c10:-ms-input-placeholder {
color: #8e8ea9;
opacity: 1;
}
.c10::placeholder {
color: #8e8ea9;
opacity: 1;
}
.c10[aria-disabled='true'] {
background: inherit;
color: inherit;
}
.c10:focus {
outline: none;
box-shadow: none;
}
.c5 {
border: 1px solid #dcdce4;
border-radius: 4px;
background: #ffffff;
height: 2rem;
outline: none;
box-shadow: 0;
-webkit-transition-property: border-color,box-shadow,fill;
transition-property: border-color,box-shadow,fill;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
}
.c5:focus-within {
border: 1px solid #4945ff;
box-shadow: #4945ff 0px 0px 0px 2px;
}
.c1 {
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;
}
.c9 {
font-size: 0.8rem;
}
.c9 svg path {
fill: #32324d;
}
.c0 {
border-radius: 4px;
box-shadow: 0px 1px 4px rgba(33,33,52,0.1);
outline: none;
box-shadow: 0;
-webkit-transition-property: border-color,box-shadow,fill;
transition-property: border-color,box-shadow,fill;
-webkit-transition-duration: 0.2s;
transition-duration: 0.2s;
}
.c0:focus-within .c7 svg path {
fill: #4945ff;
}
.c0 .c3 {
border: 1px solid transparent;
}
.c0 .c3:focus-within {
border: 1px solid #4945ff;
box-shadow: #4945ff 0px 0px 0px 2px;
}
<div>
<div>
<form
role="search"
>
<div
class="c0"
>
<div>
<div
class="c1"
>
<label
class="c2"
for="field-1"
>
Search label
</label>
</div>
<div
class="c3 c4 c5"
>
<div
class="c6"
>
<div
class="c7 c8 c9"
>
<svg
aria-hidden="true"
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>
</div>
</div>
<input
aria-disabled="false"
aria-invalid="false"
class="c10"
id="field-1"
name="search"
value=""
/>
</div>
</div>
</div>
</form>
</div>
<div
class="c11"
>
<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 push value to query params', async () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history));
fireEvent.click(container.querySelector('button[type="button"]'));
const input = container.querySelector('input[name="search"]');
fireEvent.change(input, { target: { value: 'michka' } });
fireEvent.submit(input);
const urlSearchQuery = history.location.search;
expect(urlSearchQuery).toEqual('?_q=michka&page=1');
});
it('should clear value and update query params', async () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history));
fireEvent.click(container.querySelector('button[type="button"]'));
const input = container.querySelector('input[name="search"]');
fireEvent.change(input, { target: { value: 'michka' } });
fireEvent.submit(input);
const urlSearchQuery = history.location.search;
expect(urlSearchQuery).toEqual('?_q=michka&page=1');
fireEvent.click(container.querySelector('button[aria-label="Clear"]'));
expect(input.value).toEqual('');
const clearedUrlSearchQuery = history.location.search;
expect(clearedUrlSearchQuery).toEqual('?page=1');
});
it.only('should call trackUsage with trackedEvent props when submit', async () => {
const history = createMemoryHistory();
const { container } = render(makeApp(history, 'thisEvent'));
fireEvent.click(container.querySelector('button[type="button"]'));
const input = container.querySelector('input[name="search"]');
fireEvent.change(input, { target: { value: 'michka' } });
fireEvent.submit(input);
expect(trackUsage.mock.calls.length).toBe(1);
});
});

View File

@ -183,6 +183,7 @@ export { default as LoadingIndicatorPage } from './components/LoadingIndicatorPa
export { default as NotAllowedInput } from './components/NotAllowedInput';
export { default as SettingsPageTitle } from './components/SettingsPageTitle';
export { default as Search } from './components/Search';
export { default as SearchURLQuery } from './components/SearchURLQuery';
export { default as Status } from './components/Status';
export { default as FilterListURLQuery } from './components/FilterListURLQuery';
export { default as FilterPopoverURLQuery } from './components/FilterPopoverURLQuery';

View File

@ -7,7 +7,7 @@ import {
NoPermissions,
NoMedia,
AnErrorOccurred,
Search,
SearchURLQuery,
useSelectionState,
useQueryParams,
} from '@strapi/helper-plugin';
@ -124,7 +124,7 @@ export const MediaLibrary = () => {
</>
}
endActions={
<Search
<SearchURLQuery
label={formatMessage({
id: getTrad('search.label'),
defaultMessage: 'Search for an asset',

View File

@ -23,7 +23,7 @@ import {
useRBAC,
NoPermissions,
LoadingIndicatorPage,
Search,
SearchURLQuery,
useQueryParams,
EmptyStateLayout,
ConfirmDialog,
@ -137,7 +137,7 @@ const RoleListPage = () => {
})}
primaryAction={
<CheckPermissions permissions={permissions.createRole}>
<Button onClick={handleNewRoleClick} startIcon={<Plus />}>
<Button onClick={handleNewRoleClick} startIcon={<Plus />} size="L">
{formatMessage({
id: getTrad('List.button.roles'),
defaultMessage: 'Add new role',
@ -149,7 +149,7 @@ const RoleListPage = () => {
<ActionLayout
startActions={
<Search
<SearchURLQuery
label={formatMessage({
id: 'app.component.search.label',
defaultMessage: 'Search',

File diff suppressed because one or more lines are too long