diff --git a/__tests__/demo/demo-components/index.js b/__tests__/demo/demo-components/index.js index f8ade78a..de42d905 100644 --- a/__tests__/demo/demo-components/index.js +++ b/__tests__/demo/demo-components/index.js @@ -987,6 +987,20 @@ export function DefaultOrderIssue(props) { birthYear: 2017, birthCity: 34, id: 1 + }, + { + name: 'Mehmet', + surname: 'Terot', + birthYear: 1997, + birthCity: 63, + id: 3 + }, + { + name: 'Mehmet', + surname: 'Terot', + birthYear: 2000, + birthCity: 34, + id: 4 } ]} options={{ @@ -1253,3 +1267,93 @@ export function FixedColumnWithEdit() { /> ); } + +export function TableMultiSorting(props) { + const global_cols = [ + { + title: 'Number', + field: 'number', + minWidth: 140, + maxWidth: 400 + }, + { + title: 'Title', + field: 'title', + minWidth: 140, + maxWidth: 400, + sorting: true, + defaultSort: 'desc' + }, + { + title: 'Name', + field: 'name', + minWidth: 140, + maxWidth: 400, + sorting: true, + defaultSort: 'desc' + }, + { + title: 'Last Name', + field: 'lastName', + minWidth: 140, + maxWidth: 400, + sorting: true, + defaultSort: 'asc' + } + ]; + + const global_data1 = [ + { + number: 1, + title: 'Developer', + name: 'Mehmet', + lastName: 'Baran', + id: '1231' + }, + { + number: 22, + title: 'Developer', + name: 'Pratik', + lastName: 'N', + id: '1234' + }, + { + number: 25, + title: 'Human Resources', + name: 'Juan', + lastName: 'Lopez', + id: '1235' + }, + { + number: 3, + title: 'Consultant', + name: 'Raul', + lastName: 'Barak', + id: '1236' + } + ]; + + const orderCollection = [ + { orderBy: 1, orderDirection: 'asc', sortOrder: 1 }, + { orderBy: 2, orderDirection: 'desc', sortOrder: 2 } + ]; + + const onOrderCollectionChange = (orderByCollection) => { + console.log('onOrderCollectionChange ===>', orderByCollection); + }; + + return ( + + ); +} diff --git a/__tests__/demo/demo.js b/__tests__/demo/demo.js index 08b408c6..e8157461 100644 --- a/__tests__/demo/demo.js +++ b/__tests__/demo/demo.js @@ -45,7 +45,8 @@ import { TreeData, TableWithSummary, TableWithNumberOfPagesAround, - FixedColumnWithEdit + FixedColumnWithEdit, + TableMultiSorting } from './demo-components'; import { I1353, I1941, I122 } from './demo-components/RemoteData'; import { Table, TableCell, TableRow, Paper } from '@material-ui/core'; @@ -60,6 +61,9 @@ render(

DetailPanelRemounting

+

Multi Sorting

+ +

Switcher

diff --git a/__tests__/multiColumnSort.test.js b/__tests__/multiColumnSort.test.js new file mode 100644 index 00000000..fbb3745e --- /dev/null +++ b/__tests__/multiColumnSort.test.js @@ -0,0 +1,193 @@ +import '@testing-library/jest-dom'; +import { fireEvent, render } from '@testing-library/react'; +import * as React from 'react'; +import MaterialTable from '../src'; + +const columns = [ + { + title: 'Number', + field: 'number', + minWidth: 140, + maxWidth: 400 + }, + { + title: 'Title', + field: 'title', + minWidth: 140, + maxWidth: 400, + sorting: true + }, + { + title: 'Name', + field: 'name', + minWidth: 140, + maxWidth: 400, + sorting: true + }, + { + title: 'Last Name', + field: 'lastName', + minWidth: 140, + maxWidth: 400, + sorting: true + } +]; + +const data = [ + { + number: 1, + title: 'Developer', + name: 'Mehmet', + lastName: 'Baran', + id: '1231' + }, + { + number: 22, + title: 'Developer', + name: 'Pratik', + lastName: 'N', + id: '1234' + }, + { + number: 25, + title: 'Human Resources', + name: 'Juan', + lastName: 'Lopez', + id: '1235' + }, + { + number: 3, + title: 'Consultant', + name: 'Raul', + lastName: 'Barak', + id: '1236' + } +]; + +describe('Multi Column Sort', () => { + let initialOrderCollection = []; + let onOrderCollectionChangeSpy; + + beforeEach(() => { + jest.clearAllMocks(); + onOrderCollectionChangeSpy = jest.fn(); + initialOrderCollection = [ + { + orderBy: 1, + orderDirection: 'asc', + sortOrder: 1 + }, + { + orderBy: 2, + orderDirection: 'desc', + sortOrder: 2 + } + ]; + }); + + test('should update table by multi column', () => { + const { queryAllByTestId } = render( + + ); + + const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0]; + fireEvent.click(numberColumn); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 0, orderDirection: 'asc' } + ]); + + const titleColumn = queryAllByTestId('mtableheader-sortlabel')[1]; + fireEvent.click(titleColumn); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 0, orderDirection: 'asc' }, + { sortOrder: 2, orderBy: 1, orderDirection: 'asc' } + ]); + }); + + test('should update table by multi column and replace first if reach the maximum order columns', () => { + const { queryAllByTestId } = render( + + ); + + const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0]; + fireEvent.click(numberColumn); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 0, orderDirection: 'asc' } + ]); + + fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[1]); + fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[2]); + fireEvent.click(queryAllByTestId('mtableheader-sortlabel')[3]); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 1, orderDirection: 'asc' }, + { sortOrder: 2, orderBy: 2, orderDirection: 'asc' }, + { sortOrder: 3, orderBy: 3, orderDirection: 'asc' } + ]); + }); + + test('should order desc when secon click', () => { + const { queryAllByTestId } = render( + + ); + + const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0]; + fireEvent.click(numberColumn); + fireEvent.click(numberColumn); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 0, orderDirection: 'desc' } + ]); + }); + + test('should have being initialized by defaultOrderByCollection', () => { + const { queryAllByTestId } = render( + + ); + + const numberColumn = queryAllByTestId('mtableheader-sortlabel')[0]; + fireEvent.click(numberColumn); + + expect(onOrderCollectionChangeSpy).toHaveBeenCalledWith([ + { sortOrder: 1, orderBy: 1, orderDirection: 'asc' }, + { sortOrder: 2, orderBy: 2, orderDirection: 'desc' }, + { sortOrder: 3, orderBy: 0, orderDirection: 'asc' } + ]); + }); +}); diff --git a/__tests__/pre.build.test.js b/__tests__/pre.build.test.js index 78da0e2b..3c625374 100644 --- a/__tests__/pre.build.test.js +++ b/__tests__/pre.build.test.js @@ -39,6 +39,7 @@ describe('Render Table : Pre Build', () => { expect(screen.getAllByRole('table')).toHaveLength(2); }); }); + // Render table with data describe('when attempting to render a table with data', () => { it('renders without crashing', () => { @@ -78,6 +79,7 @@ describe('Render Table : Pre Build', () => { name: /5 rows First Page Previous Page 1-5 of 99 Next Page Last Page/i }); }); + it('navigates between the pages', () => { const data = makeData(); render(); @@ -125,6 +127,7 @@ describe('Render Table : Pre Build', () => { }); expect(screen.getAllByRole('row')).toHaveLength(8); }); + it('filters data by search input', async () => { const data = makeData(); render(); diff --git a/package-lock.json b/package-lock.json index da1314ae..8abc6046 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1224,9 +1224,9 @@ } }, "@discoveryjs/json-ext": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz", - "integrity": "sha512-ws57AidsDvREKrZKYffXddNkyaF14iHNHm8VQnZH6t99E8gczjNN0GpvcGny0imC80yQ0tHz1xVUKk/KFQSUyA==", + "version": "0.5.7", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true }, "@emotion/hash": { @@ -2913,15 +2913,15 @@ } }, "@webpack-cli/configtest": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-1.1.1.tgz", - "integrity": "sha512-1FBc1f9G4P/AxMqIgfZgeOTuRnwZMten8E7zap5zgpPInnCrP8D4Q81+4CWIch8i/Nf7nXjP0v6CjjbHOrXhKg==", + "version": "1.2.0", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/@webpack-cli/configtest/-/configtest-1.2.0.tgz", + "integrity": "sha512-4FB8Tj6xyVkyqjj1OaTqCjXYULB9FMkqQ8yGrZjRDrYh0nOE+7Lhs45WioWQQMV+ceFlE368Ukhe6xdvJM9Egg==", "dev": true }, "@webpack-cli/info": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-1.4.1.tgz", - "integrity": "sha512-PKVGmazEq3oAo46Q63tpMr4HipI3OPfP7LiNOEJg963RMgT0rqheag28NCML0o3GIzA3DmxP1ZIAv9oTX1CUIA==", + "version": "1.5.0", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/@webpack-cli/info/-/info-1.5.0.tgz", + "integrity": "sha512-e8tSXZpw2hPl2uMJY6fsMswaok5FdlGNRTktvFk2sD8RjH0hE2+XistawJx1vmKteh4NmGmNUrp+Tb2w+udPcQ==", "dev": true, "requires": { "envinfo": "^7.7.3" @@ -4288,7 +4288,7 @@ }, "clone-deep": { "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/clone-deep/-/clone-deep-4.0.1.tgz", "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", "dev": true, "requires": { @@ -4340,9 +4340,9 @@ "dev": true }, "colorette": { - "version": "2.0.16", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.16.tgz", - "integrity": "sha512-hUewv7oMjCp+wkBv5Rm0v87eJhq4woh5rSR+42YSQJKecCqgIqNkZ6lAlQms/BwHPJA5NKMRlpxPRv0n8HQW6g==", + "version": "2.0.19", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/colorette/-/colorette-2.0.19.tgz", + "integrity": "sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ==", "dev": true }, "combined-stream": { @@ -5095,7 +5095,7 @@ }, "envinfo": { "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/envinfo/-/envinfo-7.8.1.tgz", "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", "dev": true }, @@ -6157,9 +6157,9 @@ "dev": true }, "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "version": "1.0.16", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", "dev": true }, "fastq": { @@ -6895,12 +6895,6 @@ } } }, - "human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true - }, "husky": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/husky/-/husky-1.2.0.tgz", @@ -7103,7 +7097,7 @@ }, "interpret": { "version": "2.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-2.2.0.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/interpret/-/interpret-2.2.0.tgz", "integrity": "sha512-Ju0Bz/cEia55xDwUWEa8+olFpCiQoypjnQySseKtmjNrnps3P+xfpUmGr90T7yjlVJmOtybRvPXhKMbHr+fWnw==", "dev": true }, @@ -10451,7 +10445,7 @@ }, "rechoir": { "version": "0.7.1", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/rechoir/-/rechoir-0.7.1.tgz", "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", "dev": true, "requires": { @@ -11029,7 +11023,7 @@ }, "shallow-clone": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/shallow-clone/-/shallow-clone-3.0.1.tgz", "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, "requires": { @@ -12546,18 +12540,18 @@ } }, "webpack-cli": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-4.9.2.tgz", - "integrity": "sha512-m3/AACnBBzK/kMTcxWHcZFPrw/eQuY4Df1TxvIWfWM2x7mRqBQCqKEd96oCUa9jkapLBaFfRce33eGDb4Pr7YQ==", + "version": "4.10.0", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/webpack-cli/-/webpack-cli-4.10.0.tgz", + "integrity": "sha512-NLhDfH/h4O6UOy+0LSso42xvYypClINuMNBVVzX4vX98TmTaTUxwRbXdhucbFMd2qLaCTcLq/PdYrvi8onw90w==", "dev": true, "requires": { "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.1.1", - "@webpack-cli/info": "^1.4.1", - "@webpack-cli/serve": "^1.6.1", + "@webpack-cli/configtest": "^1.2.0", + "@webpack-cli/info": "^1.5.0", + "@webpack-cli/serve": "^1.7.0", "colorette": "^2.0.14", "commander": "^7.0.0", - "execa": "^5.0.0", + "cross-spawn": "^7.0.3", "fastest-levenshtein": "^1.0.12", "import-local": "^3.0.2", "interpret": "^2.2.0", @@ -12565,49 +12559,17 @@ "webpack-merge": "^5.7.3" }, "dependencies": { + "@webpack-cli/serve": { + "version": "1.7.0", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/@webpack-cli/serve/-/serve-1.7.0.tgz", + "integrity": "sha512-oxnCNGj88fL+xzV+dacXs44HcDwf1ovs3AuEzvP7mqXw7fQntqIhQ1BRmynh4qEKQSSSRSWVyXRjmTbZIX9V2Q==", + "dev": true + }, "commander": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true - }, - "execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } } } }, @@ -12859,7 +12821,7 @@ }, "webpack-merge": { "version": "5.8.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.8.0.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/webpack-merge/-/webpack-merge-5.8.0.tgz", "integrity": "sha512-/SaI7xY0831XwP6kzuwhKWVKDP9t1QY1h65lAFLbZqMPIuYcD9QAW4u9STIbU9kaJbPBB/geU/gLr1wDjOhQ+Q==", "dev": true, "requires": { @@ -12943,7 +12905,7 @@ }, "wildcard": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.0.tgz", + "resolved": "https://jdasoftware.jfrog.io/jdasoftware/api/npm/npm/wildcard/-/wildcard-2.0.0.tgz", "integrity": "sha512-JcKqAHLPxcdb9KM49dufGXn2x3ssnfjbcaQdLlfZsL9rH9wgDQjUtDxbo8NE0F6SFvydeu1VhZe7hZuHsB2/pw==", "dev": true }, diff --git a/package.json b/package.json index 6c49ba35..5e806d0b 100644 --- a/package.json +++ b/package.json @@ -102,7 +102,7 @@ "typescript": "^4.1.3", "webpack": "^5.11.0", "webpack-bundle-analyzer": "^4.3.0", - "webpack-cli": "^4.9.2", + "webpack-cli": "^4.10.0", "webpack-dev-server": "^3.11.0" }, "dependencies": { diff --git a/src/components/MTableHeader/index.js b/src/components/MTableHeader/index.js index 437db7fb..0c21f1ea 100644 --- a/src/components/MTableHeader/index.js +++ b/src/components/MTableHeader/index.js @@ -236,17 +236,20 @@ export function MTableHeader({ onColumnResized, columns, ...props }) { } } > - {columnDef.sorting !== false && options.sorting ? ( + {columnDef.sorting !== false && + options.sorting && + props.allowSorting ? ( {columnDef.title} @@ -257,18 +260,23 @@ export function MTableHeader({ onColumnResized, columns, ...props }) { )} ); - } else if (columnDef.sorting !== false && options.sorting) { + } else if ( + columnDef.sorting !== false && + options.sorting && + !props.allowSorting + ) { content = ( {columnDef.title} @@ -460,60 +468,81 @@ const computeNewOrderDirection = ( function RenderSortButton({ columnDef, - orderBy, keepSortDirectionOnColumnSwitch, - orderDirection, icon, thirdSortClick, onOrderChange, - children + children, + orderByCollection, + showColumnSortOrder, + sortOrderIndicatorStyle }) { - const active = orderBy === columnDef.tableData.id; + const activeColumn = orderByCollection.find( + ({ orderBy }) => orderBy === columnDef.tableData.id + ); + // If current sorted column or prop asked to // maintain sort order when switching sorted column, // follow computed order direction if defined // else default direction is asc const direction = - active || keepSortDirectionOnColumnSwitch ? orderDirection || 'asc' : 'asc'; - let ariaSort = 'none'; + activeColumn || keepSortDirectionOnColumnSwitch + ? (activeColumn && activeColumn.orderDirection) || 'asc' + : 'asc'; - if (active && direction === 'asc') { + let ariaSort = 'none'; + if (activeColumn && direction === 'asc') { ariaSort = columnDef.ariaSortAsc ? columnDef.ariaSortAsc : 'Ascendant'; - } - if (active && direction === 'desc') { + } else if (activeColumn && direction === 'desc') { ariaSort = columnDef.ariaSortDesc ? columnDef.ariaSortDesc : 'Descendant'; } + const orderBy = activeColumn && activeColumn.orderBy; + return ( - { - const newOrderDirection = computeNewOrderDirection( - orderBy, - orderDirection, - columnDef, - thirdSortClick, - keepSortDirectionOnColumnSwitch - ); - onOrderChange(columnDef.tableData.id, newOrderDirection); - }} - > - {children} - + <> + { + const newOrderDirection = computeNewOrderDirection( + orderBy, + direction, + columnDef, + thirdSortClick, + keepSortDirectionOnColumnSwitch + ); + onOrderChange( + columnDef.tableData.id, + newOrderDirection, + activeColumn && activeColumn.sortOrder + ); + }} + > + {children} + + {showColumnSortOrder && activeColumn && ( + + {activeColumn.sortOrder} + + )} + ); } MTableHeader.defaultProps = { dataCount: 0, selectedCount: 0, - orderBy: undefined, - orderDirection: 'asc' + orderByCollection: [], + allowSorting: true }; MTableHeader.propTypes = { @@ -523,10 +552,11 @@ MTableHeader.propTypes = { selectedCount: PropTypes.number, onAllSelected: PropTypes.func, onOrderChange: PropTypes.func, - orderBy: PropTypes.number, - orderDirection: PropTypes.string, showActionsColumn: PropTypes.bool, - tooltip: PropTypes.string + orderByCollection: PropTypes.array, + showColumnSortOrder: PropTypes.bool, + tooltip: PropTypes.string, + allowSorting: PropTypes.bool }; export const styles = (theme) => ({ diff --git a/src/defaults/props.options.js b/src/defaults/props.options.js index 15ea08e8..7dc3e919 100644 --- a/src/defaults/props.options.js +++ b/src/defaults/props.options.js @@ -43,6 +43,9 @@ export default { selection: false, selectionProps: {}, sorting: true, + maxColumnSort: 1, + defaultOrderByCollection: [], + showColumnSortOrder: false, keepSortDirectionOnColumnSwitch: true, toolbar: true, defaultExpanded: false, diff --git a/src/index.js b/src/index.js index cb82f92c..f8698261 100644 --- a/src/index.js +++ b/src/index.js @@ -56,3 +56,5 @@ export { MTableSteppedPagination, MTableToolbar } from './components'; + +export { ALL_COLUMNS } from './utils/constants'; diff --git a/src/material-table.js b/src/material-table.js index f73bb793..37ab5523 100644 --- a/src/material-table.js +++ b/src/material-table.js @@ -19,6 +19,7 @@ import { export default class MaterialTable extends React.Component { dataManager = new DataManager(); checkedForFunctions = false; + constructor(props) { super(props); @@ -42,10 +43,10 @@ export default class MaterialTable extends React.Component { (a) => a.tableData.id === renderState.orderBy ), orderDirection: renderState.orderDirection, + orderByCollection: renderState.orderByCollection, page: 0, pageSize: calculatedProps.options.pageSize, search: renderState.searchText, - totalCount: 0 }, showAddRow: false, @@ -72,6 +73,7 @@ export default class MaterialTable extends React.Component { page: this.props.options.initialPage || 0 }); } + /** * THIS WILL NEED TO BE REMOVED EVENTUALLY. * Warn consumer of renamed prop. @@ -81,6 +83,16 @@ export default class MaterialTable extends React.Component { 'Property `onDoubleRowClick` has been renamed to `onRowDoubleClick`' ); } + + /** + * THIS WILL NEED TO BE REMOVED EVENTUALLY. + * Warn consumer of deprecated prop. + */ + if (this.props.sorting !== undefined) { + console.error( + 'Property `sorting` has been deprecated, please start using `maxColumnSort` instead' + ); + } } ); } @@ -113,6 +125,10 @@ export default class MaterialTable extends React.Component { this.dataManager.setDefaultExpanded(props.options.defaultExpanded); this.dataManager.changeRowEditing(); + const { grouping, maxColumnSort } = props.options; + this.dataManager.setMaxColumnSort(grouping ? 1 : maxColumnSort); + this.dataManager.setOrderByCollection(); + if (this.isRemoteData(props)) { this.dataManager.changeApplySearch(false); this.dataManager.changeApplyFilters(false); @@ -124,47 +140,49 @@ export default class MaterialTable extends React.Component { this.dataManager.setData(props.data, props.options.idSynonym); } - let defaultSortColumnIndex = -1; - let defaultSortDirection = ''; - let prevSortColumnIndex = -1; - let prevSortDirection = ''; - if (props && props.options.sorting !== false) { - defaultSortColumnIndex = props.columns.findIndex( - (a) => a.defaultSort && a.sorting !== false + const { defaultOrderByCollection } = props.options; + let defaultCollectionSort = []; + let prevCollectionSort = []; + + if (defaultOrderByCollection && defaultOrderByCollection.length) { + defaultCollectionSort = [...defaultOrderByCollection].slice( + 0, + maxColumnSort ); - defaultSortDirection = - defaultSortColumnIndex > -1 - ? props.columns[defaultSortColumnIndex].defaultSort - : ''; - } - if (prevColumns) { - prevSortColumnIndex = prevColumns.findIndex( - (a) => a.defaultSort && a.sorting !== false + } else { + const defaultSorts = getDefaultCollectionSort( + props.columns, + prevColumns, + this.dataManager.maxColumnSort ); - prevSortDirection = - prevSortColumnIndex > -1 && props.columns[prevSortColumnIndex] - ? props.columns[prevSortColumnIndex].defaultSort - : ''; + defaultCollectionSort = [...defaultSorts[0]].slice(0, maxColumnSort); + prevCollectionSort = [...defaultSorts[1]]; } + const defaultSort = JSON.stringify(defaultCollectionSort); + const prevSort = JSON.stringify(prevCollectionSort); + const currentSort = JSON.stringify(this.dataManager.orderByCollection); // If the default sorting changed and differs from the current default sorting, it will trigger a new sorting const shouldReorder = isInit || (!this.isRemoteData() && // Only if a defaultSortingDirection is passed, it will evaluate for changes - defaultSortDirection && + defaultCollectionSort.length && // Default sorting has changed - (defaultSortColumnIndex !== prevSortColumnIndex || - defaultSortDirection !== prevSortDirection) && + defaultSort !== prevSort && // Default sorting differs from current sorting - (defaultSortColumnIndex !== this.dataManager.orderBy || - defaultSortDirection !== this.dataManager.orderDirection)); + defaultSort !== currentSort); - shouldReorder && - this.dataManager.changeOrder( - defaultSortColumnIndex, - defaultSortDirection + if ( + shouldReorder && + defaultCollectionSort.length > 0 && + maxColumnSort > 0 + ) { + defaultCollectionSort.forEach(({ orderBy, orderDirection, sortOrder }) => + this.dataManager.changeColumnOrder(orderBy, orderDirection, sortOrder) ); + } + isInit && this.dataManager.changeSearchText(props.options.searchText || ''); isInit && this.dataManager.changeSearchDebounce(props.options.searchDebounceDelay); @@ -441,25 +459,36 @@ export default class MaterialTable extends React.Component { this.setState(this.dataManager.getRenderState()); }; - onChangeOrder = (orderBy, orderDirection) => { - const newOrderBy = orderDirection === '' ? -1 : orderBy; - this.dataManager.changeOrder(newOrderBy, orderDirection); + onChangeOrder = (orderBy, orderDirection, sortOrder) => { + this.dataManager.changeColumnOrder(orderBy, orderDirection, sortOrder); if (this.isRemoteData()) { const query = { ...this.state.query }; query.page = 0; query.orderBy = this.state.columns.find( - (a) => a.tableData.id === newOrderBy + (a) => a.tableData.id === orderBy ); query.orderDirection = orderDirection; + console.warn( + 'Properties orderBy and orderDirection had been deprecated when remote data, please start using orderByCollection instead' + ); + query.orderByCollection = this.dataManager.getOrderByCollection(); this.onQueryChange(query, () => { this.props.onOrderChange && - this.props.onOrderChange(newOrderBy, orderDirection); + this.props.onOrderChange(orderBy, orderDirection); + this.props.onOrderCollectionChange && + this.props.onOrderCollectionChange( + this.dataManager.getOrderByCollection() + ); }); } else { this.setState(this.dataManager.getRenderState(), () => { this.props.onOrderChange && - this.props.onOrderChange(newOrderBy, orderDirection); + this.props.onOrderChange(orderBy, orderDirection); + this.props.onOrderCollectionChange && + this.props.onOrderCollectionChange( + this.dataManager.getOrderByCollection() + ); }); } }; @@ -949,6 +978,7 @@ export default class MaterialTable extends React.Component { ); } } + renderTable = (props) => ( a.position === 'row' || typeof a === 'function' ) } - orderBy={this.state.orderBy} - orderDirection={this.state.orderDirection} onAllSelected={this.onAllSelected} onOrderChange={this.onChangeOrder} + orderByCollection={this.dataManager.getOrderByCollection()} isTreeData={this.props.parentChildData !== undefined} treeDataMaxLevel={this.state.treeDataMaxLevel} onColumnResized={this.onColumnResized} scrollWidth={this.state.width} + allowSorting={this.dataManager.maxColumnSort !== 0} /> )} this.props.options.exportAll ? this.state.data : this.state.renderData; @@ -1283,3 +1314,34 @@ function functionlessColumns(columns) { }, {}) ); } + +function getDefaultCollectionSort(currentColumns, prevColumns, maxColumnSort) { + let defaultCollectionSort = []; + let prevCollectionSort = []; + + if (maxColumnSort > 0) { + defaultCollectionSort = reduceByDefaultSort(currentColumns); + } + + if (prevColumns) { + prevCollectionSort = reduceByDefaultSort(prevColumns); + } + + return [ + defaultCollectionSort.slice(0, maxColumnSort), + prevCollectionSort.slice(0, maxColumnSort) + ]; +} + +function reduceByDefaultSort(list) { + return list.reduce((acc, column, index) => { + if (column.defaultSort && column.sorting !== false) { + acc.push({ + orderBy: index, + orderDirection: column.defaultSort, + sortOrder: index + }); + } + return acc; + }, []); +} diff --git a/src/prop-types.js b/src/prop-types.js index 188e32c5..69f2efe7 100644 --- a/src/prop-types.js +++ b/src/prop-types.js @@ -1,4 +1,5 @@ import PropTypes from 'prop-types'; +import { ALL_COLUMNS } from './utils/constants'; const RefComponent = PropTypes.shape({ current: PropTypes.element }); const StyledComponent = PropTypes.shape({ @@ -371,7 +372,30 @@ export const propTypes = { showSelectGroupCheckbox: PropTypes.bool, showTitle: PropTypes.bool, showTextRowsSelected: PropTypes.bool, - sorting: PropTypes.bool, + sorting: PropTypes.bool, // TODO: This will be removed eventually + defaultOrderByCollection: PropTypes.arrayOf( + PropTypes.shape({ + orderBy: PropTypes.number, + oderDirection: PropTypes.string, + orderIndex: PropTypes.number + }) + ), + maxColumnSort: PropTypes.oneOf([ + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + 9, + 10, + ALL_COLUMNS + ]), + showColumnSortOrder: PropTypes.bool, + sortOrderIndicatorStyle: PropTypes.object, keepSortDirectionOnColumnSwitch: PropTypes.bool, toolbar: PropTypes.bool, thirdSortClick: PropTypes.bool, @@ -398,6 +422,7 @@ export const propTypes = { onPageChange: PropTypes.func, onChangeColumnHidden: PropTypes.func, onOrderChange: PropTypes.func, + onOrderCollectionChange: PropTypes.func, onRowClick: PropTypes.func, onRowDoubleClick: PropTypes.func, onTreeExpandChange: PropTypes.func, diff --git a/src/utils/constants.js b/src/utils/constants.js new file mode 100644 index 00000000..84660d66 --- /dev/null +++ b/src/utils/constants.js @@ -0,0 +1 @@ +export const ALL_COLUMNS = 'all_columns'; diff --git a/src/utils/data-manager.js b/src/utils/data-manager.js index 1d26eaa3..27cdbdfb 100644 --- a/src/utils/data-manager.js +++ b/src/utils/data-manager.js @@ -2,6 +2,7 @@ import formatDate from 'date-fns/format'; import uuid from 'uuid'; import { selectFromObject } from './'; import { widthToNumber } from './common-values'; +import { ALL_COLUMNS } from './constants'; export default class DataManager { checkForId = false; @@ -12,8 +13,8 @@ export default class DataManager { detailPanelType = 'multiple'; lastDetailPanelRow = undefined; lastEditingRow = undefined; - orderBy = -1; - orderDirection = 'desc'; + maxColumnSort = 1; + orderByCollection = []; pageSize = 5; paging = true; parentFunc = null; @@ -183,6 +184,26 @@ export default class DataManager { this.defaultExpanded = expanded; } + setMaxColumnSort(maxColumnSort) { + const availableColumnsLength = this.columns.filter( + (column) => column.sorting !== false + ).length; + + if (maxColumnSort === ALL_COLUMNS) { + this.maxColumnSort = availableColumnsLength; + } else { + this.maxColumnSort = Math.min(maxColumnSort, availableColumnsLength); + } + } + + setOrderByCollection() { + this.orderByCollection = this.columns.map((columnDef) => ({ + orderBy: columnDef.tableData.id, + sortOrder: undefined, + orderDirection: '' + })); + } + changeApplySearch(applySearch) { this.applySearch = applySearch; this.searched = false; @@ -369,11 +390,55 @@ export default class DataManager { setCheck([currentGroup]); }; - changeOrder(orderBy, orderDirection) { - this.orderBy = orderBy; - this.orderDirection = orderDirection; - this.currentPage = 0; + getOrderByCollection = () => { + return this.orderByCollection.filter((collection) => collection.sortOrder); + }; + + sortOrderCollection = (list) => { + return list.sort((a, b) => { + if (!a.sortOrder) return 1; + if (!b.sortOrder) return -1; + return a.sortOrder - b.sortOrder; + }); + }; + + changeColumnOrder(orderBy, orderDirection, sortOrder) { + let prevColumns = []; + const sortColumns = this.getOrderByCollection(); + + if (sortColumns.length === this.maxColumnSort && !sortOrder) { + this.orderByCollection[0].orderDirection = ''; + this.orderByCollection[0].sortOrder = undefined; + + prevColumns = this.orderByCollection.map((collection) => { + if (collection.sortOrder) { + collection.sortOrder -= 1; + } else if (collection.orderBy === orderBy && orderDirection) { + collection.sortOrder = sortColumns.length; + collection.orderDirection = orderDirection; + } + + return collection; + }); + } else { + prevColumns = this.orderByCollection.map((collection) => { + if (collection.orderBy === orderBy && orderDirection) { + collection.orderDirection = orderDirection; + collection.sortOrder = sortOrder || sortColumns.length + 1; + } else if (!orderDirection && collection.orderBy === orderBy) { + collection.orderDirection = orderDirection; + collection.sortOrder = undefined; + } else if (!orderDirection && sortOrder < collection.sortOrder) { + collection.sortOrder -= 1; + } + return collection; + }); + } + + prevColumns = this.sortOrderCollection(prevColumns); + this.orderByCollection = [...prevColumns]; + this.currentPage = 0; this.sorted = false; } @@ -708,39 +773,59 @@ export default class DataManager { } sortList(list) { - let columnDef = this.columns.find((_) => _.tableData.id === this.orderBy); - if (!columnDef) { - columnDef = this.columns[0]; - } - let result = list; + const collectionIds = this.orderByCollection.map( + (collection) => collection.orderBy + ); + const columnsDefs = new Map(); + this.columns.forEach((column) => { + const columnId = column.tableData.id; + if (collectionIds.includes(columnId)) { + columnsDefs.set(columnId, column); + } + }); + + const sort = this.sort; + const getFieldValue = this.getFieldValue; + const orderByCollection = this.orderByCollection; + + return list.sort(function sortData( + a, + b, + columns = columnsDefs, + collection = orderByCollection + ) { + const { orderBy, orderDirection } = collection[0]; + + const columnDef = columns.get(orderBy); - if (columnDef.customSort) { - if (this.orderDirection === 'desc') { - result = list.sort((a, b) => columnDef.customSort(b, a, 'row', 'desc')); + let compareValue = 0; + if (columnDef.customSort) { + if (orderDirection === 'desc') { + compareValue = columnDef.customSort(b, a, 'row', orderDirection); + } else { + compareValue = columnDef.customSort(a, b, 'row', orderDirection); + } } else { - result = list.sort((a, b) => - columnDef.customSort(a, b, 'row', this.orderDirection) + // Calculate compare value and modify based on order + compareValue = sort( + getFieldValue(a, columnDef), + getFieldValue(b, columnDef), + columnDef.type ); + + compareValue = + orderDirection.toLowerCase() === 'desc' + ? compareValue * -1 + : compareValue; } - } else { - result = list.sort( - this.orderDirection === 'desc' - ? (a, b) => - this.sort( - this.getFieldValue(b, columnDef), - this.getFieldValue(a, columnDef), - columnDef.type - ) - : (a, b) => - this.sort( - this.getFieldValue(a, columnDef), - this.getFieldValue(b, columnDef), - columnDef.type - ) - ); - } - return result; + // See if the next key needs to be considered + const checkNextKey = compareValue === 0 && collection.length !== 1; + + return checkNextKey + ? sortData(a, b, columns, collection.slice(1)) + : compareValue; + }); } getRenderState = () => { @@ -773,8 +858,8 @@ export default class DataManager { currentPage: this.currentPage, data: this.sortedData, lastEditingRow: this.lastEditingRow, - orderBy: this.orderBy, - orderDirection: this.orderDirection, + orderByCollection: this.orderByCollection, + maxColumnSort: this.maxColumnSort, originalData: [...this.data], pageSize: this.pageSize, renderData: this.pagedData, @@ -1188,9 +1273,12 @@ export default class DataManager { element.groupsIndex = getGroupsIndex(element.groups); sortGroupData(element.groups, level + 1); } else { - if (this.orderBy >= 0 && this.orderDirection) { + if ( + this.maxColumnSort > 0 && + this.getOrderByCollection().length > 0 + ) { element.data = this.sortList(element.data); - } else if (this.orderDirection === '') { + } else if (this.maxColumnSort > 0) { element.data = element.data.sort((a, b) => { return ( this.data.findIndex( @@ -1209,7 +1297,7 @@ export default class DataManager { sortGroupData(this.sortedData, 1); } else if (this.isDataType('tree')) { this.sortedData = [...this.treefiedData]; - if (this.orderBy != -1) { + if (this.maxColumnSort > 0 && this.getOrderByCollection().length > 0) { this.sortedData = this.sortList(this.sortedData); const sortTree = (list) => { @@ -1227,7 +1315,11 @@ export default class DataManager { } } else if (this.isDataType('normal')) { this.sortedData = [...this.searchedData]; - if (this.orderBy != -1 && this.applySort) { + if ( + this.maxColumnSort > 0 && + this.getOrderByCollection().length > 0 && + this.applySort + ) { this.sortedData = this.sortList(this.sortedData); } }