From 40cf4abffe9f9480f4204560867b16b84a018846 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 10 Feb 2020 05:04:51 -0300 Subject: [PATCH 01/28] Added notification popup Challenge: 30114259 Submission: 348696 --- .../__snapshots__/TopcoderHeader.jsx.snap | 5 ++++ src/shared/components/Header/index.jsx | 20 ++++++++++--- .../Contentful/MenuLoader/index.jsx | 1 - src/shared/containers/TopcoderHeader.js | 30 +++++++++++++++++-- 4 files changed, 49 insertions(+), 7 deletions(-) diff --git a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap index 1c2bdc7866..ad4c12e891 100644 --- a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap +++ b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap @@ -5,6 +5,11 @@ exports[`Matches shallow snapshot 1`] = ` closeMenu={[Function]} closeMobileMenu={[Function]} closeSearch={[Function]} + loadNotifications={[Function]} + markAllNotificationAsRead={[Function]} + markAllNotificationAsSeen={[Function]} + markNotificationAsRead={[Function]} + notifications={Array []} openMenu={[Function]} openMobileMenu={[Function]} openSearch={[Function]} diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index 4ea2d4855f..3f87a77ceb 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -16,7 +16,10 @@ try { // window is undefined } -const Header = ({ profile }) => { +const Header = ({ + profile, notifications, loadNotifications, markNotificationAsRead, markAllNotificationAsRead, + markAllNotificationAsSeen, +}) => { const [activeLevel1Id, setActiveLevel1Id] = useState(); const [path, setPath] = useState(); const [openMore, setOpenMore] = useState(true); @@ -47,6 +50,7 @@ const Header = ({ profile }) => { useEffect(() => { setPath(window.location.pathname); + loadNotifications(); }, []); if (TopNavRef) { return ( @@ -56,13 +60,16 @@ const Header = ({ profile }) => { rightMenu={( @@ -93,6 +100,11 @@ Header.propTypes = { photoURL: PT.string, handle: PT.string, }), + notifications: PT.arrayOf(PT.object).isRequired, + loadNotifications: PT.func.isRequired, + markNotificationAsRead: PT.func.isRequired, + markAllNotificationAsRead: PT.func.isRequired, + markAllNotificationAsSeen: PT.func.isRequired, }; export default Header; diff --git a/src/shared/containers/Contentful/MenuLoader/index.jsx b/src/shared/containers/Contentful/MenuLoader/index.jsx index ae7cf42fd7..f6b05aaac1 100644 --- a/src/shared/containers/Contentful/MenuLoader/index.jsx +++ b/src/shared/containers/Contentful/MenuLoader/index.jsx @@ -100,7 +100,6 @@ class MenuLoaderContainer extends React.Component { switchText={config.ACCOUNT_MENU_SWITCH_TEXT} onSwitch={this.handleSwitchMenu} onMenuOpen={this.handleCloseOpenMore} - showNotification={false} profile={normalizedProfile} authURLs={config.HEADER_AUTH_URLS} /> diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index 2f6b060f9f..608912a8b2 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -3,11 +3,34 @@ */ import _ from 'lodash'; -import actions from 'actions/topcoder_header'; +import headerActions from 'actions/topcoder_header'; +import { actions } from 'topcoder-react-lib'; + import TopcoderHeader from 'components/Header'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +function mapDispatchToProps(dispatch) { + return { + ...bindActionCreators(headerActions.topcoderHeader, dispatch), + loadNotifications: () => { + dispatch(actions.notifications.getNotificationsInit()); + dispatch(actions.notifications.getNotificationsDone()); + }, + markNotificationAsRead: (item) => { + dispatch(actions.notifications.markNotificationAsReadInit()); + dispatch(actions.notifications.markNotificationAsReadDone(item)); + }, + markAllNotificationAsRead: () => { + dispatch(actions.notifications.markAllNotificationAsReadInit()); + dispatch(actions.notifications.markAllNotificationAsReadDone()); + }, + markAllNotificationAsSeen: () => { + dispatch(actions.notifications.markAllNotificationAsSeenInit()); + dispatch(actions.notifications.markAllNotificationAsSeenDone()); + }, + }; +} export default connect( state => ({ ...state.topcoderHeader, @@ -15,6 +38,9 @@ export default connect( ...state.auth.profile, ..._.pickBy({ roles: state.auth.user ? state.auth.user.roles : undefined }), }, + notifications: (state.notifications + && state.notifications.items + && [...state.notifications.items]) || [], }), - dispatch => bindActionCreators(actions.topcoderHeader, dispatch), + mapDispatchToProps, )(TopcoderHeader); From c6d120ed66a93ddee5289b3848a7d85c83726f84 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 10 Feb 2020 16:37:35 -0300 Subject: [PATCH 02/28] Update package.json to packages use notifications branch --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 59914c658b..0e896c936e 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,7 @@ "moment-timezone": "^0.5.21", "money": "^0.2.0", "morgan": "^1.9.0", - "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#develop", + "navigation-component": "git+https://github.com/topcoder-platform/navigation-component.git#notifications", "node-forge": "^0.7.5", "nuka-carousel": "^4.5.3", "postcss": "^6.0.23", @@ -120,7 +120,7 @@ "supertest": "^3.1.0", "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", - "topcoder-react-lib": "0.10.0", + "topcoder-react-lib": "git+https://github.com/topcoder-platform/topcoder-react-lib.git#notifications", "topcoder-react-ui-kit": "^1.0.11", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", From 05375ac0cdf8f61075abcc6c2f7ac6ad9b9e66ae Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 10 Feb 2020 20:45:23 -0300 Subject: [PATCH 03/28] Fix tests --- .../shared/components/Header/__snapshots__/index.jsx.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap index 73577bdc8e..3f07eb44fe 100644 --- a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap @@ -157,7 +157,7 @@ exports[`Default render 1`] = ` } } loggedIn={true} - notificationButtonState="none" + notificationButtonState="new" notifications={Array []} onMenuOpen={[Function]} onSwitch={[Function]} @@ -169,7 +169,7 @@ exports[`Default render 1`] = ` "photoURL": "https://d1aahxkjiobka8.cloudfront.net/avatar/https%3A%2F%2Ftopcoder-dev-media.s3.amazonaws.com%2Fmember%2Fprofile%2Fhuanner-1552562543506.png?size=32", } } - showNotification={false} + showNotification={true} switchText={ Object { "href": "https://connect.topcoder-dev.com", From 4574f37d872ce5136b2024bbe8131061a7048e5b Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Tue, 11 Feb 2020 14:32:11 -0300 Subject: [PATCH 04/28] Updated package.json to install topcoder-react-lib notifications branch --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0e896c936e..6a0ce1fda5 100644 --- a/package.json +++ b/package.json @@ -120,7 +120,7 @@ "supertest": "^3.1.0", "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", - "topcoder-react-lib": "git+https://github.com/topcoder-platform/topcoder-react-lib.git#notifications", + "topcoder-react-lib": "github:topcoder-platform/topcoder-react-lib#notifications", "topcoder-react-ui-kit": "^1.0.11", "topcoder-react-utils": "0.7.8", "turndown": "^4.0.2", From a9112f59307dc7b4bf286cf8573cd546f9c04e0b Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 17 Feb 2020 09:53:00 -0300 Subject: [PATCH 05/28] Notifications Popup Fixes Challenge: 30115261 Submission: 351307 --- .../shared/containers/__snapshots__/TopcoderHeader.jsx.snap | 1 + src/shared/components/Header/index.jsx | 4 +++- src/shared/containers/TopcoderHeader.js | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap index ad4c12e891..bf3fe98a7b 100644 --- a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap +++ b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap @@ -5,6 +5,7 @@ exports[`Matches shallow snapshot 1`] = ` closeMenu={[Function]} closeMobileMenu={[Function]} closeSearch={[Function]} + dismissChallengeNotifications={[Function]} loadNotifications={[Function]} markAllNotificationAsRead={[Function]} markAllNotificationAsSeen={[Function]} diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index 3f87a77ceb..8c8c09028b 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -18,7 +18,7 @@ try { const Header = ({ profile, notifications, loadNotifications, markNotificationAsRead, markAllNotificationAsRead, - markAllNotificationAsSeen, + markAllNotificationAsSeen, dismissChallengeNotifications, }) => { const [activeLevel1Id, setActiveLevel1Id] = useState(); const [path, setPath] = useState(); @@ -65,6 +65,7 @@ const Header = ({ markNotificationAsRead={markNotificationAsRead} markAllNotificationAsRead={markAllNotificationAsRead} markAllNotificationAsSeen={markAllNotificationAsSeen} + dismissChallengeNotifications={dismissChallengeNotifications} accountMenu={config.ACCOUNT_MENU} switchText={config.ACCOUNT_MENU_SWITCH_TEXT} onSwitch={handleSwitchMenu} @@ -105,6 +106,7 @@ Header.propTypes = { markNotificationAsRead: PT.func.isRequired, markAllNotificationAsRead: PT.func.isRequired, markAllNotificationAsSeen: PT.func.isRequired, + dismissChallengeNotifications: PT.func.isRequired, }; export default Header; diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index 608912a8b2..1f8f45fabc 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -29,6 +29,10 @@ function mapDispatchToProps(dispatch) { dispatch(actions.notifications.markAllNotificationAsSeenInit()); dispatch(actions.notifications.markAllNotificationAsSeenDone()); }, + dismissChallengeNotifications: (challegeId) => { + dispatch(actions.notifications.dismissChallengeNotificationsInit()); + dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId)); + }, }; } export default connect( From bf6e262761adc797e04b07cacb402f68ca8ba7f4 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Wed, 19 Feb 2020 13:31:23 +0530 Subject: [PATCH 06/28] ci: deploying on test for QA ci: deploying on test for QA --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index e34d387a8c..25cba0efcb 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -182,7 +182,7 @@ workflows: filters: branches: only: - - develop + - notifications # This is beta env for production soft releases - "build-prod-beta": context : org-global From c5240369750fe17cfc12a889579dfd9abfb1b6c0 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Thu, 27 Feb 2020 18:19:00 -0300 Subject: [PATCH 07/28] Notifications Listing Page Challenge: 30116087 Submission: 353063 --- src/assets/images/notifications/arrow.svg | 5 + .../Notifications/TabsPanel/index.jsx | 76 ++++++ .../Notifications/TabsPanel/style.scss | 55 +++++ src/shared/components/Notifications/index.jsx | 211 ++++++++++++++++ .../components/Notifications/style.scss | 227 ++++++++++++++++++ src/shared/containers/Notifications/index.jsx | 41 ++++ src/shared/routes/Topcoder/Notifications.jsx | 19 ++ src/shared/routes/Topcoder/Routes.jsx | 2 + 8 files changed, 636 insertions(+) create mode 100644 src/assets/images/notifications/arrow.svg create mode 100644 src/shared/components/Notifications/TabsPanel/index.jsx create mode 100644 src/shared/components/Notifications/TabsPanel/style.scss create mode 100644 src/shared/components/Notifications/index.jsx create mode 100644 src/shared/components/Notifications/style.scss create mode 100644 src/shared/containers/Notifications/index.jsx create mode 100644 src/shared/routes/Topcoder/Notifications.jsx diff --git a/src/assets/images/notifications/arrow.svg b/src/assets/images/notifications/arrow.svg new file mode 100644 index 0000000000..0dd82cf0b2 --- /dev/null +++ b/src/assets/images/notifications/arrow.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/shared/components/Notifications/TabsPanel/index.jsx b/src/shared/components/Notifications/TabsPanel/index.jsx new file mode 100644 index 0000000000..099100bed5 --- /dev/null +++ b/src/shared/components/Notifications/TabsPanel/index.jsx @@ -0,0 +1,76 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import styles from './style.scss'; + + +const TABS = { + COMPLETED: 'completed', + ACTIVE: 'active', +}; + +export default class TabsPanel extends React.Component { + constructor(props) { + super(props); + this.state = { + tab: TABS.ACTIVE, + }; + } + + + render() { + const { showActive, showCompleted } = this.props; + const { tab } = this.state; + return ( +
+
+
{ + this.setState({ tab: TABS.ACTIVE }); + showActive(); + } + } + onKeyPress={ + () => { + this.setState({ tab: TABS.ACTIVE }); + showActive(); + } + } + >ACTIVE CHALLENGES +
+
{ + this.setState({ tab: TABS.COMPLETED }); + showCompleted(); + } + } + onKeyPress={ + () => { + this.setState({ tab: TABS.COMPLETED }); + showCompleted(); + } + } + >COMPLETED CHALLENGES +
+
+
+
Notification Settings
+
+
+ ); + } +} + + +TabsPanel.propTypes = { + showActive: PropTypes.func.isRequired, + showCompleted: PropTypes.func.isRequired, +}; diff --git a/src/shared/components/Notifications/TabsPanel/style.scss b/src/shared/components/Notifications/TabsPanel/style.scss new file mode 100644 index 0000000000..1b36bed143 --- /dev/null +++ b/src/shared/components/Notifications/TabsPanel/style.scss @@ -0,0 +1,55 @@ +@import "~styles/mixins"; + +.container { + display: flex; + justify-content: space-between; + width: 100%; + height: 30px; + margin-top: 50px; + margin-bottom: 20px; + padding-left: 14px; + + .lefts { + display: flex; + justify-content: space-between; + max-width: 345px; + + .btn { + color: #2a2a2a; + background-color: $tc-white; + + @include roboto-bold; + + font-size: 12px; + font-weight: 400; + line-height: 30px; + text-align: center; + width: 177px; + height: 30px; + cursor: pointer; + text-transform: uppercase; + } + + .active { + color: #fff; + background-color: #7f7f7f; + box-shadow: inset 0 0 2px 0 rgba(0, 0, 0, 0.25); + border-radius: 15px; + } + } + + .rights { + .notification-setting { + color: #0d61bf; + + @include roboto-bold; + + font-size: 14px; + font-weight: 400; + line-height: 22px; + text-align: left; + text-decoration: underline; + cursor: pointer; + } + } +} diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx new file mode 100644 index 0000000000..6b080114cc --- /dev/null +++ b/src/shared/components/Notifications/index.jsx @@ -0,0 +1,211 @@ +import React, { Fragment } from 'react'; +import PropTypes from 'prop-types'; +import cn from 'classnames'; +import _ from 'lodash'; +import moment from 'moment'; +import IconArrow from 'assets/images/notifications/arrow.svg'; +import styles from './style.scss'; +import TabsPanel from './TabsPanel'; + +const eventTypes = { + PROJECT: { + ACTIVE: 'connect.notification.project.active', + COMPLETED: 'connect.notification.project.completed', + }, +}; +const Item = ({ item, markNotificationAsRead, showPoint }) => ( +
+
+

{item.contents}

+ {moment(item.date).fromNow()} +
+
+ { + !item.isRead + && showPoint + && ( +
{ markNotificationAsRead(item); }} + onKeyPress={() => { markNotificationAsRead(item); }} + tabIndex="0" + /> + )} +
+
+); +Item.propTypes = { + item: PropTypes.shape.isRequired, + markNotificationAsRead: PropTypes.func.isRequired, + showPoint: PropTypes.bool.isRequired, +}; + +const challenges = (listReceived) => { + const list = listReceived || []; + const challengeTitles = _.uniq( + list.map(noti => noti.sourceName).filter(x => x), + ); + const group = challengeTitles.map(title => ({ + challengeTitle: title, items: list.filter(t => t.sourceName === title), + })); + + return group; +}; + +export default class NotificationList extends React.Component { + constructor(props) { + super(props); + this.state = { + collapsedChallenges: {}, + showCompleted: false, + }; + } + + componentWillUnmount() { + // mark all notifications as seen when go to another page + const { markAllNotificationAsSeen } = this.props; + markAllNotificationAsSeen(); + } + + toggleChallenge = (challengeIdx, collapsedChallenges) => { + const collapsed = collapsedChallenges || {}; + if (collapsed[challengeIdx]) { + collapsed[challengeIdx] = false; + } else { + collapsed[challengeIdx] = true; + } + this.setState({ collapsedChallenges: collapsed }); + } + + render() { + const { + notifications, markNotificationAsRead, + dismissChallengeNotifications, + } = this.props; + const { collapsedChallenges, showCompleted } = this.state; + let challengesList = []; + if (showCompleted) { + challengesList = _.filter((notifications || []), + t => t.eventType === eventTypes.PROJECT.COMPLETED); + } else { + challengesList = _.filter((notifications || []), + t => t.eventType !== eventTypes.PROJECT.COMPLETED); + } + return ( +
+

Notifications

+
+ { + this.setState({ showCompleted: false }); + }} + showCompleted={() => this.setState({ showCompleted: true })} + /> +
+ + { + challenges(challengesList).map((challenge) => { + const challegeId = challenge && challenge.items && challenge.items.length + && challenge.items[0].sourceId; + + return ( + +
+ {challenge.challengeTitle} +
+
{ + if (challegeId) { + dismissChallengeNotifications(challegeId); + } + }} + onKeyPress={() => { + if (challegeId) { + dismissChallengeNotifications(challegeId); + } + }} + >× +
+ + { + if (challegeId) { + this.toggleChallenge(challegeId, collapsedChallenges); + } + }} + /> +
+
+ { + (!collapsedChallenges[challegeId]) + && challenge.items.map(item => ( + + ))} +
+ ); + }) + } +
+
+
+
+ ); + } +} + +NotificationList.defaultProps = { + notifications: [], +}; + +NotificationList.propTypes = { + /** + * Array of Notifications, each with properties: + * + * - id {number} message identifier + * - sourceId {number} identifies the associated challenge + * - sourceName {string} challenge title + * - eventType {string} indicates if challenge is active(connect.notification.project.active) + * or completed(connect.notification.project.completed) + * - date {date} when notification was raised + * - isRead {boolean} indicates if is read + * - isSeen {boolean} indicates if is seen + * - contents {string} message + * + */ + notifications: PropTypes.arrayOf(PropTypes.object), + + + /** + * Called with item to be marked as read. + * + * @param item {object} Item to be marked as read + */ + markNotificationAsRead: PropTypes.func.isRequired, + + /** + * Called with challenge id to be marked for dismiss. + * + * @param challengeId {number} challange to be marked for dismiss. + */ + dismissChallengeNotifications: PropTypes.func.isRequired, + + /** + * Called to be mark all notifications as seen. + * + */ + markAllNotificationAsSeen: PropTypes.func.isRequired, +}; diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss new file mode 100644 index 0000000000..1881a614ad --- /dev/null +++ b/src/shared/components/Notifications/style.scss @@ -0,0 +1,227 @@ +@import "~styles/mixins"; +$white: white; +$turquoise-dark: turquoise; +$turquoise-super-dark: turquoise; + +.outer-container { + width: 1000px; + height: auto; + background-color: $tc-white; + margin: 70px auto 150px auto; + + .heading { + color: $tc-black; + + @include barlow-condensed; + + font-size: 60px; + font-weight: 500; + line-height: 58px; + text-align: left; + text-transform: uppercase; + } + + .notifications-panel { + border-radius: 10px; + display: flex; + flex-direction: column; + background-color: #fff; + min-height: 685px; + + [role="button"] { + cursor: pointer; + } + + .noti-body { + border: 1px solid #e0e0e0; + + &.center { + text-align: center; + } + + .txt { + margin: 0; + color: $tc-gray-90; + font-size: 14px; + + @include roboto-regular; + + line-height: 22px; + + &.center-txt { + text-align: center; + margin: 15px auto 25px auto; + } + } + + .challenge-title { + background-color: #f4f4f4; + color: $tc-gray-90; + font-size: 14px; + + @include roboto-bold; + + font-weight: 500; + line-height: 22px; + // padding-left: 20px; + margin-top: -2px; + padding: 5px 20px; + display: flex; + justify-content: space-between; + + .challenge-header-rights { + display: flex; + width: 45.4px; + justify-content: space-between; + + &.hide-challenge-header-rights { + display: none; + } + + .dismiss-challenge { + cursor: pointer; + color: #aaa; + + @include roboto-regular; + + font-size: 20px; + // margin: auto 0px; + width: 10px; + height: 10px; + } + + .arrow { + margin: auto; + cursor: pointer; + + &.up { + transform: rotateZ(180deg); + } + + &.down { + transform: rotateZ(0deg); + } + } + } + } + + .right-remove { + position: absolute; + right: 1px; + top: 50%; + margin-top: -10px; + z-index: 666; + display: none; + + .btn-close { + display: block; + // background: url(#{$img-path}/e-remove.svg) center center no-repeat; + background-size: 10px; + width: 20px; + height: 20px; + + &:hover { + // background: url(#{$img-path}/delete-small.svg) center center no-repeat; + } + } + + .black-txt { + position: absolute; + top: -28px; + left: -76px; + background-color: $tc-gray-90; + border-radius: 2px; + padding: 6px 0; + color: $tc-white; + font-size: 11px; + + @include roboto-regular; + + line-height: 13px; + text-align: center; + min-width: 110px; + display: none; + + &::after { + content: ""; + display: block; + position: absolute; + bottom: -5px; + left: calc(50% + 30px); + margin-left: -3px; + width: 0; + height: 0; + border-left: 4px solid transparent; + border-right: 4px solid transparent; + border-top: 5px solid $tc-gray-90; + } + } + + &:hover { + .black-txt { + display: block; + } + } + } + + .noti-item { + background-color: $tc-white; + padding: 5px 20px; + display: flex; + justify-content: space-between; + border-bottom: 1px #e0e0e0 solid; + margin-left: 20px; + margin-right: 20px; + + .left { + display: flex; + flex-direction: column; + padding-left: 30px; + + .txt { + margin: 0; + color: $tc-gray-90; + font-size: 14px; + + @include roboto-regular; + + line-height: 22px; + } + + .time-txt { + display: inline-block; + vertical-align: middle; + color: #aaa; + font-size: 12px; + + @include roboto-regular; + + line-height: 20px; + } + } + + .right { + margin: auto 0; + + .point { + width: 10px; + height: 10px; + background-color: $tc-white; + border-radius: 100%; + display: inline-block; + vertical-align: middle; + cursor: pointer; + } + + .point-red { + background-color: $tc-red; + } + + .point-grey { + background-color: $tc-gray-10; + } + } + } + } + } +} diff --git a/src/shared/containers/Notifications/index.jsx b/src/shared/containers/Notifications/index.jsx new file mode 100644 index 0000000000..cf92d780e1 --- /dev/null +++ b/src/shared/containers/Notifications/index.jsx @@ -0,0 +1,41 @@ +/** + * Container for the notifications page. + */ + +import { actions } from 'topcoder-react-lib'; + +import Notifications from 'components/Notifications'; +import { connect } from 'react-redux'; + +function mapDispatchToProps(dispatch) { + return { + loadNotifications: () => { + dispatch(actions.notifications.getNotificationsInit()); + dispatch(actions.notifications.getNotificationsDone()); + }, + markNotificationAsRead: (item) => { + dispatch(actions.notifications.markNotificationAsReadInit()); + dispatch(actions.notifications.markNotificationAsReadDone(item)); + }, + markAllNotificationAsRead: () => { + dispatch(actions.notifications.markAllNotificationAsReadInit()); + dispatch(actions.notifications.markAllNotificationAsReadDone()); + }, + markAllNotificationAsSeen: () => { + dispatch(actions.notifications.markAllNotificationAsSeenInit()); + dispatch(actions.notifications.markAllNotificationAsSeenDone()); + }, + dismissChallengeNotifications: (challegeId) => { + dispatch(actions.notifications.dismissChallengeNotificationsInit()); + dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId)); + }, + }; +} +export default connect( + state => ({ + notifications: (state.notifications + && state.notifications.items + && [...state.notifications.items]) || [], + }), + mapDispatchToProps, +)(Notifications); diff --git a/src/shared/routes/Topcoder/Notifications.jsx b/src/shared/routes/Topcoder/Notifications.jsx new file mode 100644 index 0000000000..be4b2de193 --- /dev/null +++ b/src/shared/routes/Topcoder/Notifications.jsx @@ -0,0 +1,19 @@ +import LoadingIndicator from 'components/LoadingIndicator'; +import React from 'react'; +import { AppChunk } from 'topcoder-react-utils'; + +export default function NotificationsRoute(props) { + return ( + import(/* webpackChunkName: "notifications/chunk" */'containers/Notifications') + .then(({ default: Notifications }) => ( + + )) + } + renderPlaceholder={() => } + /> + ); +} diff --git a/src/shared/routes/Topcoder/Routes.jsx b/src/shared/routes/Topcoder/Routes.jsx index bfc3a1b84a..39ca78bb8d 100644 --- a/src/shared/routes/Topcoder/Routes.jsx +++ b/src/shared/routes/Topcoder/Routes.jsx @@ -27,6 +27,7 @@ import EDUTracks from 'containers/EDU/Tracks'; import EDUSearch from 'containers/EDU/Search'; import ChallengeListing from './ChallengeListing'; import Dashboard from './Dashboard'; +import Notifications from './Notifications'; import Settings from '../Settings'; import HallOfFame from '../HallOfFame'; import Profile from '../Profile'; @@ -60,6 +61,7 @@ export default function Topcoder() { path="/challenges/:challengeId(\d{8}|\d{5})" /> + Date: Mon, 2 Mar 2020 11:01:42 -0300 Subject: [PATCH 08/28] Fix Issue #4041 : Align tab to left --- .../components/Notifications/TabsPanel/style.scss | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/shared/components/Notifications/TabsPanel/style.scss b/src/shared/components/Notifications/TabsPanel/style.scss index 1b36bed143..78c1ff1273 100644 --- a/src/shared/components/Notifications/TabsPanel/style.scss +++ b/src/shared/components/Notifications/TabsPanel/style.scss @@ -7,7 +7,6 @@ height: 30px; margin-top: 50px; margin-bottom: 20px; - padding-left: 14px; .lefts { display: flex; @@ -15,19 +14,21 @@ max-width: 345px; .btn { + @include roboto-bold; color: #2a2a2a; background-color: $tc-white; - - @include roboto-bold; - font-size: 12px; font-weight: 400; + text-transform: uppercase; line-height: 30px; text-align: center; - width: 177px; height: 30px; + padding: 0 15px; cursor: pointer; - text-transform: uppercase; + + &:not(:first-of-type) { + margin-left: 5px; + } } .active { From 991b67ae173beb603bd1bd80a15b1ee83331e30f Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 2 Mar 2020 11:02:23 -0300 Subject: [PATCH 09/28] Fix Issue #4040 : Remove blue border (focus) from links --- src/shared/components/Notifications/style.scss | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss index 1881a614ad..e603f4d119 100644 --- a/src/shared/components/Notifications/style.scss +++ b/src/shared/components/Notifications/style.scss @@ -3,6 +3,10 @@ $white: white; $turquoise-dark: turquoise; $turquoise-super-dark: turquoise; +*:focus { + outline: none; +} + .outer-container { width: 1000px; height: auto; @@ -63,7 +67,6 @@ $turquoise-super-dark: turquoise; font-weight: 500; line-height: 22px; - // padding-left: 20px; margin-top: -2px; padding: 5px 20px; display: flex; @@ -85,7 +88,6 @@ $turquoise-super-dark: turquoise; @include roboto-regular; font-size: 20px; - // margin: auto 0px; width: 10px; height: 10px; } @@ -115,14 +117,9 @@ $turquoise-super-dark: turquoise; .btn-close { display: block; - // background: url(#{$img-path}/e-remove.svg) center center no-repeat; background-size: 10px; width: 20px; height: 20px; - - &:hover { - // background: url(#{$img-path}/delete-small.svg) center center no-repeat; - } } .black-txt { From 86ab4a3e2bde998e8516290d55785b3c24b04890 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 2 Mar 2020 14:34:50 -0300 Subject: [PATCH 10/28] Notifications: Fix listing page --- src/shared/components/Notifications/TabsPanel/style.scss | 6 ++++++ src/shared/components/Notifications/style.scss | 9 +++++++++ src/styles/_mixins/_variables.scss | 3 +++ 3 files changed, 18 insertions(+) diff --git a/src/shared/components/Notifications/TabsPanel/style.scss b/src/shared/components/Notifications/TabsPanel/style.scss index 78c1ff1273..06a2cbdfef 100644 --- a/src/shared/components/Notifications/TabsPanel/style.scss +++ b/src/shared/components/Notifications/TabsPanel/style.scss @@ -54,3 +54,9 @@ } } } + +@media (max-width: $screen-sm + 1px) { + .container { + margin: 15px 0; + } +} diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss index e603f4d119..a50125403d 100644 --- a/src/shared/components/Notifications/style.scss +++ b/src/shared/components/Notifications/style.scss @@ -222,3 +222,12 @@ $turquoise-super-dark: turquoise; } } } +@media (max-width: $screen-sm + 1px) { + .outer-container { + margin: 15px auto 50px auto; + padding: 0 10px; + .heading { + font-size: 40px; + } + } +} diff --git a/src/styles/_mixins/_variables.scss b/src/styles/_mixins/_variables.scss index 3b27227388..d81daa6378 100644 --- a/src/styles/_mixins/_variables.scss +++ b/src/styles/_mixins/_variables.scss @@ -1,2 +1,5 @@ /* colors. */ $tco-black: #2a2a2a; + +/* @media */ +$screen-sm: 768px; \ No newline at end of file From 4d35f30b7374b56fdf6d42097e20f66cb1cc95e5 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 2 Mar 2020 14:48:06 -0300 Subject: [PATCH 11/28] Fix lint errors --- src/shared/components/Notifications/TabsPanel/style.scss | 1 + src/shared/components/Notifications/style.scss | 2 ++ src/styles/_mixins/_variables.scss | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/shared/components/Notifications/TabsPanel/style.scss b/src/shared/components/Notifications/TabsPanel/style.scss index 06a2cbdfef..f22ab30d74 100644 --- a/src/shared/components/Notifications/TabsPanel/style.scss +++ b/src/shared/components/Notifications/TabsPanel/style.scss @@ -15,6 +15,7 @@ .btn { @include roboto-bold; + color: #2a2a2a; background-color: $tc-white; font-size: 12px; diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss index a50125403d..60a4dcbe49 100644 --- a/src/shared/components/Notifications/style.scss +++ b/src/shared/components/Notifications/style.scss @@ -222,10 +222,12 @@ $turquoise-super-dark: turquoise; } } } + @media (max-width: $screen-sm + 1px) { .outer-container { margin: 15px auto 50px auto; padding: 0 10px; + .heading { font-size: 40px; } diff --git a/src/styles/_mixins/_variables.scss b/src/styles/_mixins/_variables.scss index d81daa6378..441c0b04c2 100644 --- a/src/styles/_mixins/_variables.scss +++ b/src/styles/_mixins/_variables.scss @@ -2,4 +2,4 @@ $tco-black: #2a2a2a; /* @media */ -$screen-sm: 768px; \ No newline at end of file +$screen-sm: 768px; From 227add9f67741a7345e9160f335deed49661157e Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 9 Mar 2020 12:22:36 -0300 Subject: [PATCH 12/28] Backend Integration + Fixes + Broadcast type --- .../__snapshots__/TopcoderHeader.jsx.snap | 5 ++ src/shared/components/Header/index.jsx | 10 ++- .../Notifications/TabsPanel/index.jsx | 32 ++++++-- .../Notifications/TabsPanel/style.scss | 1 - src/shared/components/Notifications/index.jsx | 79 ++++++++++++------- .../Contentful/MenuLoader/index.jsx | 1 + src/shared/containers/Notifications/index.jsx | 32 +++++--- src/shared/containers/TopcoderHeader.js | 32 +++++--- 8 files changed, 128 insertions(+), 64 deletions(-) diff --git a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap index bf3fe98a7b..54b150a474 100644 --- a/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap +++ b/__tests__/shared/containers/__snapshots__/TopcoderHeader.jsx.snap @@ -2,6 +2,11 @@ exports[`Matches shallow snapshot 1`] = `
{ const [activeLevel1Id, setActiveLevel1Id] = useState(); const [path, setPath] = useState(); @@ -50,7 +50,7 @@ const Header = ({ useEffect(() => { setPath(window.location.pathname); - loadNotifications(); + loadNotifications(auth.tokenV3); }, []); if (TopNavRef) { return ( @@ -62,6 +62,7 @@ const Header = ({ loggedIn={!_.isEmpty(profile)} notificationButtonState="new" notifications={notifications || []} + loadNotifications={loadNotifications} markNotificationAsRead={markNotificationAsRead} markAllNotificationAsRead={markAllNotificationAsRead} markAllNotificationAsSeen={markAllNotificationAsSeen} @@ -71,6 +72,7 @@ const Header = ({ onSwitch={handleSwitchMenu} onMenuOpen={handleCloseOpenMore} showNotification + auth={auth} profile={normalizedProfile} authURLs={config.HEADER_AUTH_URLS} /> @@ -94,6 +96,7 @@ const Header = ({ Header.defaultProps = { profile: null, + auth: null, }; Header.propTypes = { @@ -101,6 +104,7 @@ Header.propTypes = { photoURL: PT.string, handle: PT.string, }), + auth: PT.shape(), notifications: PT.arrayOf(PT.object).isRequired, loadNotifications: PT.func.isRequired, markNotificationAsRead: PT.func.isRequired, diff --git a/src/shared/components/Notifications/TabsPanel/index.jsx b/src/shared/components/Notifications/TabsPanel/index.jsx index 099100bed5..c2654bc54b 100644 --- a/src/shared/components/Notifications/TabsPanel/index.jsx +++ b/src/shared/components/Notifications/TabsPanel/index.jsx @@ -6,6 +6,7 @@ import styles from './style.scss'; const TABS = { COMPLETED: 'completed', + BROADCAST: 'broadcast', ACTIVE: 'active', }; @@ -19,7 +20,7 @@ export default class TabsPanel extends React.Component { render() { - const { showActive, showCompleted } = this.props; + const { changeTab } = this.props; const { tab } = this.state; return (
@@ -31,17 +32,35 @@ export default class TabsPanel extends React.Component { onClick={ () => { this.setState({ tab: TABS.ACTIVE }); - showActive(); + changeTab(TABS.ACTIVE); } } onKeyPress={ () => { this.setState({ tab: TABS.ACTIVE }); - showActive(); + changeTab(TABS.ACTIVE); } } >ACTIVE CHALLENGES
+
{ + this.setState({ tab: TABS.BROADCAST }); + changeTab(TABS.BROADCAST); + } + } + onKeyPress={ + () => { + this.setState({ tab: TABS.BROADCAST }); + changeTab(TABS.BROADCAST); + } + } + >NOTIFICATIONS +
{ this.setState({ tab: TABS.COMPLETED }); - showCompleted(); + changeTab(TABS.COMPLETED); } } onKeyPress={ () => { this.setState({ tab: TABS.COMPLETED }); - showCompleted(); + changeTab(TABS.COMPLETED); } } >COMPLETED CHALLENGES @@ -71,6 +90,5 @@ export default class TabsPanel extends React.Component { TabsPanel.propTypes = { - showActive: PropTypes.func.isRequired, - showCompleted: PropTypes.func.isRequired, + changeTab: PropTypes.func.isRequired, }; diff --git a/src/shared/components/Notifications/TabsPanel/style.scss b/src/shared/components/Notifications/TabsPanel/style.scss index f22ab30d74..c7cd384a5f 100644 --- a/src/shared/components/Notifications/TabsPanel/style.scss +++ b/src/shared/components/Notifications/TabsPanel/style.scss @@ -11,7 +11,6 @@ .lefts { display: flex; justify-content: space-between; - max-width: 345px; .btn { @include roboto-bold; diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index 6b080114cc..f9e6fa31cc 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -7,13 +7,20 @@ import IconArrow from 'assets/images/notifications/arrow.svg'; import styles from './style.scss'; import TabsPanel from './TabsPanel'; +// TODO: We change this later based on API event mapping const eventTypes = { PROJECT: { - ACTIVE: 'connect.notification.project.active', - COMPLETED: 'connect.notification.project.completed', + ACTIVE: [ + 'challenge.notification.events', + 'submission.notification.create', + ], + COMPLETED: 'challenge.notification.completed', }, + BROADCAST: 'admin.notification.broadcast', }; -const Item = ({ item, markNotificationAsRead, showPoint }) => ( +const Item = ({ + item, auth, markNotificationAsRead, showPoint, +}) => (

{item.contents}

@@ -27,8 +34,12 @@ const Item = ({ item, markNotificationAsRead, showPoint }) => (
{ markNotificationAsRead(item); }} - onKeyPress={() => { markNotificationAsRead(item); }} + onClick={() => { + markNotificationAsRead(item, auth.tokenV3); + }} + onKeyPress={() => { + markNotificationAsRead(item, auth.tokenV3); + }} tabIndex="0" /> )} @@ -36,7 +47,8 @@ const Item = ({ item, markNotificationAsRead, showPoint }) => (
); Item.propTypes = { - item: PropTypes.shape.isRequired, + item: PropTypes.shape().isRequired, + auth: PropTypes.shape().isRequired, markNotificationAsRead: PropTypes.func.isRequired, showPoint: PropTypes.bool.isRequired, }; @@ -58,14 +70,26 @@ export default class NotificationList extends React.Component { super(props); this.state = { collapsedChallenges: {}, - showCompleted: false, + activeTab: 'active', }; } componentWillUnmount() { // mark all notifications as seen when go to another page - const { markAllNotificationAsSeen } = this.props; - markAllNotificationAsSeen(); + const { + markAllNotificationAsSeen, notifications, auth, + } = this.props; + const notificationsList = _.filter((notifications || []), t => !t.isSeen); + const result = _.map(notificationsList, 'id').join('-'); + if (result) { + markAllNotificationAsSeen(result, auth.tokenV3); + } + } + + changeTab = (tab) => { + this.setState({ + activeTab: tab, + }); } toggleChallenge = (challengeIdx, collapsedChallenges) => { @@ -80,27 +104,27 @@ export default class NotificationList extends React.Component { render() { const { - notifications, markNotificationAsRead, + auth, notifications, loadNotifications, markNotificationAsRead, dismissChallengeNotifications, } = this.props; - const { collapsedChallenges, showCompleted } = this.state; + const { collapsedChallenges, activeTab } = this.state; let challengesList = []; - if (showCompleted) { + if (activeTab === 'active') { + challengesList = _.filter((notifications || []), + t => eventTypes.PROJECT.ACTIVE.includes(t.eventType)); + } else if (activeTab === 'completed') { challengesList = _.filter((notifications || []), - t => t.eventType === eventTypes.PROJECT.COMPLETED); + t => eventTypes.PROJECT.COMPLETED.includes(t.eventType)); } else { challengesList = _.filter((notifications || []), - t => t.eventType !== eventTypes.PROJECT.COMPLETED); + t => eventTypes.BROADCAST.includes(t.eventType)); } return (

Notifications

{ - this.setState({ showCompleted: false }); - }} - showCompleted={() => this.setState({ showCompleted: true })} + changeTab={tab => this.setState({ activeTab: tab })} />
@@ -114,7 +138,7 @@ export default class NotificationList extends React.Component {
{challenge.challengeTitle}
{ if (challegeId) { - dismissChallengeNotifications(challegeId); + dismissChallengeNotifications(challegeId, auth.tokenV3); } }} onKeyPress={() => { if (challegeId) { - dismissChallengeNotifications(challegeId); + dismissChallengeNotifications(challegeId, auth.tokenV3); } }} >× @@ -149,10 +173,13 @@ export default class NotificationList extends React.Component { (!collapsedChallenges[challegeId]) && challenge.items.map(item => ( ))} @@ -167,11 +194,8 @@ export default class NotificationList extends React.Component { } } -NotificationList.defaultProps = { - notifications: [], -}; - NotificationList.propTypes = { + auth: PropTypes.shape().isRequired, /** * Array of Notifications, each with properties: * @@ -186,8 +210,9 @@ NotificationList.propTypes = { * - contents {string} message * */ - notifications: PropTypes.arrayOf(PropTypes.object), + notifications: PropTypes.arrayOf(PropTypes.object).isRequired, + loadNotifications: PropTypes.func.isRequired, /** * Called with item to be marked as read. diff --git a/src/shared/containers/Contentful/MenuLoader/index.jsx b/src/shared/containers/Contentful/MenuLoader/index.jsx index f6b05aaac1..0669f3db78 100644 --- a/src/shared/containers/Contentful/MenuLoader/index.jsx +++ b/src/shared/containers/Contentful/MenuLoader/index.jsx @@ -101,6 +101,7 @@ class MenuLoaderContainer extends React.Component { onSwitch={this.handleSwitchMenu} onMenuOpen={this.handleCloseOpenMore} profile={normalizedProfile} + auth={auth} authURLs={config.HEADER_AUTH_URLS} /> )} diff --git a/src/shared/containers/Notifications/index.jsx b/src/shared/containers/Notifications/index.jsx index cf92d780e1..eb0127efc4 100644 --- a/src/shared/containers/Notifications/index.jsx +++ b/src/shared/containers/Notifications/index.jsx @@ -9,33 +9,39 @@ import { connect } from 'react-redux'; function mapDispatchToProps(dispatch) { return { - loadNotifications: () => { + loadNotifications: (tokenV3) => { dispatch(actions.notifications.getNotificationsInit()); - dispatch(actions.notifications.getNotificationsDone()); + dispatch(actions.notifications.getNotificationsDone(tokenV3)); }, - markNotificationAsRead: (item) => { + markNotificationAsRead: (item, tokenV3) => { dispatch(actions.notifications.markNotificationAsReadInit()); - dispatch(actions.notifications.markNotificationAsReadDone(item)); + dispatch(actions.notifications.markNotificationAsReadDone(item, tokenV3)); }, - markAllNotificationAsRead: () => { + markAllNotificationAsRead: (tokenV3) => { dispatch(actions.notifications.markAllNotificationAsReadInit()); - dispatch(actions.notifications.markAllNotificationAsReadDone()); + dispatch(actions.notifications.markAllNotificationAsReadDone(tokenV3)); }, - markAllNotificationAsSeen: () => { + markAllNotificationAsSeen: (items, tokenV3) => { dispatch(actions.notifications.markAllNotificationAsSeenInit()); - dispatch(actions.notifications.markAllNotificationAsSeenDone()); + dispatch(actions.notifications.markAllNotificationAsSeenDone(items, tokenV3)); }, - dismissChallengeNotifications: (challegeId) => { + dismissChallengeNotifications: (challegeId, tokenV3) => { dispatch(actions.notifications.dismissChallengeNotificationsInit()); - dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId)); + dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId, tokenV3)); }, }; } -export default connect( - state => ({ + +function mapStateToProps(state) { + return { notifications: (state.notifications && state.notifications.items && [...state.notifications.items]) || [], - }), + auth: state.auth, + }; +} + +export default connect( + mapStateToProps, mapDispatchToProps, )(Notifications); diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index 1f8f45fabc..a31eec4738 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -13,30 +13,31 @@ import { bindActionCreators } from 'redux'; function mapDispatchToProps(dispatch) { return { ...bindActionCreators(headerActions.topcoderHeader, dispatch), - loadNotifications: () => { + loadNotifications: (tokenV3) => { dispatch(actions.notifications.getNotificationsInit()); - dispatch(actions.notifications.getNotificationsDone()); + dispatch(actions.notifications.getNotificationsDone(tokenV3)); }, - markNotificationAsRead: (item) => { + markNotificationAsRead: (item, tokenV3) => { dispatch(actions.notifications.markNotificationAsReadInit()); - dispatch(actions.notifications.markNotificationAsReadDone(item)); + dispatch(actions.notifications.markNotificationAsReadDone(item, tokenV3)); }, - markAllNotificationAsRead: () => { + markAllNotificationAsRead: (tokenV3) => { dispatch(actions.notifications.markAllNotificationAsReadInit()); - dispatch(actions.notifications.markAllNotificationAsReadDone()); + dispatch(actions.notifications.markAllNotificationAsReadDone(tokenV3)); }, - markAllNotificationAsSeen: () => { + markAllNotificationAsSeen: (items, tokenV3) => { dispatch(actions.notifications.markAllNotificationAsSeenInit()); - dispatch(actions.notifications.markAllNotificationAsSeenDone()); + dispatch(actions.notifications.markAllNotificationAsSeenDone(items, tokenV3)); }, - dismissChallengeNotifications: (challegeId) => { + dismissChallengeNotifications: (challegeId, tokenV3) => { dispatch(actions.notifications.dismissChallengeNotificationsInit()); - dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId)); + dispatch(actions.notifications.dismissChallengeNotificationsDone(challegeId, tokenV3)); }, }; } -export default connect( - state => ({ + +function mapStateToProps(state) { + return { ...state.topcoderHeader, profile: { ...state.auth.profile, @@ -45,6 +46,11 @@ export default connect( notifications: (state.notifications && state.notifications.items && [...state.notifications.items]) || [], - }), + auth: state.auth, + }; +} + +export default connect( + mapStateToProps, mapDispatchToProps, )(TopcoderHeader); From 2c34da7cbd816ed8f0fe491314981ed99d3b459a Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Mon, 9 Mar 2020 13:31:24 -0300 Subject: [PATCH 13/28] Fix CircleCI errors --- __tests__/shared/components/Header/__snapshots__/index.jsx.snap | 1 + 1 file changed, 1 insertion(+) diff --git a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap index 3f07eb44fe..6b17ef6fb5 100644 --- a/__tests__/shared/components/Header/__snapshots__/index.jsx.snap +++ b/__tests__/shared/components/Header/__snapshots__/index.jsx.snap @@ -150,6 +150,7 @@ exports[`Default render 1`] = ` }, ] } + auth={null} authURLs={ Object { "href": "https://accounts.topcoder-dev.com/member/registration?utm_source=community-app-main", From e11e1c29b62da79357ee357f9f983baa4fd5e275 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Tue, 10 Mar 2020 03:38:07 -0300 Subject: [PATCH 14/28] #4085 : Notification items clickable --- src/shared/components/Notifications/index.jsx | 88 +++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index f9e6fa31cc..59aaccd818 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -3,6 +3,7 @@ import PropTypes from 'prop-types'; import cn from 'classnames'; import _ from 'lodash'; import moment from 'moment'; +import { Link } from 'topcoder-react-utils'; import IconArrow from 'assets/images/notifications/arrow.svg'; import styles from './style.scss'; import TabsPanel from './TabsPanel'; @@ -18,39 +19,68 @@ const eventTypes = { }, BROADCAST: 'admin.notification.broadcast', }; + +// Dynamic element, to select between Link and Div +const ConditionalWrapper = ({ + condition, renderLink, renderDiv, children, +}) => ( + condition ? renderLink(children) : renderDiv(children) +); + const Item = ({ - item, auth, markNotificationAsRead, showPoint, + item, auth, markNotificationAsRead, showPoint, isLink, }) => ( -
-
-

{item.contents}

- {moment(item.date).fromNow()} -
-
- { - !item.isRead - && showPoint - && ( -
{ - markNotificationAsRead(item, auth.tokenV3); - }} - onKeyPress={() => { - markNotificationAsRead(item, auth.tokenV3); - }} - tabIndex="0" - /> - )} -
-
+ ( + !item.isRead && markNotificationAsRead(item, auth.tokenV3)} + > + {children} + + )} + renderDiv={children => ( +
+ {children} +
+ )} + > + +
+

{item.contents}

+ {moment(item.date).fromNow()} +
+
+ { + !item.isRead + && showPoint + && ( +
{ + if (!isLink) { + markNotificationAsRead(item, auth.tokenV3); + } + }} + onKeyPress={() => { + markNotificationAsRead(item, auth.tokenV3); + }} + tabIndex="0" + /> + )} +
+ + ); Item.propTypes = { item: PropTypes.shape().isRequired, auth: PropTypes.shape().isRequired, markNotificationAsRead: PropTypes.func.isRequired, showPoint: PropTypes.bool.isRequired, + isLink: PropTypes.bool.isRequired, }; const challenges = (listReceived) => { @@ -102,6 +132,13 @@ export default class NotificationList extends React.Component { this.setState({ collapsedChallenges: collapsed }); } + isLink = (item) => { + const ret = (eventTypes.PROJECT.ACTIVE.includes(item.eventType) + || eventTypes.PROJECT.COMPLETED.includes(item.eventType)) + && item.sourceId; + return ret; + } + render() { const { auth, notifications, loadNotifications, markNotificationAsRead, @@ -180,6 +217,7 @@ export default class NotificationList extends React.Component { markNotificationAsRead={markNotificationAsRead} key={`noti-item-${item.id}`} showPoint={activeTab !== 'completed'} + isLink={this.isLink(item)} /> ))} From 9d8899889282a59247c3404b73310e23adf61a56 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Tue, 10 Mar 2020 11:44:03 -0300 Subject: [PATCH 15/28] #4013: Fix duplication KEY in notification list --- src/shared/components/Notifications/index.jsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index 59aaccd818..01eac8056e 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -135,7 +135,7 @@ export default class NotificationList extends React.Component { isLink = (item) => { const ret = (eventTypes.PROJECT.ACTIVE.includes(item.eventType) || eventTypes.PROJECT.COMPLETED.includes(item.eventType)) - && item.sourceId; + && item.sourceId > 0; return ret; } @@ -171,8 +171,8 @@ export default class NotificationList extends React.Component { && challenge.items[0].sourceId; return ( - -
+ +
{challenge.challengeTitle}
Date: Wed, 11 Mar 2020 11:05:32 -0300 Subject: [PATCH 16/28] Remove submission event. --- src/shared/components/Notifications/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index 01eac8056e..a3646f62b2 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -13,7 +13,7 @@ const eventTypes = { PROJECT: { ACTIVE: [ 'challenge.notification.events', - 'submission.notification.create', + 'notifications.autopilot.events', ], COMPLETED: 'challenge.notification.completed', }, From 642e6ddf49515fb6e305437d9031c0ebc2fc1a97 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Wed, 11 Mar 2020 18:26:13 -0300 Subject: [PATCH 17/28] Prevent follow link when click in "Mark As Read" button --- src/shared/components/Notifications/index.jsx | 9 +++++---- src/shared/components/Notifications/style.scss | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index a3646f62b2..ba193aff63 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -60,10 +60,11 @@ const Item = ({
{ - if (!isLink) { - markNotificationAsRead(item, auth.tokenV3); - } + onClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + e.nativeEvent.stopImmediatePropagation(); + markNotificationAsRead(item, auth.tokenV3); }} onKeyPress={() => { markNotificationAsRead(item, auth.tokenV3); diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss index 60a4dcbe49..8c65274aa5 100644 --- a/src/shared/components/Notifications/style.scss +++ b/src/shared/components/Notifications/style.scss @@ -208,6 +208,7 @@ $turquoise-super-dark: turquoise; display: inline-block; vertical-align: middle; cursor: pointer; + z-index: 10; } .point-red { From 26c3ac706c241fc0fa81c5ce2187722d9c13a4e7 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Fri, 13 Mar 2020 17:33:48 -0300 Subject: [PATCH 18/28] Hide Notificaitons Settings until this page ready --- src/shared/components/Notifications/TabsPanel/index.jsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/shared/components/Notifications/TabsPanel/index.jsx b/src/shared/components/Notifications/TabsPanel/index.jsx index c2654bc54b..9d16f9dede 100644 --- a/src/shared/components/Notifications/TabsPanel/index.jsx +++ b/src/shared/components/Notifications/TabsPanel/index.jsx @@ -80,9 +80,13 @@ export default class TabsPanel extends React.Component { >COMPLETED CHALLENGES
+ {/* + * Disabled until Settings page is ready + *
Notification Settings
+ */}
); } From af9d7ce7ea53d0daba36ab0a3f098b02c2b78a1d Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Tue, 17 Mar 2020 15:31:00 -0300 Subject: [PATCH 19/28] Temporary remove "Complete Challenge" from Listing Page --- src/shared/components/Notifications/TabsPanel/index.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/shared/components/Notifications/TabsPanel/index.jsx b/src/shared/components/Notifications/TabsPanel/index.jsx index 9d16f9dede..13977acada 100644 --- a/src/shared/components/Notifications/TabsPanel/index.jsx +++ b/src/shared/components/Notifications/TabsPanel/index.jsx @@ -41,7 +41,7 @@ export default class TabsPanel extends React.Component { changeTab(TABS.ACTIVE); } } - >ACTIVE CHALLENGES + >CHALLENGES
NOTIFICATIONS
+ {/* + * Disabled until Backend updated (add flag completed in notifications) + *
COMPLETED CHALLENGES
+ */}
{/* * Disabled until Settings page is ready From 112fd58a68249d67779bd0c335907a6373b37e93 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Sat, 21 Mar 2020 16:20:59 -0300 Subject: [PATCH 20/28] #4122: Allow HTML tag in notification msg --- src/shared/components/Notifications/index.jsx | 7 ++++++- src/shared/components/Notifications/style.scss | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/shared/components/Notifications/index.jsx b/src/shared/components/Notifications/index.jsx index ba193aff63..ed0400a9ca 100644 --- a/src/shared/components/Notifications/index.jsx +++ b/src/shared/components/Notifications/index.jsx @@ -49,7 +49,12 @@ const Item = ({ >
-

{item.contents}

+

{moment(item.date).fromNow()}

diff --git a/src/shared/components/Notifications/style.scss b/src/shared/components/Notifications/style.scss index 8c65274aa5..fecad3ffc1 100644 --- a/src/shared/components/Notifications/style.scss +++ b/src/shared/components/Notifications/style.scss @@ -56,6 +56,12 @@ $turquoise-super-dark: turquoise; text-align: center; margin: 15px auto 25px auto; } + + a { + color: #0d61bf; + text-decoration: underline; + cursor: pointer; + } } .challenge-title { From 2ced31a4ef34b1889f05a186ba8419d85881c4a1 Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Mon, 23 Mar 2020 13:37:21 +0530 Subject: [PATCH 21/28] ci: deploying on beta for QA --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 25cba0efcb..37d430c04f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -190,7 +190,7 @@ workflows: branches: only: - develop - - feature-contentful + - notifications # Production builds are exectuted only on tagged commits to the # master branch. - "build-prod": From 94723292521c9c55156e4208d2c79c27b687d808 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Tue, 24 Mar 2020 19:49:47 +0800 Subject: [PATCH 22/28] ci: deploying to beta --- .circleci/config.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 37d430c04f..af1aec5aec 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -189,7 +189,6 @@ workflows: filters: branches: only: - - develop - notifications # Production builds are exectuted only on tagged commits to the # master branch. From d8f1fc967c076d4c686b3328cbdf33cbcdfb47e2 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Fri, 27 Mar 2020 16:28:05 -0300 Subject: [PATCH 23/28] #4140: Added check if token expired in Header --- src/shared/containers/TopcoderHeader.js | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index a31eec4738..9ff9575189 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -1,6 +1,8 @@ /** * Container for the standard Topcoder header. */ +/* global location */ +/* eslint-disable no-restricted-globals */ import _ from 'lodash'; import headerActions from 'actions/topcoder_header'; @@ -10,6 +12,29 @@ import TopcoderHeader from 'components/Header'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; +import { isTokenExpired } from 'tc-accounts'; +import { config } from 'topcoder-react-utils'; + +/** + * Does nothing if a valid TC API v3 token is passed in; otherwise redirects + * user to the TC auth page, with proper return URL. + * @param {String} tokenV3 + * @return {Boolean} `true` if the user is authenticated; `false` otherwise. + */ +function authCheck(tokenV3) { + if (tokenV3 && !isTokenExpired(tokenV3)) return true; + + /* This implements front-end redirection. Once the server-side rendering of + * the Dashboard is in place, this should be updated to work for the server + * side rendering as well. */ + let url = `retUrl=${encodeURIComponent(location.href)}`; + url = `${config.URL.AUTH}/member?${url}&utm_source=community-app-main`; + location.href = url; + + _.noop(this); + return false; +} + function mapDispatchToProps(dispatch) { return { ...bindActionCreators(headerActions.topcoderHeader, dispatch), @@ -37,6 +62,8 @@ function mapDispatchToProps(dispatch) { } function mapStateToProps(state) { + authCheck(state.auth.tokenV3); + return { ...state.topcoderHeader, profile: { From 04479f813188f157ba2f1efdb27fe8b069923f97 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Fri, 27 Mar 2020 23:06:09 -0300 Subject: [PATCH 24/28] #4140: Change check token to load in right place --- src/shared/components/Header/index.jsx | 9 +++++++++ src/shared/containers/TopcoderHeader.js | 25 ------------------------- 2 files changed, 9 insertions(+), 25 deletions(-) diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index b28ed80b64..5914e300ee 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -2,6 +2,8 @@ import _ from 'lodash'; import React, { useState, useEffect } from 'react'; import PT from 'prop-types'; import { config } from 'topcoder-react-utils'; +import { isTokenExpired } from 'tc-accounts'; +import { goToLogin } from 'utils/tc'; import Logo from 'assets/images/tc-logo.svg'; let TopNavRef; @@ -50,6 +52,13 @@ const Header = ({ useEffect(() => { setPath(window.location.pathname); + + // Check auth token, go to login page if expired + if (auth.tokenV3 && isTokenExpired(auth.tokenV3)) { + goToLogin('community-app-main'); + return; + } + loadNotifications(auth.tokenV3); }, []); if (TopNavRef) { diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index 9ff9575189..e7abe88a1d 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -12,29 +12,6 @@ import TopcoderHeader from 'components/Header'; import { connect } from 'react-redux'; import { bindActionCreators } from 'redux'; -import { isTokenExpired } from 'tc-accounts'; -import { config } from 'topcoder-react-utils'; - -/** - * Does nothing if a valid TC API v3 token is passed in; otherwise redirects - * user to the TC auth page, with proper return URL. - * @param {String} tokenV3 - * @return {Boolean} `true` if the user is authenticated; `false` otherwise. - */ -function authCheck(tokenV3) { - if (tokenV3 && !isTokenExpired(tokenV3)) return true; - - /* This implements front-end redirection. Once the server-side rendering of - * the Dashboard is in place, this should be updated to work for the server - * side rendering as well. */ - let url = `retUrl=${encodeURIComponent(location.href)}`; - url = `${config.URL.AUTH}/member?${url}&utm_source=community-app-main`; - location.href = url; - - _.noop(this); - return false; -} - function mapDispatchToProps(dispatch) { return { ...bindActionCreators(headerActions.topcoderHeader, dispatch), @@ -62,8 +39,6 @@ function mapDispatchToProps(dispatch) { } function mapStateToProps(state) { - authCheck(state.auth.tokenV3); - return { ...state.topcoderHeader, profile: { From 2a85acdb771dfcc92bc04fbb8580c91cb3f0a962 Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Sat, 28 Mar 2020 00:01:07 -0300 Subject: [PATCH 25/28] #4140: Added "authenticating" check before Notification load --- src/shared/components/Header/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index 5914e300ee..17cfc83a8a 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -53,6 +53,8 @@ const Header = ({ useEffect(() => { setPath(window.location.pathname); + if (auth.authenticating) return; + // Check auth token, go to login page if expired if (auth.tokenV3 && isTokenExpired(auth.tokenV3)) { goToLogin('community-app-main'); From be09cf47be389cc15ecfbc86a95c32ef8a5f788e Mon Sep 17 00:00:00 2001 From: "Luiz R. Rodrigues" Date: Sat, 28 Mar 2020 16:00:02 -0300 Subject: [PATCH 26/28] #4140: Changed the approach to reload notifications if auth token is changed. --- src/shared/components/Header/index.jsx | 21 ++++++++++----------- src/shared/containers/TopcoderHeader.js | 4 +++- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/shared/components/Header/index.jsx b/src/shared/components/Header/index.jsx index 17cfc83a8a..1e52ce9c90 100644 --- a/src/shared/components/Header/index.jsx +++ b/src/shared/components/Header/index.jsx @@ -2,8 +2,6 @@ import _ from 'lodash'; import React, { useState, useEffect } from 'react'; import PT from 'prop-types'; import { config } from 'topcoder-react-utils'; -import { isTokenExpired } from 'tc-accounts'; -import { goToLogin } from 'utils/tc'; import Logo from 'assets/images/tc-logo.svg'; let TopNavRef; @@ -52,17 +50,18 @@ const Header = ({ useEffect(() => { setPath(window.location.pathname); + }, []); - if (auth.authenticating) return; - - // Check auth token, go to login page if expired - if (auth.tokenV3 && isTokenExpired(auth.tokenV3)) { - goToLogin('community-app-main'); - return; - } + /* + * Reload notificaitons if token was changed + * This prevent to use expired token in API call + */ + if (auth) { + useEffect(() => { + loadNotifications(auth.tokenV3); + }, [auth.tokenV3]); + } - loadNotifications(auth.tokenV3); - }, []); if (TopNavRef) { return (
diff --git a/src/shared/containers/TopcoderHeader.js b/src/shared/containers/TopcoderHeader.js index e7abe88a1d..537372ed51 100644 --- a/src/shared/containers/TopcoderHeader.js +++ b/src/shared/containers/TopcoderHeader.js @@ -48,7 +48,9 @@ function mapStateToProps(state) { notifications: (state.notifications && state.notifications.items && [...state.notifications.items]) || [], - auth: state.auth, + auth: { + ...state.auth, + }, }; } From e06275a33ebfda90109014fe19482788a1b90256 Mon Sep 17 00:00:00 2001 From: LieutenantRoger Date: Mon, 30 Mar 2020 12:36:25 +0800 Subject: [PATCH 27/28] ci:deploying to test --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index af1aec5aec..9a483b9366 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -175,13 +175,14 @@ workflows: branches: only: - develop - - feature-contentful + - feature-stats-history # This is alternate dev env for parallel testing - "build-test": context : org-global filters: branches: only: + - develop - notifications # This is beta env for production soft releases - "build-prod-beta": From b51436ef35761b8ddc1b7d2d65299526d5393ebe Mon Sep 17 00:00:00 2001 From: Sushil Shinde Date: Mon, 30 Mar 2020 16:39:15 +0530 Subject: [PATCH 28/28] feat: notifications feature release --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 853362e146..0c5075dde7 100644 --- a/package.json +++ b/package.json @@ -127,7 +127,7 @@ "supertest": "^3.1.0", "tc-accounts": "git+https://github.com/appirio-tech/accounts-app.git#dev", "tc-core-library-js": "github:appirio-tech/tc-core-library-js#v2.6.3", - "topcoder-react-lib": "1000.13.1", + "topcoder-react-lib": "0.13.0", "tc-ui": "^1.0.12", "topcoder-react-ui-kit": "^1.0.11", "topcoder-react-utils": "0.7.8",