mirror of
https://github.com/strapi/strapi.git
synced 2025-12-29 16:16:20 +00:00
Merge pull request #5354 from strapi/front/media-library-filter
Media library Filters
This commit is contained in:
commit
268fa19f0c
@ -13,6 +13,8 @@ const sizes = {
|
||||
},
|
||||
padding: {
|
||||
// TODO
|
||||
xs: '5px',
|
||||
sm: '10px',
|
||||
md: '30px',
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,6 +11,7 @@ const generateFiltersFromSearch = search => {
|
||||
!x.includes('_limit') &&
|
||||
!x.includes('_page') &&
|
||||
!x.includes('_sort') &&
|
||||
!x.includes('_start') &&
|
||||
!x.includes('_q=') &&
|
||||
x !== ''
|
||||
)
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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 };
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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) {
|
||||
@ -2,6 +2,9 @@ import styled from 'styled-components';
|
||||
|
||||
const InputWrapper = styled.div`
|
||||
margin-bottom: 11px;
|
||||
#datetime {
|
||||
max-width: 130px;
|
||||
}
|
||||
`;
|
||||
|
||||
export default InputWrapper;
|
||||
@ -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;
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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 };
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
@ -11,10 +11,7 @@ const filtersForm = {
|
||||
},
|
||||
size: {
|
||||
type: 'size',
|
||||
defaultValue: {
|
||||
size: 0,
|
||||
format: 'KB',
|
||||
},
|
||||
defaultValue: '0KB',
|
||||
},
|
||||
file_type: {
|
||||
type: 'enum',
|
||||
0
packages/strapi-plugin-upload/admin/src/components/FiltersPicker/Wrapper.js
Executable file → Normal file
0
packages/strapi-plugin-upload/admin/src/components/FiltersPicker/Wrapper.js
Executable file → Normal 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,
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -1,9 +0,0 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
.col-6:last-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default Wrapper;
|
||||
@ -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;
|
||||
@ -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};
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
|
||||
@ -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';
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user