AttributeFilter in CM

This commit is contained in:
mfrachet 2021-06-29 09:08:33 +02:00
parent f3c76c29d8
commit 321ed4041c
16 changed files with 285 additions and 693 deletions

View File

@ -0,0 +1,63 @@
import React from 'react';
import PropTypes from 'prop-types';
import moment from 'moment';
import { DateTime } from '@buffetjs/custom';
import { DatePicker, InputText, InputNumber, Select, TimePicker } from '@buffetjs/core';
import { DateWrapper } from './components';
function GenericInput({ type, onChange, value, ...rest }) {
switch (type) {
case 'boolean':
return <Select onChange={e => onChange(e.target.value)} value={value} {...rest} />;
case 'date':
case 'timestamp':
case 'timestampUpdate': {
const momentValue = moment(value);
return (
<DateWrapper type={type}>
<DatePicker onChange={e => onChange(e.target.value._d)} value={momentValue} {...rest} />
</DateWrapper>
);
}
case 'datetime': {
const momentValue = moment(value);
return (
<DateWrapper type={type}>
<DateTime onChange={e => onChange(e.target.value)} value={momentValue} {...rest} />
</DateWrapper>
);
}
case 'enumeration':
return <Select onChange={e => onChange(e.target.value)} value={value} {...rest} />;
case 'integer':
case 'decimal':
case 'float':
return <InputNumber onChange={e => onChange(e.target.value)} value={value} {...rest} />;
case 'time':
return <TimePicker onChange={e => onChange(e.target.value)} value={value} {...rest} />;
/**
* "biginteger" type falls into this section
*/
default:
return <InputText onChange={e => onChange(e.target.value)} value={value} {...rest} />;
}
}
GenericInput.defaultProps = {
value: undefined,
};
GenericInput.propTypes = {
onChange: PropTypes.func.isRequired,
type: PropTypes.string.isRequired,
value: PropTypes.any,
};
export default GenericInput;

View File

@ -0,0 +1,24 @@
import { Button } from '@buffetjs/core';
import styled from 'styled-components';
export const StyledButton = styled(Button)`
width: 100%;
`;
export const FormWrapper = styled.form`
min-width: 330px;
max-width: 400px;
padding: 13px 15px;
& > * + * {
margin-top: 11px;
}
`;
export const DateWrapper = styled.div`
display: ${({ type }) => (type === 'datetime' ? 'flex' : 'block')};
input {
width: 100%;
}
`;

View File

@ -0,0 +1,47 @@
import get from 'lodash/get';
import { useRBACProvider, findMatchingPermissions } from '@strapi/helper-plugin';
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone'];
const useAllowedAttributes = (contentType, slug) => {
const { allPermissions } = useRBACProvider();
let timestamps = get(contentType, ['options', 'timestamps']);
if (!Array.isArray(timestamps)) {
timestamps = [];
}
const readPermissionsForSlug = findMatchingPermissions(allPermissions, [
{
action: 'plugins::content-manager.explorer.read',
subject: slug,
},
]);
const readPermissionForAttr = get(readPermissionsForSlug, ['0', 'properties', 'fields'], []);
const attributesArray = Object.keys(get(contentType, ['attributes']), {});
const allowedAttributes = attributesArray
.filter(attr => {
const current = get(contentType, ['attributes', attr], {});
if (!current.type) {
return false;
}
if (NOT_ALLOWED_FILTERS.includes(current.type)) {
return false;
}
if (!readPermissionForAttr.includes(attr) && attr !== 'id' && !timestamps.includes(attr)) {
return false;
}
return true;
})
.sort();
return allowedAttributes;
};
export default useAllowedAttributes;

View File

