Merge pull request #5354 from strapi/front/media-library-filter

Media library Filters
This commit is contained in:
cyril lopez 2020-03-05 17:32:31 +01:00 committed by GitHub
commit 268fa19f0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 640 additions and 354 deletions

View File

@ -13,6 +13,8 @@ const sizes = {
},
padding: {
// TODO
xs: '5px',
sm: '10px',
md: '30px',
},
};

View File

@ -11,6 +11,7 @@ const generateFiltersFromSearch = search => {
!x.includes('_limit') &&
!x.includes('_page') &&
!x.includes('_sort') &&
!x.includes('_start') &&
!x.includes('_q=') &&
x !== ''
)

View File

@ -0,0 +1,174 @@
import getFilterType from '../getFilterType';
describe('HELPER PLUGIN | utils | getFilterType', () => {
describe('Text types', () => {
const expected = [
{
id: 'components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
{
id: 'components.FilterOptions.FILTER_TYPES._lt',
value: '_lt',
},
{
id: 'components.FilterOptions.FILTER_TYPES._lte',
value: '_lte',
},
{
id: 'components.FilterOptions.FILTER_TYPES._gt',
value: '_gt',
},
{
id: 'components.FilterOptions.FILTER_TYPES._gte',
value: '_gte',
},
{
id: 'components.FilterOptions.FILTER_TYPES._contains',
value: '_contains',
},
{
id: 'components.FilterOptions.FILTER_TYPES._containss',
value: '_containss',
},
{
id: 'components.FilterOptions.FILTER_TYPES._in',
value: '_in',
},
{
id: 'components.FilterOptions.FILTER_TYPES._nin',
value: '_nin',
},
];
it('should generate the expected array if type is text', () => {
const type = 'text';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is string', () => {
const type = 'string';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is password', () => {
const type = 'password';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is email', () => {
const type = 'email';
expect(getFilterType(type)).toEqual(expected);
});
});
describe('Number and timestamp types', () => {
const expected = [
{
id: 'components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
{
id: 'components.FilterOptions.FILTER_TYPES._lt',
value: '_lt',
},
{
id: 'components.FilterOptions.FILTER_TYPES._lte',
value: '_lte',
},
{
id: 'components.FilterOptions.FILTER_TYPES._gt',
value: '_gt',
},
{
id: 'components.FilterOptions.FILTER_TYPES._gte',
value: '_gte',
},
];
it('should generate the expected array if type is integer', () => {
const type = 'integer';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is biginteger', () => {
const type = 'biginteger';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is float', () => {
const type = 'float';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is decimal', () => {
const type = 'decimal';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is date', () => {
const type = 'date';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is datetime', () => {
const type = 'datetime';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is time', () => {
const type = 'time';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is timestamp', () => {
const type = 'timestamp';
expect(getFilterType(type)).toEqual(expected);
});
it('should generate the expected array if type is timestampUpdate', () => {
const type = 'timestampUpdate';
expect(getFilterType(type)).toEqual(expected);
});
});
describe('Other types', () => {
const expected = [
{
id: 'components.FilterOptions.FILTER_TYPES.=',
value: '=',
},
{
id: 'components.FilterOptions.FILTER_TYPES._ne',
value: '_ne',
},
];
it('should generate the expected array if type is size', () => {
const type = 'size';
expect(getFilterType(type)).toEqual(expected);
});
});
});

View File

@ -2,7 +2,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { get, toString } from 'lodash';
import moment from 'moment';
import { dateFormats, FilterButton } from 'strapi-helper-plugin';
import { FilterButton } from 'strapi-helper-plugin';
import dateFormats from '../../utils/dateFormats';
function Filter({
changeParams,

View File

@ -0,0 +1,12 @@
import { dateFormats as defaultDateFormats } from 'strapi-helper-plugin';
const dateFormats = {
...defaultDateFormats,
// Customise the format by uncommenting the one you wan to override it corresponds to the type of your field
// date: 'dddd, MMMM Do YYYY',
// datetime: 'dddd, MMMM Do YYYY HH:mm',
// time: 'HH:mm A',
// timestamp: 'dddd, MMMM Do YYYY HH:mm',
};
export default dateFormats;

View File

@ -0,0 +1,34 @@
import React from 'react';
import PropTypes from 'prop-types';
import FiltersList from '../FiltersList';
import FiltersPicker from '../FiltersPicker';
const Filters = ({ onChange, onClick, filters }) => {
return (
<>
<FiltersPicker onChange={onChange} />
<FiltersList filters={filters} onClick={onClick} />
</>
);
};
Filters.defaultProps = {
filters: [],
onChange: () => {},
onClick: () => {},
};
Filters.propTypes = {
filters: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
filter: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
})
),
onChange: PropTypes.func,
onClick: PropTypes.func,
};
export default Filters;

View File

@ -1,22 +0,0 @@
import { fromJS } from 'immutable';
import moment from 'moment';
const initialState = fromJS({
name: 'created_at',
filter: '=',
value: moment(),
});
function reducer(state, action) {
switch (action.type) {
case 'ON_CHANGE':
return state.update(action.name, () => action.value);
case 'RESET_FORM':
return initialState;
default:
return state;
}
}
export default reducer;
export { initialState };

View File

@ -1,28 +0,0 @@
import styled from 'styled-components';
const FiltersListItem = styled.div`
display: flex;
align-items: center;
height: 32px;
margin-bottom: 4px;
background: rgba(0, 126, 255, 0.08);
border: 1px solid rgba(0, 126, 255, 0.24);
border-radius: 2px;
color: #007eff;
font-size: 13px;
span {
padding-left: 15px;
padding-right: 15px;
line-height: 30px;
}
button {
display: flex;
justify-items: center;
height: 13px;
padding-left: 10px;
padding-right: 10px;
border-left: 2px solid rgba(0, 126, 255, 0.1);
}
`;
export default FiltersListItem;

View File

@ -1,8 +1,8 @@
import styled from 'styled-components';
import { Button } from '@buffetjs/core';
const InputWrapper = styled(Button)`
const FilterButton = styled(Button)`
width: 100%;
`;
export default InputWrapper;
export default FilterButton;

View File

@ -9,7 +9,7 @@ import PropTypes from 'prop-types';
import { DateTime } from '@buffetjs/custom';
import { InputText, Select } from '@buffetjs/core';
import SizeInput from '../SizeInput';
import SizeInput from './SizeInput';
const getInputType = type => {
switch (type) {

View File

@ -2,6 +2,9 @@ import styled from 'styled-components';
const InputWrapper = styled.div`
margin-bottom: 11px;
#datetime {
max-width: 130px;
}
`;
export default InputWrapper;

View File

@ -0,0 +1,60 @@
/**
*
* SizeInput
*/
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { InputNumber, Select } from '@buffetjs/core';
import Flex from '../../Flex';
import Padded from '../../Padded';
function SizeInput({ onChange, value, ...rest }) {
const options = ['KB', 'MB', 'GB'];
const [size, setSize] = useState(0);
const [format, setFormat] = useState('KB');
const handleChangeValue = ({ target: { value } }) => {
setSize(value);
onChange({
target: {
name: 'value',
value: `${value}${format}`,
},
});
};
const handleChangeFormat = ({ target: { value } }) => {
setFormat(value);
onChange({
target: {
name: 'value',
value: `${size}${value}`,
},
});
};
return (
<Flex justifyContent="space-between">
<InputNumber {...rest} name="size_value" onChange={handleChangeValue} value={size} />
<Padded left />
<Select name="format_value" onChange={handleChangeFormat} options={options} value={format} />
</Flex>
);
}
SizeInput.defaultProps = {
onChange: () => {},
value: null,
};
SizeInput.propTypes = {
onChange: PropTypes.func,
value: PropTypes.string,
};
export default SizeInput;

View File

@ -4,24 +4,23 @@ import { FormattedMessage } from 'react-intl';
import { Select } from '@buffetjs/core';
import { getFilterType } from 'strapi-helper-plugin';
import getTrad from '../../utils/getTrad';
import getTrad from '../../../utils/getTrad';
import reducer, { initialState } from './reducer';
import filters from './utils/filtersForm';
import Wrapper from './Wrapper';
import Button from './Button';
import InputWrapper from './InputWrapper';
import FilterButton from './FilterButton';
import FilterInput from './FilterInput';
import Input from '../FilterInput';
const FiltersCard = ({ filters, onChange }) => {
const FiltersCard = ({ onChange }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const name = state.get('name');
const { name, filter, value } = state.toJS();
const type = filters[name].type;
const defaultValue = filters[name].defaultValue;
const filtersOptions = getFilterType(type);
const handleChange = ({ target: { name, value } }) => {
@ -40,62 +39,48 @@ const FiltersCard = ({ filters, onChange }) => {
});
};
const renderFiltersOptions = () => {
return filtersOptions.map(({ id, value }) => (
<FormattedMessage id={id} key={id}>
{msg => <option value={value}>{msg}</option>}
</FormattedMessage>
));
};
return (
<Wrapper>
<InputWrapper>
<Select
onChange={e => {
// Change the attribute
handleChange(e);
// Change other inputs so it reset values
const {
target: { value },
} = e;
handleChange({ target: { name: 'filter', value: '=' } });
handleChange({
target: { name: 'value', value: filters[value].defaultValue },
});
}}
name="name"
options={Object.keys(filters)}
value={name}
/>
<Select onChange={handleChange} name="name" options={Object.keys(filters)} value={name} />
</InputWrapper>
<InputWrapper>
<Select
onChange={handleChange}
name="filter"
options={filtersOptions.map(({ id, value }) => (
<FormattedMessage id={id} key={id}>
{msg => <option value={value}>{msg}</option>}
</FormattedMessage>
))}
value={state.get('filter')}
options={renderFiltersOptions()}
value={filter}
/>
</InputWrapper>
<InputWrapper>
<Input
<FilterInput
type={type}
onChange={handleChange}
name="value"
options={['image', 'video', 'files']}
value={state.get('value') || defaultValue}
value={value}
/>
</InputWrapper>
<Button icon onClick={addFilter}>
<FilterButton icon onClick={addFilter}>
<FormattedMessage id={getTrad('filter.add')} />
</Button>
</FilterButton>
</Wrapper>
);
};
FiltersCard.defaultProps = {
filters: {},
onChange: () => {},
};
FiltersCard.propTypes = {
filters: PropTypes.object,
onChange: PropTypes.func,
};

View File

@ -0,0 +1,34 @@
import { fromJS } from 'immutable';
import moment from 'moment';
import filters from './utils/filtersForm';
const initialState = fromJS({
name: 'created_at',
filter: '=',
value: moment(),
});
function reducer(state, action) {
switch (action.type) {
case 'ON_CHANGE': {
const { name, value } = action;
if (name === 'name') {
return state
.update(name, () => value)
.update('filter', () => '=')
.update('value', () => filters[value].defaultValue);
}
return state.update(name, () => value);
}
case 'RESET_FORM':
return initialState;
default:
return state;
}
}
export default reducer;
export { initialState };

View File

@ -0,0 +1,46 @@
import reducer, { initialState } from '../reducer';
describe('Upload | components | FiltersCard | reducer', () => {
it('should return the state with the default value', () => {
const state = initialState;
const action = {
type: 'ON_CHANGE',
name: 'name',
value: 'size',
};
const actual = reducer(state, action);
const expected = state.set('name', 'size').set('value', '0KB');
expect(actual).toEqual(expected);
});
it('should return the state with the updated value', () => {
const state = initialState;
const action = {
type: 'ON_CHANGE',
name: 'filter',
value: '>',
};
const actual = reducer(state, action);
const expected = state.set('filter', '>');
expect(actual).toEqual(expected);
});
it('should return the initialState on reset', () => {
const state = initialState.set('filter', '>');
const action = {
type: 'RESET_FORM',
};
const actual = reducer(state, action);
const expected = initialState;
expect(actual).toEqual(expected);
});
});

View File

@ -11,10 +11,7 @@ const filtersForm = {
},
size: {
type: 'size',
defaultValue: {
size: 0,
format: 'KB',
},
defaultValue: '0KB',
},
file_type: {
type: 'enum',

View File

@ -1,62 +1,59 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { some } from 'lodash';
import { useLocation } from 'react-router-dom';
import moment from 'moment';
import { isObject } from 'lodash';
import { FormattedMessage } from 'react-intl';
import { FilterIcon } from 'strapi-helper-plugin';
import { FilterIcon, generateFiltersFromSearch } from 'strapi-helper-plugin';
import DropdownButton from '../DropdownButton';
import DropdownSection from '../DropdownSection';
import FiltersCard from './FiltersCard';
import Picker from '../Picker';
import Wrapper from './Wrapper';
import FiltersCard from '../FiltersCard';
const FiltersPicker = ({ filters, onChange }) => {
const [isOpen, setIsOpen] = useState(false);
const FiltersPicker = ({ onChange }) => {
const { search } = useLocation();
const filters = generateFiltersFromSearch(search);
const handleChange = ({ target: { value } }) => {
if (value.value) {
let formattedValue = value;
let formattedValue = value;
if (value.value._isAMomentObject === true) {
formattedValue.value = moment(
value.value,
'YYYY-MM-DD HH:mm:ss'
).format();
} else if (isObject(value)) {
formattedValue.value = Object.values(value.value).join('');
}
onChange({ target: { value: formattedValue } });
// moment format if datetime value
if (value.value._isAMomentObject === true) {
formattedValue.value = moment(value.value).format();
}
hangleToggle();
};
// Send updated filters
if (!some(filters, formattedValue)) {
filters.push(formattedValue);
const hangleToggle = () => {
setIsOpen(!isOpen);
onChange({ target: { name: 'filters', value: filters } });
}
};
return (
<Wrapper>
<DropdownButton onClick={hangleToggle} isActive={isOpen}>
<FilterIcon />
<FormattedMessage id="app.utils.filters" />
</DropdownButton>
<DropdownSection isOpen={isOpen}>
<FiltersCard onChange={handleChange} filters={filters} />
</DropdownSection>
</Wrapper>
<Picker
renderButtonContent={() => (
<>
<FilterIcon />
<FormattedMessage id="app.utils.filters" />
</>
)}
renderSectionContent={onToggle => (
<FiltersCard
onChange={e => {
handleChange(e);
onToggle();
}}
/>
)}
/>
);
};
FiltersPicker.defaultProps = {
filters: {},
onChange: () => {},
};
FiltersPicker.propTypes = {
filters: PropTypes.object,
onChange: PropTypes.func,
};

View File

@ -4,14 +4,17 @@ import PropTypes from 'prop-types';
const Flex = styled.div`
display: flex;
justify-content: ${({ justifyContent }) => justifyContent};
flex-direction: ${({ flexDirection }) => flexDirection};
`;
Flex.defaultProps = {
justifyContent: 'normal',
flexDirection: 'row',
};
Flex.propTypes = {
justifyContent: PropTypes.string,
flexDirection: PropTypes.string,
};
export default Flex;

View File

@ -0,0 +1,26 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import Text from '../Text';
const IntlText = ({ id, defaultMessage, values, ...textProps }) => (
<FormattedMessage id={id} defaultMessage={defaultMessage} values={values}>
{msg => <Text {...textProps}>{msg}</Text>}
</FormattedMessage>
);
IntlText.defaultProps = {
id: 'app.utils.defaultMessage',
defaultMessage: '',
values: {},
};
IntlText.propTypes = {
id: PropTypes.string,
defaultMessage: PropTypes.string,
values: PropTypes.object,
// TODO - textProps type to specify
};
export default IntlText;

View File

@ -4,28 +4,11 @@ const Wrapper = styled.div`
position: relative;
margin-top: 19px;
padding: 0;
.btn-wrapper {
position: absolute;
top: 161px;
height: 100px;
top: 109px;
width: 100%;
margin-left: -50px;
margin-top: -50px;
text-align: center;
> p {
line-height: 18px;
}
.title {
margin-bottom: 2px;
font-size: 18px;
font-weight: 500;
}
.subtitle {
font-size: 13px;
}
}
`;

View File

@ -2,10 +2,13 @@ import React from 'react';
import { Button } from '@buffetjs/core';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
import getTrad from '../../utils/getTrad';
import generateRows from './utils/generateRows';
import CardEmpty from '../CardEmpty';
import Wrapper from './Wrapper';
import IntlText from '../IntlText';
const ListEmpty = ({ onClick }) => {
const rows = generateRows(3);
@ -26,23 +29,12 @@ const ListEmpty = ({ onClick }) => {
);
})}
<div className="btn-wrapper">
<FormattedMessage id={getTrad('list.assets-empty.title')}>
{content => <p className="title">{content}</p>}
</FormattedMessage>
<FormattedMessage id={getTrad('list.assets-empty.subtitle')}>
{content => <p className="subtitle">{content}</p>}
</FormattedMessage>
<FormattedMessage id={getTrad('header.actions.upload-assets')}>
{label => (
<Button
color="primary"
label={label}
onClick={onClick}
type="button"
/>
)}
</FormattedMessage>
<IntlText id={getTrad('list.assets-empty.title')} fontSize="lg" fontWeight="semiBold" />
<IntlText id={getTrad('list.assets-empty.subtitle')} fontSize="md" lineHeight="19px" />
<div style={{ paddingBottom: '1.1rem' }} />
<Button color="primary" onClick={onClick} type="button">
<FormattedMessage id={getTrad('header.actions.upload-assets')} />
</Button>
</div>
</Wrapper>
);

View File

@ -0,0 +1,27 @@
import PropTypes from 'prop-types';
import styled from 'styled-components';
const Padded = styled.div`
padding-top: ${({ theme, size, top }) => top && theme.main.sizes.padding[size]};
padding-right: ${({ theme, size, right }) => right && theme.main.sizes.padding[size]};
padding-bottom: ${({ theme, size, bottom }) => bottom && theme.main.sizes.padding[size]};
padding-left: ${({ theme, size, left }) => left && theme.main.sizes.padding[size]};
`;
Padded.defaultProps = {
bottom: false,
left: false,
right: false,
top: false,
size: 'sm',
};
Padded.propTypes = {
bottom: PropTypes.bool,
left: PropTypes.bool,
right: PropTypes.bool,
top: PropTypes.bool,
size: PropTypes.string,
};
export default Padded;

View File

@ -0,0 +1,40 @@
import React, { useState, useRef } from 'react';
import PropTypes from 'prop-types';
import { useClickAwayListener } from '@buffetjs/hooks';
import DropdownButton from '../DropdownButton';
import DropdownSection from '../DropdownSection';
import Wrapper from './Wrapper';
const Picker = ({ renderButtonContent, renderSectionContent }) => {
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef();
useClickAwayListener(dropdownRef, () => setIsOpen(false));
const handleToggle = () => {
setIsOpen(v => !v);
};
return (
<Wrapper ref={dropdownRef}>
<DropdownButton onClick={handleToggle} isActive={isOpen}>
{renderButtonContent(isOpen)}
</DropdownButton>
<DropdownSection isOpen={isOpen}>{renderSectionContent(handleToggle)}</DropdownSection>
</Wrapper>
);
};
Picker.defaultProps = {
renderButtonContent: () => {},
renderSectionContent: () => {},
};
Picker.propTypes = {
renderButtonContent: PropTypes.func,
renderSectionContent: PropTypes.func,
};
export default Picker;

View File

@ -1,9 +0,0 @@
import styled from 'styled-components';
const Wrapper = styled.div`
.col-6:last-of-type {
padding-left: 0;
}
`;
export default Wrapper;

View File

@ -1,79 +0,0 @@
/**
*
* SizeInput
*/
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { InputNumber, Select } from '@buffetjs/core';
import Wrapper from './Wrapper';
function SizeInput({ onChange, value, ...rest }) {
const options = ['KB', 'MB', 'GB'];
const [size, setSize] = useState(0);
const [format, setFormat] = useState('KB');
const handleChangeValue = ({ target: { value } }) => {
setSize(value);
handleChange();
};
const handleChangeFormat = ({ target: { value } }) => {
setFormat(value);
handleChange();
};
const handleChange = () => {
onChange({
target: {
name: 'value',
value: {
size,
format,
},
},
});
};
return (
<Wrapper>
<div className="row">
<div className="col-6">
<InputNumber
{...rest}
name="size_value"
onChange={handleChangeValue}
value={value.size}
/>
</div>
<div className="col-6">
<Select
name="format_value"
onChange={handleChangeFormat}
options={options}
value={value.format}
/>
</div>
</div>
</Wrapper>
);
}
SizeInput.defaultProps = {
onChange: () => {},
value: {},
};
SizeInput.propTypes = {
onChange: PropTypes.func,
value: PropTypes.shape({
size: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
format: PropTypes.string,
}),
};
export default SizeInput;

View File

@ -1,12 +1,14 @@
import React from 'react';
import styled from 'styled-components';
import { themePropTypes } from 'strapi-helper-plugin';
const Wrapper = styled.ul`
import Text from '../Text';
const Wrapper = styled(props => <Text as="ul" fontSize="md" {...props} />)`
margin-bottom: 0;
padding: 0;
min-width: 230px;
list-style-type: none;
font-size: ${({ theme }) => theme.main.fontSizes.md};
background-color: ${({ theme }) => theme.main.colors.white};
border: 1px solid ${({ theme }) => theme.main.colors.darkGrey};
box-shadow: 0 2px 4px ${({ theme }) => theme.main.colors.greyAlpha};

View File

@ -1,19 +1,19 @@
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import getTrad from '../../utils/getTrad';
import Wrapper from './Wrapper';
import IntlText from '../IntlText';
const SortListItem = ({ onClick, selectedItem, label, value }) => {
const handleClick = () => {
onClick(value);
onClick({ target: { name: '_sort', value } });
};
return (
<Wrapper isActive={selectedItem === value} onClick={handleClick}>
<FormattedMessage id={getTrad(`sort.${label}`)} />
<IntlText id={getTrad(`sort.${label}`)} lineHeight="23px" />
</Wrapper>
);
};

View File

@ -1,18 +1,14 @@
import React, { useState } from 'react';
import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { Carret } from '@buffetjs/icons';
import getTrad from '../../utils/getTrad';
import DropdownButton from '../DropdownButton';
import DropdownSection from '../DropdownSection';
import SortList from '../SortList';
import Wrapper from './Wrapper';
import Picker from '../Picker';
const SortPicker = ({ onChange, value }) => {
const [isOpen, setIsOpen] = useState(false);
const orders = {
created_at_asc: 'created_at:ASC',
created_at_desc: 'created_at:DESC',
@ -22,31 +18,25 @@ const SortPicker = ({ onChange, value }) => {
updated_at_desc: 'updated_at:DESC',
};
const handleChange = value => {
onChange({ target: { name: '_sort', value } });
hangleToggle();
};
const hangleToggle = () => {
setIsOpen(v => !v);
};
return (
<Wrapper>
<DropdownButton onClick={hangleToggle} isActive={isOpen}>
<FormattedMessage id={getTrad('sort.label')} />
<Carret fill={isOpen ? '#007EFF' : '#292b2c'} />
</DropdownButton>
<DropdownSection isOpen={isOpen}>
<Picker
renderButtonContent={isOpen => (
<>
<FormattedMessage id={getTrad('sort.label')} />
<Carret fill={isOpen ? '#007EFF' : '#292b2c'} />
</>
)}
renderSectionContent={onToggle => (
<SortList
isShown={isOpen}
list={orders}
selectedItem={value}
onClick={handleChange}
onClick={e => {
onChange(e);
onToggle();
}}
/>
</DropdownSection>
</Wrapper>
)}
/>
);
};

View File

@ -1,8 +1,9 @@
import styled from 'styled-components';
import PropTypes from 'prop-types';
const Text = styled.p`
margin: 0;
line-height: 18px;
line-height: ${({ lineHeight }) => lineHeight};
color: ${({ theme, color }) => theme.main.colors[color] || color};
font-size: ${({ theme, fontSize }) => theme.main.fontSizes[fontSize]};
font-weight: ${({ theme, fontWeight }) => theme.main.fontWeights[fontWeight]};
@ -13,7 +14,16 @@ Text.defaultProps = {
color: 'greyDark',
fontSize: 'md',
fontWeight: 'regular',
lineHeight: 'normal',
textTransform: 'none',
};
Text.propTypes = {
color: PropTypes.string,
fontSize: PropTypes.string,
fontWeight: PropTypes.string,
lineHeight: PropTypes.string,
textTransform: PropTypes.string,
};
export default Text;

