From ac1e47bc8a25184f79d3d4688c539dae15c6610f Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Wed, 3 Jun 2020 17:38:10 +0530 Subject: [PATCH 001/224] 1. Update mappings for availability, skills 2. Update configurations 3. Miscellaneous --- .gitignore | 3 +- README.md | 79 ++++++----------------- package-lock.json | 12 ++-- src/components/EditProfileModal/index.jsx | 14 ++-- src/components/ProfileCard/index.js | 20 ++++-- src/config.js | 11 ++-- src/pages/Search/index.jsx | 63 +++++++++--------- src/services/api.js | 28 +++++--- 8 files changed, 108 insertions(+), 122 deletions(-) diff --git a/.gitignore b/.gitignore index 6475d86..e395e06 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,5 @@ yarn-debug.log* yarn-error.log* /.changelog .npm/ -yarn.lock \ No newline at end of file +yarn.lock +.env.local diff --git a/README.md b/README.md index 54ef094..3a9e752 100644 --- a/README.md +++ b/README.md @@ -1,68 +1,31 @@ -This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). +# U-Bahn user interface -## Available Scripts +This code base represents the user interface for the U-Bahn project. -In the project directory, you can run: +## Local Deployment -### `npm start` +Before you deploy, you need to configure the following in the application: -Runs the app in the development mode.
-Open [http://localhost:3000](http://localhost:3000) to view it in the browser. +```text +REACT_APP_API_URL => The endpoint from which the application retrieves the users (and groups and most of the data) as well as to which the updates are pushed to +REACT_APP_SEARCH_UI_API_URL => The endpoint from which the user can download the bulk upload template files as well as upload the bulk user upload file to +REACT_BULK_UPLOAD_TEMPLATE_ID => The id of the database record which is associated with the bulk upload template file. You would need to query the endpoint under REACT_APP_SEARCH_UI_API_URL to get the id and then set it against this variable +``` -The page will reload if you make edits.
-You will also see any lint errors in the console. +You can create a `.env.local` file and provide the above configuration. -### `npm test` +Once the configuration is set, you can proceed to deploy. -Launches the test runner in the interactive watch mode.
-See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. +The code base has been setup using [Create React App](https://github.com/facebook/create-react-app). Thus, to start the application locally, you need to first (and only once) run the following command: -### `npm run build` +```bash +$ npm install +// Will install the dependenceis +``` -Builds the app for production to the `build` folder.
-It correctly bundles React in production mode and optimizes the build for the best performance. +followed by -The build is minified and the filenames include the hashes.
-Your app is ready to be deployed! - -See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. - -### `npm run eject` - -**Note: this is a one-way operation. Once you `eject`, you can’t go back!** - -If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. - -Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. - -You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. - -## Learn More - -You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). - -To learn React, check out the [React documentation](https://reactjs.org/). - -### Code Splitting - -This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting - -### Analyzing the Bundle Size - -This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size - -### Making a Progressive Web App - -This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app - -### Advanced Configuration - -This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration - -### Deployment - -This section has moved here: https://facebook.github.io/create-react-app/docs/deployment - -### `npm run build` fails to minify - -This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify +```bash +$ npm start +// Will start the application at http://localhost:3000 +``` diff --git a/package-lock.json b/package-lock.json index 97c1dfe..8b6bdb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5398,9 +5398,9 @@ "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=" }, "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==" }, "events": { "version": "3.1.0", @@ -6671,9 +6671,9 @@ "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "requires": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index 4254665..bc59725 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -13,12 +13,14 @@ import { makeColorIterator, avatarColors } from "../../lib/colors"; const colorIterator = makeColorIterator(avatarColors); const nextColor = colorIterator.next(); +// TODO - Role is not an attribute but a nested property on user +// TODO - Remove it from Common Attribute and use it like other nested attributes (like skill / achievements) const COMMON_ATTRIBUTES = ["role", "company", "location", "isAvailable"]; export default function EditProfileModal({ api, onCancel, updateUser, user }) { const [localUser, setLocalUser] = React.useState({ ...user }); const [skillInputValue, setSkillInputValue] = React.useState(""); - const [achieInputValue, setAchieInputValue] = React.useState(""); + const [achievementInputValue, setAchievementInputValue] = React.useState(""); return ( @@ -152,7 +154,7 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { {localUser.skills.map((it, key) => ( { setLocalUser({ ...localUser, @@ -169,7 +171,7 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { placeholder="Enter achievements to add" onKeyUp={({ key }) => { if (key === "Enter") { - const achie = achieInputValue.trim(); + const achie = achievementInputValue.trim(); if (achie) { setLocalUser({ ...localUser, @@ -181,7 +183,7 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { ], }); } - setAchieInputValue(""); + setAchievementInputValue(""); } }} onChange={({ target }) => { @@ -201,10 +203,10 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { } value = ""; } - setAchieInputValue(value); + setAchievementInputValue(value); setImmediate(() => target.focus()); }} - value={achieInputValue} + value={achievementInputValue} /> {localUser.achievements.map((it, key) => ( { setProfileData(profile); - setAvailable(profile.isAvailable); + setAvailable(profile.isAvailable === "true"); }, [profile]); const switchAvailability = (profile) => { const updated = profile; - updated.isAvailable = !updated.isAvailable; - setAvailable(updated.isAvailable); - updateUser(updated); + updated.isAvailable = updated.isAvailable === "true" ? "false" : "true"; + setAvailable(updated.isAvailable === "true"); + // TODO - This seems to call the generic update user api + // TODO - However, we need to update the attribute associated with availability + // TODO - Availability is NOT a property on the User model. It exists as an attribute + // TODO - of the user and is part of the UserAttribute + Attribute model where the + // TODO - id exists under attribute and the value under user attribute + // TODO - Thus, update this api call to update the attribute value and not the user model + // updateUser(updated); }; const removeGroupFromProfile = (group) => { @@ -79,7 +87,7 @@ export default function ProfileCard({
- {profileData.isAvailable ? "Available" : "Unavailable"} + {available ? "Available" : "Unavailable"}
{/* switchAvailability(profileData)} />*/} new Api({ token: "dummy-auth-token" })); const [page, setPage] = React.useState(1); - const byPage = 10; + const byPage = 12; const [totalResults, setTotalResults] = React.useState(0); const [search, setSearch] = React.useState(null); const [tab, setTab] = React.useState(TABS.SEARCH); @@ -34,7 +45,7 @@ export default function SearchPage() { const [myGroups, setMyGroups] = React.useState([]); const [allGroups, setAllGroups] = React.useState([]); - const [sortBy, setSortBy] = React.useState("Rating"); + const [sortBy, setSortBy] = React.useState("Name"); const [sortByDropdownShown, setSortByDropdownShown] = React.useState(false); const [windowWidth, setWindowWidth] = React.useState(window.innerWidth); @@ -103,26 +114,28 @@ export default function SearchPage() { }); data = data.map((p) => { - if (!p.groups) p.groups = []; - if (!p.skills) p.skills = []; - if (!p.achievements) p.achievements = []; + NESTEDPROPERTIES.forEach((nestedProperty) => { + if (!p[nestedProperty]) { + p[nestedProperty] = []; + } + }); + + // TODO - In the original code, p.role used to exist and read from + // TODO - attributes. Roles is property on the user object itself and one + // TODO - need not read from attribute. So I need to figure out how to accommodate it - if (p.attributes && p.attributes.length !== 0) { - p.isAvailable = ( - p.attributes.find((attr) => attr.attributeName === "isAvailable") || - {} - ).value; - p.title = ( - p.attributes.find((attr) => attr.attributeName === "role") || {} - ).value; - p.company = ( - p.attributes.find((attr) => attr.attributeName === "company") || {} - ).value; - p.location = ( - p.attributes.find((attr) => attr.attributeName === "location") || {} - ).value; + if (p.attributes) { + for (let i = 0; i < p.attributes.length; i++) { + const userAttribute = p.attributes[i]; + + if (USERATTRIBUTES.includes(userAttribute.attribute.name)) { + p[userAttribute.attribute.name] = userAttribute.value; + } + } } + p.name = `${p.firstName} ${p.lastName}`; + return p; }); @@ -204,14 +217,6 @@ export default function SearchPage() { {sortByDropdownShown && (
    -
  • { - handleSort("Rating"); - }} - > - Rating -
  • { @@ -231,10 +236,10 @@ export default function SearchPage() {
  • { - handleSort("Avaibility"); + handleSort("Availability"); }} > - Avaibility + Availability
)} diff --git a/src/services/api.js b/src/services/api.js index 8d70e21..bf9fc60 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -83,7 +83,7 @@ export default class Api { * @return {Promise} Template meta object. */ async getTemplate(templateId) { - const url = `${config.UI_API_BASE}/templates/${templateId}`; + const url = `${config.SEARCH_UI_API_URL}/templates/${templateId}`; const { data } = await axios(url); return data; } @@ -94,9 +94,7 @@ export default class Api { * @return {Promise} Resolves to user object. */ async getUser(userId) { - const { data } = await this.api( - `${config.SEARCH_API_BASE}/users/${userId}` - ); + const { data } = await this.api(`${config.API_URL}/users/${userId}`); return data; } @@ -117,11 +115,20 @@ export default class Api { criteria, sortBy, } = {}) { - let { headers, data } = await this.api(`${config.SEARCH_API_BASE}/users`, { - params: { search, groupId, roleId, page, limit, criteria, sortBy }, + let { headers, data } = await this.api(`${config.API_URL}/users`, { + params: { + search, + groupId, + roleId, + page, + limit, + criteria, + sortBy, + enrich: true, + }, }); - const total = headers["x-total-count"] || 0; + const total = headers["x-total"] || 0; return { total, data }; } @@ -138,12 +145,13 @@ export default class Api { let data; try { - const response = await this.api.post( - `${config.SEARCH_API_BASE}/users/${user.id}`, + const response = await this.api.patch( + `${config.API_URL}/users/${user.id}`, entity ); data = response.data; } catch (error) { + // TODO - What should happen when update fails? const mockData = { ...user }; data = mockData; } @@ -162,7 +170,7 @@ export default class Api { * @return {Promise} Resolves to the API response payload. */ async upload(data, options = {}) { - const res = await axios.post(`${config.UI_API_BASE}/uploads`, data, { + const res = await axios.post(`${config.SEARCH_UI_API_URL}/uploads`, data, { cancelToken: options.cancelToken, headers: { "Content-Type": "multipart/form-data", From 0c4cb1fa08617a55902fcf102dc10fb262b9a14c Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Thu, 4 Jun 2020 16:10:29 +0530 Subject: [PATCH 002/224] Integration of download template and upload profiles => RESOLVED --- src/components/Upload/Initial/index.jsx | 5 ++++- .../Upload/Message/style.module.scss | 1 + src/components/Upload/Progress/index.jsx | 6 +----- src/components/Upload/index.jsx | 20 +++++++----------- src/config.js | 2 +- src/pages/Search/index.jsx | 6 +++++- src/pages/Search/style.module.scss | 1 + src/services/api.js | 21 ++++++++++--------- 8 files changed, 31 insertions(+), 31 deletions(-) diff --git a/src/components/Upload/Initial/index.jsx b/src/components/Upload/Initial/index.jsx index f7d9a92..2ce5684 100644 --- a/src/components/Upload/Initial/index.jsx +++ b/src/components/Upload/Initial/index.jsx @@ -34,7 +34,10 @@ export default function Initial({ api, onError, onUpload, templateId }) { }; const upload = (files) => { - const allowedMineTypes = ["application/vnd.ms-excel", "text/csv"]; + const allowedMineTypes = [ + "application/vnd.ms-excel", + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ]; if (files && files[0] && allowedMineTypes.indexOf(files[0].type) !== -1) onUpload(files[0]); else setInvalidFileExtension(true); diff --git a/src/components/Upload/Message/style.module.scss b/src/components/Upload/Message/style.module.scss index 96e3e0a..2ab5910 100644 --- a/src/components/Upload/Message/style.module.scss +++ b/src/components/Upload/Message/style.module.scss @@ -20,4 +20,5 @@ padding: 0 20% 20px; max-width: 100%; box-sizing: border-box; + text-align: center; } diff --git a/src/components/Upload/Progress/index.jsx b/src/components/Upload/Progress/index.jsx index d10e2cd..297c373 100644 --- a/src/components/Upload/Progress/index.jsx +++ b/src/components/Upload/Progress/index.jsx @@ -1,24 +1,20 @@ import React from "react"; import PT from "prop-types"; -import Button from "../../Button"; - import style from "./style.module.scss"; -export default function Progress({ onAbort, progress }) { +export default function Progress({ progress }) { return (
Uploading profiles...
-
); } Progress.propTypes = { - onAbort: PT.func.isRequired, progress: PT.number, }; diff --git a/src/components/Upload/index.jsx b/src/components/Upload/index.jsx index f10a0ba..b853027 100644 --- a/src/components/Upload/index.jsx +++ b/src/components/Upload/index.jsx @@ -1,6 +1,5 @@ import React from "react"; -import axios from "axios"; import FormData from "form-data"; import PT from "prop-types"; @@ -37,20 +36,17 @@ export default function Upload({ api, templateId }) { const upload = async (file) => { const data = new FormData(); data.append("upload", file); - const source = axios.CancelToken.source(); setState({ type: STATES.UPLOADING, - data: { abort: source.cancel, progress: 0 }, + data: { progress: 0 }, }); try { - const res = await api.upload(data, { - cancelToken: source.token, + await api.upload(data, { onUploadProgress: ({ loaded, total }) => { setState({ type: STATES.UPLOADING, data: { progress: loaded / total, - abort: source.cancel, }, }); }, @@ -58,13 +54,13 @@ export default function Upload({ api, templateId }) { setState({ type: STATES.MESSAGE, data: { - title: "Import Confirmation", - message: JSON.stringify(res), + title: "Profiles uploaded successfully", + message: + "The uploaded profiles are now being processed. This may take some time, depending on the number of profiles. Once processed, you will be able to see the profiles in the search page.", }, }); } catch (error) { - if (error instanceof axios.Cancel) setState({ type: STATES.INITIAL }); - else showError(error); + showError(error); } }; @@ -90,9 +86,7 @@ export default function Upload({ api, templateId }) { ); break; case STATES.UPLOADING: - content = ( - - ); + content = ; break; default: throw Error("Invalid state"); diff --git a/src/config.js b/src/config.js index 98b3f22..dea6ee3 100644 --- a/src/config.js +++ b/src/config.js @@ -8,5 +8,5 @@ export default { SEARCH_UI_API_URL: process.env.REACT_APP_SEARCH_UI_API_URL || "https://u-bahn-search-ui-api-dev.herokuapp.com/api", - BULK_UPLOAD_TEMPLATE_ID: process.env.REACT_BULK_UPLOAD_TEMPLATE_ID, + BULK_UPLOAD_TEMPLATE_ID: process.env.REACT_APP_BULK_UPLOAD_TEMPLATE_ID, }; diff --git a/src/pages/Search/index.jsx b/src/pages/Search/index.jsx index bf00f20..2e36458 100644 --- a/src/pages/Search/index.jsx +++ b/src/pages/Search/index.jsx @@ -17,6 +17,8 @@ import Api from "../../services/api"; import style from "./style.module.scss"; import { useSearch, FILTERS } from "../../lib/search"; import { makeColorIterator, avatarColors } from "../../lib/colors"; +import config from "../../config"; + const colorIterator = makeColorIterator(avatarColors); const NESTEDPROPERTIES = [ @@ -276,7 +278,9 @@ export default function SearchPage() { ); break; case TABS.UPLOADS: - mainContent = ; + mainContent = ( + + ); break; default: throw Error("Invalid tab"); diff --git a/src/pages/Search/style.module.scss b/src/pages/Search/style.module.scss index 769a5b3..da61c6d 100644 --- a/src/pages/Search/style.module.scss +++ b/src/pages/Search/style.module.scss @@ -53,6 +53,7 @@ .mainArea { display: flex; padding-top: 30px; + margin-bottom: 100px; } .pills { diff --git a/src/services/api.js b/src/services/api.js index bf9fc60..747dad6 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -84,7 +84,7 @@ export default class Api { */ async getTemplate(templateId) { const url = `${config.SEARCH_UI_API_URL}/templates/${templateId}`; - const { data } = await axios(url); + const { data } = await this.api(url); return data; } @@ -165,18 +165,19 @@ export default class Api { * @param {object} [options] * @param {function} [options.onUploadProgress] Optional. Upload progress * callback. - * @param {CancelToken} [options.cancelToken] Optional. Cancel token from - * axios. * @return {Promise} Resolves to the API response payload. */ async upload(data, options = {}) { - const res = await axios.post(`${config.SEARCH_UI_API_URL}/uploads`, data, { - cancelToken: options.cancelToken, - headers: { - "Content-Type": "multipart/form-data", - }, - onUploadProgress: options.onUploadProgress, - }); + const res = await this.api.post( + `${config.SEARCH_UI_API_URL}/uploads`, + data, + { + headers: { + "Content-Type": "multipart/form-data", + }, + onUploadProgress: options.onUploadProgress, + } + ); return res.data; } From 436d11e7849edd7681f624cea04674a05c8e106a Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Thu, 4 Jun 2020 16:23:37 +0530 Subject: [PATCH 003/224] Display message when searching for location yields no results --- src/components/tagList/index.js | 9 ++++++--- src/components/tagList/tagList.module.css | 10 ---------- src/components/tagList/tagList.module.scss | 20 ++++++++++++++++++++ 3 files changed, 26 insertions(+), 13 deletions(-) delete mode 100644 src/components/tagList/tagList.module.css create mode 100644 src/components/tagList/tagList.module.scss diff --git a/src/components/tagList/index.js b/src/components/tagList/index.js index bc44156..61cc0b0 100644 --- a/src/components/tagList/index.js +++ b/src/components/tagList/index.js @@ -4,7 +4,7 @@ import Tag from "../tag"; import { useSearch } from "../../lib/search"; -import styles from "./tagList.module.css"; +import styles from "./tagList.module.scss"; export default function TagList({ tags, selected, selector }) { const search = useSearch(); @@ -27,7 +27,7 @@ export default function TagList({ tags, selected, selector }) { search[selector](selection); }; - const handleShowMores = () => { + const handleShowMore = () => { setShowAll(true); }; @@ -65,7 +65,10 @@ export default function TagList({ tags, selected, selector }) { return null; })} {!showAll && tags.length > 10 && ( - + + )} + {tags.length === 0 && ( + No location found )} ); diff --git a/src/components/tagList/tagList.module.css b/src/components/tagList/tagList.module.css deleted file mode 100644 index 11111b2..0000000 --- a/src/components/tagList/tagList.module.css +++ /dev/null @@ -1,10 +0,0 @@ -/* TagList */ -div.tagList { - margin-left: -8px; - margin-top: 11px; -} - -div.tagList > button { - margin-left: 8px; - margin-top: 8px; -} diff --git a/src/components/tagList/tagList.module.scss b/src/components/tagList/tagList.module.scss new file mode 100644 index 0000000..4faef4f --- /dev/null +++ b/src/components/tagList/tagList.module.scss @@ -0,0 +1,20 @@ +@import "../../styles/mixins.scss"; + +/* TagList */ +div.tagList { + margin-left: -8px; + margin-top: 11px; +} + +div.tagList > button { + margin-left: 8px; + margin-top: 8px; +} + +.message { + @include textTypeB; + padding: 0 20% 20px; + max-width: 100%; + box-sizing: border-box; + text-align: center; +} From 75d5d9cfbf69801ee02bc78123724c610f79d9c8 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Thu, 4 Jun 2020 17:26:05 +0530 Subject: [PATCH 004/224] Display message when there are no search results in filter --- src/components/FiltersSideMenu/filters.js | 3 +++ src/components/tagList/index.js | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/FiltersSideMenu/filters.js b/src/components/FiltersSideMenu/filters.js index 6ef747f..2c21e06 100644 --- a/src/components/FiltersSideMenu/filters.js +++ b/src/components/FiltersSideMenu/filters.js @@ -122,6 +122,7 @@ export default function SearchTabFilters({ locations, skills, achievements }) { tags={locationsData} selected={search.selectedLocations} selector={"selectLocations"} + noResultsText={"No location found"} /> @@ -153,6 +154,7 @@ export default function SearchTabFilters({ locations, skills, achievements }) { tags={skillsData} selected={search.selectedSkills} selector={"selectSkills"} + noResultsText={"No skill found"} /> @@ -171,6 +173,7 @@ export default function SearchTabFilters({ locations, skills, achievements }) { tags={achievementsData} selected={search.selectedAchievements} selector={"selectAchievements"} + noResultsText={"No achievement found"} /> diff --git a/src/components/tagList/index.js b/src/components/tagList/index.js index 61cc0b0..d18fd6e 100644 --- a/src/components/tagList/index.js +++ b/src/components/tagList/index.js @@ -6,7 +6,7 @@ import { useSearch } from "../../lib/search"; import styles from "./tagList.module.scss"; -export default function TagList({ tags, selected, selector }) { +export default function TagList({ tags, selected, selector, noResultsText }) { const search = useSearch(); const [selectedTags, setSelectedTags] = useState(selected); @@ -68,7 +68,7 @@ export default function TagList({ tags, selected, selector }) { )} {tags.length === 0 && ( - No location found + {noResultsText} )} ); From e3a90bdc035ed246e064391bf0c9d30f0805aa0f Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Fri, 5 Jun 2020 15:05:08 +0530 Subject: [PATCH 005/224] Refactor code to restrict user model to the Profile card instead of the entire page. Also, allow update to availability of user --- src/components/GroupsSideMenu/index.jsx | 13 +- src/components/ProfileCard/index.js | 342 +++++++++++++++--------- src/config.js | 6 + src/pages/Search/index.jsx | 60 ++--- src/services/api.js | 68 ++++- 5 files changed, 307 insertions(+), 182 deletions(-) diff --git a/src/components/GroupsSideMenu/index.jsx b/src/components/GroupsSideMenu/index.jsx index 3f78d32..49b60e3 100644 --- a/src/components/GroupsSideMenu/index.jsx +++ b/src/components/GroupsSideMenu/index.jsx @@ -4,22 +4,17 @@ import PT from "prop-types"; import style from "./style.module.scss"; import GroupTabFilters from "./filters"; -export default function GroupsSideMenu({ userGroups, allGroups, profiles }) { - const [data, setData] = useState(profiles); +export default function GroupsSideMenu({ userGroups, allGroups }) { const [userGroupsData, setUserGroupsData] = useState(userGroups); const [allGroupsData, setAllGroupsData] = useState(userGroups); useEffect(() => { - setData(profiles); setUserGroupsData(userGroups); setAllGroupsData(userGroups); - }, [profiles, userGroups, allGroups]); + }, [userGroups, allGroups]); const handleGroupSelected = (group) => { - const newData = profiles.filter((item) => { - return item.groups && item.groups.indexOf(group.name) > -1; - }); - setData(newData); + // TODO - When a group is selected, make the search request again }; return ( @@ -27,7 +22,6 @@ export default function GroupsSideMenu({ userGroups, allGroups, profiles }) { @@ -37,5 +31,4 @@ export default function GroupsSideMenu({ userGroups, allGroups, profiles }) { GroupsSideMenu.propTypes = { userGroups: PT.array.isRequired, allGroups: PT.array.isRequired, - profiles: PT.array.isRequired, }; diff --git a/src/components/ProfileCard/index.js b/src/components/ProfileCard/index.js index cdf496b..f0fbf32 100644 --- a/src/components/ProfileCard/index.js +++ b/src/components/ProfileCard/index.js @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import PT from "prop-types"; import Switch from "../Switch"; @@ -10,146 +10,242 @@ import EditProfileModal from "../EditProfileModal"; import styles from "./profileCard.module.css"; import iconStyles from "../../styles/icons.module.css"; +import config from "../../config"; + +/** + * Returns the availability of the user + * Availability is an attribute on the user and thus + * needs to be extracted from the user's profile + * @param {Object} profile The user profile + * @param {String} attributeName The attribute for which the value is requested + */ +function getAttributeDetails(profile, attributeName) { + const detail = profile.attributes.find( + (a) => a.attribute.name === attributeName + ); + + switch (attributeName) { + case config.PRIMARY_ATTRIBUTES.availability: + return { + id: detail.attribute.id, + value: detail.value === "true", + }; + case config.PRIMARY_ATTRIBUTES.company: + case config.PRIMARY_ATTRIBUTES.location: + return { + id: detail.attribute.id, + value: detail.value, + }; + default: + throw Error(`Unknown attribute ${attributeName}`); + } +} + +/** + * Returns the initials for the user using the user name + * @param {String} userName The user name + */ +function getUserNameInitial(userName) { + let initials = userName.match(/\b\w/g) || []; + initials = ((initials.shift() || "") + (initials.pop() || "")).toUpperCase(); + + return initials; +} + /** * ProfileCard - a profile card component * profile: the user profile * avatarColor: the color of the avatar */ -export default function ProfileCard({ - api, - stripped, - profile, - avatarColor, - updateUser, -}) { - let initials = profile.name.match(/\b\w/g) || []; - initials = ((initials.shift() || "") + (initials.pop() || "")).toUpperCase(); - const [profileData, setProfileData] = useState(profile); - const [showAddToGroup, setShowAddToGroup] = React.useState(false); - const [showEditModal, setShowEditModal] = React.useState(false); - const [available, setAvailable] = React.useState( - profile.isAvailable === "true" - ); +class ProfileCard extends React.Component { + constructor(props) { + super(props); - useEffect(() => { - setProfileData(profile); - setAvailable(profile.isAvailable === "true"); - }, [profile]); - - const switchAvailability = (profile) => { - const updated = profile; - updated.isAvailable = updated.isAvailable === "true" ? "false" : "true"; - setAvailable(updated.isAvailable === "true"); - // TODO - This seems to call the generic update user api - // TODO - However, we need to update the attribute associated with availability - // TODO - Availability is NOT a property on the User model. It exists as an attribute - // TODO - of the user and is part of the UserAttribute + Attribute model where the - // TODO - id exists under attribute and the value under user attribute - // TODO - Thus, update this api call to update the attribute value and not the user model - // updateUser(updated); - }; - - const removeGroupFromProfile = (group) => { - const updated = profile; - updated.groups = updated.groups.filter((g) => g !== group); - updateUser(updated); - }; - - let containerStyle = styles.profileCard; - if (stripped) containerStyle += ` ${styles.stripped}`; + const { profile } = props; - return ( -
- {showAddToGroup ? ( - setShowAddToGroup(false)} - updateUser={updateUser} - user={profile} - /> - ) : null} - {showEditModal ? ( - setShowEditModal(false)} - updateUser={updateUser} - user={profile} - /> - ) : null} -
-
-
-
{initials}
-
-
-
- {available ? "Available" : "Unavailable"} + // The profile data structure received is converted to a format + // that is easy to use for rendering the UI + const user = { + id: profile.id, + handle: profile.handle, + firstName: profile.firstName, + lastName: profile.lastName, + groups: [], // TODO + skills: [], // TODO + achievement: [], // TODO + role: "", // TODO + availability: getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.availability + ), + company: getAttributeDetails(profile, config.PRIMARY_ATTRIBUTES.company), + location: getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.location + ), + customAttributes: [], + }; + + this.state = { + user, + showManageGroupsModal: false, + showEditUserModal: false, + }; + } + + /** + * Shows / hides the manage groups modal + */ + toggleManageGroupsModal() { + this.setState((prevState) => ({ + showManageGroupsModal: !prevState.showManageGroupsModal, + })); + } + + /** + * Shows / hides the edit user modal + */ + toggleEditUserModal() { + this.setState((prevState) => ({ + showEditUserModal: !prevState.showEditUserModal, + })); + } + + toggleUserAvailability() { + this.setState( + (prevState) => { + const user = JSON.parse(JSON.stringify(prevState.user)); + + user.availability.value = !user.availability.value; + + return { user }; + }, + () => this.updateUserAttribute(config.PRIMARY_ATTRIBUTES.availability) + ); + } + + async updateUserAttribute(attributeName) { + let payload = {}; + const { user } = this.state; + + switch (attributeName) { + case config.PRIMARY_ATTRIBUTES.availability: + payload.userId = user.id; + payload.attributeId = user.availability.id; + payload.value = user.availability.value ? "true" : "false"; + break; + default: + throw Error(`Unknown attribute name ${attributeName}`); + } + + await this.props.api.updateUserAttribute(payload); + } + + render() { + const { api, stripped, avatarColor } = this.props; + const { user, showManageGroupsModal, showEditUserModal } = this.state; + + console.log(user.availability); + + let containerStyle = styles.profileCard; + + if (stripped) { + containerStyle += ` ${styles.stripped}`; + } + + return ( +
+ {showManageGroupsModal ? ( + this.toggleManageGroupsModal()} + // TODO updateUser={updateUser} + user={user} + /> + ) : null} + {showEditUserModal ? ( + this.toggleEditUserModal()} + // TODO updateUser={updateUser} + user={user} + /> + ) : null} +
+
+
+
+ {getUserNameInitial(`${user.firstName} ${user.lastName}`)} +
+
+
+
+ {user.availability.value ? "Available" : "Unavailable"} +
+ this.toggleUserAvailability()} + /> + this.toggleEditUserModal()} />
- {/* switchAvailability(profileData)} />*/} - switchAvailability(profileData)} - /> - setShowEditModal(true)} />
-
-
-
-
-
{profileData.name}
- {!!profileData.rating && ( -
{profileData.rating}
- )} -
-
-
{profileData.handle}
-
-
-
{profileData.title}
-
-
-
{profileData.company}
+
+
+
+
{`${user.firstName} ${user.lastName}`}
+
+
+
{user.handle}
+
+
+
{user.role}
+
+
+
{user.company.value}
+
-
-
-
-
-
-
Group
-
-
- {profileData.groups.map((group, index) => { - return ( - removeGroupFromProfile(group)} - /> - ); - })} +
+
+
+
+
Group
+
+
+ {user.groups.map((group, index) => { + return ( + removeGroupFromProfile(group)} + /> + ); + })} -
setShowAddToGroup(!showAddToGroup)} - > -
-
+
setShowAddToGroup(!showAddToGroup)} + > +
+
+
-
- ); + ); + } } ProfileCard.propTypes = { @@ -163,3 +259,5 @@ function EditButton({ onClick }) {
); } + +export default ProfileCard; diff --git a/src/config.js b/src/config.js index dea6ee3..7f1782e 100644 --- a/src/config.js +++ b/src/config.js @@ -9,4 +9,10 @@ export default { process.env.REACT_APP_SEARCH_UI_API_URL || "https://u-bahn-search-ui-api-dev.herokuapp.com/api", BULK_UPLOAD_TEMPLATE_ID: process.env.REACT_APP_BULK_UPLOAD_TEMPLATE_ID, + + PRIMARY_ATTRIBUTES: { + availability: "isAvailable", + company: "company", + location: "location", + }, }; diff --git a/src/pages/Search/index.jsx b/src/pages/Search/index.jsx index 2e36458..ab781b4 100644 --- a/src/pages/Search/index.jsx +++ b/src/pages/Search/index.jsx @@ -33,7 +33,7 @@ const NESTEDPROPERTIES = [ const USERATTRIBUTES = ["isAvailable", "company", "location"]; export default function SearchPage() { - const [api] = React.useState(() => new Api({ token: "dummy-auth-token" })); + const [api] = React.useState(() => new Api()); const [page, setPage] = React.useState(1); const byPage = 12; const [totalResults, setTotalResults] = React.useState(0); @@ -78,7 +78,7 @@ export default function SearchPage() { searchContext.filters[FILTERS.SKILLS].active && searchContext.selectedSkills.length > 0 ) { - criteria.skills = searchContext.selectedSkills; + criteria["skill.name"] = searchContext.selectedSkills; } if ( searchContext.filters[FILTERS.ACHIEVEMENTS].active && @@ -115,31 +115,31 @@ export default function SearchPage() { sortBy, }); - data = data.map((p) => { - NESTEDPROPERTIES.forEach((nestedProperty) => { - if (!p[nestedProperty]) { - p[nestedProperty] = []; - } - }); + // data = data.map((p) => { + // NESTEDPROPERTIES.forEach((nestedProperty) => { + // if (!p[nestedProperty]) { + // p[nestedProperty] = []; + // } + // }); - // TODO - In the original code, p.role used to exist and read from - // TODO - attributes. Roles is property on the user object itself and one - // TODO - need not read from attribute. So I need to figure out how to accommodate it + // // TODO - In the original code, p.role used to exist and read from + // // TODO - attributes. Roles is property on the user object itself and one + // // TODO - need not read from attribute. So I need to figure out how to accommodate it - if (p.attributes) { - for (let i = 0; i < p.attributes.length; i++) { - const userAttribute = p.attributes[i]; + // if (p.attributes) { + // for (let i = 0; i < p.attributes.length; i++) { + // const userAttribute = p.attributes[i]; - if (USERATTRIBUTES.includes(userAttribute.attribute.name)) { - p[userAttribute.attribute.name] = userAttribute.value; - } - } - } + // if (USERATTRIBUTES.includes(userAttribute.attribute.name)) { + // p[userAttribute.attribute.name] = userAttribute.value; + // } + // } + // } - p.name = `${p.firstName} ${p.lastName}`; + // p.name = `${p.firstName} ${p.lastName}`; - return p; - }); + // return p; + // }); const locations = await api.getLocations(); const skills = await api.getSkills(); @@ -158,7 +158,6 @@ export default function SearchPage() { })(); }, [api, search, page, byPage, sortBy, searchContext]); - let filteredUsers = users; // if (tab === TABS.GROUPS) { // const currentGroup = (groups.find(g => g.current) || {}).name; // if (currentGroup) { @@ -168,8 +167,6 @@ export default function SearchPage() { // } // } - const visibleUsers = filteredUsers; - const handleSort = (attr) => { setSortBy(attr); }; @@ -188,11 +185,7 @@ export default function SearchPage() { achievements={achievements} /> ) : ( - + )}
@@ -248,7 +241,7 @@ export default function SearchPage() {
- {visibleUsers.map((user, index) => { + {users.map((user, index) => { const nextColor = colorIterator.next(); return ( { - const u = [...users]; - u[index] = await api.updateUser(updatedUser); - setUsers(u); - }} /> ); })} diff --git a/src/services/api.js b/src/services/api.js index 747dad6..d3c09d9 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -13,13 +13,16 @@ export default class Api { * @param {string} [token] Optional. The authentication token to use with API * requests. */ - constructor({ token }) { + constructor() { this.api = axios.create({ headers: { "Content-Type": "application/json", }, }); - if (token) this.api.defaults.headers.common.Authorization = token; + + // TODO - Replace with actual token once login is integrated + this.api.defaults.headers.common.Authorization = + process.env.REACT_APP_DEV_TOKEN; this.mocks = { groups: [ @@ -36,6 +39,19 @@ export default class Api { this.mocks.groups.sort(); } + /** + * Returns the attribute id and the value on the user + * @param {Object} user The user object + * @param {String} attributeName The attribute for which we need details about + */ + _getAttributeDetails(user, attributeName) { + const detail = user.attributes.find( + (a) => a.attribute.name === attributeName + ); + + return { id: detail.attribute.id, value: detail.value }; + } + /** * Creates a new group. * @param {string} group @@ -122,9 +138,9 @@ export default class Api { roleId, page, limit, - criteria, sortBy, enrich: true, + ...criteria, }, }); @@ -133,30 +149,54 @@ export default class Api { return { total, data }; } + async updateUserAttribute(attribute) { + console.log(attribute); + const url = `${config.API_URL}/users/${attribute.userId}/attributes/${attribute.attributeId}`; + const payload = { value: attribute.value }; + + await this.api.patch(url, payload); + } + /** * Stores updated user object into API. * @param {object} user + * @param {String} attribute Which attribute on the user gets updated * @return {Promise} Resolves to the resulting user object. */ - async updateUser(user) { - const entity = { ...user }; - delete entity.id; + async updateUser(user, attribute) { + let url; + let payload; + let response; - let data; + const { id } = user; + switch (attribute) { + case config.PRIMARY_ATTRIBUTES.availability: + const detail = this._getAttributeDetails( + user, + config.PRIMARY_ATTRIBUTES.availability + ); + url = `${config.API_URL}/users/${id}/attributes/${detail.id}`; + payload = { + value: detail.value, + }; + break; + default: + throw Error(`Unsupported attribute ${attribute}`); + } + + console.log(url, payload); + + return user; try { - const response = await this.api.patch( - `${config.API_URL}/users/${user.id}`, - entity - ); - data = response.data; + response = await this.api.patch(url, payload); } catch (error) { // TODO - What should happen when update fails? const mockData = { ...user }; - data = mockData; + response = { data: mockData }; } - return data; + return response.data; } /** From 8e03f654a1a1f365d8f5379c5b1fe0c6df7c57cd Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Fri, 5 Jun 2020 20:57:19 +0530 Subject: [PATCH 006/224] Allow updates to availability, name, company and location --- package.json | 1 + src/components/EditProfileModal/index.jsx | 66 ++++--- src/components/ProfileCard/index.js | 228 +++++++++++++++++----- src/config.js | 2 + src/pages/Search/index.jsx | 26 --- src/services/api.js | 51 ++--- 6 files changed, 237 insertions(+), 137 deletions(-) diff --git a/package.json b/package.json index 4e54b5d..5d6dcdb 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "form-data": "^3.0.0", "husky": "^4.2.5", "lint-staged": "^10.2.6", + "lodash": "^4.17.15", "node-sass": "^4.14.1", "prettier": "^2.0.5", "prop-types": "^15.7.2", diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index bc59725..2b0ed06 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -9,27 +9,29 @@ import ProfileCard from "../ProfileCard"; import style from "./style.module.scss"; -import { makeColorIterator, avatarColors } from "../../lib/colors"; -const colorIterator = makeColorIterator(avatarColors); -const nextColor = colorIterator.next(); - // TODO - Role is not an attribute but a nested property on user // TODO - Remove it from Common Attribute and use it like other nested attributes (like skill / achievements) const COMMON_ATTRIBUTES = ["role", "company", "location", "isAvailable"]; export default function EditProfileModal({ api, onCancel, updateUser, user }) { - const [localUser, setLocalUser] = React.useState({ ...user }); + const [localUser, setLocalUser] = React.useState(user); const [skillInputValue, setSkillInputValue] = React.useState(""); const [achievementInputValue, setAchievementInputValue] = React.useState(""); + const updateUserFromChild = (userDataFromChild) => { + setLocalUser(userDataFromChild); + }; + return (
@@ -38,6 +40,7 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) {

Custom attributes

- {localUser.attributes + {localUser.customAttributes .filter((attr) => !COMMON_ATTRIBUTES.includes(attr.attributeName)) .map((attr, key) => ( { setLocalUser({ ...localUser, - attributes: localUser.attributes.map((el) => + attributes: localUser.customAttributes.map((el) => el.attributeName === attr.attributeName ? { ...el, value: target.value } : el @@ -240,7 +250,7 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { }); setImmediate(() => target.focus()); }} - value={localUser.attributes[key].value} + value={localUser.customAttributes[key].value} /> ))}
diff --git a/src/components/ProfileCard/index.js b/src/components/ProfileCard/index.js index f0fbf32..6a45632 100644 --- a/src/components/ProfileCard/index.js +++ b/src/components/ProfileCard/index.js @@ -1,5 +1,6 @@ import React from "react"; import PT from "prop-types"; +import _ from "lodash"; import Switch from "../Switch"; import Tag, { TAG_ICONS } from "../tag"; @@ -62,36 +63,48 @@ class ProfileCard extends React.Component { constructor(props) { super(props); - const { profile } = props; - - // The profile data structure received is converted to a format - // that is easy to use for rendering the UI - const user = { - id: profile.id, - handle: profile.handle, - firstName: profile.firstName, - lastName: profile.lastName, - groups: [], // TODO - skills: [], // TODO - achievement: [], // TODO - role: "", // TODO - availability: getAttributeDetails( - profile, - config.PRIMARY_ATTRIBUTES.availability - ), - company: getAttributeDetails(profile, config.PRIMARY_ATTRIBUTES.company), - location: getAttributeDetails( - profile, - config.PRIMARY_ATTRIBUTES.location - ), - customAttributes: [], - }; + const { profile, avatarColor, formatData } = props; + let user; + + if (formatData) { + // The profile data structure received from api is converted to a format + // that is easy to use for rendering the UI as well as updating the fields + user = { + id: profile.id, + handle: profile.handle, + firstName: profile.firstName, + lastName: profile.lastName, + groups: [], // TODO + skills: [], // TODO + achievements: [], // TODO + roles: [], // TODO + isAvailable: getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.availability + ), + company: getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.company + ), + location: getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.location + ), + customAttributes: [], + avatarColor, + }; + } else { + // Data is already in the format seen above. No further processing needed + user = profile; + } this.state = { user, showManageGroupsModal: false, showEditUserModal: false, }; + + this.updateUserFromChild = this.updateUserFromChild.bind(this); } /** @@ -112,12 +125,16 @@ class ProfileCard extends React.Component { })); } + /** + * Switch between availability and unavailability of user + * ! Will call api after state update, to update database with new value + */ toggleUserAvailability() { this.setState( (prevState) => { const user = JSON.parse(JSON.stringify(prevState.user)); - user.availability.value = !user.availability.value; + user.isAvailable.value = !user.isAvailable.value; return { user }; }, @@ -125,15 +142,130 @@ class ProfileCard extends React.Component { ); } + /** + * Update the state of user from child components + * Function called by child components to keep data in sync + * @param {Object} newUser The user object + */ + updateUserFromChild(newUser) { + let updatedUser = {}; + let updatedKeys = []; + const { user: oldUser } = JSON.parse(JSON.stringify(this.state)); + + delete oldUser.id; + + const userKeys = Object.keys(oldUser); + + for (let i = 0; i < userKeys.length; i++) { + if (newUser[userKeys[i]]) { + if (!_.isEqual(oldUser[userKeys[i]], newUser[userKeys[i]])) { + updatedUser[userKeys[i]] = newUser[userKeys[i]]; + updatedKeys.push(userKeys[i]); + } + } + } + + this.setState( + { + user: Object.assign(this.state.user, updatedUser), + }, + () => this.updateUser(updatedKeys) + ); + } + + /** + * Will call individual apis to update the user data in the database + * @param {Array} changedKeys The properties on the user object that have changed + */ + async updateUser(changedKeys) { + const { user } = this.state; + let updatedName = false; + let payload; + + for (let i = 0; i < changedKeys.length; i++) { + switch (changedKeys[i]) { + case config.PRIMARY_ATTRIBUTES.availability: + case config.PRIMARY_ATTRIBUTES.company: + case config.PRIMARY_ATTRIBUTES.location: + await this.updateUserAttribute(changedKeys[i]); + + break; + case config.PRIMARY_ATTRIBUTES.firstName: + // Combine updates to first and last name (since they are on the same model) + if (!updatedName) { + if (changedKeys.includes(config.PRIMARY_ATTRIBUTES.lastName)) { + payload = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + }; + updatedName = true; + } else { + payload = { + id: user.id, + firstName: user.firstName, + }; + } + + await this.props.api.updateUser(payload); + } + + break; + case config.PRIMARY_ATTRIBUTES.lastName: + // Combine updates to first and last name (since they are on the same model) + if (!updatedName) { + if (changedKeys.includes(config.PRIMARY_ATTRIBUTES.firstName)) { + payload = { + id: user.id, + firstName: user.firstName, + lastName: user.lastName, + }; + updatedName = true; + } else { + payload = { + id: user.id, + lastName: user.lastName, + }; + } + + await this.props.api.updateUser(payload); + } + + break; + default: + // For now, until all key updates are implemented, we do nothing + // TODO throw Error(`Unknown attribute ${changedKeys[i]}`); + } + } + } + + /** + * Updates an attribute of the user + * ! Will call api + * @param {String} attributeName The attribute to update + */ async updateUserAttribute(attributeName) { let payload = {}; const { user } = this.state; + // For the edit user modal - changes are not saved by the card, but by the modal itself + if (!this.props.saveChanges) { + this.props.updateUser(user); + + return; + } + switch (attributeName) { case config.PRIMARY_ATTRIBUTES.availability: payload.userId = user.id; - payload.attributeId = user.availability.id; - payload.value = user.availability.value ? "true" : "false"; + payload.attributeId = user.isAvailable.id; + payload.value = user.isAvailable.value ? "true" : "false"; + break; + case config.PRIMARY_ATTRIBUTES.company: + case config.PRIMARY_ATTRIBUTES.location: + payload.userId = user.id; + payload.attributeId = user[attributeName].id; + payload.value = user[attributeName].value; break; default: throw Error(`Unknown attribute name ${attributeName}`); @@ -146,8 +278,6 @@ class ProfileCard extends React.Component { const { api, stripped, avatarColor } = this.props; const { user, showManageGroupsModal, showEditUserModal } = this.state; - console.log(user.availability); - let containerStyle = styles.profileCard; if (stripped) { @@ -168,7 +298,7 @@ class ProfileCard extends React.Component { this.toggleEditUserModal()} - // TODO updateUser={updateUser} + updateUser={this.updateUserFromChild} user={user} /> ) : null} @@ -184,10 +314,10 @@ class ProfileCard extends React.Component {
- {user.availability.value ? "Available" : "Unavailable"} + {user.isAvailable.value ? "Available" : "Unavailable"}
this.toggleUserAvailability()} /> this.toggleEditUserModal()} /> @@ -202,7 +332,7 @@ class ProfileCard extends React.Component { >{`${user.firstName} ${user.lastName}`}
-
{user.handle}
+
@{user.handle}
{user.role}
@@ -219,18 +349,19 @@ class ProfileCard extends React.Component {
Group
- {user.groups.map((group, index) => { - return ( - removeGroupFromProfile(group)} - /> - ); - })} + {user.groups && + user.groups.map((group, index) => { + return ( + removeGroupFromProfile(group)} + /> + ); + })}
{ - // NESTEDPROPERTIES.forEach((nestedProperty) => { - // if (!p[nestedProperty]) { - // p[nestedProperty] = []; - // } - // }); - - // // TODO - In the original code, p.role used to exist and read from - // // TODO - attributes. Roles is property on the user object itself and one - // // TODO - need not read from attribute. So I need to figure out how to accommodate it - - // if (p.attributes) { - // for (let i = 0; i < p.attributes.length; i++) { - // const userAttribute = p.attributes[i]; - - // if (USERATTRIBUTES.includes(userAttribute.attribute.name)) { - // p[userAttribute.attribute.name] = userAttribute.value; - // } - // } - // } - - // p.name = `${p.firstName} ${p.lastName}`; - - // return p; - // }); - const locations = await api.getLocations(); const skills = await api.getSkills(); const achievements = await api.getAchievements(); diff --git a/src/services/api.js b/src/services/api.js index d3c09d9..d35a0f0 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -149,8 +149,11 @@ export default class Api { return { total, data }; } + /** + * Updates attributes on a user + * @param {Object} attribute The attribute object containing all the details to update the attribute + */ async updateUserAttribute(attribute) { - console.log(attribute); const url = `${config.API_URL}/users/${attribute.userId}/attributes/${attribute.attributeId}`; const payload = { value: attribute.value }; @@ -158,45 +161,17 @@ export default class Api { } /** - * Stores updated user object into API. - * @param {object} user - * @param {String} attribute Which attribute on the user gets updated - * @return {Promise} Resolves to the resulting user object. + * Updates properties that directly exist on the user object + * @param {Object} user The data that the user needs to be updated to */ - async updateUser(user, attribute) { - let url; - let payload; - let response; - - const { id } = user; - switch (attribute) { - case config.PRIMARY_ATTRIBUTES.availability: - const detail = this._getAttributeDetails( - user, - config.PRIMARY_ATTRIBUTES.availability - ); - url = `${config.API_URL}/users/${id}/attributes/${detail.id}`; - payload = { - value: detail.value, - }; - break; - default: - throw Error(`Unsupported attribute ${attribute}`); - } - - console.log(url, payload); - - return user; - - try { - response = await this.api.patch(url, payload); - } catch (error) { - // TODO - What should happen when update fails? - const mockData = { ...user }; - response = { data: mockData }; - } + async updateUser(user) { + const url = `${config.API_URL}/users/${user.id}`; + const payload = { + firstName: user.firstName, + lastName: user.lastName, + }; - return response.data; + await this.api.patch(url, payload); } /** From 5d447bacd92d9bf9ab821f0ac7164ffe198d4c80 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Fri, 5 Jun 2020 21:09:58 +0530 Subject: [PATCH 007/224] trigger heroku build --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3a9e752..dfbd0ab 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ $ npm install // Will install the dependenceis ``` -followed by +followed by: ```bash $ npm start From bd70a91fd324bfbb2de8d5f19fa0ebe6ae832e9b Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Sat, 6 Jun 2020 19:10:32 +0530 Subject: [PATCH 008/224] Read and allow updates to user role (title attribute) --- src/components/EditProfileModal/index.jsx | 12 +++++------- src/components/ProfileCard/index.js | 7 +++++-- src/config.js | 1 + 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index 2b0ed06..0dcec93 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -77,16 +77,14 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { onChange={({ target }) => { setLocalUser({ ...localUser, - attributes: localUser.customAttributes.map((el) => - el.attributeName === "role" - ? { ...el, value: target.value } - : el - ), - title: target.value, + title: { + id: localUser.title.id, + value: target.value, + }, }); setImmediate(() => target.focus()); }} - value={localUser.title} + value={localUser.title.value} /> @{user.handle}
-
{user.role}
+
{user.title.value}
{user.company.value}
diff --git a/src/config.js b/src/config.js index a6673d9..4ec5957 100644 --- a/src/config.js +++ b/src/config.js @@ -16,5 +16,6 @@ export default { location: "location", firstName: "firstName", lastName: "lastName", + title: "title", }, }; From cb6d7ddc5d563c54befc539cab83a2ed682e04a1 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Sat, 6 Jun 2020 19:26:46 +0530 Subject: [PATCH 009/224] Display achievements for a user --- src/components/EditProfileModal/index.jsx | 64 +++---------------- .../EditProfileModal/style.module.scss | 7 ++ src/components/ProfileCard/index.js | 13 +++- 3 files changed, 28 insertions(+), 56 deletions(-) diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index 0dcec93..697cbc4 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -16,7 +16,6 @@ const COMMON_ATTRIBUTES = ["role", "company", "location", "isAvailable"]; export default function EditProfileModal({ api, onCancel, updateUser, user }) { const [localUser, setLocalUser] = React.useState(user); const [skillInputValue, setSkillInputValue] = React.useState(""); - const [achievementInputValue, setAchievementInputValue] = React.useState(""); const updateUserFromChild = (userDataFromChild) => { setLocalUser(userDataFromChild); @@ -174,60 +173,15 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) {

Achievements

- { - if (key === "Enter") { - const achie = achievementInputValue.trim(); - if (achie) { - setLocalUser({ - ...localUser, - achievements: [ - ...localUser.achievements, - { - name: achie, - }, - ], - }); - } - setAchievementInputValue(""); - } - }} - onChange={({ target }) => { - let { value } = target; - if (value.endsWith(",")) { - const achie = value.slice(0, -1).trim(); - if (achie) { - setLocalUser({ - ...localUser, - achievements: [ - ...localUser.achievements, - { - name: achie, - }, - ], - }); - } - value = ""; - } - setAchievementInputValue(value); - setImmediate(() => target.focus()); - }} - value={achievementInputValue} - /> - {localUser.achievements.map((it, key) => ( - { - setLocalUser({ - ...localUser, - achievements: localUser.achievements.filter((a) => a !== it), - }); - }} - /> - ))} + {localUser.achievements.length > 0 && + localUser.achievements.map((value, key) => ( + + ))} + {localUser.achievements.length === 0 && ( + + {"This user has no achievements"} + + )}

Custom attributes

diff --git a/src/components/EditProfileModal/style.module.scss b/src/components/EditProfileModal/style.module.scss index d4458f3..b4204fd 100644 --- a/src/components/EditProfileModal/style.module.scss +++ b/src/components/EditProfileModal/style.module.scss @@ -77,3 +77,10 @@ background: $blue; } } + +.message { + @include textTypeB; + padding: 3px; + max-width: 100%; + box-sizing: border-box; +} diff --git a/src/components/ProfileCard/index.js b/src/components/ProfileCard/index.js index be2851e..c2803f1 100644 --- a/src/components/ProfileCard/index.js +++ b/src/components/ProfileCard/index.js @@ -43,6 +43,17 @@ function getAttributeDetails(profile, attributeName) { } } +/** + * Returns the user's achievements + */ +function getAchievements(profile) { + const achievements = profile.achievements + ? profile.achievements.map((a) => a.name) + : []; + + return achievements; +} + /** * Returns the initials for the user using the user name * @param {String} userName The user name @@ -77,7 +88,7 @@ class ProfileCard extends React.Component { lastName: profile.lastName, groups: [], // TODO skills: [], // TODO - achievements: [], // TODO + achievements: getAchievements(profile), title: getAttributeDetails(profile, config.PRIMARY_ATTRIBUTES.title), isAvailable: getAttributeDetails( profile, From db93c5798d319d624d6a91f5839b6aa5ff581d25 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Sat, 6 Jun 2020 20:41:54 +0530 Subject: [PATCH 010/224] Integrate delete user feature --- src/components/EditProfileModal/index.jsx | 17 +++- src/components/ProfileCard/helper.js | 53 ++++++++++ src/components/ProfileCard/index.js | 99 +++++++------------ .../ProfileCard/profileCard.module.css | 20 ++++ src/services/api.js | 6 ++ 5 files changed, 130 insertions(+), 65 deletions(-) create mode 100644 src/components/ProfileCard/helper.js diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index 697cbc4..fa1c905 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -13,7 +13,13 @@ import style from "./style.module.scss"; // TODO - Remove it from Common Attribute and use it like other nested attributes (like skill / achievements) const COMMON_ATTRIBUTES = ["role", "company", "location", "isAvailable"]; -export default function EditProfileModal({ api, onCancel, updateUser, user }) { +export default function EditProfileModal({ + api, + onCancel, + updateUser, + user, + deleteUser, +}) { const [localUser, setLocalUser] = React.useState(user); const [skillInputValue, setSkillInputValue] = React.useState(""); @@ -21,6 +27,12 @@ export default function EditProfileModal({ api, onCancel, updateUser, user }) { setLocalUser(userDataFromChild); }; + const confirmDeleteUser = () => { + if (window.confirm("Are you sure you want to delete this user?")) { + deleteUser(); + } + }; + return ( ))}
- @@ -218,5 +230,6 @@ EditProfileModal.propTypes = { api: PT.shape().isRequired, onCancel: PT.func.isRequired, updateUser: PT.func.isRequired, + deleteUser: PT.func.isRequired, user: PT.shape().isRequired, }; diff --git a/src/components/ProfileCard/helper.js b/src/components/ProfileCard/helper.js new file mode 100644 index 0000000..338b6a8 --- /dev/null +++ b/src/components/ProfileCard/helper.js @@ -0,0 +1,53 @@ +import config from "../../config"; + +/** + * Returns the availability of the user + * Availability is an attribute on the user and thus + * needs to be extracted from the user's profile + * @param {Object} profile The user profile + * @param {String} attributeName The attribute for which the value is requested + */ +export function getAttributeDetails(profile, attributeName) { + const detail = profile.attributes.find( + (a) => a.attribute.name === attributeName + ); + + switch (attributeName) { + case config.PRIMARY_ATTRIBUTES.availability: + return { + id: detail.attribute.id, + value: detail.value === "true", + }; + case config.PRIMARY_ATTRIBUTES.title: + case config.PRIMARY_ATTRIBUTES.company: + case config.PRIMARY_ATTRIBUTES.location: + return { + id: detail.attribute.id, + value: detail.value, + }; + default: + throw Error(`Unknown attribute ${attributeName}`); + } +} + +/** + * Returns the user's achievements + */ +export function getAchievements(profile) { + const achievements = profile.achievements + ? profile.achievements.map((a) => a.name) + : []; + + return achievements; +} + +/** + * Returns the initials for the user using the user name + * @param {String} userName The user name + */ +export function getUserNameInitial(userName) { + let initials = userName.match(/\b\w/g) || []; + initials = ((initials.shift() || "") + (initials.pop() || "")).toUpperCase(); + + return initials; +} diff --git a/src/components/ProfileCard/index.js b/src/components/ProfileCard/index.js index c2803f1..6dd5f48 100644 --- a/src/components/ProfileCard/index.js +++ b/src/components/ProfileCard/index.js @@ -13,63 +13,7 @@ import iconStyles from "../../styles/icons.module.css"; import config from "../../config"; -/** - * Returns the availability of the user - * Availability is an attribute on the user and thus - * needs to be extracted from the user's profile - * @param {Object} profile The user profile - * @param {String} attributeName The attribute for which the value is requested - */ -function getAttributeDetails(profile, attributeName) { - const detail = profile.attributes.find( - (a) => a.attribute.name === attributeName - ); - - switch (attributeName) { - case config.PRIMARY_ATTRIBUTES.availability: - return { - id: detail.attribute.id, - value: detail.value === "true", - }; - case config.PRIMARY_ATTRIBUTES.title: - case config.PRIMARY_ATTRIBUTES.company: - case config.PRIMARY_ATTRIBUTES.location: - return { - id: detail.attribute.id, - value: detail.value, - }; - default: - throw Error(`Unknown attribute ${attributeName}`); - } -} - -/** - * Returns the user's achievements - */ -function getAchievements(profile) { - const achievements = profile.achievements - ? profile.achievements.map((a) => a.name) - : []; - - return achievements; -} - -/** - * Returns the initials for the user using the user name - * @param {String} userName The user name - */ -function getUserNameInitial(userName) { - let initials = userName.match(/\b\w/g) || []; - initials = ((initials.shift() || "") + (initials.pop() || "")).toUpperCase(); - - return initials; -} - -/** - * ProfileCard - a profile card component - * profile: the user profile - * avatarColor: the color of the avatar - */ +import * as cardHelper from "./helper"; class ProfileCard extends React.Component { constructor(props) { @@ -88,22 +32,28 @@ class ProfileCard extends React.Component { lastName: profile.lastName, groups: [], // TODO skills: [], // TODO - achievements: getAchievements(profile), - title: getAttributeDetails(profile, config.PRIMARY_ATTRIBUTES.title), - isAvailable: getAttributeDetails( + achievements: cardHelper.getAchievements(profile), + title: cardHelper.getAttributeDetails( + profile, + config.PRIMARY_ATTRIBUTES.title + ), + isAvailable: cardHelper.getAttributeDetails( profile, config.PRIMARY_ATTRIBUTES.availability ), - company: getAttributeDetails( + company: cardHelper.getAttributeDetails( profile, config.PRIMARY_ATTRIBUTES.company ), - location: getAttributeDetails( + location: cardHelper.getAttributeDetails( profile, config.PRIMARY_ATTRIBUTES.location ), customAttributes: [], avatarColor, + // Indicates if the user has been deleted. The user is still shown in this case, but with a + // clear indicator about its deleted status. + isDeleted: false, }; } else { // Data is already in the format seen above. No further processing needed @@ -117,6 +67,7 @@ class ProfileCard extends React.Component { }; this.updateUserFromChild = this.updateUserFromChild.bind(this); + this.deleteUser = this.deleteUser.bind(this); } /** @@ -288,6 +239,20 @@ class ProfileCard extends React.Component { await this.props.api.updateUserAttribute(payload); } + /** + * Deletes the user + * ! Will call api + */ + async deleteUser() { + await this.props.api.deleteUser({ id: this.state.user.id }); + + this.toggleEditUserModal(); + + this.setState({ + user: Object.assign(this.state.user, { isDeleted: true }), + }); + } + render() { const { api, stripped, avatarColor } = this.props; const { user, showManageGroupsModal, showEditUserModal } = this.state; @@ -314,6 +279,7 @@ class ProfileCard extends React.Component { onCancel={() => this.toggleEditUserModal()} updateUser={this.updateUserFromChild} user={user} + deleteUser={this.deleteUser} /> ) : null}
@@ -323,7 +289,9 @@ class ProfileCard extends React.Component { style={{ backgroundColor: avatarColor }} >
- {getUserNameInitial(`${user.firstName} ${user.lastName}`)} + {cardHelper.getUserNameInitial( + `${user.firstName} ${user.lastName}` + )}
@@ -388,6 +356,11 @@ class ProfileCard extends React.Component {
+ {user.isDeleted && ( +
+ This user has been deleted +
+ )} ); } diff --git a/src/components/ProfileCard/profileCard.module.css b/src/components/ProfileCard/profileCard.module.css index 045619b..fb695ba 100644 --- a/src/components/ProfileCard/profileCard.module.css +++ b/src/components/ProfileCard/profileCard.module.css @@ -14,6 +14,8 @@ display: inline-flex; flex-direction: column; + + position: relative; } .profileCard.stripped { @@ -35,6 +37,24 @@ display: none; } +.deletedCard { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.8); + border-radius: inherit; + text-align: center; + display: table; +} + +.deletedCard span { + display: table-cell; + vertical-align: middle; + font-family: Helvetica; + font-size: 16px; + color: white; +} + .profileCardHeaderContainer { margin-bottom: 0; align-self: flex-start; diff --git a/src/services/api.js b/src/services/api.js index d35a0f0..c8fea49 100644 --- a/src/services/api.js +++ b/src/services/api.js @@ -174,6 +174,12 @@ export default class Api { await this.api.patch(url, payload); } + async deleteUser(user) { + const url = `${config.API_URL}/users/${user.id}`; + + await this.api.delete(url); + } + /** * Uploads profiles to import. * @param {FormData} data Payload to upload. From ff93dda3eb95e3e36ff8ad8e91697a15501ebf25 Mon Sep 17 00:00:00 2001 From: Mithun Kamath Date: Sun, 7 Jun 2020 16:17:35 +0530 Subject: [PATCH 011/224] 1. Support filters for location, availability, skill and achievements 2. Support sorty by name, location and availability --- src/components/FiltersSideMenu/filters.js | 1 + src/components/Modal/style.module.scss | 4 +- src/components/Pagination/index.jsx | 22 +- src/components/availability/index.js | 2 + src/config.js | 4 + src/lib/search.js | 14 ++ src/pages/Search/index.jsx | 239 +++++++++++++--------- src/pages/Search/style.module.scss | 22 +- src/services/api.js | 69 +++++-- 9 files changed, 242 insertions(+), 135 deletions(-) diff --git a/src/components/FiltersSideMenu/filters.js b/src/components/FiltersSideMenu/filters.js index 2c21e06..86dc464 100644 --- a/src/components/FiltersSideMenu/filters.js +++ b/src/components/FiltersSideMenu/filters.js @@ -209,6 +209,7 @@ function Summary({ filtersApplied }) { isAvailableSelected: false, isUnavailableSelected: false, }); + search.changePageNumber(1); }; return ( diff --git a/src/components/Modal/style.module.scss b/src/components/Modal/style.module.scss index ed031d9..313d774 100644 --- a/src/components/Modal/style.module.scss +++ b/src/components/Modal/style.module.scss @@ -16,7 +16,7 @@ position: fixed; top: 0; width: 100%; - z-index: 998; + z-index: 999; } .container { @@ -32,5 +32,5 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - z-index: 999; + z-index: 1000; } diff --git a/src/components/Pagination/index.jsx b/src/components/Pagination/index.jsx index 29e39cb..df27fd5 100644 --- a/src/components/Pagination/index.jsx +++ b/src/components/Pagination/index.jsx @@ -2,13 +2,16 @@ import React from "react"; import PT from "prop-types"; import style from "./style.module.scss"; +import { useSearch } from "../../lib/search"; + +export default function Pagination({ currentPage, itemsPerPage, numPages }) { + const search = useSearch(); -export default function Pagination({ currentPage, byPage, numPages, onPage }) { const buttons = [ ); } - if (currentPage < numPages / byPage - 2) { + if (currentPage < numPages / itemsPerPage - 2) { buttons.push( ... @@ -55,10 +58,10 @@ export default function Pagination({ currentPage, byPage, numPages, onPage }) { buttons.push( ); @@ -17,8 +17,10 @@ Button.propTypes = { children: PT.string, className: PT.string, onClick: PT.func, + disabled: PT.bool, }; Button.defaultProps = { children: "Button", + disabled: false, }; diff --git a/src/components/EditProfileModal/index.jsx b/src/components/EditProfileModal/index.jsx index fa1c905..8c08f68 100644 --- a/src/components/EditProfileModal/index.jsx +++ b/src/components/EditProfileModal/index.jsx @@ -14,7 +14,6 @@ import style from "./style.module.scss"; const COMMON_ATTRIBUTES = ["role", "company", "location", "isAvailable"]; export default function EditProfileModal({ - api, onCancel, updateUser, user, @@ -22,9 +21,17 @@ export default function EditProfileModal({ }) { const [localUser, setLocalUser] = React.useState(user); const [skillInputValue, setSkillInputValue] = React.useState(""); + const [isSavingChanges, setIsSavingChanges] = React.useState(false); const updateUserFromChild = (userDataFromChild) => { - setLocalUser(userDataFromChild); + // Only availability can be updated + setLocalUser({ + ...localUser, + isAvailable: { + id: localUser.isAvailable.id, + value: userDataFromChild.isAvailable.value, + }, + }); }; const confirmDeleteUser = () => { @@ -36,7 +43,6 @@ export default function EditProfileModal({ return (

Edit Profile

- +

General

@@ -227,7 +237,6 @@ export default function EditProfileModal({ } EditProfileModal.propTypes = { - api: PT.shape().isRequired, onCancel: PT.func.isRequired, updateUser: PT.func.isRequired, deleteUser: PT.func.isRequired, diff --git a/src/components/EditProfileModal/style.module.scss b/src/components/EditProfileModal/style.module.scss index b4204fd..cd85da1 100644 --- a/src/components/EditProfileModal/style.module.scss +++ b/src/components/EditProfileModal/style.module.scss @@ -78,6 +78,13 @@ } } +.disabledButton { + background: $blue !important; + border: none; + color: $white; + margin-left: 20px; +} + .message { @include textTypeB; padding: 3px; diff --git a/src/components/Header/index.jsx b/src/components/Header/index.jsx index 9575f11..d094486 100644 --- a/src/components/Header/index.jsx +++ b/src/components/Header/index.jsx @@ -1,7 +1,7 @@ import React from "react"; import PT from "prop-types"; -import Api from "../../services/api"; +import staticData from "../../services/static-data"; import { ReactComponent as DownArrow } from "../../assets/images/down-arrow.svg"; import { ReactComponent as SearchTabIcon } from "../../assets/images/search-tab-icon.svg"; @@ -20,7 +20,6 @@ export const TABS = { }; export default function Header({ - api, currentTab, onSearch, onTabChange, @@ -32,9 +31,9 @@ export default function Header({ React.useEffect(() => { (async () => { - setOrg(await api.getOrganization(organizationId)); + setOrg(await staticData.getOrganization(organizationId)); })(); - }, [api, organizationId]); + }, [organizationId]); const handleSearch = (value) => { value = value || searchText; @@ -125,7 +124,6 @@ export default function Header({ } Header.propTypes = { - api: PT.instanceOf(Api).isRequired, currentTab: PT.oneOf(Object.values(TABS)), onSearch: PT.func.isRequired, onTabChange: PT.func.isRequired, diff --git a/src/components/Modal/index.jsx b/src/components/Modal/index.jsx index 389fc72..25f5da0 100644 --- a/src/components/Modal/index.jsx +++ b/src/components/Modal/index.jsx @@ -4,7 +4,7 @@ import PT from "prop-types"; import style from "./style.module.scss"; -export default function Modal({ children, className, onCancel }) { +export default function Modal({ children, className }) { const [portal, setPortal] = React.useState(); React.useEffect(() => { @@ -32,10 +32,6 @@ export default function Modal({ children, className, onCancel }) {