@ -0,0 +1,114 @@
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import { Picker, Select } from '@buffetjs/core';
import {
FilterIcon,
getFilterType as comparatorsForType,
useTracking,
useQueryParams,
} from '@strapi/helper-plugin';
import { FormattedMessage } from 'react-intl';
import useAllowedAttributes from './hooks/useAllowedAttributes';
import getTrad from '../../utils/getTrad';
import formatAttribute from './utils/formatAttribute';
import getAttributeType from './utils/getAttributeType';
import GenericInput from './GenericInput';
import { StyledButton, FormWrapper } from './components';
const AttributeFilter = ({ contentType, slug, metaData }) => {
const { trackUsage } = useTracking();
const [{ query }, setQuery] = useQueryParams();
const allowedAttributes = useAllowedAttributes(contentType, slug);
const [attribute, setAttribute] = useState(allowedAttributes[0]);
const attributeType = getAttributeType(attribute, contentType, metaData);
const comparators = comparatorsForType(attributeType);
const [comparator, setComparator] = useState(comparators[0].value);
const [value, setValue] = useState();
return (
<Picker
renderButtonContent={() => (
<>
<FilterIcon />
<FormattedMessage id="app.utils.filters" />
</>
)}
renderSectionContent={onToggle => {
const handleSubmit = e => {
e.preventDefault();
const formattedAttribute = formatAttribute(attribute, metaData);
/**
* When dealing with a "=" comparator, the filter should have a shape of {'attributeName': 'some value}
* otherwise, it should look like { 'attributeName_comparatorName' : 'some value' }
*/
const newFilter =
comparator === '='
? { [formattedAttribute]: value }
: { [`${formattedAttribute}${comparator}`]: value };
/**
* Pushing the filter in the URL for later refreshes or fast access
*/
const _where = query._where || [];
_where.push(newFilter);
setQuery({ ...query, _where, page: 1 });
/**
* Tracking stuff
*/
const useRelation = _where.some(obj => Object.keys(obj)[0].includes('.'));
trackUsage('didFilterEntries', { useRelation });
/**
* Reset to initial state
*/
setAttribute(allowedAttributes[0]);
setComparator(comparators[0].value);
setValue(undefined);
onToggle();
};
return (
<FormWrapper onSubmit={handleSubmit}>
<Select
onChange={e => setAttribute(e.target.value)}
name="ct-filter"
options={allowedAttributes}
value={attribute}
/>
<Select
onChange={e => setComparator(e.target.value)}
name="comparator"
value={comparator}
options={comparators.map(comparator => (
<FormattedMessage id={comparator.id} key={comparator.value}>
{msg => <option value={comparator.value}>{msg}</option>}
</FormattedMessage>
))}
/>
<GenericInput name="input" onChange={setValue} type={attributeType} value={value} />
<StyledButton icon type="submit">
<FormattedMessage
id={getTrad('components.FiltersPickWrapper.PluginHeader.actions.apply')}
/>
</StyledButton>
</FormWrapper>
);
}}
/>
);
};
AttributeFilter.propTypes = {
contentType: PropTypes.object.isRequired,
metaData: PropTypes.object.isRequired,
slug: PropTypes.string.isRequired,
};
export default AttributeFilter;

View File

@ -0,0 +1,13 @@
import get from 'lodash/get';
const formatAttribute = (attributeName, metaData) => {
const mainField = get(metaData, [attributeName, 'list', 'mainField', 'name']);
if (mainField) {
return `${attributeName}.${mainField}`;
}
return attributeName;
};
export default formatAttribute;

View File

@ -0,0 +1,13 @@
import get from 'lodash/get';
const getAttributeType = (attributeName, contentType, metaData) => {
let attributeType = get(contentType, ['attributes', attributeName, 'type'], '');
if (attributeType === 'relation') {
attributeType = get(metaData, [attributeName, 'list', 'mainField', 'schema', 'type'], 'string');
}
return attributeType === 'string' ? 'text' : attributeType;
};
export default getAttributeType;

View File

