diff --git a/docs/translations.md b/docs/translations.md index 27662262d..6b1261896 100644 --- a/docs/translations.md +++ b/docs/translations.md @@ -133,6 +133,11 @@ Here is a JSON notation containing all keys and default translations: "Pager": { "NEXT": "Next", "PREVIOUS": "Previous" + }, + "EpicTable": { + "EpicDetail": { + "CLOSE": "Close" + } } } ``` diff --git a/package-lock.json b/package-lock.json index fc23e4174..bce71c14f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1725,11 +1725,136 @@ "prop-types": "^15.7.2" }, "dependencies": { + "@storybook/addons": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@storybook/addons/-/addons-5.2.4.tgz", + "integrity": "sha512-Q+bnVlBA308qnELxnh18hBDRSUgltR9KbV537285dUL/okv/NC6n51mxJwIaG+ksBW2wU+5e6tqSayaKF3uHLw==", + "dev": true, + "requires": { + "@storybook/api": "5.2.4", + "@storybook/channels": "5.2.4", + "@storybook/client-logger": "5.2.4", + "@storybook/core-events": "5.2.4", + "core-js": "^3.0.1", + "global": "^4.3.2", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/api": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@storybook/api/-/api-5.2.4.tgz", + "integrity": "sha512-KqAB+NkHIHdwu749NDP+7i44jy1bFgpq7GTJlG+sx/XLZHQveK/8yn109g9bXHFth7SvdXI1+9GA/apzwBU/Mw==", + "dev": true, + "requires": { + "@storybook/channels": "5.2.4", + "@storybook/client-logger": "5.2.4", + "@storybook/core-events": "5.2.4", + "@storybook/router": "5.2.4", + "@storybook/theming": "5.2.4", + "core-js": "^3.0.1", + "fast-deep-equal": "^2.0.1", + "global": "^4.3.2", + "lodash": "^4.17.11", + "memoizerific": "^1.11.3", + "prop-types": "^15.6.2", + "react": "^16.8.3", + "semver": "^6.0.0", + "shallow-equal": "^1.1.0", + "store2": "^2.7.1", + "telejson": "^3.0.2", + "util-deprecate": "^1.0.2" + } + }, + "@storybook/components": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@storybook/components/-/components-5.2.4.tgz", + "integrity": "sha512-APhw+XGag0RTCRJ8eCWKVr8dLt9SRqnS8LtzcZJbokCYRxRTFzhmX2eVEE1v+d0gHib1/yh2COxOjMzv3m/rQA==", + "dev": true, + "requires": { + "@storybook/client-logger": "5.2.4", + "@storybook/theming": "5.2.4", + "@types/react-syntax-highlighter": "10.1.0", + "core-js": "^3.0.1", + "global": "^4.3.2", + "markdown-to-jsx": "^6.9.1", + "memoizerific": "^1.11.3", + "polished": "^3.3.1", + "popper.js": "^1.14.7", + "prop-types": "^15.7.2", + "react": "^16.8.3", + "react-dom": "^16.8.3", + "react-focus-lock": "^1.18.3", + "react-helmet-async": "^1.0.2", + "react-popper-tooltip": "^2.8.3", + "react-syntax-highlighter": "^8.0.1", + "react-textarea-autosize": "^7.1.0", + "simplebar-react": "^1.0.0-alpha.6" + } + }, + "@storybook/router": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@storybook/router/-/router-5.2.4.tgz", + "integrity": "sha512-GL7eGdj5oYST0mE9fThJB9ye9tTTgrP+aP3okZ6MeMGtNytb7bmJRpAD2E4ouuPTQVppyHI5re8g/HUxUNOT1g==", + "dev": true, + "requires": { + "@reach/router": "^1.2.1", + "@types/reach__router": "^1.2.3", + "core-js": "^3.0.1", + "global": "^4.3.2", + "lodash": "^4.17.11", + "memoizerific": "^1.11.3", + "qs": "^6.6.0" + } + }, + "@storybook/theming": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/@storybook/theming/-/theming-5.2.4.tgz", + "integrity": "sha512-2ZlqBrmnm8N0352Fnu2+GB3pEsHL4Eb2eKxV0VLLgkjJuAlm7CK6+I/e4ZknQWxwYm2pQj1y6ta68A62fGBYyA==", + "dev": true, + "requires": { + "@emotion/core": "^10.0.14", + "@emotion/styled": "^10.0.14", + "@storybook/client-logger": "5.2.4", + "common-tags": "^1.8.0", + "core-js": "^3.0.1", + "deep-object-diff": "^1.1.0", + "emotion-theming": "^10.0.14", + "global": "^4.3.2", + "memoizerific": "^1.11.3", + "polished": "^3.3.1", + "prop-types": "^15.7.2", + "resolve-from": "^5.0.0" + } + }, "core-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", - "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.2.1.tgz", + "integrity": "sha512-Qa5XSVefSVPRxy2XfUC13WbvqkxhkwB3ve+pgCQveNgYzbM/UxZeu1dcOX/xr4UmfUd+muuvsaxilQzCyUurMw==", "dev": true + }, + "simplebar": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-4.2.3.tgz", + "integrity": "sha512-9no0pK7/1y+8/oTF3sy/+kx0PjQ3uk4cYwld5F1CJGk2gx+prRyUq8GRfvcVLq5niYWSozZdX73a2wIr1o9l/g==", + "dev": true, + "requires": { + "can-use-dom": "^0.1.0", + "core-js": "^3.0.1", + "lodash.debounce": "^4.0.8", + "lodash.memoize": "^4.1.2", + "lodash.throttle": "^4.1.1", + "resize-observer-polyfill": "^1.5.1" + } + }, + "simplebar-react": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-1.2.3.tgz", + "integrity": "sha512-1EOWJzFC7eqHUp1igD1/tb8GBv5aPQA5ZMvpeDnVkpNJ3jAuvmrL2kir3HuijlxhG7njvw9ssxjjBa89E5DrJg==", + "dev": true, + "requires": { + "prop-types": "^15.6.1", + "simplebar": "^4.2.3" + } } } }, @@ -1912,6 +2037,30 @@ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==", "dev": true + }, + "simplebar": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-4.2.3.tgz", + "integrity": "sha512-9no0pK7/1y+8/oTF3sy/+kx0PjQ3uk4cYwld5F1CJGk2gx+prRyUq8GRfvcVLq5niYWSozZdX73a2wIr1o9l/g==", + "dev": true, + "requires": { + "can-use-dom": "^0.1.0", + "core-js": "^3.0.1", + "lodash.debounce": "^4.0.8", + "lodash.memoize": "^4.1.2", + "lodash.throttle": "^4.1.1", + "resize-observer-polyfill": "^1.5.1" + } + }, + "simplebar-react": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-1.2.3.tgz", + "integrity": "sha512-1EOWJzFC7eqHUp1igD1/tb8GBv5aPQA5ZMvpeDnVkpNJ3jAuvmrL2kir3HuijlxhG7njvw9ssxjjBa89E5DrJg==", + "dev": true, + "requires": { + "prop-types": "^15.6.1", + "simplebar": "^4.2.3" + } } } }, @@ -2604,6 +2753,12 @@ "integrity": "sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==", "dev": true }, + "@types/overlayscrollbars": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@types/overlayscrollbars/-/overlayscrollbars-1.9.0.tgz", + "integrity": "sha512-U3t35G0IH39eCoWKI6auh+3NkeP8jrmLyeZhiL3fKn4foOvbl223Ccd9cfUb1vhcxDmpTNAfKphDvl5jYJOD3w==", + "dev": true + }, "@types/prop-types": { "version": "15.7.3", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.3.tgz", @@ -9148,9 +9303,9 @@ } }, "handlebars": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.3.1.tgz", - "integrity": "sha512-c0HoNHzDiHpBt4Kqe99N8tdLPKAnGCQ73gYMPWtAYM4PwGnf7xl8PBUHJqh9ijlzt2uQKaSRxbXRt+rZ7M2/kA==", + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.5.1.tgz", + "integrity": "sha512-C29UoFzHe9yM61lOsIlCE5/mQVGrnIOrOq7maQl76L7tYPCgC1og0Ajt6uWnX4ZTxBPnjw+CUvawphwCfJgUnA==", "dev": true, "requires": { "neo-async": "^2.6.0", @@ -9587,9 +9742,9 @@ "dev": true }, "https-proxy-agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.4.tgz", + "integrity": "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg==", "dev": true, "requires": { "agent-base": "^4.3.0", @@ -13946,6 +14101,16 @@ "os-tmpdir": "^1.0.0" } }, + "overlayscrollbars": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/overlayscrollbars/-/overlayscrollbars-1.9.1.tgz", + "integrity": "sha512-MKOIiIaxfvemDQu1o03M/4lSs8EsSEqtaKMfspTtgF6bzxh8+ymzDtMCDGRIy4BQBQlu1ThP1QuSCdM9VAO1Qw==" + }, + "overlayscrollbars-react": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/overlayscrollbars-react/-/overlayscrollbars-react-0.1.0.tgz", + "integrity": "sha512-5ZmZtWoj++DBMhrRxE7k5hDWXPcvdowapgZHjetl41mZ94F0bD4EfzgPjMv4e/gqYRpsgdLEcozOlatay5WYyw==" + }, "override-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/override-require/-/override-require-1.1.1.tgz", @@ -15406,9 +15571,9 @@ } }, "react-is": { - "version": "16.9.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.9.0.tgz", - "integrity": "sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==" + "version": "16.10.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.10.2.tgz", + "integrity": "sha512-INBT1QEgtcCCgvccr5/86CfD71fw9EPmDxgiJX4I2Ddr6ZsV6iFXsuby+qWJPtmNuMY0zByTsG4468P7nHuNWA==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -17319,38 +17484,6 @@ "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=", "dev": true }, - "simplebar": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/simplebar/-/simplebar-4.2.3.tgz", - "integrity": "sha512-9no0pK7/1y+8/oTF3sy/+kx0PjQ3uk4cYwld5F1CJGk2gx+prRyUq8GRfvcVLq5niYWSozZdX73a2wIr1o9l/g==", - "dev": true, - "requires": { - "can-use-dom": "^0.1.0", - "core-js": "^3.0.1", - "lodash.debounce": "^4.0.8", - "lodash.memoize": "^4.1.2", - "lodash.throttle": "^4.1.1", - "resize-observer-polyfill": "^1.5.1" - }, - "dependencies": { - "core-js": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.3.2.tgz", - "integrity": "sha512-S1FfZpeBchkhyoY76YAdFzKS4zz9aOK7EeFaNA2aJlyXyA+sgqz6xdxmLPGXEAf0nF44MVN1kSjrA9Kt3ATDQg==", - "dev": true - } - } - }, - "simplebar-react": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/simplebar-react/-/simplebar-react-1.2.3.tgz", - "integrity": "sha512-1EOWJzFC7eqHUp1igD1/tb8GBv5aPQA5ZMvpeDnVkpNJ3jAuvmrL2kir3HuijlxhG7njvw9ssxjjBa89E5DrJg==", - "dev": true, - "requires": { - "prop-types": "^15.6.1", - "simplebar": "^4.2.3" - } - }, "sisteransi": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.3.tgz", diff --git a/package.json b/package.json index a7a6b37ea..9c664649d 100644 --- a/package.json +++ b/package.json @@ -36,12 +36,15 @@ "bootstrap": "4.3.1", "material-design-icons": "3.0.1", "moment": "2.24.0", + "overlayscrollbars": "1.9.1", + "overlayscrollbars-react": "0.1.0", "pica": "5.1.0", "react-avatar-editor": "11.0.7", "react-bootstrap-typeahead": "4.0.0-alpha.6", "react-datetime": "2.16.3", "react-display-name": "0.2.4", "react-final-form": "6.3.0", + "react-is": "16.10.2", "react-quill": "1.3.3", "react-text-mask": "5.4.3", "react-textarea-autosize": "7.1.0" @@ -78,6 +81,7 @@ "@types/react": "16.9.9", "@types/lodash": "4.14.144", "@types/react-avatar-editor": "10.3.4", + "@types/overlayscrollbars": "1.9.0", "@types/react-bootstrap-typeahead": "3.4.5", "@types/react-dom": "16.9.2", "@types/react-router-dom": "5.1.0", diff --git a/src/core/ContentState/ContentState.tsx b/src/core/ContentState/ContentState.tsx index e17d26e41..f363473ca 100644 --- a/src/core/ContentState/ContentState.tsx +++ b/src/core/ContentState/ContentState.tsx @@ -4,6 +4,8 @@ import classNames from 'classnames'; import Icon from '../Icon/Icon'; import Spinner from '../Spinner/Spinner'; +export type ContentStateMode = 'empty' | 'no-results' | 'error' | 'loading'; + interface Props { /** * The mode of the ContentState: @@ -19,7 +21,7 @@ interface Props { * Use `loading` for when something is still loading. * */ - mode: 'empty' | 'no-results' | 'error' | 'loading'; + mode: ContentStateMode; /** * Optional custom content you want to render below the titles. diff --git a/src/core/Icon/Icon.scss b/src/core/Icon/Icon.scss index b39dce3dc..16174e6fb 100644 --- a/src/core/Icon/Icon.scss +++ b/src/core/Icon/Icon.scss @@ -17,6 +17,10 @@ } } +i.material-icons { + user-select: none; +} + .nav-link { i.material-icons { vertical-align: middle; diff --git a/src/form/CheckboxMultipleSelect/CheckboxMultipleSelect.stories.tsx b/src/form/CheckboxMultipleSelect/CheckboxMultipleSelect.stories.tsx index d352169c5..ba2fcc79c 100644 --- a/src/form/CheckboxMultipleSelect/CheckboxMultipleSelect.stories.tsx +++ b/src/form/CheckboxMultipleSelect/CheckboxMultipleSelect.stories.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useState} from 'react'; import { storiesOf } from '@storybook/react'; import { range } from 'lodash'; @@ -8,7 +8,6 @@ import CheckboxMultipleSelect, { import { FinalForm, Form } from '../story-utils'; import { pageOfUsers } from '../../test/fixtures'; import { User } from '../../test/types'; -import { useState } from '@storybook/addons'; interface SubjectOption { value: string; diff --git a/src/index.ts b/src/index.ts index f2102acdb..b304f74e8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,7 +4,7 @@ export { default as AvatarStack } from './core/Avatar/AvatarStack'; export { default as Button } from './core/Button/Button'; export { default as useShowSpinner } from './core/Button/useShowSpinner'; export { default as ConfirmButton } from './core/ConfirmButton/ConfirmButton'; -export { default as ContentState } from './core/ContentState/ContentState'; +export { default as ContentState, ContentStateMode } from './core/ContentState/ContentState'; export { default as FlashMessage } from './core/FlashMessage/FlashMessage'; export { Icon, IconType } from './core/Icon/index'; export { default as InfoBadge } from './core/InfoBadge/InfoBadge'; @@ -81,6 +81,24 @@ export { JarbValuePicker, } from './form/ValuePicker/ValuePicker'; +// Table +export { EpicTable } from './table/EpicTable/EpicTable'; + +export { EpicCell } from './table/EpicTable/cells/EpicCell/EpicCell'; +export { EpicHeader } from './table/EpicTable/cells/EpicHeader/EpicHeader'; +export { EpicCellLayout } from './table/EpicTable/cells/EpicCellLayout/EpicCellLayout'; + +export { EpicRow } from './table/EpicTable/rows/EpicRow/EpicRow'; +export { EpicDetailRow } from './table/EpicTable/rows/EpicDetailRow/EpicDetailRow'; +export { EpicExpanderRow } from './table/EpicTable/rows/EpicExpanderRow/EpicExpanderRow'; + +export { EpicDetail } from './table/EpicTable/widgets/EpicDetail/EpicDetail'; +export { EpicExpander } from './table/EpicTable/widgets/EpicExpander/EpicExpander'; +export { EpicSelection } from './table/EpicTable/widgets/EpicSelection/EpicSelection'; +export { EpicSort } from './table/EpicTable/widgets/EpicSort/EpicSort'; + +export { Direction } from './table/EpicTable/types'; + // Utilities export { t } from './utilities/translation/translation'; export { setTranslator } from './utilities/translation/translator'; diff --git a/src/main.scss b/src/main.scss index f2237c63a..b7041e621 100644 --- a/src/main.scss +++ b/src/main.scss @@ -28,6 +28,7 @@ @import '~react-bootstrap-typeahead/css/Typeahead.css'; @import '~react-bootstrap-typeahead/css/Typeahead-bs4.css'; @import '~react-quill/dist/quill.snow.css'; +@import '~overlayscrollbars/css/OverlayScrollbars.css'; // Components @import './form/ImageUpload/ImageUpload'; @@ -39,6 +40,9 @@ @import './form/Toggle/Toggle'; @import './form/Typeahead/Typeahead'; +// Table +@import './table/EpicTable/EpicTable'; + // Defaults body { button, diff --git a/src/table/EpicTable/EpicTable.scss b/src/table/EpicTable/EpicTable.scss new file mode 100644 index 000000000..57a5309e7 --- /dev/null +++ b/src/table/EpicTable/EpicTable.scss @@ -0,0 +1,81 @@ +.epic-table { + background-color: $gray-100; + + &-container { + position: relative; + } + + /* + We style the `overlayscrollbars` so it is sticky always stays + at the bottom of the screen. + */ + .os-host { + &-overflow { + /* Override the overflow back to `normal` so sticky works. */ + overflow: visible !important; + } + + .os-scrollbar-horizontal { + position: sticky; + top: 50px; + } + } + + &-fixed-header { + position: fixed; + overflow: hidden; + z-index: 2; + top: 0; + } + + &-header { + background-color: $primary; + color: color-yiq($primary); + font-weight: bold; + flex-grow: 1; + + &-resizeable { + width: 8px; + height: 100%; + cursor: col-resize; + } + + .form-control { + height: calc(1.5em + 0.2rem + 2px); + border-radius: 0; + padding: 0.375rem 0.2rem; + } + + select.form-control { + padding: 0rem 0.2rem; + } + + .form-group { + margin-bottom: 0; + } + } + + &-cell { + white-space: nowrap; + overflow: hidden; + display: flex; + align-items: center; + flex-grow: 1; + } + + &-expander-row, + &-detail-row { + position: absolute; + z-index: 3; + } + + &-overlay { + position: absolute; + z-index: 4; + background-color: $white; + } +} + +.user-select-none { + user-select: none; +} diff --git a/src/table/EpicTable/EpicTable.stories.tsx b/src/table/EpicTable/EpicTable.stories.tsx new file mode 100644 index 000000000..68dfec938 --- /dev/null +++ b/src/table/EpicTable/EpicTable.stories.tsx @@ -0,0 +1,1913 @@ +import React, { Fragment, useState, useEffect } from 'react'; +import { storiesOf } from '@storybook/react'; +import { Card, Alert, Row, Col, Input } from 'reactstrap'; +import { groupBy, every, startsWith, lowerCase } from 'lodash'; + +import { EpicTable } from './EpicTable'; +import { EpicRow } from './rows/EpicRow/EpicRow'; +import { EpicHeader } from './cells/EpicHeader/EpicHeader'; +import { EpicCell } from './cells/EpicCell/EpicCell'; +import { EpicExpanderRow } from './rows/EpicExpanderRow/EpicExpanderRow'; +import { EpicCellLayout } from './cells/EpicCellLayout/EpicCellLayout'; + +import { EpicExpander } from './widgets/EpicExpander/EpicExpander'; +import { EpicDetailRow } from './rows/EpicDetailRow/EpicDetailRow'; +import { EpicDetail } from './widgets/EpicDetail/EpicDetail'; +import { EpicSelection } from './widgets/EpicSelection/EpicSelection'; +import { EpicSort } from './widgets/EpicSort/EpicSort'; + +import { Direction } from './types'; +import { pageOf } from '../../test/utils'; +import { + Tag, + Pagination, + DateTimeInput, + Button, + ContentState, + ContentStateMode +} from '../..'; +import moment from 'moment'; + +storiesOf('table|EpicTable', module) + .addParameters({ component: EpicTable }) + .add('full example', () => { + const [widths, setWidths] = useState(() => ({ + firstName: 300, + lastName: 200, + age: 200, + eyeColor: 200, + height: 150, + weight: 150, + jobTitle: 200, + favoriteMovie: 300, + favoriteFood: 200, + birthDate: 200, + sex: 200, + actions: 300 + })); + + function changeSize(name: keyof Person, width: number) { + setWidths(widths => ({ ...widths, [name]: width })); + } + + const [filters, setFilters] = useState(() => ({ + firstName: '', + lastName: '', + age: '', + eyeColor: '', + height: '', + weight: '', + jobTitle: '', + favoriteMovie: '', + favoriteFood: '', + birthDate: '', + sex: '' + })); + + function filterChanged(name: keyof Person, value: string) { + setFilters({ ...filters, [name]: value }); + } + + const filteredPersons = persons.filter(person => { + return every(filters, (value, key) => { + const text = person[key]; + + if (!value || value === 'all') { + return true; + } + + return startsWith(lowerCase(text), lowerCase(value)); + }); + }); + + const [sort, setSort] = useState<{ + direction: Direction; + column: string; + }>({ direction: 'NONE', column: 'firstName' }); + + function changeSort(column: keyof Person, direction: Direction) { + setPage(1); + setSort({ direction, column }); + } + + const sortFn = + sort.direction === 'ASC' + ? (a, b) => `${a[sort.column]}`.localeCompare(`${b[sort.column]}`) + : (a, b) => `${b[sort.column]}`.localeCompare(`${a[sort.column]}`); + + const sortedPersons = filteredPersons.sort(sortFn); + + const [page, setPage] = useState(1); + + const pageOfPersons = pageOf(sortedPersons, page); + + const [selected, setSelected] = useState([]); + + function onSelect(person: Person, checked: boolean) { + if (checked) { + selected.push(person); + setSelected([...selected]); + } else { + const nextSelected = selected.filter(p => p.id !== person.id); + + setSelected(nextSelected); + } + } + + const allPersonsSelected = + selected.length > 0 && + pageOfPersons.content.every(p => selected.some(ps => ps.id === p.id)); + + function selectAllClicked(checked: boolean) { + if (checked) { + pageOfPersons.content.forEach(p => { + if (selected.every(ps => p.id !== ps.id)) { + selected.push(p); + } + + setSelected([...selected]); + }); + } else { + setSelected([]); + } + } + + const [detail, setDetail] = useState(-1); + + const [loading, setLoading] = useState(false); + + useEffect(() => { + setLoading(true); + + setTimeout(() => { + setLoading(false); + }, 1000); + }, []); + + const overlay = loading ? ( + + ) : pageOfPersons.totalElements === 0 ? ( + + ) : null; + + return ( + + {selected.length > 0 ? ( + <> +

Selected

+
+ {selected.map(person => ( + onSelect(person, false)} + /> + ))} +
+ + ) : null} + + + + changeSize('firstName', width)} + > + + + selectAllClicked(checked)} + > + First name + + + changeSort('firstName', direction)} + /> + + { + event.preventDefault(); + filterChanged('firstName', event.target.value); + }} + /> + + + changeSize('lastName', width)} + > + + + Last name + changeSort('lastName', direction)} + /> + + + filterChanged('lastName', event.target.value) + } + /> + + + changeSize('age', width)} + > + + + Age + changeSort('age', direction)} + /> + + filterChanged('age', event.target.value)} + /> + + + changeSize('eyeColor', width)} + > + + + Eye color + changeSort('eyeColor', direction)} + /> + + + filterChanged('eyeColor', event.target.value) + } + > + + + + + + + + changeSize('height', width)} + > + + + Height + changeSort('height', direction)} + /> + + + filterChanged('height', event.target.value) + } + /> + + + changeSize('weight', width)} + > + + + Weight + changeSort('weight', direction)} + /> + + + filterChanged('weight', event.target.value) + } + /> + + + changeSize('jobTitle', width)} + > + + + Job title + changeSort('jobTitle', direction)} + /> + + + filterChanged('jobTitle', event.target.value) + } + /> + + + changeSize('favoriteMovie', width)} + > + + + Favorite movie + + changeSort('favoriteMovie', direction) + } + /> + + + + + changeSize('favoriteFood', width)} + > + + + Favorite food + + changeSort('favoriteFood', direction) + } + /> + + + filterChanged('favoriteFood', event.target.value) + } + /> + + + changeSize('birthDate', width)} + > + + + Birth date + changeSort('birthDate', direction)} + /> + + + + filterChanged( + 'birthDate', + date ? moment(date).format('YYYY-MM-DD') : '' + ) + } + /> + + + changeSize('sex', width)} + > + +
+ Sex + changeSort('sex', direction)} + /> +
+ filterChanged('sex', event.target.value)} + > + + + + +
+
+ +
Actions
+
+
+ + {pageOfPersons.content.map((person, index) => ( + + + + p.id === person.id)} + onChange={checked => onSelect(person, checked)} + /> + + setDetail(index)} + > + {person.firstName} + + + + {person.lastName} + + + {person.age} + + + {person.eyeColor} + + + {person.height} + + + {person.weight} + + + {person.jobTitle} + + + {person.favoriteMovie} + + + {person.favoriteFood} + + + {person.birthDate} + + + {person.sex} + + + + + + + + + + } + > + + + First name + + + Last name + + + Age + + + Eye color + + + Height + + + Weight + + + Job title + + + Favorite movie + + + Favorite food + + + Birth date + + + Sex + + + Actions + + + +
+ ); + }) + .add('with sort', () => { + const [direction, setDirection] = useState('NONE'); + + const sortFn = + direction === 'ASC' + ? (a, b) => a.firstName.localeCompare(b.firstName) + : (a, b) => b.firstName.localeCompare(a.firstName); + + const sortedPersons = persons.sort(sortFn); + + return ( + + + + +
+ First name + +
+
+ + Last name + + + Age + + + Eye color + + + Height + + + Weight + + + Job title + + + Favorite movie + + + Favorite food + + + Birth date + + + Sex + + + Actions + +
+ + {sortedPersons.map(person => ( + + + {person.firstName} + + + {person.lastName} + + + {person.age} + + + {person.eyeColor} + + + {person.height} + + + {person.weight} + + + {person.jobTitle} + + + {person.favoriteMovie} + + + {person.favoriteFood} + + + {person.birthDate} + + + {person.sex} + + +