mirror of
https://github.com/strapi/strapi.git
synced 2025-11-09 06:40:42 +00:00
AttributeFilter in CM
This commit is contained in:
parent
f3c76c29d8
commit
321ed4041c
@ -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;
|
||||||
@ -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%;
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -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 };
|
|
||||||
@ -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)} -
|
|
||||||
<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" />
|
|
||||||
|
|
||||||
</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));
|
|
||||||
@ -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;
|
|
||||||
@ -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 };
|
|
||||||
@ -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);
|
|
||||||
@ -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;
|
|
||||||
@ -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 };
|
|
||||||
@ -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);
|
|
||||||
@ -1,46 +1,10 @@
|
|||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
import { Button, FilterIcon as Filter } from '@strapi/helper-plugin';
|
|
||||||
import RemoveIcon from '../../assets/images/icon-cross-blue.svg';
|
import RemoveIcon from '../../assets/images/icon-cross-blue.svg';
|
||||||
|
|
||||||
const Wrapper = styled.div`
|
const Wrapper = styled.div`
|
||||||
padding-top: 1px;
|
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`
|
const Img = styled.img`
|
||||||
height: 7px;
|
height: 7px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
@ -124,15 +88,4 @@ const Remove = styled.span`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export {
|
export { FooterWrapper, Img, Label, SelectWrapper, FilterWrapper, Separator, Remove, Wrapper };
|
||||||
AddFilterCta,
|
|
||||||
FilterIcon,
|
|
||||||
FooterWrapper,
|
|
||||||
Img,
|
|
||||||
Label,
|
|
||||||
SelectWrapper,
|
|
||||||
FilterWrapper,
|
|
||||||
Separator,
|
|
||||||
Remove,
|
|
||||||
Wrapper,
|
|
||||||
};
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
|||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators, compose } from 'redux';
|
import { bindActionCreators, compose } from 'redux';
|
||||||
import { get, isEmpty } from 'lodash';
|
import { get, isEmpty } from 'lodash';
|
||||||
import { FormattedMessage, useIntl } from 'react-intl';
|
import { useIntl } from 'react-intl';
|
||||||
import { useHistory, useLocation } from 'react-router-dom';
|
import { useHistory, useLocation } from 'react-router-dom';
|
||||||
import { Header } from '@buffetjs/custom';
|
import { Header } from '@buffetjs/custom';
|
||||||
import { Flex, Padded } from '@buffetjs/core';
|
import { Flex, Padded } from '@buffetjs/core';
|
||||||
@ -26,11 +26,10 @@ import permissions from '../../../permissions';
|
|||||||
import { formatFiltersFromQuery, getRequestUrl, getTrad } from '../../utils';
|
import { formatFiltersFromQuery, getRequestUrl, getTrad } from '../../utils';
|
||||||
import Container from '../../components/Container';
|
import Container from '../../components/Container';
|
||||||
import CustomTable from '../../components/CustomTable';
|
import CustomTable from '../../components/CustomTable';
|
||||||
import FilterPicker from '../../components/FilterPicker';
|
|
||||||
import Search from '../../components/Search';
|
import Search from '../../components/Search';
|
||||||
import ListViewProvider from '../../components/ListViewProvider';
|
import ListViewProvider from '../../components/ListViewProvider';
|
||||||
import InjectionZoneList from '../../components/InjectionZoneList';
|
import InjectionZoneList from '../../components/InjectionZoneList';
|
||||||
import { AddFilterCta, FilterIcon, Wrapper } from './components';
|
import { Wrapper } from './components';
|
||||||
import FieldPicker from './FieldPicker';
|
import FieldPicker from './FieldPicker';
|
||||||
import Filter from './Filter';
|
import Filter from './Filter';
|
||||||
import Footer from './Footer';
|
import Footer from './Footer';
|
||||||
@ -51,6 +50,7 @@ import {
|
|||||||
} from './actions';
|
} from './actions';
|
||||||
import makeSelectListView from './selectors';
|
import makeSelectListView from './selectors';
|
||||||
import { getAllAllowedHeaders, getFirstSortableHeader, buildQueryString } from './utils';
|
import { getAllAllowedHeaders, getFirstSortableHeader, buildQueryString } from './utils';
|
||||||
|
import AttributeFilter from '../../components/AttributeFilter';
|
||||||
|
|
||||||
const cmPermissions = permissions.contentManager;
|
const cmPermissions = permissions.contentManager;
|
||||||
|
|
||||||
@ -395,16 +395,6 @@ function ListView({
|
|||||||
toggleModalDeleteAll={handleToggleModalDeleteAll}
|
toggleModalDeleteAll={handleToggleModalDeleteAll}
|
||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
>
|
>
|
||||||
<FilterPicker
|
|
||||||
contentType={contentType}
|
|
||||||
filters={filters}
|
|
||||||
isOpen={isFilterPickerOpen}
|
|
||||||
metadatas={metadatas}
|
|
||||||
name={label}
|
|
||||||
toggleFilterPickerState={toggleFilterPickerState}
|
|
||||||
setQuery={setQuery}
|
|
||||||
slug={slug}
|
|
||||||
/>
|
|
||||||
<Container className="container-fluid">
|
<Container className="container-fluid">
|
||||||
{!isFilterPickerOpen && <Header {...headerProps} isLoading={isLoading && canRead} />}
|
{!isFilterPickerOpen && <Header {...headerProps} isLoading={isLoading && canRead} />}
|
||||||
{isSearchable && canRead && (
|
{isSearchable && canRead && (
|
||||||
@ -432,10 +422,13 @@ function ListView({
|
|||||||
<div className="row" style={{ marginLeft: 0, marginRight: 0 }}>
|
<div className="row" style={{ marginLeft: 0, marginRight: 0 }}>
|
||||||
{isFilterable && (
|
{isFilterable && (
|
||||||
<>
|
<>
|
||||||
<AddFilterCta type="button" onClick={toggleFilterPickerState}>
|
<Padded right size="sm">
|
||||||
<FilterIcon />
|
<AttributeFilter
|
||||||
<FormattedMessage id="app.utils.filters" />
|
contentType={contentType}
|
||||||
</AddFilterCta>
|
slug={slug}
|
||||||
|
metaData={metadatas}
|
||||||
|
/>
|
||||||
|
</Padded>
|
||||||
{filters.map(({ filter: filterName, name, value }, key) => (
|
{filters.map(({ filter: filterName, name, value }, key) => (
|
||||||
<Filter
|
<Filter
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user