@ -1,56 +0,0 @@
import styled from 'styled-components';
/* eslint-disable indent */
const Wrapper = styled.div`
margin-top: -6px;
> div {
padding-top: 2px;
&:not(:first-of-type) {
padding-top: 9px;
padding-bottom: 2px;
&:last-of-type:nth-of-type(even) {
padding-bottom: 11px;
}
}
}
`;
const Span = styled.span`
vertical-align: text-top;
cursor: pointer;
&:after {
margin-left: 2px;
content: '\f077';
font-family: FontAwesome;
font-size: 10px;
}
`;
const Flex = styled.div`
display: flex;
justify-content: flex-end;
padding: 0 0 10px 30px !important;
margin-top: -10px;
color: #c3c5c8;
font-size: 13px;
`;
const Div = styled.div`
width: calc(100% + 60px);
margin: ${props => (props.show ? '-100px -30px 30px' : `-${props.number}px -30px 103px`)};
background: #fff;
box-shadow: 3px 2px 4px #e3e9f3;
padding: 18px 30px 0px 30px;
transition: ${props => {
if (props.anim) {
return props.show
? 'margin-top .3s ease-out, margin-bottom .2s ease-out'
: 'margin .3s ease-in';
}
return '';
}};
`;
export { Div, Flex, Span, Wrapper };

View File

