diff --git a/.gitignore b/.gitignore index 961e938..3a2bf37 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ lib/ .env-* .idea *.log +.history/ \ No newline at end of file diff --git a/package.json b/package.json index 1331aab..01c4e98 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@indec/react-commons", - "version": "5.4.2", + "version": "5.5.0", "description": "Common reactjs components for apps", "private": false, "main": "index.js", diff --git a/src/components/Table/SortIcon/SortIcon.js b/src/components/Table/SortIcon/SortIcon.js new file mode 100644 index 0000000..3012d54 --- /dev/null +++ b/src/components/Table/SortIcon/SortIcon.js @@ -0,0 +1,30 @@ +import React from 'react'; +import {sortDirections} from '@/constants'; +import {Icon} from '@chakra-ui/react'; +import {FaChevronDown, FaChevronUp} from 'react-icons/fa'; +import PropTypes from 'prop-types'; + +const SortIcon = ({classified, columnKey, ...props}) => classified && classified?.sort === columnKey && ( + +); + +SortIcon.propTypes = { + classified: PropTypes.shape({ + sortBy: PropTypes.oneOf([sortDirections.ASC, sortDirections.DESC]), + sort: PropTypes.string + }), + columnKey: PropTypes.string +}; + +SortIcon.defaultProps = { + classified: undefined, + columnKey: undefined +}; + +export default SortIcon; diff --git a/src/components/Table/SortIcon/index.js b/src/components/Table/SortIcon/index.js new file mode 100644 index 0000000..09521a4 --- /dev/null +++ b/src/components/Table/SortIcon/index.js @@ -0,0 +1,3 @@ +import SortIcon from './SortIcon'; + +export default SortIcon; diff --git a/src/components/Table/Table.js b/src/components/Table/Table.js index 5645bcd..ca756e8 100644 --- a/src/components/Table/Table.js +++ b/src/components/Table/Table.js @@ -1,4 +1,4 @@ -import React, {isValidElement} from 'react'; +import React, {useState, isValidElement} from 'react'; import PropTypes from 'prop-types'; import { Flex, @@ -9,32 +9,55 @@ import { Thead, Tr, VStack, - Table as ChakraTable + Table as ChakraTable, + HStack, + Text } from '@chakra-ui/react'; +import {sortDirections} from '@/constants'; import {LoadingPage, Pagination} from '@/components'; import {buildRows} from '@/utils'; import TableFooter from '@/components/Table/TableFooter'; +import SortIcon from '@/components/Table/SortIcon'; const Table = ({ - name, + caption, columns, data, - caption, - isLoading, emptyMessage, - total, - showDefaultFooter, - perPage, + footer: Footer, + isLoading, + name, onSearch, + onSort, + paginationStyles, params, - footer: Footer, + perPage, + showDefaultFooter, showPagination, - paginationStyles, + total, ...props }) => { const columnsData = Array.isArray(columns) ? columns : []; const sizeHeader = columnsData.length; + const initialClassified = {sort: null, sortBy: null}; + const [classified, setClassified] = useState(initialClassified); + + const handleSort = ({target: {id}}) => { + if (!id) { + return; + } + let sort; + if (id === 'action') { + sort = initialClassified; + } else if (classified?.sort === id && classified?.sortBy === sortDirections.ASC) { + sort = {sort: id, sortBy: sortDirections.DESC}; + } else { + sort = {sort: id, sortBy: sortDirections.ASC}; + } + setClassified(sort); + onSort(sort); + }; return ( @@ -56,10 +79,20 @@ const Table = ({ - {column.label} + + {data.length > 0 && } + + {column.label || ''} + + ))} @@ -107,38 +140,38 @@ const Table = ({ }; Table.propTypes = { - onSearch: PropTypes.func, + caption: PropTypes.string, columns: PropTypes.arrayOf(PropTypes.shape({})).isRequired, - params: PropTypes.shape({ - skip: PropTypes.number - }), - name: PropTypes.string, data: PropTypes.arrayOf(PropTypes.shape({})), - caption: PropTypes.string, - isLoading: PropTypes.bool, - showDefaultFooter: PropTypes.bool, emptyMessage: PropTypes.string, - total: PropTypes.number, footer: PropTypes.element, + isLoading: PropTypes.bool, + name: PropTypes.string, + onSearch: PropTypes.func, + onSort: PropTypes.func, + paginationStyles: PropTypes.shape({}), + params: PropTypes.shape({skip: PropTypes.number}), perPage: PropTypes.number, + showDefaultFooter: PropTypes.bool, showPagination: PropTypes.bool, - paginationStyles: PropTypes.shape({}) + total: PropTypes.number }; Table.defaultProps = { - name: 'table', - onSearch: () => {}, caption: null, + data: [], + footer: undefined, + emptyMessage: 'No hay resultados', isLoading: false, - params: undefined, + name: 'table', + onSearch: () => {}, + onSort: undefined, paginationStyles: undefined, + params: undefined, + perPage: 0, showDefaultFooter: true, showPagination: true, - data: [], - total: 0, - perPage: 0, - footer: undefined, - emptyMessage: 'No hay resultados' + total: 0 }; export default Table; diff --git a/src/components/Table/Table.stories.js b/src/components/Table/Table.stories.js index fc3de26..6858e94 100644 --- a/src/components/Table/Table.stories.js +++ b/src/components/Table/Table.stories.js @@ -31,28 +31,42 @@ const getRows = () => users.map(user => { user.documentId, user.role, user.state, - user.deleted + user.status ]; return ({key: user.id, values: rows}); }); export const Basic = args => { - const spliceRows = getRows().slice(0, 5); + const rows = getRows(); const [prevArgs, updateArgs] = useArgs(); const handleSearch = ({target: {id, value}}) => updateArgs( {...prevArgs, params: {...prevArgs.params, [id]: value}} ); + const handleSort = ({sort, sortBy}) => { + const orderedData = rows.sort((firstRow, secondRow) => { + const selectedColumn = columns.findIndex(column => column.key === sort); + if (firstRow.values[selectedColumn] < secondRow.values[selectedColumn]) { + return sortBy === 'asc' ? -1 : 1; + } + if (firstRow.values[selectedColumn] > secondRow.values[selectedColumn]) { + return sortBy === 'asc' ? 1 : -1; + } + return 0; + }); + updateArgs({...prevArgs, data: orderedData}); + }; return ( ', () => { const {container} = getComponent(); expect(queryByText(container, 'No hay resultados')).toBeNull(); }); + + describe('when one column is clicked and is not an action', () => { + beforeEach(() => { + props.onSort = jest.fn(); + const {container} = getComponent(); + const firstColumn = getByTestId(container, 'column-text-name'); + fireEvent.click(firstColumn); + }); + + it('should fire `props.onSort` in asc order', () => { + expect(props.onSort).toHaveBeenCalledTimes(1); + expect(props.onSort).toHaveBeenCalledWith({sort: 'name', sortBy: 'asc'}); + }); + + describe('when the same column is clicked again', () => { + beforeEach(() => { + const {container} = getComponent(); + const firstColumn = getByTestId(container, 'column-text-name'); + fireEvent.click(firstColumn); + }); + + it('should fire `props.onSort` in desc order', () => { + expect(props.onSort).toHaveBeenCalledWith({sort: 'name', sortBy: 'desc'}); + }); + }); + }); + + describe('when one column is clicked and is an action', () => { + beforeEach(() => { + props.onSort = jest.fn(); + const {container} = getComponent(); + const secondColumn = getByTestId(container, 'column-text-action'); + fireEvent.click(secondColumn); + }); + + it('should fire `props.onSort` with sort and sortBy in null', () => { + expect(props.onSort).toHaveBeenCalledTimes(1); + expect(props.onSort).toHaveBeenCalledWith({sort: null, sortBy: null}); + }); + }); }); describe('when `props.caption` is defined', () => { diff --git a/src/constants/index.js b/src/constants/index.js index a2aec92..5fab4e9 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -1,3 +1,4 @@ export {default as headerOptions} from './headerOptions'; export {default as selectActions} from './selectActions'; +export {default as sortDirections} from './sortDirections'; export {default as users} from './users'; diff --git a/src/constants/sortDirections.js b/src/constants/sortDirections.js new file mode 100644 index 0000000..78f1a07 --- /dev/null +++ b/src/constants/sortDirections.js @@ -0,0 +1,4 @@ +export default { + ASC: 'asc', + DESC: 'desc' +};