diff --git a/.github/workflows/code_reviewer.yml b/.github/workflows/code_reviewer.yml
new file mode 100644
index 00000000..1d313051
--- /dev/null
+++ b/.github/workflows/code_reviewer.yml
@@ -0,0 +1,22 @@
+name: AI PR Reviewer
+
+on:
+ pull_request:
+ types:
+ - opened
+ - synchronize
+permissions:
+ pull-requests: write
+jobs:
+ tc-ai-pr-review:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Repo
+ uses: actions/checkout@v3
+
+ - name: TC AI PR Reviewer
+ uses: topcoder-platform/tc-ai-pr-reviewer@master
+ with:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # The GITHUB_TOKEN is there by default so you just need to keep it like it is and not necessarily need to add it as secret as it will throw an error. [More Details](https://docs.github.com/en/actions/security-guides/automatic-token-authentication#about-the-github_token-secret)
+ LAB45_API_KEY: ${{ secrets.LAB45_API_KEY }}
+ exclude: "**/*.json, **/*.md, **/*.jpg, **/*.png, **/*.jpeg, **/*.bmp, **/*.webp" # Optional: exclude patterns separated by commas
diff --git a/public/static/comment.jpg b/public/static/comment.jpg
new file mode 100644
index 00000000..52a7ce58
Binary files /dev/null and b/public/static/comment.jpg differ
diff --git a/public/static/logo.jpg b/public/static/logo.jpg
new file mode 100644
index 00000000..44cf1a5f
Binary files /dev/null and b/public/static/logo.jpg differ
diff --git a/src/actions/challenges.js b/src/actions/challenges.js
index f1dabb7e..60eb631f 100644
--- a/src/actions/challenges.js
+++ b/src/actions/challenges.js
@@ -57,7 +57,7 @@ import {
} from '../config/constants'
import { loadProject } from './projects'
import { removeChallengeFromPhaseProduct, saveChallengeAsPhaseProduct } from '../services/projects'
-import { checkAdmin } from '../util/tc'
+import { checkAdmin, checkManager } from '../util/tc'
/**
* Member challenges related redux actions
@@ -159,7 +159,11 @@ export function loadChallengesByPage (
filters['projectId'] = projectId
} else if (_.isObject(projectId) && projectId.value > 0) {
filters['projectId'] = projectId.value
- } else if (!checkAdmin(getState().auth.token) && userId) {
+ } else if (
+ !checkAdmin(getState().auth.token) &&
+ !checkManager(getState().auth.token) &&
+ userId
+ ) {
// Note that we only add the memberId field if *no* project ID is given,
// so that the list of *all challenges shows only those that the member is on
filters['memberId'] = userId
diff --git a/src/actions/projects.js b/src/actions/projects.js
index 2d88e519..dfc6bf9b 100644
--- a/src/actions/projects.js
+++ b/src/actions/projects.js
@@ -20,7 +20,8 @@ import {
UPDATE_PROJECT_FAILURE,
ADD_PROJECT_ATTACHMENT_SUCCESS,
UPDATE_PROJECT_ATTACHMENT_SUCCESS,
- REMOVE_PROJECT_ATTACHMENT_SUCCESS
+ REMOVE_PROJECT_ATTACHMENT_SUCCESS,
+ LOAD_PROJECT_INVITES
} from '../config/constants'
import {
fetchProjectById,
@@ -30,9 +31,10 @@ import {
createProjectApi,
fetchBillingAccounts,
fetchMemberProjects,
- updateProjectApi
+ updateProjectApi,
+ getProjectInvites
} from '../services/projects'
-import { checkAdmin } from '../util/tc'
+import { checkAdmin, checkManager } from '../util/tc'
function _loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
return (dispatch, getState) => {
@@ -54,7 +56,7 @@ function _loadProjects (projectNameOrIdFilter = '', paramFilters = {}) {
}
}
- if (!checkAdmin(getState().auth.token)) {
+ if (!checkAdmin(getState().auth.token) && !checkManager(getState().auth.token)) {
filters['memberOnly'] = true
}
@@ -171,6 +173,18 @@ export function loadProjectTypes () {
}
}
+/**
+ * Loads project invites
+ */
+export function loadProjectInvites (projectId) {
+ return (dispatch) => {
+ return dispatch({
+ type: LOAD_PROJECT_INVITES,
+ payload: getProjectInvites(projectId)
+ })
+ }
+}
+
/**
* Creates a project
*/
diff --git a/src/actions/sidebar.js b/src/actions/sidebar.js
index 4fd02fd1..0c02d8f4 100644
--- a/src/actions/sidebar.js
+++ b/src/actions/sidebar.js
@@ -11,7 +11,7 @@ import {
UNLOAD_PROJECTS_SUCCESS,
PROJECTS_PAGE_SIZE
} from '../config/constants'
-import { checkAdmin } from '../util/tc'
+import { checkAdmin, checkManager } from '../util/tc'
import _ from 'lodash'
/**
@@ -50,7 +50,7 @@ export function loadProjects (filterProjectName = '', paramFilters = {}) {
}
}
- if (!checkAdmin(getState().auth.token)) {
+ if (!checkAdmin(getState().auth.token) && !checkManager(getState().auth.token)) {
filters['memberOnly'] = true
}
diff --git a/src/actions/users.js b/src/actions/users.js
index 46a9a56f..31a65ab2 100644
--- a/src/actions/users.js
+++ b/src/actions/users.js
@@ -10,33 +10,55 @@ import {
SEARCH_USER_PROJECTS_SUCCESS,
SEARCH_USER_PROJECTS_FAILURE
} from '../config/constants'
+import _ from 'lodash'
/**
* Loads projects of the authenticated user
*/
-export function loadAllUserProjects (isAdmin = true) {
- return (dispatch) => {
+export function loadAllUserProjects (params, isAdmin = true, isManager = true) {
+ return (dispatch, getState) => {
dispatch({
type: LOAD_ALL_USER_PROJECTS_PENDING
})
+ const state = getState().users
+
const filters = {
status: 'active',
- sort: 'lastActivityAt desc'
+ sort: 'lastActivityAt desc',
+ perPage: 20,
+ ...params
}
- if (!isAdmin) {
+
+ if (!isAdmin && !isManager) {
filters['memberOnly'] = true
}
- fetchMemberProjects(filters).then(({ projects }) => dispatch({
+ fetchMemberProjects(filters).then(({ projects, pagination }) => dispatch({
type: LOAD_ALL_USER_PROJECTS_SUCCESS,
- projects
+ projects: _.uniqBy((filters.page ? state.allUserProjects || [] : []).concat(projects), 'id'),
+ total: pagination.xTotal,
+ page: pagination.xPage
})).catch(() => dispatch({
type: LOAD_ALL_USER_PROJECTS_FAILURE
}))
}
}
+export function loadNextProjects (isAdmin = true, isManager = true) {
+ return (dispatch, getState) => {
+ const { page, total, allUserProjects } = getState().users
+ if (allUserProjects.length >= total) {
+ return
+ }
+
+ loadAllUserProjects(_.assign({}, {
+ perPage: 20,
+ page: page + 1
+ }), isAdmin, isManager)(dispatch, getState)
+ }
+}
+
/**
* Filter projects of the authenticated user
*
diff --git a/src/components/ProjectCard/index.js b/src/components/ProjectCard/index.js
index 9d3d7675..97376dc4 100644
--- a/src/components/ProjectCard/index.js
+++ b/src/components/ProjectCard/index.js
@@ -8,11 +8,11 @@ import { PROJECT_STATUSES } from '../../config/constants'
import styles from './ProjectCard.module.scss'
-const ProjectCard = ({ projectName, projectStatus, projectId, selected }) => {
+const ProjectCard = ({ projectName, projectStatus, projectId, selected, isInvited }) => {
return (
@@ -28,6 +28,7 @@ ProjectCard.propTypes = {
projectStatus: PT.string.isRequired,
projectId: PT.number.isRequired,
projectName: PT.string.isRequired,
+ isInvited: PT.bool.isRequired,
selected: PT.bool
}
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 07d862dc..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 } from '../../util/tc'
-import { addUserToProject, removeUserFromProject } from '../../services/projects'
+import { checkAdmin, checkManager } from '../../util/tc'
+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,10 +155,12 @@ class Users extends Component {
const {
projects,
projectMembers,
+ invitedMembers,
updateProjectNember,
isEditable,
isSearchingUserProjects,
- resultSearchUserProjects
+ resultSearchUserProjects,
+ loadNextProjects
} = this.props
const {
searchKey
@@ -225,10 +172,11 @@ 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 showAddUser = isEditable && this.state.projectOption && (isCopilotOrManager || isAdmin)
+ const isManager = checkManager(this.props.auth.token)
+ const showAddUser = isEditable && this.state.projectOption && (isCopilotOrManager || isAdmin || isManager)
return (
@@ -246,6 +194,7 @@ class Users extends Component {
onChange={(e) => { this.setProjectOption(e) }}
onInputChange={this.debouncedOnInputChange}
isLoading={isSearchingUserProjects}
+ onMenuScrollBottom={loadNextProjects}
filterOption={() => true}
noOptionsMessage={() => isSearchingUserProjects ? 'Searching...' : 'No options'}
/>
@@ -260,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 (
+ -
+
+
+ )
+ })
+ }
+
>
)
}
@@ -450,14 +313,17 @@ 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)
+ resultSearchUserProjects: PropTypes.arrayOf(PropTypes.object),
+ loadNextProjects: PropTypes.func.isRequired
}
export default Users
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 6aa99441..8729c02c 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
@@ -179,6 +181,11 @@ export const LOAD_PROJECT_TYPES_SUCCESS = 'LOAD_PROJECT_TYPES_SUCCESS'
export const LOAD_PROJECT_TYPES_PENDING = 'LOAD_PROJECT_TYPES_PENDING'
export const LOAD_PROJECT_TYPES_FAILURE = 'LOAD_PROJECT_TYPES_FAILURE'
+export const LOAD_PROJECT_INVITES = 'LOAD_PROJECT_INVITES'
+export const LOAD_PROJECT_INVITES_SUCCESS = 'LOAD_PROJECT_INVITES_SUCCESS'
+export const LOAD_PROJECT_INVITES_PENDING = 'LOAD_PROJECT_INVITES_PENDING'
+export const LOAD_PROJECT_INVITES_FAILURE = 'LOAD_PROJECT_INVITES_FAILURE'
+
export const CREATE_PROJECT = 'CREATE_PROJECT'
export const CREATE_PROJECT_PENDING = 'CREATE_PROJECT_PENDING'
export const CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS'
@@ -193,6 +200,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 +257,7 @@ export const MARATHON_MATCH_SUBTRACKS = [
export const PROJECT_ROLES = {
READ: 'observer',
+ CUSTOMER: 'customer',
WRITE: 'customer',
MANAGER: 'manager',
COPILOT: 'copilot'
@@ -308,6 +324,10 @@ export const COPILOT_ROLES = [
'copilot'
]
+export const MANAGER_ROLES = [
+ 'project manager'
+]
+
export const downloadAttachmentURL = (challengeId, attachmentId, token) =>
`${CHALLENGE_API_URL}/${challengeId}/attachments/${attachmentId}/download?token=${token}`
diff --git a/src/containers/Challenges/index.js b/src/containers/Challenges/index.js
index ca7759f5..f3edb55a 100644
--- a/src/containers/Challenges/index.js
+++ b/src/containers/Challenges/index.js
@@ -14,13 +14,14 @@ import {
deleteChallenge,
loadChallengeTypes
} from '../../actions/challenges'
-import { loadProject, updateProject } from '../../actions/projects'
+import { loadProject, loadProjects, updateProject } from '../../actions/projects'
import {
loadNextProjects,
setActiveProject,
resetSidebarActiveParams
} from '../../actions/sidebar'
-import { checkAdmin } from '../../util/tc'
+import { checkAdmin, checkIsUserInvitedToProject } from '../../util/tc'
+import { withRouter } from 'react-router-dom'
class Challenges extends Component {
constructor (props) {
@@ -42,6 +43,7 @@ class Challenges extends Component {
} = this.props
loadChallengeTypes()
if (dashboard) {
+ this.props.loadProjects('', {})
this.reloadChallenges(this.props, true, true)
}
if (menu === 'NULL' && activeProjectId !== -1) {
@@ -55,6 +57,14 @@ class Challenges extends Component {
}
}
+ componentDidUpdate () {
+ const { auth } = this.props
+
+ if (checkIsUserInvitedToProject(auth.token, this.props.projectDetail)) {
+ this.props.history.push(`/projects/${this.props.projectId}/invitation`)
+ }
+ }
+
componentWillReceiveProps (nextProps) {
if (
(nextProps.dashboard && this.props.dashboard !== nextProps.dashboard) ||
@@ -194,6 +204,7 @@ Challenges.defaultProps = {
}
Challenges.propTypes = {
+ history: PropTypes.object,
projects: PropTypes.arrayOf(PropTypes.shape()),
menu: PropTypes.string,
challenges: PropTypes.arrayOf(PropTypes.object),
@@ -234,7 +245,8 @@ Challenges.propTypes = {
fetchNextProjects: PropTypes.func.isRequired,
metadata: PropTypes.shape({
challengeTypes: PropTypes.array
- })
+ }),
+ loadProjects: PropTypes.func.isRequired
}
const mapStateToProps = ({ challenges, sidebar, projects, auth }) => ({
@@ -265,7 +277,10 @@ const mapDispatchToProps = {
loadChallengeTypes,
setActiveProject,
partiallyUpdateChallengeDetails,
- deleteChallenge
+ deleteChallenge,
+ loadProjects
}
-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..2627e99e 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, checkIsUserInvitedToProject } 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 (checkIsUserInvitedToProject(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..df5b80e4
--- /dev/null
+++ b/src/containers/ProjectInvitations/index.js
@@ -0,0 +1,125 @@
+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 { checkIsUserInvitedToProject } from '../../util/tc'
+import { isEmpty } from 'lodash'
+import { loadProjectInvites } 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'
+import { delay } from '../../util/delay'
+
+const theme = {
+ container: styles.modalContainer
+}
+
+const ProjectInvitations = ({ match, auth, isProjectLoading, history, projectDetail, loadProjectInvites }) => {
+ 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(() => checkIsUserInvitedToProject(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) {
+ loadProjectInvites(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)
+
+ // await for the project details to propagate
+ await delay(1000)
+ await loadProjectInvites(projectId)
+ toastr.success('Success', `Successfully ${status} the invitation.`)
+
+ // await for the project details to fetch
+ await delay(1000)
+ history.push(status === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED ? `/projects/${projectId}/challenges` : '/projects')
+ }, [projectId, invitation, loadProjectInvites, history])
+
+ 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
+ }
+
+ if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_ACCEPTED) {
+ acceptInvite()
+ } else if (automaticAction === PROJECT_MEMBER_INVITE_STATUS_REFUSED) {
+ declineInvite()
+ }
+ }, [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,
+ loadProjectInvites: PropTypes.func.isRequired,
+ projectDetail: PropTypes.object
+}
+
+const mapStateToProps = ({ projects, auth }) => {
+ return {
+ projectDetail: projects.projectDetail,
+ isProjectLoading: projects.isLoading || projects.isProjectInvitationsLoading,
+ auth
+ }
+}
+
+const mapDispatchToProps = {
+ loadProjectInvites
+}
+
+export default withRouter(
+ connect(mapStateToProps, mapDispatchToProps)(ProjectInvitations)
+)
diff --git a/src/containers/Projects/index.js b/src/containers/Projects/index.js
index 733c044c..893f3f78 100644
--- a/src/containers/Projects/index.js
+++ b/src/containers/Projects/index.js
@@ -5,7 +5,7 @@ import { withRouter, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import PropTypes from 'prop-types'
import Loader from '../../components/Loader'
-import { checkAdminOrCopilot } from '../../util/tc'
+import { checkAdminOrCopilot, checkIsUserInvitedToProject, checkManager } from '../../util/tc'
import { PrimaryButton } from '../../components/Buttons'
import Select from '../../components/Select'
import ProjectCard from '../../components/ProjectCard'
@@ -18,11 +18,21 @@ import styles from './styles.module.scss'
const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, loadMoreProjects, unloadProjects }) => {
const [search, setSearch] = useState()
const [projectStatus, setProjectStatus] = useState('')
+ const [showOnlyMyProjects, setOnlyMyProjects] = useState(false)
const selectedStatus = useMemo(() => PROJECT_STATUSES.find(s => s.value === projectStatus))
+ const isProjectManager = checkManager(auth.token)
useEffect(() => {
- loadProjects(search, projectStatus ? { status: projectStatus } : {})
- }, [search, projectStatus])
+ const params = {}
+ if (projectStatus) {
+ params.status = projectStatus
+ }
+
+ if (isProjectManager) {
+ params.memberOnly = showOnlyMyProjects
+ }
+ loadProjects(search, params)
+ }, [search, projectStatus, showOnlyMyProjects, isProjectManager])
// unload projects on dismount
useEffect(() => () => unloadProjects, [])
@@ -46,7 +56,7 @@ const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, load
)}
-
+
@@ -61,7 +71,7 @@ const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, load
/>
-
+
@@ -76,6 +86,25 @@ const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, load
/>
+
+ {
+ checkManager(auth.token) && (
+
+
setOnlyMyProjects(!showOnlyMyProjects)}
+ />
+
+
+ )
+ }
+
{projects.length > 0 ? (
<>
@@ -83,6 +112,7 @@ const Projects = ({ projects, auth, isLoading, projectsCount, loadProjects, load
{projects.map(p => (
{
const projectMembers = _.get(project, 'members')
+ const invitedMembers = _.get(project, 'invites')
this.setState({
- projectMembers
+ projectMembers,
+ invitedMembers
})
const { loggedInUser } = this.props
this.updateLoginUserRoleInProject(projectMembers, loggedInUser)
@@ -88,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)
}
@@ -110,6 +130,15 @@ class Users extends Component {
this.updateLoginUserRoleInProject(newProjectMembers, loggedInUser)
}
+ addNewProjectInvite (invitedMember) {
+ this.setState(() => ({
+ invitedMembers: [
+ ...(this.state.invitedMembers || []),
+ invitedMember
+ ]
+ }))
+ }
+
render () {
const {
projects,
@@ -120,6 +149,7 @@ class Users extends Component {
} = this.props
const {
projectMembers,
+ invitedMembers,
isAdmin
} = this.state
return (
@@ -129,7 +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()}
@@ -146,6 +179,7 @@ class Users extends Component {
const mapStateToProps = ({ users, auth }) => {
return {
projects: users.allUserProjects,
+ page: users.page,
isLoading: users.isLoadingAllUserProjects,
resultSearchUserProjects: users.searchUserProjects,
isSearchingUserProjects: users.isSearchingUserProjects,
@@ -164,12 +198,15 @@ Users.propTypes = {
isLoading: PT.bool,
isSearchingUserProjects: PT.bool,
loadAllUserProjects: PT.func.isRequired,
- searchUserProjects: PT.func.isRequired
+ searchUserProjects: PT.func.isRequired,
+ loadNextProjects: PT.func.isRequired,
+ page: PT.number
}
const mapDispatchToProps = {
loadAllUserProjects,
- searchUserProjects
+ searchUserProjects,
+ loadNextProjects
}
export default connect(mapStateToProps, mapDispatchToProps)(Users)
diff --git a/src/reducers/projects.js b/src/reducers/projects.js
index e26c90d3..f81713c6 100644
--- a/src/reducers/projects.js
+++ b/src/reducers/projects.js
@@ -30,7 +30,10 @@ import {
UPDATE_PROJECT_DETAILS_SUCCESS,
ADD_PROJECT_ATTACHMENT_SUCCESS,
UPDATE_PROJECT_ATTACHMENT_SUCCESS,
- REMOVE_PROJECT_ATTACHMENT_SUCCESS
+ REMOVE_PROJECT_ATTACHMENT_SUCCESS,
+ LOAD_PROJECT_INVITES_PENDING,
+ LOAD_PROJECT_INVITES_SUCCESS,
+ LOAD_PROJECT_INVITES_FAILURE
} from '../config/constants'
import { toastSuccess, toastFailure } from '../util/toaster'
import moment from 'moment-timezone'
@@ -78,6 +81,7 @@ const initialState = {
isBillingAccountExpired: false,
isBillingAccountLoading: false,
isBillingAccountLoadingFailed: false,
+ isProjectInvitationsLoading: false,
currentBillingAccount: null,
billingStartDate: null,
billingEndDate: null,
@@ -261,6 +265,29 @@ export default function (state = initialState, action) {
...state,
isProjectTypesLoading: false
}
+ case LOAD_PROJECT_INVITES_PENDING:
+ return {
+ ...state,
+ projectDetail: {
+ ...state.projectDetail,
+ invites: []
+ },
+ isProjectInvitationsLoading: true
+ }
+ case LOAD_PROJECT_INVITES_SUCCESS:
+ return {
+ ...state,
+ projectDetail: {
+ ...state.projectDetail,
+ invites: action.payload
+ },
+ isProjectInvitationsLoading: false
+ }
+ case LOAD_PROJECT_INVITES_FAILURE:
+ return {
+ ...state,
+ isProjectInvitationsLoading: false
+ }
case UPDATE_PROJECT_PENDING:
return {
...state,
diff --git a/src/reducers/users.js b/src/reducers/users.js
index 8dc18247..e293c154 100644
--- a/src/reducers/users.js
+++ b/src/reducers/users.js
@@ -14,7 +14,9 @@ const initialState = {
allUserProjects: [],
isLoadingAllUserProjects: false,
searchUserProjects: [],
- isSearchingUserProjects: false
+ isSearchingUserProjects: false,
+ page: 1,
+ total: null
}
export default function (state = initialState, action) {
@@ -23,7 +25,9 @@ export default function (state = initialState, action) {
return {
...state,
allUserProjects: action.projects,
- isLoadingAllUserProjects: false
+ isLoadingAllUserProjects: false,
+ page: action.page,
+ total: action.total
}
case LOAD_ALL_USER_PROJECTS_PENDING:
return { ...state, isLoadingAllUserProjects: true }
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..e586a144 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