@ -1,256 +0,0 @@
import React, { memo, useCallback, useMemo, useReducer, useRef } from 'react';
import { withRouter } from 'react-router';
import PropTypes from 'prop-types';
import { capitalize, get } from 'lodash';
import { Collapse } from 'reactstrap';
import { FormattedMessage } from 'react-intl';
import {
PluginHeader,
getFilterType,
useRBACProvider,
findMatchingPermissions,
useTracking,
} from '@strapi/helper-plugin';
import { formatFiltersToQuery, getTrad } from '../../utils';
import Container from '../Container';
import FilterPickerOption from '../FilterPickerOption';
import { Flex, Span, Wrapper } from './components';
import init from './init';
import reducer, { initialState } from './reducer';
const NOT_ALLOWED_FILTERS = ['json', 'component', 'media', 'richtext', 'dynamiczone'];
function FilterPicker({
contentType,
filters,
isOpen,
metadatas,
name,
toggleFilterPickerState,
setQuery,
slug,
}) {
const { trackUsage } = useTracking();
const trackUsageRef = useRef(trackUsage);
const { allPermissions } = useRBACProvider();
const readActionAllowedFields = useMemo(() => {
const matchingPermissions = findMatchingPermissions(allPermissions, [
{
action: 'plugins::content-manager.explorer.read',
subject: slug,
},
]);
return get(matchingPermissions, ['0', 'properties', 'fields'], []);
}, [allPermissions, slug]);
let timestamps = get(contentType, ['options', 'timestamps']);
if (!Array.isArray(timestamps)) {
timestamps = [];
}
const actions = [
{
label: getTrad('components.FiltersPickWrapper.PluginHeader.actions.clearAll'),
kind: 'secondary',
onClick: () => {
toggleFilterPickerState();
setQuery({ _where: [] }, 'remove');
},
},
{
label: getTrad('components.FiltersPickWrapper.PluginHeader.actions.apply'),
kind: 'primary',
type: 'submit',
},
];
const allowedAttributes = Object.keys(get(contentType, ['attributes']), {})
.filter(attr => {
const current = get(contentType, ['attributes', attr], {});
if (!readActionAllowedFields.includes(attr) && attr !== 'id' && !timestamps.includes(attr)) {
return false;
}
return !NOT_ALLOWED_FILTERS.includes(current.type) && current.type !== undefined;
})
.sort()
.map(attr => {
const current = get(contentType, ['attributes', attr], {});
return { name: attr, type: current.type, options: current.enum || null };
});
const [state, dispatch] = useReducer(reducer, initialState, () =>
init(initialState, allowedAttributes[0] || {})
);
const modifiedData = state.get('modifiedData').toJS();
const handleChange = ({ target: { name, value } }) => {
dispatch({
type: 'ON_CHANGE',
keys: name.split('.'),
value,
});
};
const renderTitle = () => (
<FormattedMessage id={getTrad('components.FiltersPickWrapper.PluginHeader.title.filter')}>
{message => (
<span>
{capitalize(name)}&nbsp;-&nbsp;
<span>{message}</span>
</span>
)}
</FormattedMessage>
);
const initialFilter = useMemo(() => {
const type = get(allowedAttributes, [0, 'type'], '');
const [filter] = getFilterType(type);
let value = '';
switch (type) {
case 'boolean': {
value = 'true';
break;
}
case 'number': {
value = 0;
break;
}
case 'enumeration': {
value = get(allowedAttributes, [0, 'options', 0], '');
break;
}
default: {
value = '';
}
}
const initFilter = {
name: get(allowedAttributes, [0, 'name'], ''),
filter: filter.value,
value,
};
return initFilter;
}, [allowedAttributes]);
// Set the filters when the collapse is opening
const handleEntering = () => {
const currentFilters = filters;
const initialFilters = currentFilters.length ? currentFilters : [initialFilter];
dispatch({
type: 'SET_FILTERS',
initialFilters,
attributes: get(contentType, 'attributes', {}),
});
};
const addFilter = () => {
dispatch({
type: 'ADD_FILTER',
filter: initialFilter,
});
};
const handleSubmit = useCallback(
e => {
e.preventDefault();
const nextFilters = formatFiltersToQuery(modifiedData, metadatas);
const useRelation = nextFilters._where.some(obj => Object.keys(obj)[0].includes('.'));
trackUsageRef.current('didFilterEntries', { useRelation });
setQuery({ ...nextFilters, page: 1 });
toggleFilterPickerState();
},
[modifiedData, setQuery, toggleFilterPickerState, metadatas]
);
const handleRemoveFilter = index => {
if (index === 0 && modifiedData.length === 1) {
toggleFilterPickerState();
return;
}
dispatch({
type: 'REMOVE_FILTER',
index,
});
};
const getAttributeType = useCallback(
filter => {
const attributeType = get(contentType, ['attributes', filter.name, 'type'], '');
if (attributeType === 'relation') {
return get(metadatas, [filter.name, 'list', 'mainField', 'schema', 'type'], 'string');
}
return attributeType;
},
[contentType, metadatas]
);
return (
<Collapse isOpen={isOpen} onEntering={handleEntering}>
<Container style={{ backgroundColor: 'white', paddingBottom: 0 }}>
<form onSubmit={handleSubmit}>
<PluginHeader
actions={actions}
title={renderTitle}
description={{
id: getTrad('components.FiltersPickWrapper.PluginHeader.description'),
}}
/>
<Wrapper>
{modifiedData.map((filter, key) => (
<FilterPickerOption
{...filter}
allowedAttributes={allowedAttributes}
index={key}
modifiedData={modifiedData}
onChange={handleChange}
onClickAddFilter={addFilter}
onRemoveFilter={handleRemoveFilter}
type={getAttributeType(filter)}
showAddButton={key === modifiedData.length - 1}
// eslint-disable-next-line react/no-array-index-key
key={key}
/>
))}
</Wrapper>
<Flex>
<Span onClick={toggleFilterPickerState}>
<FormattedMessage id="content-manager.components.FiltersPickWrapper.hide" />
&nbsp;
</Span>
</Flex>
</form>
</Container>
</Collapse>
);
}
FilterPicker.defaultProps = {
name: '',
};
FilterPicker.propTypes = {
contentType: PropTypes.object.isRequired,
filters: PropTypes.array.isRequired,
isOpen: PropTypes.bool.isRequired,
metadatas: PropTypes.object.isRequired,
name: PropTypes.string,
setQuery: PropTypes.func.isRequired,
slug: PropTypes.string.isRequired,
toggleFilterPickerState: PropTypes.func.isRequired,
};
export default withRouter(memo(FilterPicker));

