mirror of
				https://github.com/strapi/strapi.git
				synced 2025-11-04 11:54:10 +00:00 
			
		
		
		
	Merge pull request #5290 from strapi/front-media-lib-search
Front Media lib search & sort by
This commit is contained in:
		
						commit
						54a36e6b36
					
				@ -15,9 +15,16 @@ const colors = {
 | 
			
		||||
  'gray-lighter': '#eceeef',
 | 
			
		||||
  'gray-lightest': '#f7f7f9',
 | 
			
		||||
  brightGrey: '#f0f3f8',
 | 
			
		||||
  darkGrey: '#e3e9f3',
 | 
			
		||||
  lightGrey: '#fafafa',
 | 
			
		||||
  lightestGrey: '#fbfbfb',
 | 
			
		||||
  mediumGrey: '#F2F3F4',
 | 
			
		||||
  grey: '#9ea7b8',
 | 
			
		||||
  greyDark: '#292b2c',
 | 
			
		||||
  greyAlpha: 'rgba(227, 233, 243, 0.5)',
 | 
			
		||||
  lightBlue: '#E6F0FB',
 | 
			
		||||
  mediumBlue: '#007EFF',
 | 
			
		||||
  darkBlue: '#AED4FB',
 | 
			
		||||
 | 
			
		||||
  content: {
 | 
			
		||||
    background: '#fafafb',
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
const sizes = {
 | 
			
		||||
  borderRadius: '2px',
 | 
			
		||||
  header: {
 | 
			
		||||
    height: '6rem',
 | 
			
		||||
  },
 | 
			
		||||
@ -14,7 +15,6 @@ const sizes = {
 | 
			
		||||
    // TODO
 | 
			
		||||
    md: '30px',
 | 
			
		||||
  },
 | 
			
		||||
  borderRadius: '2px',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default sizes;
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,14 @@ import SearchInfo from '../SearchInfo';
 | 
			
		||||
import Clear from './Clear';
 | 
			
		||||
import Wrapper from './Wrapper';
 | 
			
		||||
 | 
			
		||||
const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
 | 
			
		||||
const HeaderSearch = ({
 | 
			
		||||
  label,
 | 
			
		||||
  name,
 | 
			
		||||
  onChange,
 | 
			
		||||
  onClear,
 | 
			
		||||
  placeholder,
 | 
			
		||||
  value,
 | 
			
		||||
}) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Wrapper>
 | 
			
		||||
      <div>
 | 
			
		||||
@ -14,6 +21,7 @@ const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
 | 
			
		||||
      </div>
 | 
			
		||||
      <div>
 | 
			
		||||
        <input
 | 
			
		||||
          name={name}
 | 
			
		||||
          onChange={onChange}
 | 
			
		||||
          placeholder={placeholder}
 | 
			
		||||
          type="text"
 | 
			
		||||
@ -32,6 +40,7 @@ const HeaderSearch = ({ label, onChange, onClear, placeholder, value }) => {
 | 
			
		||||
 | 
			
		||||
HeaderSearch.defaultProps = {
 | 
			
		||||
  label: '',
 | 
			
		||||
  name: '',
 | 
			
		||||
  onChange: () => {},
 | 
			
		||||
  onClear: () => {},
 | 
			
		||||
  placeholder: 'Search for an entry',
 | 
			
		||||
@ -40,6 +49,7 @@ HeaderSearch.defaultProps = {
 | 
			
		||||
 | 
			
		||||
HeaderSearch.propTypes = {
 | 
			
		||||
  label: PropTypes.string,
 | 
			
		||||
  name: PropTypes.string,
 | 
			
		||||
  onChange: PropTypes.func,
 | 
			
		||||
  onClear: PropTypes.func,
 | 
			
		||||
  placeholder: PropTypes.string,
 | 
			
		||||
 | 
			
		||||
@ -87,6 +87,9 @@ export {
 | 
			
		||||
  useGlobalContext,
 | 
			
		||||
} from './contexts/GlobalContext';
 | 
			
		||||
 | 
			
		||||
// Hooks
 | 
			
		||||
export { default as useQuery } from './hooks/useQuery';
 | 
			
		||||
 | 
			
		||||
// Utils
 | 
			
		||||
export { default as auth } from './utils/auth';
 | 
			
		||||
export { default as cleanData } from './utils/cleanData';
 | 
			
		||||
@ -101,6 +104,9 @@ export { default as request } from './utils/request';
 | 
			
		||||
export { default as storeData } from './utils/storeData';
 | 
			
		||||
export { default as templateObject } from './utils/templateObject';
 | 
			
		||||
export { default as getYupInnerErrors } from './utils/getYupInnerErrors';
 | 
			
		||||
export { default as generateFiltersFromSearch } from './utils/generateFiltersFromSearch';
 | 
			
		||||
export { default as generateSearchFromFilters } from './utils/generateSearchFromFilters';
 | 
			
		||||
export { default as generateSearchFromObject } from './utils/generateSearchFromObject';
 | 
			
		||||
 | 
			
		||||
// SVGS
 | 
			
		||||
export { default as LayoutIcon } from './svgs/Layout';
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,3 @@
 | 
			
		||||
import { isEmpty, toString } from 'lodash';
 | 
			
		||||
/**
 | 
			
		||||
 * Generate filters object from string
 | 
			
		||||
 * @param  {String} search
 | 
			
		||||
@ -51,28 +50,4 @@ const generateFiltersFromSearch = search => {
 | 
			
		||||
    }, []);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const generateSearchFromFilters = filters => {
 | 
			
		||||
  return Object.keys(filters)
 | 
			
		||||
    .filter(key => !isEmpty(toString(filters[key])))
 | 
			
		||||
    .map(key => {
 | 
			
		||||
      let ret = `${key}=${filters[key]}`;
 | 
			
		||||
 | 
			
		||||
      if (key === 'filters') {
 | 
			
		||||
        const formattedFilters = filters[key]
 | 
			
		||||
          .reduce((acc, curr) => {
 | 
			
		||||
            const key =
 | 
			
		||||
              curr.filter === '=' ? curr.name : `${curr.name}${curr.filter}`;
 | 
			
		||||
            acc.push(`${key}=${curr.value}`);
 | 
			
		||||
 | 
			
		||||
            return acc;
 | 
			
		||||
          }, [])
 | 
			
		||||
          .join('&');
 | 
			
		||||
        ret = formattedFilters;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return ret;
 | 
			
		||||
    })
 | 
			
		||||
    .join('&');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export { generateFiltersFromSearch, generateSearchFromFilters };
 | 
			
		||||
export default generateFiltersFromSearch;
 | 
			
		||||
@ -0,0 +1,27 @@
 | 
			
		||||
import { isEmpty, toString } from 'lodash';
 | 
			
		||||
 | 
			
		||||
const generateSearchFromFilters = filters => {
 | 
			
		||||
  return Object.keys(filters)
 | 
			
		||||
    .filter(key => !isEmpty(toString(filters[key])))
 | 
			
		||||
    .map(key => {
 | 
			
		||||
      let ret = `${key}=${filters[key]}`;
 | 
			
		||||
 | 
			
		||||
      if (key === 'filters') {
 | 
			
		||||
        const formattedFilters = filters[key]
 | 
			
		||||
          .reduce((acc, curr) => {
 | 
			
		||||
            const key =
 | 
			
		||||
              curr.filter === '=' ? curr.name : `${curr.name}${curr.filter}`;
 | 
			
		||||
            acc.push(`${key}=${curr.value}`);
 | 
			
		||||
 | 
			
		||||
            return acc;
 | 
			
		||||
          }, [])
 | 
			
		||||
          .join('&');
 | 
			
		||||
        ret = formattedFilters;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return ret;
 | 
			
		||||
    })
 | 
			
		||||
    .join('&');
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default generateSearchFromFilters;
 | 
			
		||||
@ -0,0 +1,47 @@
 | 
			
		||||
import generateFiltersFromSearch from '../generateFiltersFromSearch';
 | 
			
		||||
 | 
			
		||||
describe('HELPER PLUGIN | utils | generateFiltersFromSearch', () => {
 | 
			
		||||
  it('should generate an array of filters', () => {
 | 
			
		||||
    const search =
 | 
			
		||||
      '?_sort=id:ASC&bool=true&big_number_ne=1&created_at_lt=2019-08-01T00:00:00Z&date_lte=2019-08-02T00:00:00Z&decimal_number_gt=2&enum_ne=noon&float_number_gte=3';
 | 
			
		||||
    const expected = [
 | 
			
		||||
      {
 | 
			
		||||
        name: 'bool',
 | 
			
		||||
        filter: '=',
 | 
			
		||||
        value: 'true',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'big_number',
 | 
			
		||||
        filter: '_ne',
 | 
			
		||||
        value: '1',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'created_at',
 | 
			
		||||
        filter: '_lt',
 | 
			
		||||
        value: '2019-08-01T00:00:00Z',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'date',
 | 
			
		||||
        filter: '_lte',
 | 
			
		||||
        value: '2019-08-02T00:00:00Z',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'decimal_number',
 | 
			
		||||
        filter: '_gt',
 | 
			
		||||
        value: '2',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'enum',
 | 
			
		||||
        filter: '_ne',
 | 
			
		||||
        value: 'noon',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        name: 'float_number',
 | 
			
		||||
        filter: '_gte',
 | 
			
		||||
        value: '3',
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    expect(generateFiltersFromSearch(search)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,52 @@
 | 
			
		||||
import generateSearchFromFilters from '../generateSearchFromFilters';
 | 
			
		||||
 | 
			
		||||
describe('HELPER PLUGIN | utils | generateSearchFromFilters', () => {
 | 
			
		||||
  it('should return a string with all the applied filters', () => {
 | 
			
		||||
    const data = {
 | 
			
		||||
      _limit: 10,
 | 
			
		||||
      _sort: 'id:ASC',
 | 
			
		||||
      _page: 2,
 | 
			
		||||
      filters: [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'bool',
 | 
			
		||||
          filter: '=',
 | 
			
		||||
          value: 'true',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'big_number',
 | 
			
		||||
          filter: '_ne',
 | 
			
		||||
          value: '1',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'created_at',
 | 
			
		||||
          filter: '_lt',
 | 
			
		||||
          value: '2019-08-01T00:00:00Z',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'date',
 | 
			
		||||
          filter: '_lte',
 | 
			
		||||
          value: '2019-08-02T00:00:00Z',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'decimal_number',
 | 
			
		||||
          filter: '_gt',
 | 
			
		||||
          value: '2',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'enum',
 | 
			
		||||
          filter: '_ne',
 | 
			
		||||
          value: 'noon',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'float_number',
 | 
			
		||||
          filter: '_gte',
 | 
			
		||||
          value: '3',
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const expected =
 | 
			
		||||
      '_limit=10&_sort=id:ASC&_page=2&bool=true&big_number_ne=1&created_at_lt=2019-08-01T00:00:00Z&date_lte=2019-08-02T00:00:00Z&decimal_number_gt=2&enum_ne=noon&float_number_gte=3';
 | 
			
		||||
    expect(generateSearchFromFilters(data)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -0,0 +1,44 @@
 | 
			
		||||
import generateSearchFromObject from '../generateSearchFromObject';
 | 
			
		||||
 | 
			
		||||
describe('HELPER PLUGIN | utils | generateSearchFromObject', () => {
 | 
			
		||||
  it('should return a string containing the _limit, _start and order', () => {
 | 
			
		||||
    const search = { _page: 1, _limit: 10, _sort: 'city:ASC' };
 | 
			
		||||
    const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
    expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should remove the _q param from the search if it is empty', () => {
 | 
			
		||||
    const search = { _page: 1, _limit: 10, _sort: 'city:ASC', _q: '' };
 | 
			
		||||
    const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
    expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should not add the filters if it is empty', () => {
 | 
			
		||||
    const search = {
 | 
			
		||||
      _page: 1,
 | 
			
		||||
      _limit: 10,
 | 
			
		||||
      _sort: 'city:ASC',
 | 
			
		||||
      _q: '',
 | 
			
		||||
      filters: [],
 | 
			
		||||
    };
 | 
			
		||||
    const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
    expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should handle the filters correctly', () => {
 | 
			
		||||
    const search = {
 | 
			
		||||
      _limit: 10,
 | 
			
		||||
      _page: 1,
 | 
			
		||||
      _q: '',
 | 
			
		||||
      _sort: 'city:ASC',
 | 
			
		||||
      filters: [{ name: 'city', filter: '=', value: 'test' }],
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const expected = '_limit=10&_sort=city:ASC&city=test&_start=0';
 | 
			
		||||
 | 
			
		||||
    expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -7,20 +7,21 @@ import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { Header } from '@buffetjs/custom';
 | 
			
		||||
import {
 | 
			
		||||
  PopUpWarning,
 | 
			
		||||
  generateFiltersFromSearch,
 | 
			
		||||
  generateSearchFromFilters,
 | 
			
		||||
  generateSearchFromObject,
 | 
			
		||||
  getQueryParameters,
 | 
			
		||||
  useGlobalContext,
 | 
			
		||||
  request,
 | 
			
		||||
} from 'strapi-helper-plugin';
 | 
			
		||||
 | 
			
		||||
import pluginId from '../../pluginId';
 | 
			
		||||
import DisplayedFieldsDropdown from '../../components/DisplayedFieldsDropdown';
 | 
			
		||||
import Container from '../../components/Container';
 | 
			
		||||
import CustomTable from '../../components/CustomTable';
 | 
			
		||||
import FilterPicker from '../../components/FilterPicker';
 | 
			
		||||
import Search from '../../components/Search';
 | 
			
		||||
import {
 | 
			
		||||
  generateFiltersFromSearch,
 | 
			
		||||
  generateSearchFromFilters,
 | 
			
		||||
} from '../../utils/search';
 | 
			
		||||
 | 
			
		||||
import ListViewProvider from '../ListViewProvider';
 | 
			
		||||
import { onChangeListLabels, resetListLabels } from '../Main/actions';
 | 
			
		||||
import { AddFilterCta, FilterIcon, Wrapper } from './components';
 | 
			
		||||
@ -39,7 +40,6 @@ import {
 | 
			
		||||
import reducer from './reducer';
 | 
			
		||||
import makeSelectListView from './selectors';
 | 
			
		||||
import getRequestUrl from '../../utils/getRequestUrl';
 | 
			
		||||
import generateSearchFromObject from './utils/generateSearchFromObject';
 | 
			
		||||
 | 
			
		||||
/* eslint-disable react/no-array-index-key */
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,46 +0,0 @@
 | 
			
		||||
import generateSearchFromObject from '../generateSearchFromObject';
 | 
			
		||||
 | 
			
		||||
describe('CONTENT MANAGER | containers | ListView | utils', () => {
 | 
			
		||||
  describe('generateSearchFromObject', () => {
 | 
			
		||||
    it('should return a string containing the _limit, _start and order', () => {
 | 
			
		||||
      const search = { _page: 1, _limit: 10, _sort: 'city:ASC' };
 | 
			
		||||
      const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
      expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should remove the _q param from the search if it is empty', () => {
 | 
			
		||||
      const search = { _page: 1, _limit: 10, _sort: 'city:ASC', _q: '' };
 | 
			
		||||
      const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
      expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should not add the filters if it is empty', () => {
 | 
			
		||||
      const search = {
 | 
			
		||||
        _page: 1,
 | 
			
		||||
        _limit: 10,
 | 
			
		||||
        _sort: 'city:ASC',
 | 
			
		||||
        _q: '',
 | 
			
		||||
        filters: [],
 | 
			
		||||
      };
 | 
			
		||||
      const expected = '_limit=10&_sort=city:ASC&_start=0';
 | 
			
		||||
 | 
			
		||||
      expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should handle the filters correctly', () => {
 | 
			
		||||
      const search = {
 | 
			
		||||
        _limit: 10,
 | 
			
		||||
        _page: 1,
 | 
			
		||||
        _q: '',
 | 
			
		||||
        _sort: 'city:ASC',
 | 
			
		||||
        filters: [{ name: 'city', filter: '=', value: 'test' }],
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
      const expected = '_limit=10&_sort=city:ASC&city=test&_start=0';
 | 
			
		||||
 | 
			
		||||
      expect(generateSearchFromObject(search)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -1,102 +0,0 @@
 | 
			
		||||
import {
 | 
			
		||||
  generateFiltersFromSearch,
 | 
			
		||||
  generateSearchFromFilters,
 | 
			
		||||
} from '../search';
 | 
			
		||||
 | 
			
		||||
describe('Content Manager | utils | search', () => {
 | 
			
		||||
  describe('generateFiltersFromSearch', () => {
 | 
			
		||||
    it('should generate an array of filters', () => {
 | 
			
		||||
      const search =
 | 
			
		||||
        '?_sort=id:ASC&bool=true&big_number_ne=1&created_at_lt=2019-08-01T00:00:00Z&date_lte=2019-08-02T00:00:00Z&decimal_number_gt=2&enum_ne=noon&float_number_gte=3';
 | 
			
		||||
      const expected = [
 | 
			
		||||
        {
 | 
			
		||||
          name: 'bool',
 | 
			
		||||
          filter: '=',
 | 
			
		||||
          value: 'true',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'big_number',
 | 
			
		||||
          filter: '_ne',
 | 
			
		||||
          value: '1',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'created_at',
 | 
			
		||||
          filter: '_lt',
 | 
			
		||||
          value: '2019-08-01T00:00:00Z',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'date',
 | 
			
		||||
          filter: '_lte',
 | 
			
		||||
          value: '2019-08-02T00:00:00Z',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'decimal_number',
 | 
			
		||||
          filter: '_gt',
 | 
			
		||||
          value: '2',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'enum',
 | 
			
		||||
          filter: '_ne',
 | 
			
		||||
          value: 'noon',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          name: 'float_number',
 | 
			
		||||
          filter: '_gte',
 | 
			
		||||
          value: '3',
 | 
			
		||||
        },
 | 
			
		||||
      ];
 | 
			
		||||
 | 
			
		||||
      expect(generateFiltersFromSearch(search)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('generateSearchFromFilters', () => {
 | 
			
		||||
    it('should return a string with all the applied filters', () => {
 | 
			
		||||
      const data = {
 | 
			
		||||
        _limit: 10,
 | 
			
		||||
        _sort: 'id:ASC',
 | 
			
		||||
        _page: 2,
 | 
			
		||||
        filters: [
 | 
			
		||||
          {
 | 
			
		||||
            name: 'bool',
 | 
			
		||||
            filter: '=',
 | 
			
		||||
            value: 'true',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'big_number',
 | 
			
		||||
            filter: '_ne',
 | 
			
		||||
            value: '1',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'created_at',
 | 
			
		||||
            filter: '_lt',
 | 
			
		||||
            value: '2019-08-01T00:00:00Z',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'date',
 | 
			
		||||
            filter: '_lte',
 | 
			
		||||
            value: '2019-08-02T00:00:00Z',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'decimal_number',
 | 
			
		||||
            filter: '_gt',
 | 
			
		||||
            value: '2',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'enum',
 | 
			
		||||
            filter: '_ne',
 | 
			
		||||
            value: 'noon',
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            name: 'float_number',
 | 
			
		||||
            filter: '_gte',
 | 
			
		||||
            value: '3',
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      };
 | 
			
		||||
      const expected =
 | 
			
		||||
        '_limit=10&_sort=id:ASC&_page=2&bool=true&big_number_ne=1&created_at_lt=2019-08-01T00:00:00Z&date_lte=2019-08-02T00:00:00Z&decimal_number_gt=2&enum_ne=noon&float_number_gte=3';
 | 
			
		||||
      expect(generateSearchFromFilters(data)).toEqual(expected);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@ -14,10 +14,9 @@ import { AttributeIcon } from '@buffetjs/core';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { useGlobalContext } from 'strapi-helper-plugin';
 | 
			
		||||
import { useGlobalContext, useQuery } from 'strapi-helper-plugin';
 | 
			
		||||
import getTrad from '../../utils/getTrad';
 | 
			
		||||
import makeSearch from '../../utils/makeSearch';
 | 
			
		||||
import useQuery from '../../hooks/useQuery';
 | 
			
		||||
import Button from './Button';
 | 
			
		||||
import Card from './Card';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,8 +2,8 @@ import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { components } from 'react-select';
 | 
			
		||||
import { upperFirst } from 'lodash';
 | 
			
		||||
import { useQuery } from 'strapi-helper-plugin';
 | 
			
		||||
import useDataManager from '../../hooks/useDataManager';
 | 
			
		||||
import useQuery from '../../hooks/useQuery';
 | 
			
		||||
import Category from './Category';
 | 
			
		||||
import Ul from './Ul';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -3,10 +3,10 @@ import PropTypes from 'prop-types';
 | 
			
		||||
import { components } from 'react-select';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { get } from 'lodash';
 | 
			
		||||
import { useQuery } from 'strapi-helper-plugin';
 | 
			
		||||
import { Checkbox, CheckboxWrapper, Label } from '@buffetjs/styles';
 | 
			
		||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
 | 
			
		||||
import useDataManager from '../../hooks/useDataManager';
 | 
			
		||||
import useQuery from '../../hooks/useQuery';
 | 
			
		||||
import getTrad from '../../utils/getTrad';
 | 
			
		||||
import UpperFirst from '../UpperFirst';
 | 
			
		||||
import SubUl from './SubUl';
 | 
			
		||||
 | 
			
		||||
@ -8,6 +8,7 @@ import {
 | 
			
		||||
  ModalForm,
 | 
			
		||||
  getYupInnerErrors,
 | 
			
		||||
  useGlobalContext,
 | 
			
		||||
  useQuery,
 | 
			
		||||
  InputsIndex,
 | 
			
		||||
} from 'strapi-helper-plugin';
 | 
			
		||||
import { Button } from '@buffetjs/core';
 | 
			
		||||
@ -16,7 +17,6 @@ import { useHistory, useLocation } from 'react-router-dom';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
import { get, has, isEmpty, set, toLower, toString, upperFirst } from 'lodash';
 | 
			
		||||
import pluginId from '../../pluginId';
 | 
			
		||||
import useQuery from '../../hooks/useQuery';
 | 
			
		||||
import useDataManager from '../../hooks/useDataManager';
 | 
			
		||||
import AttributeOption from '../../components/AttributeOption';
 | 
			
		||||
import BooleanBox from '../../components/BooleanBox';
 | 
			
		||||
 | 
			
		||||
@ -0,0 +1,31 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { themePropTypes } from 'strapi-helper-plugin';
 | 
			
		||||
 | 
			
		||||
const Wrapper = styled.ul`
 | 
			
		||||
  display: none;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  top: 38px;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  min-width: 230px;
 | 
			
		||||
  z-index: 1;
 | 
			
		||||
  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};
 | 
			
		||||
  ${({ isShown }) => isShown && 'display: block;'}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
Wrapper.defaultProps = {
 | 
			
		||||
  isShown: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
Wrapper.propTypes = {
 | 
			
		||||
  isShown: PropTypes.bool,
 | 
			
		||||
  ...themePropTypes,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Wrapper;
 | 
			
		||||
@ -0,0 +1,39 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
 | 
			
		||||
import Wrapper from './Wrapper';
 | 
			
		||||
import SortListItem from '../SortListItem';
 | 
			
		||||
 | 
			
		||||
const SortList = ({ isShown, list, onClick, selectedItem }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Wrapper isShown={isShown}>
 | 
			
		||||
      {Object.keys(list).map(item => {
 | 
			
		||||
        return (
 | 
			
		||||
          <SortListItem
 | 
			
		||||
            key={item}
 | 
			
		||||
            label={item}
 | 
			
		||||
            value={list[item]}
 | 
			
		||||
            onClick={onClick}
 | 
			
		||||
            selectedItem={selectedItem}
 | 
			
		||||
          />
 | 
			
		||||
        );
 | 
			
		||||
      })}
 | 
			
		||||
    </Wrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortList.defaultProps = {
 | 
			
		||||
  list: {},
 | 
			
		||||
  isShown: false,
 | 
			
		||||
  onClick: () => {},
 | 
			
		||||
  selectedItem: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortList.propTypes = {
 | 
			
		||||
  list: PropTypes.object,
 | 
			
		||||
  isShown: PropTypes.bool,
 | 
			
		||||
  onClick: PropTypes.func,
 | 
			
		||||
  selectedItem: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SortList;
 | 
			
		||||
@ -0,0 +1,29 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { themePropTypes } from 'strapi-helper-plugin';
 | 
			
		||||
 | 
			
		||||
const SortListItem = styled.li`
 | 
			
		||||
  padding: 0 14px;
 | 
			
		||||
  height: 27px;
 | 
			
		||||
  line-height: 27px;
 | 
			
		||||
  &:hover {
 | 
			
		||||
    cursor: pointer;
 | 
			
		||||
    background-color: ${({ theme }) => theme.main.colors.mediumGrey};
 | 
			
		||||
  }
 | 
			
		||||
  ${({ isActive, theme }) =>
 | 
			
		||||
    isActive &&
 | 
			
		||||
    `
 | 
			
		||||
    background-color: ${theme.main.colors.mediumGrey};
 | 
			
		||||
  `}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
SortListItem.defaultProps = {
 | 
			
		||||
  isActive: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortListItem.propTypes = {
 | 
			
		||||
  isActive: PropTypes.bool,
 | 
			
		||||
  ...themePropTypes,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SortListItem;
 | 
			
		||||
@ -0,0 +1,35 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
import getTrad from '../../utils/getTrad';
 | 
			
		||||
 | 
			
		||||
import Wrapper from './Wrapper';
 | 
			
		||||
 | 
			
		||||
const SortListItem = ({ onClick, selectedItem, label, value }) => {
 | 
			
		||||
  const handleClick = () => {
 | 
			
		||||
    onClick(value);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Wrapper isActive={selectedItem === value} onClick={handleClick}>
 | 
			
		||||
      <FormattedMessage id={getTrad(`sort.${label}`)} />
 | 
			
		||||
    </Wrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortListItem.defaultProps = {
 | 
			
		||||
  selectedItem: null,
 | 
			
		||||
  label: '',
 | 
			
		||||
  onClick: () => {},
 | 
			
		||||
  value: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortListItem.propTypes = {
 | 
			
		||||
  selectedItem: PropTypes.string,
 | 
			
		||||
  label: PropTypes.string,
 | 
			
		||||
  onClick: PropTypes.func,
 | 
			
		||||
  value: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SortListItem;
 | 
			
		||||
@ -0,0 +1,49 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { themePropTypes } from 'strapi-helper-plugin';
 | 
			
		||||
 | 
			
		||||
import Text from '../Text';
 | 
			
		||||
 | 
			
		||||
const SortButton = styled(props => (
 | 
			
		||||
  <Text
 | 
			
		||||
    as="button"
 | 
			
		||||
    fontWeight="semiBold"
 | 
			
		||||
    color={props.isActive ? 'mediumBlue' : 'greyDark'}
 | 
			
		||||
    {...props}
 | 
			
		||||
  />
 | 
			
		||||
))`
 | 
			
		||||
  height: 32px;
 | 
			
		||||
  padding: 0 10px;
 | 
			
		||||
  line-height: 30px;
 | 
			
		||||
  background-color: ${({ theme }) => theme.main.colors.white};
 | 
			
		||||
  border: 1px solid ${({ theme }) => theme.main.colors.darkGrey};
 | 
			
		||||
  border-radius: ${({ theme }) => theme.main.sizes.borderRadius};
 | 
			
		||||
  &:active,
 | 
			
		||||
  &:focus {
 | 
			
		||||
    outline: 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ${({ isActive, theme }) =>
 | 
			
		||||
    isActive
 | 
			
		||||
      ? `
 | 
			
		||||
      background-color: ${theme.main.colors.lightBlue};
 | 
			
		||||
      border: 1px solid ${theme.main.colors.darkBlue};
 | 
			
		||||
    `
 | 
			
		||||
      : `
 | 
			
		||||
      &:hover {
 | 
			
		||||
        background-color: ${theme.main.colors.lightestGrey};
 | 
			
		||||
      }
 | 
			
		||||
    `}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
SortButton.defaultProps = {
 | 
			
		||||
  isActive: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortButton.propTypes = {
 | 
			
		||||
  isActive: PropTypes.bool,
 | 
			
		||||
  ...themePropTypes,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SortButton;
 | 
			
		||||
@ -0,0 +1,7 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
 | 
			
		||||
const Wrapper = styled.div`
 | 
			
		||||
  position: relative;
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
export default Wrapper;
 | 
			
		||||
@ -1,14 +1,58 @@
 | 
			
		||||
import styled from 'styled-components';
 | 
			
		||||
import React, { useState } from 'react';
 | 
			
		||||
import PropTypes from 'prop-types';
 | 
			
		||||
import { FormattedMessage } from 'react-intl';
 | 
			
		||||
 | 
			
		||||
const SortPicker = styled.div`
 | 
			
		||||
  height: 32px;
 | 
			
		||||
  padding: 0 10px;
 | 
			
		||||
  background: #ffffff;
 | 
			
		||||
  line-height: 30px;
 | 
			
		||||
  border: 1px solid #e3e9f3;
 | 
			
		||||
  border-radius: 2px;
 | 
			
		||||
  font-size: 13px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
`;
 | 
			
		||||
import getTrad from '../../utils/getTrad';
 | 
			
		||||
 | 
			
		||||
import Wrapper from './Wrapper';
 | 
			
		||||
import SortButton from './SortButton';
 | 
			
		||||
import SortList from '../SortList';
 | 
			
		||||
 | 
			
		||||
const SortPicker = ({ onChange, value }) => {
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
 | 
			
		||||
  const orders = {
 | 
			
		||||
    created_at_asc: 'created_at:ASC',
 | 
			
		||||
    created_at_desc: 'created_at:DESC',
 | 
			
		||||
    name_asc: 'name:ASC',
 | 
			
		||||
    name_desc: 'name:DESC',
 | 
			
		||||
    updated_at_asc: 'updated_at:ASC',
 | 
			
		||||
    updated_at_desc: 'updated_at:DESC',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleChange = value => {
 | 
			
		||||
    onChange({ target: { name: '_sort', value } });
 | 
			
		||||
 | 
			
		||||
    hangleToggle();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const hangleToggle = () => {
 | 
			
		||||
    setIsOpen(v => !v);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Wrapper>
 | 
			
		||||
      <SortButton onClick={hangleToggle} isActive={isOpen}>
 | 
			
		||||
        <FormattedMessage id={getTrad('sort.label')} />
 | 
			
		||||
      </SortButton>
 | 
			
		||||
      <SortList
 | 
			
		||||
        isShown={isOpen}
 | 
			
		||||
        list={orders}
 | 
			
		||||
        selectedItem={value}
 | 
			
		||||
        onClick={handleChange}
 | 
			
		||||
      />
 | 
			
		||||
    </Wrapper>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortPicker.defaultProps = {
 | 
			
		||||
  onChange: () => {},
 | 
			
		||||
  value: null,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
SortPicker.propTypes = {
 | 
			
		||||
  onChange: PropTypes.func,
 | 
			
		||||
  value: PropTypes.string,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default SortPicker;
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,12 @@
 | 
			
		||||
import React, { useReducer, useState } from 'react';
 | 
			
		||||
import React, { useReducer, useState, useEffect } from 'react';
 | 
			
		||||
import { useHistory } from 'react-router-dom';
 | 
			
		||||
import { Header } from '@buffetjs/custom';
 | 
			
		||||
import { HeaderSearch, useGlobalContext } from 'strapi-helper-plugin';
 | 
			
		||||
import {
 | 
			
		||||
  HeaderSearch,
 | 
			
		||||
  useGlobalContext,
 | 
			
		||||
  useQuery,
 | 
			
		||||
  generateSearchFromFilters,
 | 
			
		||||
} from 'strapi-helper-plugin';
 | 
			
		||||
import getTrad from '../../utils/getTrad';
 | 
			
		||||
import Container from '../../components/Container';
 | 
			
		||||
import ControlsWrapper from '../../components/ControlsWrapper';
 | 
			
		||||
@ -17,18 +23,56 @@ import AddFilterCTA from '../../components/AddFilterCTA';
 | 
			
		||||
const HomePage = () => {
 | 
			
		||||
  const { formatMessage } = useGlobalContext();
 | 
			
		||||
  const [reducerState, dispatch] = useReducer(reducer, initialState, init);
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(true);
 | 
			
		||||
  const { data, dataToDelete, _q } = reducerState.toJS();
 | 
			
		||||
  const [isOpen, setIsOpen] = useState(false);
 | 
			
		||||
  const { push } = useHistory();
 | 
			
		||||
  const query = useQuery();
 | 
			
		||||
  const { data, dataToDelete } = reducerState.toJS();
 | 
			
		||||
  const pluginName = formatMessage({ id: getTrad('plugin.name') });
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // TODO - Retrieve data
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: 'GET_DATA_SUCCEEDED',
 | 
			
		||||
      data: [],
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const getSearchParams = () => {
 | 
			
		||||
    const params = {};
 | 
			
		||||
    query.forEach((value, key) => {
 | 
			
		||||
      params[key] = value;
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    return params;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getUpdatedSearchParams = updatedParams => {
 | 
			
		||||
    return {
 | 
			
		||||
      ...getSearchParams(),
 | 
			
		||||
      ...updatedParams,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getQueryValue = key => {
 | 
			
		||||
    const queryParams = getSearchParams();
 | 
			
		||||
 | 
			
		||||
    return queryParams[key];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleChangeParams = ({ target: { name, value } }) => {
 | 
			
		||||
    const updatedSearch = getUpdatedSearchParams({ [name]: value });
 | 
			
		||||
    const newSearch = generateSearchFromFilters(updatedSearch);
 | 
			
		||||
 | 
			
		||||
    push({ search: encodeURI(newSearch) });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleClearSearch = () => {
 | 
			
		||||
    handleChangeParams({ target: { name: '_q', value: '' } });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleClickToggleModal = () => {
 | 
			
		||||
    setIsOpen(prev => !prev);
 | 
			
		||||
  };
 | 
			
		||||
  const handleClearSearch = () => {
 | 
			
		||||
    dispatch({
 | 
			
		||||
      type: 'ON_CLEAR_SEARCH',
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const headerProps = {
 | 
			
		||||
    title: {
 | 
			
		||||
@ -65,18 +109,19 @@ const HomePage = () => {
 | 
			
		||||
      <Header {...headerProps} />
 | 
			
		||||
      <HeaderSearch
 | 
			
		||||
        label={pluginName}
 | 
			
		||||
        // TODO: search
 | 
			
		||||
        onChange={() => {}}
 | 
			
		||||
        onChange={handleChangeParams}
 | 
			
		||||
        onClear={handleClearSearch}
 | 
			
		||||
        placeholder={formatMessage({ id: getTrad('search.placeholder') })}
 | 
			
		||||
        value={_q}
 | 
			
		||||
        name="_q"
 | 
			
		||||
        value={getQueryValue('_q') || ''}
 | 
			
		||||
      />
 | 
			
		||||
 | 
			
		||||
      <ControlsWrapper>
 | 
			
		||||
        <SelectAll />
 | 
			
		||||
        <SortPicker>
 | 
			
		||||
          <span> Sort By</span>
 | 
			
		||||
        </SortPicker>
 | 
			
		||||
        <SortPicker
 | 
			
		||||
          onChange={handleChangeParams}
 | 
			
		||||
          value={getQueryValue('_sort') || null}
 | 
			
		||||
        />
 | 
			
		||||
        <AddFilterCTA />
 | 
			
		||||
      </ControlsWrapper>
 | 
			
		||||
      <ListEmpty onClick={handleClickToggleModal} />
 | 
			
		||||
 | 
			
		||||
@ -3,14 +3,12 @@ import { fromJS } from 'immutable';
 | 
			
		||||
const initialState = fromJS({
 | 
			
		||||
  data: [],
 | 
			
		||||
  dataToDelete: [],
 | 
			
		||||
  // TODO: set to empty string
 | 
			
		||||
  _q: 'super asset',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const reducer = (state, action) => {
 | 
			
		||||
  switch (action.type) {
 | 
			
		||||
    case 'ON_CLEAR_SEARCH':
 | 
			
		||||
      return state.update('_q', () => '');
 | 
			
		||||
    case 'GET_DATA_SUCCEEDED':
 | 
			
		||||
      return state.update('data', () => action.data);
 | 
			
		||||
    default:
 | 
			
		||||
      return state;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -27,5 +27,12 @@
 | 
			
		||||
  "plugin.description.long": "Media file management.",
 | 
			
		||||
  "plugin.description.short": "Media file management.",
 | 
			
		||||
  "search.placeholder": "Search for an asset...",
 | 
			
		||||
  "sort.label": "Sort by",
 | 
			
		||||
  "sort.created_at_asc": "Most recent uploads",
 | 
			
		||||
  "sort.created_at_desc": "Oldest uploads",
 | 
			
		||||
  "sort.name_asc": "Alphabetical order (A to Z)",
 | 
			
		||||
  "sort.name_desc": "Reverse alphabetical order (Z to A)",
 | 
			
		||||
  "sort.updated_at_asc": "Most recent updates",
 | 
			
		||||
  "sort.updated_at_desc": "Oldest updates",
 | 
			
		||||
  "window.confirm.close-modal": "Are you sure? You have some files that have not been uploaded yet."
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user