diff --git a/src/components/UserCard/index.js b/src/components/UserCard/index.js
index 87387714..67f1cc08 100644
--- a/src/components/UserCard/index.js
+++ b/src/components/UserCard/index.js
@@ -1,3 +1,5 @@
+import _ from 'lodash'
+import moment from 'moment'
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import cn from 'classnames'
@@ -6,7 +8,6 @@ import { PROJECT_ROLES } from '../../config/constants'
import PrimaryButton from '../Buttons/PrimaryButton'
import AlertModal from '../Modal/AlertModal'
import { updateProjectMemberRole } from '../../services/projects'
-import _ from 'lodash'
const theme = {
container: styles.modalContainer
@@ -58,7 +59,7 @@ class UserCard extends Component {
}
render () {
- const { user, onRemoveClick, isEditable } = this.props
+ const { isInvite, user, onRemoveClick, isEditable } = this.props
const showRadioButtons = _.includes(_.values(PROJECT_ROLES), user.role)
return (
@@ -90,76 +91,90 @@ class UserCard extends Component {
)}
- {user.handle}
-
-
- {showRadioButtons && (
-
e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
- />
-
-
)}
-
-
- {showRadioButtons && (
-
e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
- />
-
-
)}
-
-
- {showRadioButtons && (
-
e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
- />
-
-
)}
-
-
- {showRadioButtons && (
-
e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
- />
-
-
)}
+ {isInvite ? user.email : user.handle}
+ {!isInvite && (
+ <>
+
+ {showRadioButtons && (
+
e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
+ />
+
+
)}
+
+
+ {showRadioButtons && (
+
e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
+ />
+
+
)}
+
+
+ {showRadioButtons && (
+
e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
+ />
+
+
)}
+
+
+ {showRadioButtons && (
+
e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
+ />
+
+
)}
+
+ >
+ )}
+ {isInvite && (
+ <>
+
+
+ Invited {moment(user.createdAt).format('MMM D, YY')}
+
+
+
+ >
+ )}
{isEditable ? (
* {
+ width: 125px;
+ }
}
.addUserContentContainer {
diff --git a/src/components/Users/index.js b/src/components/Users/index.js
index 1eb1b46f..e328e230 100644
--- a/src/components/Users/index.js
+++ b/src/components/Users/index.js
@@ -6,12 +6,13 @@ import styles from './Users.module.scss'
import Select from '../Select'
import UserCard from '../UserCard'
import PrimaryButton from '../Buttons/PrimaryButton'
-import Modal from '../Modal'
-import SelectUserAutocomplete from '../SelectUserAutocomplete'
import { PROJECT_ROLES, AUTOCOMPLETE_DEBOUNCE_TIME_MS } from '../../config/constants'
import { checkAdmin, checkManager } from '../../util/tc'
-import { addUserToProject, removeUserFromProject } from '../../services/projects'
+import { removeUserFromProject } from '../../services/projects'
+import { deleteProjectMemberInvite } from '../../services/projectMemberInvites'
import ConfirmationModal from '../Modal/ConfirmationModal'
+import UserAddModalContent from './user-add.modal'
+import InviteUserModalContent from './invite-user.modal' // Import the new component
const theme = {
container: styles.modalContainer
@@ -23,11 +24,7 @@ class Users extends Component {
this.state = {
projectOption: null,
showAddUserModal: false,
- userToAdd: null,
- userPermissionToAdd: PROJECT_ROLES.READ,
- showSelectUserError: false,
- isAdding: false,
- addUserError: false,
+ showInviteUserModal: false, // Add state for invite user modal
isRemoving: false,
removeError: null,
showRemoveConfirmationModal: false,
@@ -36,10 +33,9 @@ class Users extends Component {
}
this.setProjectOption = this.setProjectOption.bind(this)
this.onAddUserClick = this.onAddUserClick.bind(this)
+ this.onInviteUserClick = this.onInviteUserClick.bind(this) // Bind the new method
this.resetAddUserState = this.resetAddUserState.bind(this)
- this.onUpdateUserToAdd = this.onUpdateUserToAdd.bind(this)
- this.onAddUserConfirmClick = this.onAddUserConfirmClick.bind(this)
- this.updatePermission = this.updatePermission.bind(this)
+ this.resetInviteUserState = this.resetInviteUserState.bind(this) // Bind reset method
this.onRemoveClick = this.onRemoveClick.bind(this)
this.resetRemoveUserState = this.resetRemoveUserState.bind(this)
this.onRemoveConfirmClick = this.onRemoveConfirmClick.bind(this)
@@ -54,78 +50,24 @@ class Users extends Component {
loadProject(projectOption.value, false)
}
- updatePermission (newRole) {
- this.setState({
- userPermissionToAdd: newRole
- })
- }
-
onAddUserClick () {
this.setState({
showAddUserModal: true
})
}
- resetAddUserState () {
+ onInviteUserClick () {
this.setState({
- userToAdd: null,
- showSelectUserError: false,
- isAdding: false,
- showAddUserModal: false,
- userPermissionToAdd: PROJECT_ROLES.READ,
- addUserError: null
+ showInviteUserModal: true
})
}
- onUpdateUserToAdd (option) {
- let userToAdd = null
- if (option && option.value) {
- userToAdd = {
- handle: option.label,
- userId: parseInt(option.value, 10)
- }
- }
-
- this.setState({
- userToAdd,
- showSelectUserError: !userToAdd
- })
+ resetAddUserState () {
+ this.setState({ showAddUserModal: false })
}
- async onAddUserConfirmClick () {
- const { addNewProjectMember } = this.props
- if (this.state.isAdding) { return }
-
- this.setState({
- showSelectUserError: false,
- addUserError: null
- })
-
- if (!this.state.userToAdd) {
- this.setState({
- showSelectUserError: true
- })
- return
- }
-
- this.setState({
- isAdding: true
- })
-
- try {
- const newUserInfo = await addUserToProject(this.state.projectOption.value, this.state.userToAdd.userId, this.state.userPermissionToAdd)
- newUserInfo.handle = this.state.userToAdd.handle
- // wait for a second so that project's members are updated
- addNewProjectMember(newUserInfo)
- this.resetAddUserState()
- } catch (e) {
- const error = _.get(
- e,
- 'response.data.message',
- `Unable to add user`
- )
- this.setState({ isAdding: false, addUserError: error })
- }
+ resetInviteUserState () {
+ this.setState({ showInviteUserModal: false })
}
getHandle () {
@@ -167,11 +109,14 @@ class Users extends Component {
async onRemoveConfirmClick () {
if (this.state.isRemoving) { return }
- const { removeProjectNember } = this.props
+ const { removeProjectNember, invitedMembers } = this.props
const userToRemove = this.state.userToRemove
+ const isInvite = !!_.find(invitedMembers, { email: userToRemove.email })
try {
this.setState({ isRemoving: true })
- await removeUserFromProject(userToRemove.projectId, userToRemove.id)
+ await (
+ isInvite ? deleteProjectMemberInvite(userToRemove.projectId, userToRemove.id) : removeUserFromProject(userToRemove.projectId, userToRemove.id)
+ )
removeProjectNember(userToRemove)
this.resetRemoveUserState()
@@ -210,6 +155,7 @@ class Users extends Component {
const {
projects,
projectMembers,
+ invitedMembers,
updateProjectNember,
isEditable,
isSearchingUserProjects,
@@ -226,7 +172,7 @@ class Users extends Component {
}
})
const loggedInHandle = this.getHandle()
- const membersExist = projectMembers && projectMembers.length > 0
+ const membersExist = (projectMembers && projectMembers.length > 0) || (invitedMembers && invitedMembers.length > 0)
const isCopilotOrManager = this.checkIsCopilotOrManager(projectMembers, loggedInHandle)
const isAdmin = checkAdmin(this.props.auth.token)
const isManager = checkManager(this.props.auth.token)
@@ -263,140 +209,38 @@ class Users extends Component {
text={'Add User'}
type={'info'}
onClick={() => this.onAddUserClick()} />
+ this.onInviteUserClick()} />
)
}
{
this.state.showAddUserModal && (
-
this.resetAddUserState()}>
-
-
Add User
-
-
- {
- this.state.showSelectUserError && (
-
-
Please select a member.
-
- )
- }
-
-
-
-
-
-
-
e.target.checked && this.updatePermission(PROJECT_ROLES.READ)}
- />
-
-
-
-
-
-
e.target.checked && this.updatePermission(PROJECT_ROLES.WRITE)}
- />
-
-
-
-
-
-
e.target.checked && this.updatePermission(PROJECT_ROLES.MANAGER)}
- />
-
-
-
-
-
-
e.target.checked && this.updatePermission(PROJECT_ROLES.COPILOT)}
- />
-
-
-
-
- {
- this.state.addUserError && (
-
- {this.state.addUserError}
-
- )
- }
-
-
-
-
-
this.resetAddUserState()}
- />
-
-
-
this.onAddUserConfirmClick()}
- />
-
-
-
-
+
+ )
+ }
+ {
+ this.state.showInviteUserModal && (
+
)
}
{
this.state.showRemoveConfirmationModal && (
+
+ {
+ _.map(invitedMembers, (member) => {
+ return (
+ -
+
+
+ )
+ })
+ }
+
>
)
}
@@ -453,12 +313,14 @@ Users.propTypes = {
loadProject: PropTypes.func.isRequired,
updateProjectNember: PropTypes.func.isRequired,
removeProjectNember: PropTypes.func.isRequired,
+ addNewProjectInvite: PropTypes.func.isRequired,
addNewProjectMember: PropTypes.func.isRequired,
auth: PropTypes.object,
isEditable: PropTypes.bool,
isSearchingUserProjects: PropTypes.bool,
projects: PropTypes.arrayOf(PropTypes.object),
projectMembers: PropTypes.arrayOf(PropTypes.object),
+ invitedMembers: PropTypes.arrayOf(PropTypes.object),
searchUserProjects: PropTypes.func.isRequired,
resultSearchUserProjects: PropTypes.arrayOf(PropTypes.object),
loadNextProjects: PropTypes.func.isRequired
diff --git a/src/components/Users/invite-user.modal.js b/src/components/Users/invite-user.modal.js
new file mode 100644
index 00000000..41b1dff4
--- /dev/null
+++ b/src/components/Users/invite-user.modal.js
@@ -0,0 +1,138 @@
+import React, { useState } from 'react'
+import PropTypes from 'prop-types'
+import cn from 'classnames'
+import { find, get } from 'lodash'
+import Modal from '../Modal'
+import PrimaryButton from '../Buttons/PrimaryButton'
+import { inviteUserToProject } from '../../services/projects'
+import { PROJECT_ROLES } from '../../config/constants'
+
+import styles from './Users.module.scss'
+
+const theme = {
+ container: styles.modalContainer
+}
+
+const validateEmail = (email) => {
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+ return emailRegex.test(email)
+}
+
+const InviteUserModalContent = ({ projectId, onClose, onMemberInvited, projectMembers, invitedMembers }) => {
+ const [emailToInvite, setEmailToInvite] = useState('')
+ const [showEmailError, setShowEmailError] = useState(false)
+ const [inviteUserError, setInviteUserError] = useState(null)
+ const [isInviting, setIsInviting] = useState(false)
+
+ const checkEmail = () => {
+ if (!validateEmail(emailToInvite)) {
+ setShowEmailError(true)
+ return false
+ }
+
+ if (find(invitedMembers, { email: emailToInvite })) {
+ setInviteUserError('Email is already invited!')
+ return false
+ }
+
+ if (find(projectMembers, { email: emailToInvite })) {
+ setInviteUserError('Member already part of the project!')
+ return false
+ }
+
+ return true
+ }
+
+ const onInviteUserConfirmClick = async () => {
+ if (isInviting) return
+
+ if (!checkEmail()) {
+ return
+ }
+
+ setIsInviting(true)
+ setInviteUserError(null)
+
+ try {
+ // api restriction: ONLY "customer" role can be invited via email
+ const { success: invitations = [], failed } = await inviteUserToProject(projectId, emailToInvite, PROJECT_ROLES.CUSTOMER)
+
+ if (failed) {
+ const error = get(failed, '0.message', 'Unable to invite user')
+ setInviteUserError(error)
+ setIsInviting(false)
+ } else {
+ onMemberInvited(invitations[0] || {})
+ onClose()
+ }
+ } catch (e) {
+ const error = get(e, 'response.data.message', 'Unable to invite user')
+ setInviteUserError(error)
+ setIsInviting(false)
+ }
+ }
+
+ return (
+
+
+
Invite User
+
+
+
+ Email* :
+
+
+ {
+ setEmailToInvite(e.target.value)
+ setShowEmailError(false)
+ setInviteUserError(null)
+ }}
+ onBlur={checkEmail}
+ />
+
+
+ {showEmailError && (
+
+
Please enter a valid email address.
+
+ )}
+ {inviteUserError && (
+
+ )}
+
+
+
+
+ )
+}
+
+InviteUserModalContent.propTypes = {
+ projectId: PropTypes.number.isRequired,
+ onClose: PropTypes.func.isRequired,
+ onMemberInvited: PropTypes.func.isRequired,
+ projectMembers: PropTypes.arrayOf(PropTypes.object),
+ invitedMembers: PropTypes.arrayOf(PropTypes.object)
+}
+
+export default InviteUserModalContent
diff --git a/src/components/Users/user-add.modal.js b/src/components/Users/user-add.modal.js
new file mode 100644
index 00000000..79f08f93
--- /dev/null
+++ b/src/components/Users/user-add.modal.js
@@ -0,0 +1,175 @@
+import React, { useState } from 'react'
+import PropTypes from 'prop-types'
+import cn from 'classnames'
+import { get } from 'lodash'
+import Modal from '../Modal'
+import SelectUserAutocomplete from '../SelectUserAutocomplete'
+import { PROJECT_ROLES } from '../../config/constants'
+import PrimaryButton from '../Buttons/PrimaryButton'
+import { addUserToProject } from '../../services/projects'
+
+import styles from './Users.module.scss'
+
+const theme = {
+ container: styles.modalContainer
+}
+
+const UserAddModalContent = ({ projectId, addNewProjectMember, onClose }) => {
+ const [userToAdd, setUserToAdd] = useState(null)
+ const [userPermissionToAdd, setUserPermissionToAdd] = useState(PROJECT_ROLES.READ)
+ const [showSelectUserError, setShowSelectUserError] = useState(false)
+ const [addUserError, setAddUserError] = useState(null)
+ const [isAdding, setIsAdding] = useState(false)
+
+ const onUpdateUserToAdd = (option) => {
+ if (option && option.value) {
+ setUserToAdd({
+ handle: option.label,
+ userId: parseInt(option.value, 10)
+ })
+ setShowSelectUserError(false)
+ } else {
+ setUserToAdd(null)
+ }
+ }
+
+ const onAddUserConfirmClick = async () => {
+ if (isAdding) return
+
+ if (!userToAdd) {
+ setShowSelectUserError(true)
+ return
+ }
+
+ setIsAdding(true)
+ setAddUserError(null)
+
+ try {
+ const newUserInfo = await addUserToProject(projectId, userToAdd.userId, userPermissionToAdd)
+ newUserInfo.handle = userToAdd.handle
+ addNewProjectMember(newUserInfo)
+ onClose()
+ } catch (e) {
+ const error = get(e, 'response.data.message', 'Unable to add user')
+ setAddUserError(error)
+ setIsAdding(false)
+ }
+ }
+
+ return (
+
+
+
Add User
+
+
+ {showSelectUserError && (
+
+
Please select a member.
+
+ )}
+
+
+
+
+
+
+
setUserPermissionToAdd(PROJECT_ROLES.READ)}
+ />
+
+
+
+
+
+
setUserPermissionToAdd(PROJECT_ROLES.WRITE)}
+ />
+
+
+
+
+
+
setUserPermissionToAdd(PROJECT_ROLES.MANAGER)}
+ />
+
+
+
+
+
+
setUserPermissionToAdd(PROJECT_ROLES.COPILOT)}
+ />
+
+
+
+
+ {addUserError && (
+
{addUserError}
+ )}
+
+
+
+
+ )
+}
+UserAddModalContent.propTypes = {
+ projectId: PropTypes.number.isRequired,
+ addNewProjectMember: PropTypes.func.isRequired,
+ onClose: PropTypes.func.isRequired
+}
+
+export default UserAddModalContent
diff --git a/src/config/constants.js b/src/config/constants.js
index 1310acb4..befe6681 100644
--- a/src/config/constants.js
+++ b/src/config/constants.js
@@ -33,7 +33,9 @@ export const {
TYPEFORM_URL,
PROFILE_URL
} = process.env
+
export const CREATE_FORUM_TYPE_IDS = typeof process.env.CREATE_FORUM_TYPE_IDS === 'string' ? process.env.CREATE_FORUM_TYPE_IDS.split(',') : process.env.CREATE_FORUM_TYPE_IDS
+export const PROJECTS_API_URL = process.env.PROJECTS_API_URL || process.env.PROJECT_API_URL
/**
* Filepicker config
@@ -193,6 +195,14 @@ export const UPDATE_PROJECT_PENDING = 'UPDATE_PROJECT_PENDING'
export const UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS'
export const UPDATE_PROJECT_FAILURE = 'UPDATE_PROJECT_FAILURE'
+export const PROJECT_MEMBER_INVITE_STATUS_ACCEPTED = 'accepted'
+export const PROJECT_MEMBER_INVITE_STATUS_REFUSED = 'refused'
+export const PROJECT_MEMBER_INVITE_STATUS_CANCELED = 'canceled'
+export const PROJECT_MEMBER_INVITE_STATUS_PENDING = 'pending'
+export const PROJECT_MEMBER_INVITE_STATUS_REQUESTED = 'requested'
+export const PROJECT_MEMBER_INVITE_STATUS_REQUEST_APPROVED = 'request_approved'
+export const PROJECT_MEMBER_INVITE_STATUS_REQUEST_REJECTED = 'request_rejected'
+
// Name of challenge tracks
export const CHALLENGE_TRACKS = {
DESIGN: DES_TRACK_ID,
@@ -242,6 +252,7 @@ export const MARATHON_MATCH_SUBTRACKS = [
export const PROJECT_ROLES = {
READ: 'observer',
+ CUSTOMER: 'customer',
WRITE: 'customer',
MANAGER: 'manager',
COPILOT: 'copilot'
diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js
index ca7759f5..8b5d5868 100644
--- a/src/containers/Challenges/index.js
+++ b/src/containers/Challenges/index.js
@@ -20,7 +20,8 @@ import {
setActiveProject,
resetSidebarActiveParams
} from '../../actions/sidebar'
-import { checkAdmin } from '../../util/tc'
+import { checkAdmin, checkIsUserInvited } from '../../util/tc'
+import { withRouter } from 'react-router-dom'
class Challenges extends Component {
constructor (props) {
@@ -55,6 +56,14 @@ class Challenges extends Component {
}
}
+ componentDidUpdate () {
+ const { auth } = this.props
+
+ if (checkIsUserInvited(auth.token, this.props.projectDetail)) {
+ this.props.history.push(`/projects/${this.props.projectDetail.id}/invitation`)
+ }
+ }
+
componentWillReceiveProps (nextProps) {
if (
(nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) ||
@@ -194,6 +203,7 @@ Challenges.defaultProps = {
}
Challenges.propTypes = {
+ history: PropTypes.object,
projects: PropTypes.arrayOf(PropTypes.shape()),
menu: PropTypes.string,
challenges: PropTypes.arrayOf(PropTypes.object),
@@ -268,4 +278,6 @@ const mapDispatchToProps = {
deleteChallenge
}
-export default connect(mapStateToProps, mapDispatchToProps)(Challenges)
+export default withRouter(
+ connect(mapStateToProps, mapDispatchToProps)(Challenges)
+)
diff --git a/src/containers/ProjectEditor/index.js b/src/containers/ProjectEditor/index.js
index 653b02b5..241562b2 100644
--- a/src/containers/ProjectEditor/index.js
+++ b/src/containers/ProjectEditor/index.js
@@ -15,7 +15,7 @@ import {
updateProject
} from '../../actions/projects'
import { setActiveProject } from '../../actions/sidebar'
-import { checkAdminOrCopilot, checkAdmin } from '../../util/tc'
+import { checkAdminOrCopilot, checkAdmin, checkIsUserInvited } from '../../util/tc'
import { PROJECT_ROLES } from '../../config/constants'
import Loader from '../../components/Loader'
@@ -37,6 +37,11 @@ class ProjectEditor extends Component {
componentDidUpdate () {
const { auth } = this.props
+
+ if (checkIsUserInvited(auth.token, this.props.projectDetail)) {
+ this.props.history.push(`/projects/${this.props.projectDetail.id}/invitation`)
+ }
+
if (!checkAdminOrCopilot(auth.token, this.props.projectDetail)) {
this.props.history.push('/projects')
}
diff --git a/src/containers/ProjectInvitations/ProjectInvitations.module.scss b/src/containers/ProjectInvitations/ProjectInvitations.module.scss
new file mode 100644
index 00000000..865f95f9
--- /dev/null
+++ b/src/containers/ProjectInvitations/ProjectInvitations.module.scss
@@ -0,0 +1,120 @@
+@import '../../styles/includes';
+
+.modalContainer {
+ padding: 0;
+ position: fixed;
+ overflow: auto;
+ z-index: 10000;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ box-sizing: border-box;
+ width: auto;
+ max-width: none;
+ transform: none;
+ background: transparent;
+ color: $text-color;
+ opacity: 1;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+
+ :global {
+ button.close {
+ margin-right: 5px;
+ margin-top: 5px;
+ }
+ }
+
+ .contentContainer {
+ box-sizing: border-box;
+ background: $white;
+ opacity: 1;
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-start;
+ align-items: center;
+ border-radius: 6px;
+ margin: 0 auto;
+ width: 852px;
+ padding: 30px;
+
+ .content {
+ padding: 30px;
+ width: 100%;
+ height: 100%;
+ }
+
+ .title {
+ @include roboto-bold();
+
+ font-size: 30px;
+ line-height: 36px;
+ margin-bottom: 30px;
+ margin-top: 0;
+ }
+
+ span {
+ @include roboto;
+
+ font-size: 22px;
+ font-weight: 400;
+ line-height: 26px;
+ }
+
+ &.confirm {
+ width: 999px;
+
+ .buttonGroup {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 30px;
+
+ .buttonSizeA {
+ width: 193px;
+ height: 40px;
+ margin-right: 33px;
+
+ span {
+ font-size: 18px;
+ font-weight: 500;
+ }
+ }
+
+ .buttonSizeB {
+ width: 160px;
+ height: 40px;
+
+ span {
+ font-size: 18px;
+ font-weight: 500;
+ line-height: 22px;
+ }
+ }
+ }
+ }
+
+ .buttonGroup {
+ display: flex;
+ justify-content: space-between;
+ margin-top: 30px;
+
+ .button {
+ width: 135px;
+ height: 40px;
+ margin-right: 66px;
+
+ span {
+ font-size: 18px;
+ font-weight: 500;
+ }
+ }
+
+ .button:last-child {
+ margin-right: 0;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/containers/ProjectInvitations/index.js b/src/containers/ProjectInvitations/index.js
new file mode 100644
index 00000000..89048b82
--- /dev/null
+++ b/src/containers/ProjectInvitations/index.js
@@ -0,0 +1,119 @@
+import PropTypes from 'prop-types'
+import React, { useCallback, useEffect, useMemo, useState } from 'react'
+import { connect } from 'react-redux'
+import { withRouter } from 'react-router-dom'
+import { toastr } from 'react-redux-toastr'
+import { checkIsUserInvited } from '../../util/tc'
+import { isEmpty } from 'lodash'
+import { loadProject } from '../../actions/projects'
+import ConfirmationModal from '../../components/Modal/ConfirmationModal'
+
+import styles from './ProjectInvitations.module.scss'
+import { updateProjectMemberInvite } from '../../services/projectMemberInvites'
+import { PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED } from '../../config/constants'
+
+const theme = {
+ container: styles.modalContainer
+}
+
+const ProjectInvitations = ({ match, auth, isProjectLoading, history, projectDetail, loadProject }) => {
+ const automaticAction = useMemo(() => [PROJECT_MEMBER_INVITE_STATUS_ACCEPTED, PROJECT_MEMBER_INVITE_STATUS_REFUSED].includes(match.params.action) ? match.params.action : undefined, [match.params])
+ const projectId = useMemo(() => parseInt(match.params.projectId), [match.params])
+ const invitation = useMemo(() => checkIsUserInvited(auth.token, projectDetail), [auth.token, projectDetail])
+ const [isUpdating, setIsUpdating] = useState(automaticAction || false)
+ const isAccepting = isUpdating === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED
+ const isDeclining = isUpdating === PROJECT_MEMBER_INVITE_STATUS_REFUSED
+
+ useEffect(() => {
+ if (!projectId) {
+ return
+ }
+
+ if (isProjectLoading || isEmpty(projectDetail)) {
+ if (!isProjectLoading) {
+ loadProject(projectId)
+ }
+ return
+ }
+
+ if (!invitation) {
+ history.push(`/projects`)
+ }
+ }, [projectId, auth, projectDetail, isProjectLoading, history])
+
+ const updateInvite = useCallback(async (status) => {
+ setIsUpdating(status)
+ await updateProjectMemberInvite(projectId, invitation.id, status)
+ toastr.success('Success', `Successfully ${status} the invitation.`)
+ history.push(status === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED ? `/projects/${projectId}/challenges` : '/projects')
+ }, [invitation])
+
+ const acceptInvite = useCallback(() => updateInvite(PROJECT_MEMBER_INVITE_STATUS_ACCEPTED), [updateInvite])
+ const declineInvite = useCallback(() => updateInvite(PROJECT_MEMBER_INVITE_STATUS_REFUSED), [updateInvite])
+
+ useEffect(() => {
+ if (!invitation || !automaticAction) {
+ return
+ }
+
+ setTimeout(() => {
+ if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED) {
+ acceptInvite()
+ } else if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_REFUSED) {
+ declineInvite()
+ }
+ }, [1500])
+ }, [invitation, automaticAction])
+
+ return (
+ <>
+ {invitation && (
+
+ )}
+ >
+ )
+}
+
+ProjectInvitations.propTypes = {
+ match: PropTypes.shape({
+ params: PropTypes.shape({
+ projectId: PropTypes.string
+ })
+ }).isRequired,
+ auth: PropTypes.object.isRequired,
+ isProjectLoading: PropTypes.bool,
+ history: PropTypes.object,
+ loadProject: PropTypes.func.isRequired,
+ projectDetail: PropTypes.object
+}
+
+const mapStateToProps = ({ projects, auth }) => {
+ return {
+ projectDetail: projects.projectDetail,
+ isProjectLoading: projects.isLoading,
+ auth
+ }
+}
+
+const mapDispatchToProps = {
+ loadProject
+}
+
+export default withRouter(
+ connect(mapStateToProps, mapDispatchToProps)(ProjectInvitations)
+)
diff --git a/src/containers/Users/index.js b/src/containers/Users/index.js
index c8666784..dabaa0b5 100644
--- a/src/containers/Users/index.js
+++ b/src/containers/Users/index.js
@@ -20,11 +20,13 @@ class Users extends Component {
this.state = {
loginUserRoleInProject: '',
projectMembers: null,
+ invitedMembers: null,
isAdmin: false
}
this.loadProject = this.loadProject.bind(this)
this.updateProjectNember = this.updateProjectNember.bind(this)
this.removeProjectNember = this.removeProjectNember.bind(this)
+ this.addNewProjectInvite = this.addNewProjectInvite.bind(this)
this.addNewProjectMember = this.addNewProjectMember.bind(this)
this.loadNextProjects = this.loadNextProjects.bind(this)
}
@@ -80,8 +82,10 @@ class Users extends Component {
loadProject (projectId) {
fetchProjectById(projectId).then((project) => {
const projectMembers = _.get(project, 'members')
+ const invitedMembers = _.get(project, 'invites')
this.setState({
- projectMembers
+ projectMembers,
+ invitedMembers
})
const { loggedInUser } = this.props
this.updateLoginUserRoleInProject(projectMembers, loggedInUser)
@@ -102,11 +106,13 @@ class Users extends Component {
}
removeProjectNember (projectMember) {
- const { projectMembers } = this.state
+ const { projectMembers, invitedMembers } = this.state
const newProjectMembers = _.filter(projectMembers, pm => pm.id !== projectMember.id)
+ const newInvitedMembers = _.filter(invitedMembers, pm => pm.id !== projectMember.id)
const { loggedInUser } = this.props
this.setState({
- projectMembers: newProjectMembers
+ projectMembers: newProjectMembers,
+ invitedMembers: newInvitedMembers
})
this.updateLoginUserRoleInProject(newProjectMembers, loggedInUser)
}
@@ -124,6 +130,15 @@ class Users extends Component {
this.updateLoginUserRoleInProject(newProjectMembers, loggedInUser)
}
+ addNewProjectInvite (invitedMember) {
+ this.setState(() => ({
+ invitedMembers: [
+ ...(this.state.invitedMembers || []),
+ invitedMember
+ ]
+ }))
+ }
+
render () {
const {
projects,
@@ -134,6 +149,7 @@ class Users extends Component {
} = this.props
const {
projectMembers,
+ invitedMembers,
isAdmin
} = this.state
return (
@@ -143,8 +159,10 @@ class Users extends Component {
updateProjectNember={this.updateProjectNember}
removeProjectNember={this.removeProjectNember}
addNewProjectMember={this.addNewProjectMember}
+ addNewProjectInvite={this.addNewProjectInvite}
loadNextProjects={this.loadNextProjects}
projectMembers={projectMembers}
+ invitedMembers={invitedMembers}
auth={auth}
isAdmin={isAdmin}
isEditable={this.isEditable()}
diff --git a/src/routes.js b/src/routes.js
index aa87a527..fed56ded 100644
--- a/src/routes.js
+++ b/src/routes.js
@@ -33,6 +33,7 @@ import ConfirmationModal from './components/Modal/ConfirmationModal'
import Users from './containers/Users'
import { isBetaMode, removeFromLocalStorage, saveToLocalStorage } from './util/localstorage'
import ProjectEditor from './containers/ProjectEditor'
+import ProjectInvitations from './containers/ProjectInvitations'
const { ACCOUNTS_APP_LOGIN_URL, IDLE_TIMEOUT_MINUTES, IDLE_TIMEOUT_GRACE_MINUTES, COMMUNITY_APP_URL } = process.env
@@ -210,6 +211,14 @@ class Routes extends React.Component {
)()}
/>
+ renderApp(
+ ,
+ ,
+ ,
+
+ )()}
+ />
renderApp(
,
diff --git a/src/services/projectMemberInvites.js b/src/services/projectMemberInvites.js
new file mode 100644
index 00000000..783155d5
--- /dev/null
+++ b/src/services/projectMemberInvites.js
@@ -0,0 +1,66 @@
+import { axiosInstance as axios } from './axiosWithAuth'
+import { PROJECTS_API_URL } from '../config/constants'
+
+/**
+ * Update project member invite based on project's id & given member
+ * @param {integer} projectId unique identifier of the project
+ * @param {integer} inviteId unique identifier of the invite
+ * @param {string} status the new status for invitation
+ * @return {object} project member invite returned by api
+ */
+export function updateProjectMemberInvite (projectId, inviteId, status) {
+ const url = `${PROJECTS_API_URL}/${projectId}/invites/${inviteId}`
+ return axios.patch(url, { status })
+ .then(resp => resp.data)
+}
+
+/**
+ * Delete project member invite based on project's id & given invite's id
+ * @param {integer} projectId unique identifier of the project
+ * @param {integer} inviteId unique identifier of the invite
+ * @return {object} project member invite returned by api
+ */
+export function deleteProjectMemberInvite (projectId, inviteId) {
+ const url = `${PROJECTS_API_URL}/${projectId}/invites/${inviteId}`
+ return axios.delete(url)
+}
+
+/**
+ * Create a project member invite based on project's id & given member
+ * @param {integer} projectId unique identifier of the project
+ * @param {object} member invite
+ * @return {object} project member invite returned by api
+ */
+export function createProjectMemberInvite (projectId, member) {
+ const fields = 'id,projectId,userId,email,role,status,createdAt,updatedAt,createdBy,updatedBy,handle'
+ const url = `${PROJECTS_API_URL}/${projectId}/invites/?fields=` + encodeURIComponent(fields)
+ return axios({
+ method: 'post',
+ url,
+ data: member,
+ validateStatus (status) {
+ return (status >= 200 && status < 300) || status === 403
+ }
+ })
+ .then(resp => resp.data)
+}
+
+export function getProjectMemberInvites (projectId) {
+ const fields = 'id,projectId,userId,email,role,status,createdAt,updatedAt,createdBy,updatedBy,handle'
+ const url = `${PROJECTS_API_URL}/${projectId}/invites/?fields=` +
+ encodeURIComponent(fields)
+ return axios.get(url)
+ .then(resp => {
+ return resp.data
+ })
+}
+
+/**
+ * Get a project member invite based on project's id
+ * @param {integer} projectId unique identifier of the project
+ * @return {object} project member invite returned by api
+ */
+export function getProjectInviteById (projectId) {
+ return axios.get(`${PROJECTS_API_URL}/${projectId}/invites`)
+ .then(resp => resp.data)
+}
diff --git a/src/services/projects.js b/src/services/projects.js
index e749ea11..e61a0f70 100644
--- a/src/services/projects.js
+++ b/src/services/projects.js
@@ -7,11 +7,11 @@ import {
GENERIC_PROJECT_MILESTONE_PRODUCT_NAME,
GENERIC_PROJECT_MILESTONE_PRODUCT_TYPE,
PHASE_PRODUCT_CHALLENGE_ID_FIELD,
- PHASE_PRODUCT_TEMPLATE_ID
+ PHASE_PRODUCT_TEMPLATE_ID,
+ PROJECTS_API_URL
} from '../config/constants'
import { paginationHeaders } from '../util/pagination'
-
-const { PROJECT_API_URL } = process.env
+import { createProjectMemberInvite } from './projectMemberInvites'
/**
* Get billing accounts based on project id
@@ -21,7 +21,7 @@ const { PROJECT_API_URL } = process.env
* @returns {Promise