diff --git a/client/src/components/AddToGroupModal/index.jsx b/client/src/components/AddToGroupModal/index.jsx index af0475f..5948292 100644 --- a/client/src/components/AddToGroupModal/index.jsx +++ b/client/src/components/AddToGroupModal/index.jsx @@ -1,4 +1,5 @@ import React, { useEffect } from "react"; +import { List } from "react-virtualized"; import PT from "prop-types"; import Button from "../Button"; @@ -159,6 +160,42 @@ export default function AddToGroupModal({ onCancel, updateUser, user }) { setFilter(""); }; + const filteredOtherGroups = otherGroups.filter((g) => + g.name.toLowerCase().includes(filter.toLowerCase()) + ); + const filteredMyGroups = myGroups.filter((g) => + g.name.toLowerCase().includes(filter.toLowerCase()) + ); + + const myGroupsListEstimatedWidth = filteredMyGroups.length > 10 ? 540 : 560; + const otherGroupsListEstimatedWidth = + filteredOtherGroups.length > 10 ? 540 : 560; + const listWidth = + myGroupsListEstimatedWidth > otherGroupsListEstimatedWidth + ? myGroupsListEstimatedWidth + : otherGroupsListEstimatedWidth; + + /** + * Row renderer for react-virtualized#List. + * Renders each item as a row. + * @param {String} key unique key for the item + * @param {Number} index index of the item (row) + * @param {Object} style + * @return {SectionRow} row element + */ + function rowRenderer({ key, index, style, items }) { + return ( +
+ switchSelected(items[index])} + /> +
+ ); + } + return (
- {!loadingGroups && - myGroups - .filter((g) => - g.name.toLowerCase().includes(filter.toLowerCase()) - ) - .map((g) => ( - switchSelected(g)} - /> - ))} + {!loadingGroups && ( + 10 + ? 450 + : filteredMyGroups.length * 45 + } + rowCount={filteredMyGroups.length} + rowHeight={45} + rowRenderer={(params) => + rowRenderer({ ...params, items: filteredMyGroups }) + } + /> + )}
{myGroups.filter((g) => g.name.toLowerCase().includes(filter.toLowerCase()) @@ -225,19 +265,22 @@ export default function AddToGroupModal({ onCancel, updateUser, user }) { Other Groups{loadingGroups && " (Loading...)"}
- {!loadingGroups && - otherGroups - .filter((g) => - g.name.toLowerCase().includes(filter.toLowerCase()) - ) - .map((g) => ( - switchSelected(g)} - /> - ))} + {!loadingGroups && ( + 10 + ? 450 + : filteredOtherGroups.length * 45 + } + rowCount={filteredOtherGroups.length} + rowHeight={45} + rowRenderer={(params) => + rowRenderer({ ...params, items: filteredOtherGroups }) + } + /> + )}
{otherGroups.filter((g) => g.name.toLowerCase().includes(filter.toLowerCase()) diff --git a/client/src/components/AddToGroupModal/style.module.scss b/client/src/components/AddToGroupModal/style.module.scss index e9cd4f4..5acfbad 100644 --- a/client/src/components/AddToGroupModal/style.module.scss +++ b/client/src/components/AddToGroupModal/style.module.scss @@ -48,6 +48,10 @@ overflow-y: auto; } +.groupsList { + outline: 0; +} + .search { background: $lightGray2; border: none; diff --git a/client/src/components/GroupsSideMenu/filters.js b/client/src/components/GroupsSideMenu/filters.js index 924185b..0c7da53 100644 --- a/client/src/components/GroupsSideMenu/filters.js +++ b/client/src/components/GroupsSideMenu/filters.js @@ -1,4 +1,5 @@ import React, { useState, useEffect } from "react"; +import { List } from "react-virtualized"; import PT from "prop-types"; import SearchBox from "../searchBox"; @@ -133,23 +134,42 @@ function GroupsSection({ selectedItemId, loadingGroups, }) { + /** + * Row renderer for react-virtualized#List. + * Renders each item as a row. + * @param {String} key unique key for the item + * @param {Number} index index of the item (row) + * @param {Object} style + * @return {SectionRow} row element + */ + function rowRenderer({ key, index, style }) { + return ( +
+ + !isSelected && onItemClicked && onItemClicked(title, items[index]) + } + selected={items[index].id === selectedItemId} + /> +
+ ); + } + return ( <>
{title}
- {items.map((item, index) => { - return ( - - !isSelected && onItemClicked && onItemClicked(title, item) - } - selected={item.id === selectedItemId} - /> - ); - })} + 7 ? 450 : items.length * 66} + rowCount={items.length} + rowHeight={66} + rowRenderer={rowRenderer} + />
{items.length === 0 && !loadingGroups && (
No results found
@@ -173,6 +193,7 @@ function SectionRow({ title, badge, selected = false, action }) { onClick={() => { action(selected); }} + title={title} >
div { margin-top: 20px; } @@ -101,8 +105,11 @@ } .sectionItemTitle { + width: 280px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; color: gray2; - word-break: break-all; padding-right: 12px; } diff --git a/client/src/config.js b/client/src/config.js index 62b8ee1..02249be 100644 --- a/client/src/config.js +++ b/client/src/config.js @@ -6,6 +6,7 @@ export default { API_URL: process.env.REACT_APP_API_URL, API_PREFIX: process.env.REACT_APP_API_PREFIX, GROUPS_API_URL: process.env.REACT_APP_GROUPS_API_URL, + GROUPS_PER_PAGE: process.env.REACT_APP_GROUPS_PER_PAGE || 1000, BULK_UPLOAD_TEMPLATE_ID: process.env.REACT_APP_BULK_UPLOAD_TEMPLATE_ID, EMSI_SKILLPROVIDER_ID: process.env.REACT_APP_EMSI_SKILLPROVIDER_ID, diff --git a/client/src/lib/groups.js b/client/src/lib/groups.js index d730ab5..9626e6a 100644 --- a/client/src/lib/groups.js +++ b/client/src/lib/groups.js @@ -39,7 +39,7 @@ export async function getGroups(apiClient, handle, cancelToken) { // Now, get my groups first try { response = await apiClient.get( - `${config.GROUPS_API_URL}?universalUID=${userId}&membershipType=user`, + `${config.GROUPS_API_URL}?universalUID=${userId}&membershipType=user&perPage=${config.GROUPS_PER_PAGE}`, { cancelToken } ); } catch (error) { @@ -66,7 +66,7 @@ export async function getGroups(apiClient, handle, cancelToken) { // Fetch all groups in the org try { response = await apiClient.get( - `${config.GROUPS_API_URL}?organizationId=${organizationId}`, + `${config.GROUPS_API_URL}?organizationId=${organizationId}&perPage=${config.GROUPS_PER_PAGE}`, { cancelToken } ); } catch (error) { diff --git a/package-lock.json b/package-lock.json index 15f9307..0ce80a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,21 @@ "js-tokens": "^4.0.0" } }, + "@babel/runtime": { + "version": "7.10.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.5.tgz", + "integrity": "sha512-otddXKhdNn7d0ptoFRHtMLa8LqDxLYwTjB4nYgM1yy5N6gU/MUf8zqyyLltCH3yAVitBzmwK4us+DD0l/MauAg==", + "requires": { + "regenerator-runtime": "^0.13.4" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.7", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", + "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" + } + } + }, "@hapi/address": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", @@ -830,6 +845,11 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" }, + "csstype": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.11.tgz", + "integrity": "sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==" + }, "dashdash": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", @@ -986,6 +1006,15 @@ "esutils": "^2.0.2" } }, + "dom-helpers": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz", + "integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^2.6.7" + } + }, "dtrace-provider": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.8.8.tgz", @@ -2150,8 +2179,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.0", @@ -2433,7 +2461,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -2974,7 +3001,6 @@ "version": "15.7.2", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -3064,8 +3090,36 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-virtualized": { + "version": "9.21.2", + "resolved": "https://registry.npmjs.org/react-virtualized/-/react-virtualized-9.21.2.tgz", + "integrity": "sha512-oX7I7KYiUM7lVXQzmhtF4Xg/4UA5duSA+/ZcAvdWlTLFCoFYq1SbauJT5gZK9cZS/wdYR6TPGpX/dqzvTqQeBA==", + "requires": { + "babel-runtime": "^6.26.0", + "clsx": "^1.0.1", + "dom-helpers": "^5.0.0", + "loose-envify": "^1.3.0", + "prop-types": "^15.6.0", + "react-lifecycles-compat": "^3.0.4" + }, + "dependencies": { + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + } + } + } }, "read-pkg": { "version": "2.0.0", @@ -3111,6 +3165,11 @@ "backoff": "~2.5.0" } }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + }, "regexpp": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", diff --git a/package.json b/package.json index 22ba665..e30bee0 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "lodash": "^4.17.19", "multer": "^1.4.2", "node-cache": "^5.1.0", + "react-virtualized": "^9.21.2", "swagger-ui-express": "^4.1.4", "tc-bus-api-wrapper": "topcoder-platform/tc-bus-api-wrapper.git#feature/auth0-proxy-server", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.4",