View File

@ -1,25 +0,0 @@
import { fromJS } from 'immutable';
import { getFilterType } from '@strapi/helper-plugin';
import { get } from 'lodash';
function init(initialState, { name, type, options }) {
// Create the first filter
const [filter] = getFilterType(type);
let value = '';
if (type === 'boolean') {
value = 'true';
} else if (type === 'number') {
value = 0;
} else if (type === 'enumeration') {
value = get(options, [0], '');
}
const initialFilter = { name, filter: filter.value, value };
return initialState
.update('initialData', () => fromJS([initialFilter]))
.update('modifiedData', () => fromJS([initialFilter]));
}
export default init;

View File

@ -1,58 +0,0 @@
import { fromJS } from 'immutable';
const initialState = fromJS({
attributes: {},
initialData: [],
modifiedData: [],
});
function reducer(state, action) {
switch (action.type) {
case 'ADD_FILTER':
return state.update('modifiedData', list => list.push(fromJS(action.filter)));
case 'ON_CHANGE': {
const [index, key] = action.keys;
return state
.updateIn(['modifiedData', ...action.keys], () => {
if (action.value && action.value._isAMomentObject === true) {
return action.value.toISOString();
}
return action.value;
})
.updateIn(['modifiedData', index, 'value'], value => {
if (key === 'name') {
const attribute = state.getIn(['attributes', action.value]);
const attributeType = attribute.get('type');
if (attributeType === 'boolean') {
return 'true';
}
if (attributeType === 'enumeration') {
return attribute.getIn(['enum', '0']) || '';
}
return '';
}
return value;
});
}
case 'REMOVE_FILTER':
return state.removeIn(['modifiedData', action.index]);
case 'RESET_FILTERS':
return initialState;
case 'SET_FILTERS':
return state
.update('attributes', () => fromJS(action.attributes))
.update('initialData', () => fromJS(action.initialFilters))
.update('modifiedData', () => fromJS(action.initialFilters));
default:
return state;
}
}
export default reducer;
export { initialState };

View File

@ -1,58 +0,0 @@
/**
*
* InputWithAutoFocus that programatically manage the autofocus of another one
*/
import React, { memo } from 'react';
import PropTypes from 'prop-types';
import { DateTime } from '@buffetjs/custom';
import { DatePicker, InputText, InputNumber, Select, TimePicker } from '@buffetjs/core';
import { InputWrapperDate } from './components';
const getInputType = attrType => {
switch (attrType) {
case 'boolean':
return Select;
case 'date':
case 'timestamp':
case 'timestampUpdate':
return DatePicker;
case 'datetime':
return DateTime;
case 'enumeration':
return Select;
case 'integer':
case 'biginteger':
case 'decimal':
case 'float':
return InputNumber;
case 'time':
return TimePicker;
default:
return InputText;
}
};
function Input({ type, ...rest }) {
const Component = getInputType(type);
let style = type !== 'time' ? { width: '210px' } : {};
if (['integer', 'biginteger', 'float', 'decimal'].includes(type)) {
style = { marginRight: '15px' };
}
const styles = type === 'boolean' ? { minWidth: '100px', maxWidth: '200px' } : style;
const wrapperStyle = { marginRight: '15px' };
return (
<InputWrapperDate type={type || 'text'} style={wrapperStyle}>
<Component {...rest} style={styles} autoComplete="off" />
</InputWrapperDate>
);
}
Input.propTypes = {
type: PropTypes.string.isRequired,
};
export default memo(Input);

View File

@ -1,14 +0,0 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import PropTypes from 'prop-types';
const Option = ({ id, value }) => {
return <FormattedMessage id={id}>{msg => <option value={value}>{msg}</option>}</FormattedMessage>;
};
Option.propTypes = {
id: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
};
export default Option;

View File

