diff --git a/README.md b/README.md index 2cd51e4..d4cd21d 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Make sure you have [Heroky CLI](https://devcenter.heroku.com/articles/heroku-cli This app exports functions to be imported by other microapps. - `login` - redirects to login page +- `businessLogin` - redirects to business (i.e. customer) login page - `logout` - clears session storage and redirects to logout page - `setAppMenu` - sets sidebar menu for the app by app's `path` - `getAuthUserTokens` - returns a promise which resolves to object with user tokens `{ tokenV3, tokenV2 }` @@ -95,6 +96,7 @@ For example see https://github.com/topcoder-platform/micro-frontends-react-app ```js module.exports = { login: () => {}, + businessLogin: () => {}, logout: () => {}, setAppMenu: () => {}, getAuthUserTokens: () => new Promise(() => {}), @@ -121,6 +123,7 @@ For example see https://github.com/topcoder-platform/micro-frontends-angular-app ```js declare module '@topcoder/micro-frontends-navbar-app' { export const login: any; + export const businessLogin: any; export const logout: any; export const setAppMenu: any; export const getAuthUserTokens: any; diff --git a/config/dev.js b/config/dev.js index 68dd4ca..c0320e9 100644 --- a/config/dev.js +++ b/config/dev.js @@ -5,6 +5,7 @@ module.exports = { TC_NOTIFICATION_URL: "https://api.topcoder-dev.com/v5/notifications", CONNECT_DOMAIN: "https://connect.topcoder-dev.com", COMMUNITY_DOMAIN: "https://www.topcoder-dev.com", + TAAS_APP: "https://platform.topcoder-dev.com/taas/myteams", }, API: { V3: "https://api.topcoder-dev.com/v3", diff --git a/config/prod.js b/config/prod.js index 8054083..ee7b020 100644 --- a/config/prod.js +++ b/config/prod.js @@ -5,6 +5,7 @@ module.exports = { TC_NOTIFICATION_URL: "https://api.topcoder.com/v5/notifications", CONNECT_DOMAIN: "https://connect.topcoder.com", COMMUNITY_DOMAIN: "https://www.topcoder.com", + TAAS_APP: "https://platform.topcoder.com/taas/myteams", }, API: { V3: "https://api.topcoder.com/v3", diff --git a/jest.config.js b/jest.config.js index add373e..0b2e6e7 100644 --- a/jest.config.js +++ b/jest.config.js @@ -6,7 +6,7 @@ module.exports = { transformIgnorePatterns: ["node_modules/?!(tc-auth-lib)"], moduleNameMapper: { "\\.(css|scss)$": "identity-obj-proxy", - "\\.svg$": "/__mocks__/fileMock.js", + "\\.(png|eot|otf|ttf|woff|woff2|svg)$": "/__mocks__/fileMock.js", }, setupFilesAfterEnv: [ "../node_modules/@testing-library/jest-dom/dist/index.js", diff --git a/package-lock.json b/package-lock.json index 82e5885..0b73009 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5569,9 +5569,9 @@ "dev": true }, "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -5727,15 +5727,16 @@ } }, "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", + "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", "dev": true, "requires": { "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" + "css-what": "^5.0.0", + "domhandler": "^4.2.0", + "domutils": "^2.6.0", + "nth-check": "^2.0.0" } }, "css-selector-tokenizer": { @@ -5749,9 +5750,9 @@ } }, "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", + "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", "dev": true }, "css.escape": { @@ -6098,9 +6099,9 @@ "dev": true }, "dns-packet": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.1.tgz", - "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", + "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", "dev": true, "requires": { "ip": "^1.1.0", @@ -6157,21 +6158,14 @@ } }, "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", "dev": true, "requires": { "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - } } }, "domain-browser": { @@ -6181,9 +6175,9 @@ "dev": true }, "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", "dev": true }, "domexception": { @@ -6196,22 +6190,23 @@ } }, "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.0.tgz", + "integrity": "sha512-zk7sgt970kzPks2Bf+dwT/PLzghLnsivb9CcxkvR8Mzr66Olr0Ofd8neSbglHJHaHa2MadfoSdNlKYAaafmWfA==", "dev": true, "requires": { - "domelementtype": "1" + "domelementtype": "^2.2.0" } }, "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.7.0.tgz", + "integrity": "sha512-8eaHa17IwJUPAiB+SoTYBo5mCdeMgdcAoXJ59m6DT1vw+5iLS3gNoqYaRowaBKtGVrOF1Jz4yDTgYKLK2kvfJg==", "dev": true, "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" } }, "dot-case": { @@ -8414,36 +8409,15 @@ } }, "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", "dev": true, "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" } }, "http-deceiver": { @@ -8553,9 +8527,9 @@ }, "dependencies": { "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -13570,12 +13544,12 @@ } }, "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", + "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", "dev": true, "requires": { - "boolbase": "~1.0.0" + "boolbase": "^1.0.0" } }, "num2fraction": { @@ -14345,9 +14319,9 @@ } }, "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "version": "7.0.36", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.36.tgz", + "integrity": "sha512-BebJSIUMwJHRH0HAQoxN4u1CN86glsrwsW0q7T+/m44eXOUAxSNdHRkNZPYz5vVUbg17hFgOQDE7fZk7li3pZw==", "dev": true, "requires": { "chalk": "^2.4.2", @@ -15396,16 +15370,16 @@ "dev": true }, "renderkid": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.5.tgz", - "integrity": "sha512-ccqoLg+HLOHq1vdfYNm4TBeaCDIi1FLt3wGojTDSvdewUv65oTmI3cnT2E4hRjl1gzKZIPK+KZrXzlUYKnR+vQ==", + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", + "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", "dev": true, "requires": { - "css-select": "^2.0.2", - "dom-converter": "^0.2", - "htmlparser2": "^3.10.1", - "lodash": "^4.17.20", - "strip-ansi": "^3.0.0" + "css-select": "^4.1.3", + "dom-converter": "^0.2.0", + "htmlparser2": "^6.1.0", + "lodash": "^4.17.21", + "strip-ansi": "^3.0.1" } }, "repeat-element": { @@ -18606,9 +18580,9 @@ } }, "ws": { - "version": "7.4.5", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.5.tgz", - "integrity": "sha512-xzyu3hFvomRfXKH8vOFMU3OguG6oOvhXMo3xsGy3xWExqaM2dxBbVxuD99O7m3ZUFMvvscsZDqxfgMaRr/Nr1g==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.1.tgz", + "integrity": "sha512-2c6faOUH/nhoQN6abwMloF7Iyl0ZS2E9HGtsiLrWn0zOOMWlhtDmdf/uihDt6jnuCxgtwGBNy6Onsoy2s2O2Ow==", "dev": true }, "xml-name-validator": { diff --git a/server.js b/server.js index e3e2927..1e44b08 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ /* global process */ -const express = require('express'); +const express = require("express"); const app = express(); @@ -7,11 +7,11 @@ app.use( "/navbar", express.static("./dist", { setHeaders: function setHeaders(res) { - res.header('Access-Control-Allow-Origin', '*'); - res.header('Access-Control-Allow-Methods', 'GET'); + res.header("Access-Control-Allow-Origin", "*"); + res.header("Access-Control-Allow-Methods", "GET"); res.header( - 'Access-Control-Allow-Headers', - 'Origin, X-Requested-With, Content-Type, Accept' + "Access-Control-Allow-Headers", + "Origin, X-Requested-With, Content-Type, Accept" ); }, }) diff --git a/src/actions/notifications.js b/src/actions/notifications.js index ca1bab3..073d4da 100644 --- a/src/actions/notifications.js +++ b/src/actions/notifications.js @@ -19,7 +19,6 @@ import { NOTIFICATIONS_PENDING, SET_NOTIFICATION_PLATFORM, RESET_NOTIFICATIONS, - RESET_COMMUNITY_NOTIFICATIONS, } from "../constants/notifications"; import notificationsService from "../services/notifications"; import { @@ -83,6 +82,25 @@ export const getNotifications = () => (dispatch) => { }); }; +export const getTaaSNotifications = () => (dispatch) => { + dispatch({ type: GET_NOTIFICATIONS_PENDING }); + notificationsService + .getTaaSNotifications() + .then((notifications) => { + dispatch({ + type: GET_NOTIFICATIONS_SUCCESS, + payload: notifications, + }); + }) + .catch((err) => { + dispatch({ + type: GET_NOTIFICATIONS_FAILURE, + payload: err, + }); + console.error(`Failed to load notifications. ${err.message}`); + }); +}; + export const getCommunityNotifications = () => (dispatch) => { dispatch({ type: GET_COMMUNITY_NOTIFICATIONS_PENDING }); notificationsService @@ -243,10 +261,6 @@ export const resetNotifications = () => (dispatch) => { dispatch({ type: RESET_NOTIFICATIONS }); }; -export const resetCommunityNotifications = () => (dispatch) => { - dispatch({ type: RESET_COMMUNITY_NOTIFICATIONS }); -}; - export default { getNotifications, getCommunityNotifications, @@ -262,5 +276,4 @@ export default { markNotificationsRead, setNotificationPlatform, resetNotifications, - resetCommunityNotifications, }; diff --git a/src/components/Menu/index.jsx b/src/components/Menu/index.jsx index 408758c..c941bb0 100644 --- a/src/components/Menu/index.jsx +++ b/src/components/Menu/index.jsx @@ -3,18 +3,18 @@ * * General component to show menu with submenu. */ -import React, { Fragment, useCallback, useState } from 'react'; -import { useLocation } from '@reach/router'; -import cn from 'classnames'; -import { includes, map } from 'lodash'; -import NavLink from '../NavLink'; -import './styles.css'; +import React, { Fragment, useCallback, useState } from "react"; +import { useLocation } from "@reach/router"; +import cn from "classnames"; +import { includes, map } from "lodash"; +import NavLink from "../NavLink"; +import "./styles.css"; const SubMenu = ({ option }) => { const location = useLocation(); const [isOpen, setIsOpen] = useState( - includes(map(option.children, 'path'), location.pathname) + includes(map(option.children, "path"), location.pathname) ); const toggleOpen = useCallback(() => { @@ -24,8 +24,8 @@ const SubMenu = ({ option }) => { return ( <> (
diff --git a/src/constants/apps.js b/src/constants/apps.js index a5b3c37..b941000 100644 --- a/src/constants/apps.js +++ b/src/constants/apps.js @@ -1,13 +1,13 @@ /** * Config for the All Apps menu. */ -import appDocumentationIcon from '../assets/images/learn.svg'; -import appTaasIcon from '../assets/images/integrations.svg'; -import appTaasAdminIcon from '../assets/images/taas-admin.png'; -import myteamsIcon from '../assets/images/my-teams.svg'; -import myteamsGreenIcon from '../assets/images/my-teams-green.svg'; -import createTeamIcon from '../assets/images/create-team.svg'; -import createTeamGreenIcon from '../assets/images/create-team-green.svg'; +import appDocumentationIcon from "../assets/images/learn.svg"; +import appTaasIcon from "../assets/images/integrations.svg"; +import appTaasAdminIcon from "../assets/images/taas-admin.png"; +import myteamsIcon from "../assets/images/my-teams.svg"; +import myteamsGreenIcon from "../assets/images/my-teams-green.svg"; +import createTeamIcon from "../assets/images/create-team.svg"; +import createTeamGreenIcon from "../assets/images/create-team-green.svg"; import earnIcon from "../assets/images/earn.svg"; /** @@ -15,23 +15,23 @@ import earnIcon from "../assets/images/earn.svg"; */ export const APP_CATEGORIES = [ { - category: 'Manage', + category: "Manage", apps: [ { - title: 'TaaS', + title: "TaaS", icon: appTaasIcon, - path: '/taas', + path: "/taas", menu: [ { - title: 'My Teams', - path: '/taas/myteams', + title: "My Teams", + path: "/taas/myteams", icon: myteamsIcon, activeIcon: myteamsGreenIcon, isExact: false, }, { - title: 'Create New Team', - path: '/taas/createnewteam', + title: "Create New Team", + path: "/taas/createnewteam", icon: createTeamIcon, activeIcon: createTeamGreenIcon, isExact: false, @@ -39,14 +39,14 @@ export const APP_CATEGORIES = [ ], }, { - title: 'TaaS Admin', + title: "TaaS Admin", icon: appTaasAdminIcon, - path: '/taas-admin', + path: "/taas-admin", menu: [], - roles: ["bookingmanager","administrator"], + roles: ["bookingmanager", "administrator"], }, { - title: 'Documentation', + title: "Documentation", icon: appDocumentationIcon, path: "/model", menu: [], @@ -57,7 +57,7 @@ export const APP_CATEGORIES = [ path: "/community-admin", menu: [], roles: ["Community Admin"], - } + }, ], }, { diff --git a/src/constants/notifications.js b/src/constants/notifications.js index 4b39ebd..ade9978 100644 --- a/src/constants/notifications.js +++ b/src/constants/notifications.js @@ -23,7 +23,6 @@ export const NOTIFICATIONS_PENDING = "NOTIFICATIONS_PENDING"; export const MARK_NOTIFICATIONS_READ = "MARK_NOTIFICATIONS_READ"; export const SET_NOTIFICATION_PLATFORM = "SET_NOTIFICATION_PLATFORM"; export const RESET_NOTIFICATIONS = "RESET_NOTIFICATIONS"; -export const RESET_COMMUNITY_NOTIFICATIONS = "RESET_COMMUNITY_NOTIFICATIONS"; /* * Project member role @@ -76,7 +75,7 @@ export const NOTIFICATIONS_LIMIT = 1000; export const PLATFORM = { CONNECT: "connect", COMMUNITY: "community", - BOTH: "connect+community", + TAAS: "taas", }; // Notifications event types @@ -141,6 +140,13 @@ export const EVENT_TYPE = { COMPLETED: "challenge.notification.completed", }, BROADCAST: "admin.notification.broadcast", + TAAS: { + POST_INTERVIEW_ACTION_REQUIRED: + "taas.notification.post-interview-action-required", + RESOURCE_BOOKING_EXPIRATION: + "taas.notification.resource-booking-expiration", + RESOURCE_BOOKING_PLACED: "taas.notification.resource-booking-placed", + }, }; export const NOTIFICATION_TYPE = { @@ -152,6 +158,7 @@ export const NOTIFICATION_TYPE = { MEMBER_ADDED: "member-added", CHALLENGE: "challenge", BROADCAST: "broadcast", + TAAS: "taas", }; /* @@ -169,6 +176,8 @@ export const GOTO = { PHASE: `${config.URL.CONNECT_DOMAIN}/projects/{{projectId}}/plan#phase-{{phaseId}}`, TOPCODER_TEAM: `${config.URL.CONNECT_DOMAIN}/projects/{{projectId}}#manageTopcoderTeam`, CHALLENGE: `${config.URL.COMMUNITY_DOMAIN}/challenges/{{id}}`, + TAAS_CANDIDATES_INTERVIEWS: `${config.URL.TAAS_APP}/{{projectId}}/positions/{{jobId}}/candidates/interviews`, + TAAS_PROJECT: `${config.URL.TAAS_APP}/{{projectId}}`, }; // each notification can be displayed differently depend on WHO see them @@ -1226,6 +1235,8 @@ export const NOTIFICATIONS = [ ], }, + /// Community notification rules + { eventType: EVENT_TYPE.CHALLENGE.ACTIVE, type: NOTIFICATION_TYPE.CHALLENGE, @@ -1258,6 +1269,47 @@ export const NOTIFICATIONS = [ }, ], }, + + /// TaaS notification rules + + { + version: 1, + eventType: EVENT_TYPE.TAAS.POST_INTERVIEW_ACTION_REQUIRED, + type: NOTIFICATION_TYPE.TAAS, + rules: [ + { + text: "Candidate action required for {{userHandle}} in job {{jobTitle}} of the team {{teamName}}", + shouldBundle: false, + goTo: GOTO.TAAS_CANDIDATES_INTERVIEWS, + }, + ], + }, + + { + version: 1, + eventType: EVENT_TYPE.TAAS.RESOURCE_BOOKING_EXPIRATION, + type: NOTIFICATION_TYPE.TAAS, + rules: [ + { + text: "{{numOfExpiringResourceBookings}} resource booking{{pluralize numOfExpiringResourceBookings '' 's'}} {{pluralize numOfExpiringResourceBookings 'is' 'are'}} expiring in the team {{teamName}}", + shouldBundle: false, + goTo: GOTO.TAAS_PROJECT, + }, + ], + }, + + { + version: 1, + eventType: EVENT_TYPE.TAAS.RESOURCE_BOOKING_PLACED, + type: NOTIFICATION_TYPE.TAAS, + rules: [ + { + text: "Resource {{userHandle}} is placed for the job {{jobTitle}} of the team {{teamName}}", + shouldBundle: false, + goTo: GOTO.TAAS_PROJECT, + }, + ], + }, ]; // list of ignored notifications diff --git a/src/containers/NotificationsContainer/index.jsx b/src/containers/NotificationsContainer/index.jsx index 93f2278..2e52611 100644 --- a/src/containers/NotificationsContainer/index.jsx +++ b/src/containers/NotificationsContainer/index.jsx @@ -279,7 +279,7 @@ class NotificationsContainer extends Component { render() { const { notifications, communityNotifications, ...restProps } = this.props; const preRenderedNotifications = preRenderNotifications(notifications); - const preRenderedNotifications2 = preRenderCommunityNotifications( + const preRenderedCommunityNotifications = preRenderCommunityNotifications( communityNotifications ); @@ -288,7 +288,7 @@ class NotificationsContainer extends Component { {...{ ...restProps, notifications: preRenderedNotifications, - communityNotifications: preRenderedNotifications2, + communityNotifications: preRenderedCommunityNotifications, }} /> ); diff --git a/src/containers/NotificationsDropdownContainer/index.jsx b/src/containers/NotificationsDropdownContainer/index.jsx index f580b5a..f41f607 100644 --- a/src/containers/NotificationsDropdownContainer/index.jsx +++ b/src/containers/NotificationsDropdownContainer/index.jsx @@ -11,6 +11,7 @@ import { TransitionGroup, Transition } from "react-transition-group"; import { getNotifications, getCommunityNotifications, + getTaaSNotifications, toggleNotificationSeen, markAllNotificationsRead, markAllNotificationsSeen, @@ -19,7 +20,6 @@ import { viewOlderNotifications, hideOlderNotifications, resetNotifications, - resetCommunityNotifications, } from "../../actions/notifications"; import { splitNotificationsBySources, @@ -70,6 +70,7 @@ const NotificationsDropdownContainerView = (props) => { toggleNotificationsDropdownMobile, toggleNotificationsDropdownWeb, markAllNotificationsSeen, + platform, } = props; if ( (!initialized && isLoading) || @@ -154,8 +155,11 @@ const NotificationsDropdownContainerView = (props) => { ); if ( - (!isLoading && !initialized) || - (!isCommunityLoading && !communityInitialized) + (!isLoading && !initialized && platform == PLATFORM.CONNECT) || + (!isLoading && !initialized && platform == PLATFORM.TAAS) || + (!isCommunityLoading && + !communityInitialized && + platform == PLATFORM.COMMUNITY) ) { notificationsEmpty = ( @@ -543,10 +547,11 @@ class NotificationsDropdownContainer extends React.Component { } componentWillReceiveProps(nextProps) { - const { platform: oldPlatform } = this.props; + const { platform: oldPlatform, resetNotifications } = this.props; const { platform } = nextProps; if (platform !== oldPlatform) { + resetNotifications(); this.getPlatformNotifications(platform); } } @@ -555,23 +560,17 @@ class NotificationsDropdownContainer extends React.Component { const { getNotifications, getCommunityNotifications, + getTaaSNotifications, platform, - resetNotifications, - resetCommunityNotifications, } = this.props; p = p || platform; - if (p === PLATFORM.BOTH) { - resetNotifications(); - resetCommunityNotifications(); - getNotifications(); - getCommunityNotifications(); - } else if (p === PLATFORM.CONNECT) { - resetCommunityNotifications(); + if (p === PLATFORM.CONNECT) { getNotifications(); + } else if (p === PLATFORM.TAAS) { + getTaaSNotifications(); } else { - resetNotifications(); getCommunityNotifications(); } } @@ -619,6 +618,7 @@ const mapStateToProps = ({ notifications }) => notifications; const mapDispatchToProps = { getNotifications, getCommunityNotifications, + getTaaSNotifications, toggleNotificationSeen, markAllNotificationsRead, markAllNotificationsSeen, @@ -627,7 +627,6 @@ const mapDispatchToProps = { viewOlderNotifications, hideOlderNotifications, resetNotifications, - resetCommunityNotifications, }; export default connect( diff --git a/src/reducers/notifications.js b/src/reducers/notifications.js index f638f3e..7033fa0 100644 --- a/src/reducers/notifications.js +++ b/src/reducers/notifications.js @@ -20,7 +20,6 @@ import { SET_NOTIFICATION_PLATFORM, PLATFORM, RESET_NOTIFICATIONS, - RESET_COMMUNITY_NOTIFICATIONS, } from "../constants/notifications"; import _ from "lodash"; import { getActiveAndBroadcastNotifications } from "../utils/notifications"; @@ -266,14 +265,10 @@ export default (state = initialState, action) => { case RESET_NOTIFICATIONS: return { ...state, + filterBy: "", notifications: [], - sources: [], - }; - - case RESET_COMMUNITY_NOTIFICATIONS: - return { - ...state, communityNotifications: [], + sources: [], communitySources: [], }; diff --git a/src/services/notifications.js b/src/services/notifications.js index 93e10d6..3cd0f84 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -5,6 +5,7 @@ import { URL } from "../../config"; import { prepareNotifications, prepareCommunityNotifications, + prepareTaaSNotifications, } from "../utils/notifications"; const logger = console; @@ -51,6 +52,14 @@ const getNotifications = () => { .then((resp) => prepareNotifications(resp.data.items)); }; +const getTaaSNotifications = () => { + return axiosInstance + .get( + `${URL.TC_NOTIFICATION_URL}/list?read=false&platform=taas&per_page=${NOTIFICATIONS_LIMIT}` + ) + .then((resp) => prepareTaaSNotifications(resp.data.items)); +}; + const getCommunityNotifications = () => { return axiosInstance .get( @@ -64,4 +73,5 @@ export default { getCommunityNotifications, markNotificationsRead, markNotificationsSeen, + getTaaSNotifications, }; diff --git a/src/topcoder-micro-frontends-navbar-app.js b/src/topcoder-micro-frontends-navbar-app.js index 5e146bd..f60723b 100644 --- a/src/topcoder-micro-frontends-navbar-app.js +++ b/src/topcoder-micro-frontends-navbar-app.js @@ -19,7 +19,7 @@ import { setNotificationPlatform, } from "./utils/exports"; -import { login, logout } from "./utils"; +import { login, businessLogin, logout } from "./utils"; import { PLATFORM } from "./constants/notifications"; const lifecycles = singleSpaReact({ @@ -37,6 +37,7 @@ export const { bootstrap, mount, unmount } = lifecycles; // list everything we want to export for other microapps here export { login, + businessLogin, logout, setAppMenu, getAuthUserTokens, diff --git a/src/utils/index.js b/src/utils/index.js index 5f37fa2..30e94c6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -16,6 +16,14 @@ export const getLoginUrl = () => window.location.href.match(/[^?]*/)[0] )}`; +/** + * Generate Business Login URL + */ +export const getBusinessLoginUrl = () => + `${config.URL.AUTH}?regSource=taasApp&mode=login&retUrl=${encodeURIComponent( + window.location.href.match(/[^?]*/)[0] + )}`; + /** * Logout user from Topcoder */ @@ -30,3 +38,10 @@ export const logout = () => { export const login = () => { window.location = getLoginUrl(); }; + +/** + * Forward user to business login page + */ +export const businessLogin = () => { + window.location = getBusinessLoginUrl(); +}; diff --git a/src/utils/notifications.js b/src/utils/notifications.js index c382379..ff49cb7 100644 --- a/src/utils/notifications.js +++ b/src/utils/notifications.js @@ -59,9 +59,27 @@ const handlebarsFallbackHelper = (value, fallbackValue) => { return new Handlebars.SafeString(out); }; +/** + * Handlebars helper which displays single or plural noun + * + * Example: + * ``` + * {{pluralize count resource resources}} + * ``` + * Will output `resource` if `count` equals 0 or 1; otherwise `resources` + * + * @param {Number} count quantity + * @param {String} single noun + * @param {String} plural nouns + */ +const handlebarsPluralizeHelper = (number, single, plural) => { + return Math.abs(number) > 1 ? plural : single; +}; + // register handlebars helpers Handlebars.registerHelper("showMore", handlebarsShowMoreHelper); Handlebars.registerHelper("fallback", handlebarsFallbackHelper); +Handlebars.registerHelper("pluralize", handlebarsPluralizeHelper); export const renderGoTo = (goTo, contents) => { let goToHandlebars = ""; @@ -244,105 +262,6 @@ export const filterReadNotifications = (notifications) => export const filterSeenNotifications = (notifications) => _.filter(notifications, { seen: false }); -/** - * Filter notifications that belongs to project:projectId - * - * @param {Array} notifications list of notifications - * - * @param {Number} projectId - * - * @return {Array} notifications list filtered of notifications - */ -export const filterNotificationsByProjectId = (notifications, projectId) => - _.filter(notifications, (notification) => { - return notification.sourceId === `${projectId}`; - }); - -/** - * Filter notifications about Topic and Post changed - * - * @param {Array} notifications list of notifications - * @param {RegExp} [tagRegExp] regexp to filter notification by tags - * - * @return {Array} notifications list filtered of notifications - */ -export const filterTopicAndPostChangedNotifications = ( - notifications, - tagRegExp -) => { - let topicAndPostNotifications = _.filter( - notifications, - (notification) => - notification.eventType === EVENT_TYPE.TOPIC.CREATED || - notification.eventType === EVENT_TYPE.POST.CREATED || - notification.eventType === EVENT_TYPE.POST.UPDATED || - notification.eventType === EVENT_TYPE.POST.MENTION - ); - - // filter messages using `tags` - if (tagRegExp) { - topicAndPostNotifications = _.filter( - topicAndPostNotifications, - (notification) => { - const tags = _.get(notification, "contents.tags", []); - - return _.some(tags, (tag) => tagRegExp.test(tag)); - } - ); - } - - return topicAndPostNotifications; -}; - -/** - * Filter notifications about Link and File changed - * - * @param {Array} notifications list of notifications - * - * @return {Array} notifications list filtered of notifications - */ -export const filterFileAndLinkChangedNotifications = (notifications) => { - return _.filter( - notifications, - (notification) => - notification.eventType === EVENT_TYPE.PROJECT.FILE_UPLOADED || - notification.eventType === EVENT_TYPE.PROJECT.LINK_CREATED - ); -}; - -/** - * Filter notification about post mentions - * @param {Array} notifications list of notifications - * - * @return {Array} notifications list filtered by post mention event type - */ -export const filterPostsMentionNotifications = (notifications) => - _.filter(notifications, (notification) => { - return notification.eventType === EVENT_TYPE.POST.MENTION; - }); - -/** - * Filter notifications about the project - * - * @param {Array} notifications list of notifications - * - * @return {Array} notifications list filtered of notifications - */ -export const filterProjectNotifications = (notifications) => - _.filter(notifications, (notification) => { - return ( - notification.eventType === EVENT_TYPE.PROJECT.CREATED || - notification.eventType === EVENT_TYPE.PROJECT.APPROVED || - notification.eventType === EVENT_TYPE.PROJECT.PAUSED || - notification.eventType === EVENT_TYPE.PROJECT.COMPLETED || - notification.eventType === EVENT_TYPE.PROJECT.SPECIFICATION_MODIFIED || - notification.eventType === EVENT_TYPE.PROJECT.SUBMITTED_FOR_REVIEW || - notification.eventType === EVENT_TYPE.PROJECT.FILE_UPLOADED || - notification.eventType === EVENT_TYPE.PROJECT.CANCELED || - notification.eventType === EVENT_TYPE.PROJECT.LINK_CREATED - ); - }); - /** * Limits notifications quantity per source * @@ -669,7 +588,49 @@ export const preRenderNotifications = (notifications) => { return preRenderedNotifications; }; -//----- // +// --- TaaS --- // + +export const prepareTaaSNotifications = (rawNotifications) => { + const notifications = rawNotifications.map((rawNotification) => ({ + id: `${rawNotification.id}`, + sourceId: rawNotification.contents.projectId + ? `${rawNotification.contents.projectId}` + : "team", + sourceName: rawNotification.contents.projectId + ? rawNotification.contents.teamName || "Team" + : "Global", + eventType: rawNotification.type, + date: rawNotification.createdAt, + isRead: rawNotification.read, + seen: rawNotification.seen, + contents: rawNotification.contents, + version: rawNotification.version, + })); + + notifications.forEach((notification) => { + const notificationRule = getNotificationRule(notification); + + if (notificationRule) { + notification.type = notificationRule.type; + if (notificationRule.goTo) { + notification.goto = renderGoTo( + notificationRule.goTo, + notification.contents + ); + } + notification.rule = notificationRule; + } else { + console.warn( + `Cannot find notification rule for eventType '${notification.eventType}' version '${notification.version}'.` + ); + } + }); + + return notifications; +}; + +// --- Community --- // + const getCommunityNotificationRule = (notification) => { const notificationRule = _.find(NOTIFICATION_RULES, (_notificationRule) => { return _notificationRule.eventType === notification.eventType; diff --git a/webpack.config.js b/webpack.config.js index 4b31b48..fa827be 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -14,6 +14,9 @@ module.exports = (webpackConfigEnv, options) => { disableHtmlGeneration: true, }); + const unusedFilesWebpackPlugin = defaultConfig.plugins.find(p => p.constructor.name === "UnusedFilesWebpackPlugin"); + unusedFilesWebpackPlugin.globOptions.ignore.push("**/assets/icons/*.svg", "**/__mocks__/**"); + let cssLocalIdent; if (options.mode === "production") { cssLocalIdent = "[hash:base64:6]";