View File

@ -2,54 +2,73 @@ import React, { useReducer, useState, useEffect } from 'react';
import { includes } from 'lodash';
import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
import { useDebounce } from '@buffetjs/hooks';
import {
HeaderSearch,
PageFooter,
useGlobalContext,
generateFiltersFromSearch,
useQuery,
generateSearchFromFilters,
request,
useQuery,
} from 'strapi-helper-plugin';
import getTrad from '../../utils/getTrad';
import getRequestUrl from '../../utils/getRequestUrl';
import Container from '../../components/Container';
import ControlsWrapper from '../../components/ControlsWrapper';
import SelectAll from '../../components/SelectAll';
import SortPicker from '../../components/SortPicker';
import FiltersPicker from '../../components/FiltersPicker';
import FiltersList from '../../components/FiltersList';
import Filters from '../../components/Filters';
// import List from '../../components/List';
import ListEmpty from '../../components/ListEmpty';
import ModalStepper from '../ModalStepper';
import {
filtersForm,
generatePageFromStart,
generateStartFromPage,
getHeaderLabel,
} from './utils';
import { generatePageFromStart, generateStartFromPage, getHeaderLabel } from './utils';
import init from './init';
import reducer, { initialState } from './reducer';
const HomePage = () => {
const { formatMessage } = useGlobalContext();
const [reducerState, dispatch] = useReducer(reducer, initialState, init);
const query = useQuery();
const [isOpen, setIsOpen] = useState(false);
const [searchValue, setSearchValue] = useState(query.get('_q') || '');
const { push } = useHistory();
const { search } = useLocation();
const query = useQuery();
const { data, dataToDelete } = reducerState.toJS();
const pluginName = formatMessage({ id: getTrad('plugin.name') });
const paramsKeys = ['_limit', '_page', '_q', '_sort'];
const paramsKeys = ['_limit', '_start', '_q', '_sort'];
const debouncedSearch = useDebounce(searchValue, 300);
useEffect(() => {
// TODO - Retrieve data
dispatch({
type: 'GET_DATA_SUCCEEDED',
data: [],
});
}, []);
handleChangeParams({ target: { name: '_q', value: searchValue } });
}, [debouncedSearch]);
useEffect(() => {
fetchData();
}, [search]);
const fetchData = async () => {
const requestURL = getRequestUrl('files');
try {
const data = await request(`${requestURL}${search}`, {
method: 'GET',
});
dispatch({
type: 'GET_DATA_SUCCEEDED',
data,
});
} catch (err) {
strapi.notification.error('notification.error');
}
};
const getSearchParams = () => {
const params = {};
query.forEach((value, key) => {
if (includes(paramsKeys, key)) {
params[key] = value;
@ -59,35 +78,14 @@ const HomePage = () => {
return params;
};
const getUpdatedSearchParams = updatedParams => {
const generateNewSearch = updatedParams => {
return {
...getSearchParams(),
filters: generateFiltersFromSearch(search),
...updatedParams,
};
};
const handleChangeFilters = ({ target: { value } }) => {
if (value) {
// Add filter
const updatedFilters = generateFiltersFromSearch(search);
updatedFilters.push(value);
handleChangeParams({
target: { name: 'filters', value: updatedFilters },
});
}
};
const handleDeleteFilter = index => {
// Remove filter
const updatedFilters = generateFiltersFromSearch(search);
updatedFilters.splice(index, 1);
handleChangeParams({
target: { name: 'filters', value: updatedFilters },
});
};
const handleChangeListParams = ({ target: { name, value } }) => {
if (name.includes('_page')) {
handleChangeParams({
@ -98,25 +96,37 @@ const HomePage = () => {
}
};
const getQueryValue = key => {
const queryParams = getSearchParams();
return queryParams[key];
};
const handleChangeParams = ({ target: { name, value } }) => {
const updatedSearch = getUpdatedSearchParams({ [name]: value });
const newSearch = generateSearchFromFilters(updatedSearch);
const updatedQueryParams = generateNewSearch({ [name]: value });
const newSearch = generateSearchFromFilters(updatedQueryParams);
push({ search: encodeURI(newSearch) });
};
const handleClearSearch = () => {
handleChangeParams({ target: { name: '_q', value: '' } });
const handleChangeSearchValue = ({ target: { value } }) => {
setSearchValue(value);
};
const handleClickToggleModal = () => {
const handleClearSearch = () => {
setSearchValue('');
};
const handleClickToggleModal = (refetch = false) => {
setIsOpen(prev => !prev);
if (refetch) {
fetchData();
}
};
const handleDeleteFilter = index => {
// Remove filter
const updatedFilters = generateFiltersFromSearch(search);
updatedFilters.splice(index, 1);
handleChangeParams({
target: { name: 'filters', value: updatedFilters },
});
};
const headerProps = {
@ -143,17 +153,17 @@ const HomePage = () => {
disabled: false,
color: 'primary',
label: formatMessage({ id: getTrad('header.actions.upload-assets') }),
onClick: handleClickToggleModal,
onClick: () => handleClickToggleModal(),
type: 'button',
},
],
};
const limit = parseInt(getQueryValue('_limit'), 10) || 10;
const start = parseInt(getQueryValue('_start'), 10) || 0;
const limit = parseInt(query.get('_limit'), 10) || 10;
const start = parseInt(query.get('_start'), 10) || 0;
const params = {
_limit: parseInt(getQueryValue('_limit'), 10) || 10,
_limit: parseInt(query.get('_limit'), 10) || 10,
_page: generatePageFromStart(start, limit),
};
@ -162,31 +172,27 @@ const HomePage = () => {
<Header {...headerProps} />
<HeaderSearch
label={pluginName}
onChange={handleChangeParams}
onChange={handleChangeSearchValue}
onClear={handleClearSearch}
placeholder={formatMessage({ id: getTrad('search.placeholder') })}
name="_q"
value={getQueryValue('_q') || ''}
value={searchValue}
/>
<ControlsWrapper>
<SelectAll />
<SortPicker
<SortPicker onChange={handleChangeParams} value={query.get('_sort') || null} />
<Filters
onChange={handleChangeParams}
value={getQueryValue('_sort') || null}
/>
<FiltersPicker onChange={handleChangeFilters} filters={filtersForm} />
<FiltersList
filters={generateFiltersFromSearch(search)}
onClick={handleDeleteFilter}
/>
</ControlsWrapper>
<ListEmpty onClick={handleClickToggleModal} />
<ListEmpty onClick={() => handleClickToggleModal()} />
{/* <List data={data} /> */}
<PageFooter
count={50}
context={{ emitEvent: () => {} }}
count={50}
onChangeParams={handleChangeListParams}
params={params}
/>

View File

@ -1,4 +1,3 @@
export { default as filtersForm } from './filtersForm';
export { default as generateStartFromPage } from './generateStartFromPage';
export { default as generatePageFromStart } from './generatePageFromStart';
export { default as getHeaderLabel } from './getHeaderLabel';