@ -1,60 +0,0 @@
import styled from 'styled-components';
/* eslint-disable */
const Wrapper = styled.div`
min-height: 38px;
border-left: ${props => props.borderLeft && '3px solid #007EFF'};
padding-left: ${props => (props.borderLeft ? '10px' : '13px')};
margin-bottom: 0px !important;
`;
const InputWrapper = styled.div`
display: flex;
input,
select {
margin: 0px 5px !important;
}
`;
const InputWrapperDate = styled.div`
margin-right: 10px;
span {
left: 5px;
}
.rc-input-number-handler-wrap {
right: -5px !important;
}
.rc-input-number-input-wrap {
max-width: 210px;
overflow: visible;
}
> div {
width: 210px;
}
${({ type }) => {
if (type === 'datetime') {
return `
> div {
width: 300px;
}
`;
}
}}
`;
const Input = styled.input`
height: 3.4rem;
margin-top: 0.9rem;
padding-left: 1rem;
background-size: 0 !important;
border: 1px solid #e3e9f3;
border-radius: 0.25rem;
line-height: 3.4rem;
font-size: 1.3rem;
font-family: 'Lato' !important;
box-shadow: 0px 1px 1px rgba(104, 118, 142, 0.05);
`;
export { InputWrapper, Wrapper, InputWrapperDate, Input };

View File

@ -1,101 +0,0 @@
import React, { memo } from 'react';
import { get, isEmpty } from 'lodash';
import PropTypes from 'prop-types';
import { CircleButton, getFilterType } from '@strapi/helper-plugin';
import { Select } from '@buffetjs/core';
import { InputWrapper, Wrapper } from './components';
import Input from './Input';
import Option from './Option';
const styles = {
select: {
minWidth: '170px',
maxWidth: '200px',
},
selectMiddle: {
minWidth: '130px',
maxWidth: '200px',
marginLeft: '10px',
marginRight: '10px',
},
};
function FilterPickerOption({
allowedAttributes,
modifiedData,
index,
onChange,
onClickAddFilter,
onRemoveFilter,
value,
showAddButton,
type,
}) {
const filtersOptions = getFilterType(type);
const currentFilterName = get(modifiedData, [index, 'name'], '');
const currentFilterData = allowedAttributes.find(attr => attr.name === currentFilterName);
const options = get(currentFilterData, ['options'], null) || ['true', 'false'];
return (
<Wrapper borderLeft={!isEmpty(value)}>
<InputWrapper>
<CircleButton type="button" isRemoveButton onClick={() => onRemoveFilter(index)} />
<Select
onChange={e => {
// Change the attribute
onChange(e);
// Change the default filter so it reset to the common one which is '='
onChange({ target: { name: `${index}.filter`, value: '=' } });
}}
name={`${index}.name`}
value={currentFilterName}
options={allowedAttributes.map(attr => attr.name)}
style={styles.select}
/>
<Select
onChange={onChange}
name={`${index}.filter`}
options={filtersOptions.map(option => (
<Option {...option} key={option.value} />
))}
style={styles.selectMiddle}
value={get(modifiedData, [index, 'filter'], '')}
/>
<Input
type={type}
name={`${index}.value`}
value={get(modifiedData, [index, 'value'], '')}
options={options}
onChange={onChange}
/>
{showAddButton && <CircleButton type="button" onClick={onClickAddFilter} />}
</InputWrapper>
</Wrapper>
);
}
FilterPickerOption.defaultProps = {
allowedAttributes: [],
modifiedData: [],
index: -1,
onChange: () => {},
onClickAddFilter: () => {},
onRemoveFilter: () => {},
value: null,
type: 'string',
};
FilterPickerOption.propTypes = {
allowedAttributes: PropTypes.array,
modifiedData: PropTypes.array,
index: PropTypes.number,
onChange: PropTypes.func,
onClickAddFilter: PropTypes.func,
onRemoveFilter: PropTypes.func,
showAddButton: PropTypes.bool.isRequired,
type: PropTypes.string,
value: PropTypes.any,
};
export default memo(FilterPickerOption);

View File

@ -1,46 +1,10 @@
import styled from 'styled-components';
import { Button, FilterIcon as Filter } from '@strapi/helper-plugin';
import RemoveIcon from '../../assets/images/icon-cross-blue.svg';
const Wrapper = styled.div`
padding-top: 1px;
`;
const FilterIcon = styled(Filter)`
padding: 0 !important;
margin: auto !important;
> g {
stroke: #282b2c;
}
`;
const AddFilterCta = styled(Button)`
display: flex;
height: 30px;
margin-right: 10px;
padding: 0 10px;
text-align: center;
background-color: #ffffff;
border: 1px solid #e3e9f3;
border-radius: 2px;
line-height: 28px;
font-size: 13px;
font-weight: 500;
font-family: Lato;
-webkit-font-smoothing: antialiased;
cursor: pointer;
&:hover {
background: #f7f8f8;
}
&:focus,
&:active {
outline: 0;
}
> span {
margin-left: 10px;
}
`;
const Img = styled.img`
height: 7px;
margin: auto;
@ -124,15 +88,4 @@ const Remove = styled.span`
}
`;
export {
AddFilterCta,
FilterIcon,
FooterWrapper,
Img,
Label,
SelectWrapper,
FilterWrapper,
Separator,
Remove,
Wrapper,
};
export { FooterWrapper, Img, Label, SelectWrapper, FilterWrapper, Separator, Remove, Wrapper };

View File

@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators, compose } from 'redux';
import { get, isEmpty } from 'lodash';
import { FormattedMessage, useIntl } from 'react-intl';
import { useIntl } from 'react-intl';
import { useHistory, useLocation } from 'react-router-dom';
import { Header } from '@buffetjs/custom';
import { Flex, Padded } from '@buffetjs/core';
@ -26,11 +26,10 @@ import permissions from '../../../permissions';
import { formatFiltersFromQuery, getRequestUrl, getTrad } from '../../utils';
import Container from '../../components/Container';
import CustomTable from '../../components/CustomTable';
import FilterPicker from '../../components/FilterPicker';
import Search from '../../components/Search';
import ListViewProvider from '../../components/ListViewProvider';
import InjectionZoneList from '../../components/InjectionZoneList';
import { AddFilterCta, FilterIcon, Wrapper } from './components';
import { Wrapper } from './components';
import FieldPicker from './FieldPicker';
import Filter from './Filter';
import Footer from './Footer';
@ -51,6 +50,7 @@ import {
} from './actions';
import makeSelectListView from './selectors';
import { getAllAllowedHeaders, getFirstSortableHeader, buildQueryString } from './utils';
import AttributeFilter from '../../components/AttributeFilter';
const cmPermissions = permissions.contentManager;
@ -395,16 +395,6 @@ function ListView({
toggleModalDeleteAll={handleToggleModalDeleteAll}
setQuery={setQuery}
>
<FilterPicker
contentType={contentType}
filters={filters}
isOpen={isFilterPickerOpen}
metadatas={metadatas}
name={label}
toggleFilterPickerState={toggleFilterPickerState}
setQuery={setQuery}
slug={slug}
/>
<Container className="container-fluid">
{!isFilterPickerOpen && <Header {...headerProps} isLoading={isLoading && canRead} />}
{isSearchable && canRead && (
@ -432,10 +422,13 @@ function ListView({
<div className="row" style={{ marginLeft: 0, marginRight: 0 }}>
{isFilterable && (
<>
<AddFilterCta type="button" onClick={toggleFilterPickerState}>
<FilterIcon />
<FormattedMessage id="app.utils.filters" />
</AddFilterCta>
<Padded right size="sm">
<AttributeFilter
contentType={contentType}
slug={slug}
metaData={metadatas}
/>
</Padded>
{filters.map(({ filter: filterName, name, value }, key) => (
<Filter
contentType={contentType}