diff --git a/.circleci/config.yml b/.circleci/config.yml index e629370d9..1515ee606 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -100,7 +100,7 @@ workflows: - test filters: branches: - only: ['dev', 'dev-msinteg'] + only: ['dev', 'dev-msinteg', 'feature/attachmentPermissions'] - deployTest02: requires: - test diff --git a/src/api/projectAttachments.js b/src/api/projectAttachments.js index 6ad90a879..afdbb7b50 100644 --- a/src/api/projectAttachments.js +++ b/src/api/projectAttachments.js @@ -13,6 +13,13 @@ export function addProjectAttachment(projectId, fileData) { } export function updateProjectAttachment(projectId, attachmentId, attachment) { + if (attachment && (!attachment.userIds || attachment.userIds.length === 0)) { + attachment = { + ...attachment, + userIds: null + } + } + return axios.patch( `${PROJECTS_API_URL}/v4/projects/${projectId}/attachments/${attachmentId}`, { param: attachment }) diff --git a/src/components/FileList/AddFilePermissions.jsx b/src/components/FileList/AddFilePermissions.jsx new file mode 100644 index 000000000..a9ee62ad1 --- /dev/null +++ b/src/components/FileList/AddFilePermissions.jsx @@ -0,0 +1,65 @@ +import React from 'react' +import PropTypes from 'prop-types' +import Modal from 'react-modal' +import { mapKeys, get } from 'lodash' + +import UserAutoComplete from '../UserAutoComplete/UserAutoComplete' + +import './AddFilePermissions.scss' +import XMarkIcon from '../../assets/icons/icon-x-mark.svg' + +const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projectMembers, loggedInUser }) => { + selectedUsers = selectedUsers || '' + const mapHandlesToUserIds = handles => { + const projectMembersByHandle = mapKeys(projectMembers, value => value.handle) + return handles.filter(handle => handle).map(h => get(projectMembersByHandle[h], 'userId')) + } + + return ( + + + + Who do you want to share this file with? + + + + {/* Share with all members */} + + + onSubmit(null)}>All project members + + + + {/* Share with specific people */} + + OR ONLY SPECIFIC PEOPLE + + + + + onSubmit(mapHandlesToUserIds(selectedUsers.split(',')))} + disabled={!selectedUsers || selectedUsers.length === 0 } + >Share with selected members + + + + + ) +} + +AddFilePermission.propTypes = { + onCancel: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + selectedUsers: PropTypes.string, + projectMembers: PropTypes.object, + loggedInUser: PropTypes.object.isRequired +} + +export default AddFilePermission diff --git a/src/components/FileList/AddFilePermissions.scss b/src/components/FileList/AddFilePermissions.scss new file mode 100644 index 000000000..b362f5257 --- /dev/null +++ b/src/components/FileList/AddFilePermissions.scss @@ -0,0 +1,4 @@ +.btn-all-members { + text-align: center; + margin-top: 8px; +} \ No newline at end of file diff --git a/src/components/FileList/FileList.jsx b/src/components/FileList/FileList.jsx index 537f02d58..2ea427f70 100644 --- a/src/components/FileList/FileList.jsx +++ b/src/components/FileList/FileList.jsx @@ -7,7 +7,8 @@ import uncontrollable from 'uncontrollable' import FileDeletionConfirmModal from './FileDeletionConfirmModal' import './FileList.scss' -const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canModify}) => ( +const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canModify, projectMembers, + loggedInUser }) => ( {deletingFile && } { @@ -34,6 +35,8 @@ const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canMod onDelete={ onDeleteIntent } onSave={ onSave } canModify={canModify} + projectMembers={projectMembers} + loggedInUser={loggedInUser} /> ) }) @@ -42,7 +45,9 @@ const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canMod ) FileList.propTypes = { - canModify: PropTypes.bool.isRequired + canModify: PropTypes.bool.isRequired, + projectMembers: PropTypes.object.isRequired, + loggedInUser: PropTypes.object.isRequired } FileList.Item = FileListItem diff --git a/src/components/FileList/FileListItem.jsx b/src/components/FileList/FileListItem.jsx index 4cc8cbb49..a2f65c64c 100644 --- a/src/components/FileList/FileListItem.jsx +++ b/src/components/FileList/FileListItem.jsx @@ -10,6 +10,7 @@ import TrashIcon from '../../assets/icons/icon-trash.svg' import CloseIcon from '../../assets/icons/icon-close.svg' import EditIcon from '../../assets/icons/icon-edit.svg' import SaveIcon from '../../assets/icons/icon-save.svg' +import UserAutoComplete from '../UserAutoComplete/UserAutoComplete' export default class FileListItem extends React.Component { @@ -19,6 +20,7 @@ export default class FileListItem extends React.Component { this.state = { title: props.title, description: props.description, + userIds: props.userIds, isEditing: false } this.handleSave = this.handleSave.bind(this) @@ -27,6 +29,7 @@ export default class FileListItem extends React.Component { this.validateForm = this.validateForm.bind(this) this.validateTitle = this.validateTitle.bind(this) this.onTitleChange = this.onTitleChange.bind(this) + this.onUserIdChange = this.onUserIdChange.bind(this) } onDelete() { @@ -34,10 +37,11 @@ export default class FileListItem extends React.Component { } startEdit() { - const {title, description} = this.props + const {title, description, userIds} = this.props this.setState({ title, description, + userIds, isEditing: true }) } @@ -48,7 +52,7 @@ export default class FileListItem extends React.Component { if (!_.isEmpty(errors)) { this.setState({ errors }) } else { - this.props.onSave(this.props.id, {title, description: this.refs.desc.value}, e) + this.props.onSave(this.props.id, {title, description: this.refs.desc.value, userIds: this.state.userIds}, e) this.setState({isEditing: false}) } } @@ -74,9 +78,28 @@ export default class FileListItem extends React.Component { this.setState({ errors }) } + onUserIdChange(selectedHandles = '') { + this.setState({ + userIds: this.handlesToUserIds(selectedHandles.split(',')) + }) + } + + userIdsToHandles(userIds) { + const { projectMembers } = this.props + userIds = userIds || [] + return userIds.map(userId => _.get(projectMembers[userId], 'handle')) + } + + handlesToUserIds(handles) { + const { projectMembers } = this.props + const projectMembersByHandle = _.mapKeys(projectMembers, value => value.handle) + handles = handles || [] + return handles.filter(handle => handle).map(handle => _.get(projectMembersByHandle[handle], 'userId')) + } + renderEditing() { - const {title, description} = this.props - const { errors } = this.state + const { title, description, projectMembers, loggedInUser } = this.props + const { errors, userIds } = this.state const onExitEdit = () => this.setState({isEditing: false, errors: {} }) return ( @@ -90,6 +113,11 @@ export default class FileListItem extends React.Component { { (errors && errors.title) && { errors.title } } { (errors && errors.desc) && { errors.desc } } + ) } @@ -156,6 +184,9 @@ FileListItem.propTypes = { createdAt: PropTypes.string.isRequired, updatedByUser: PropTypes.object, createdByUser: PropTypes.object.isRequired, + projectMembers: PropTypes.object.isRequired, + loggedInUser: PropTypes.object.isRequired, + userIds: PropTypes.array, /** * Callback fired when a save button is clicked diff --git a/src/components/LinksMenu/FileLinksMenu.jsx b/src/components/LinksMenu/FileLinksMenu.jsx index 349e0a3a5..e751d56a7 100644 --- a/src/components/LinksMenu/FileLinksMenu.jsx +++ b/src/components/LinksMenu/FileLinksMenu.jsx @@ -4,6 +4,7 @@ import {Link} from 'react-router-dom' import './LinksMenu.scss' import Panel from '../Panel/Panel' import AddFiles from '../FileList/AddFiles' +import AddFilePermission from '../FileList/AddFilePermissions' import DeleteLinkModal from './DeleteLinkModal' import EditLinkModal from './EditLinkModal' import uncontrollable from 'uncontrollable' @@ -35,7 +36,14 @@ const FileLinksMenu = ({ withHash, attachmentsStorePath, category, + selectedUsers, onAddAttachment, + onUploadAttachment, + discardAttachments, + onChangePermissions, + pendingAttachments, + projectMembers, + loggedInUser, }) => { const renderLink = (link) => { if (link.onClick) { @@ -59,6 +67,7 @@ const FileLinksMenu = ({ } const processUploadedFiles = (fpFiles, category) => { + const attachments = [] onAddingNewLink(false) fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles] _.forEach(fpFiles, f => { @@ -70,7 +79,19 @@ const FileLinksMenu = ({ filePath: f.key, contentType: f.mimetype || 'application/unknown' } - onAddAttachment(attachment) + attachments.push(attachment) + }) + onUploadAttachment(attachments) + } + + const onAddingAttachmentPermissions = (userIds) => { + const { attachments, projectId } = pendingAttachments + _.forEach(attachments, f => { + const attachment = { + ...f, + userIds + } + onAddAttachment(projectId, attachment) }) } @@ -90,11 +111,26 @@ const FileLinksMenu = ({ {(isAddingNewLink || linkToDelete >= 0) && } + { + pendingAttachments && + + } + {isAddingNewLink && UPLOAD A FILE + { + pendingAttachments && + + } ( + + onUpdate(selectedOptions)} + options={ + values(projectMembers || {}) + .filter(member => member.handle !== loggedInUser.handle) + .map(member => ({ value: member.handle, label: member.handle })) + } + /> + +) + +UserAutoComplete.propTypes = { + projectMembers: PropTypes.object, + selectedUsers: PropTypes.string, + onUpdate: PropTypes.func, + loggedInUser: PropTypes.object +} + +export default UserAutoComplete diff --git a/src/components/UserAutoComplete/UserAutoComplete.scss b/src/components/UserAutoComplete/UserAutoComplete.scss new file mode 100644 index 000000000..bbfc5ff00 --- /dev/null +++ b/src/components/UserAutoComplete/UserAutoComplete.scss @@ -0,0 +1,16 @@ +.user-select-wrapper { + width: 100%; + margin: 8px 0; +} + +:global { + .management-dialog-overlay + .project-dialog-conatiner + .project-dialog + .input-container + .user-select-wrapper { + input { + margin: 0; + } + } +} diff --git a/src/config/constants.js b/src/config/constants.js index 4ea34a87d..a869bb787 100644 --- a/src/config/constants.js +++ b/src/config/constants.js @@ -361,10 +361,13 @@ export const CLEAR_LOADED_PROJECT = 'CLEAR_LOADED_PROJECT' // Project attachments -export const ADD_PROJECT_ATTACHMENT = 'ADD_PROJECT_ATTACHMENT' -export const ADD_PROJECT_ATTACHMENT_PENDING = 'ADD_PROJECT_ATTACHMENT_PENDING' -export const ADD_PROJECT_ATTACHMENT_SUCCESS = 'ADD_PROJECT_ATTACHMENT_SUCCESS' -export const ADD_PROJECT_ATTACHMENT_FAILURE = 'ADD_PROJECT_ATTACHMENT_FAILURE' +export const ADD_PROJECT_ATTACHMENT = 'ADD_PROJECT_ATTACHMENT' +export const DISCARD_PROJECT_ATTACHMENT = 'DISCARD_PROJECT_ATTACHMENT' +export const UPLOAD_PROJECT_ATTACHMENT_FILES = 'UPLOAD_PROJECT_ATTACHMENT_FILES' +export const CHANGE_ATTACHMENT_PERMISSION = 'CHANGE_ATTACHMENT_PERMISSION' +export const ADD_PROJECT_ATTACHMENT_PENDING = 'ADD_PROJECT_ATTACHMENT_PENDING' +export const ADD_PROJECT_ATTACHMENT_SUCCESS = 'ADD_PROJECT_ATTACHMENT_SUCCESS' +export const ADD_PROJECT_ATTACHMENT_FAILURE = 'ADD_PROJECT_ATTACHMENT_FAILURE' export const REMOVE_PROJECT_ATTACHMENT = 'REMOVE_PROJECT_ATTACHMENT' export const REMOVE_PROJECT_ATTACHMENT_PENDING = 'REMOVE_PROJECT_ATTACHMENT_PENDING' diff --git a/src/projects/actions/projectAttachment.js b/src/projects/actions/projectAttachment.js index 6a203390b..10e558c84 100644 --- a/src/projects/actions/projectAttachment.js +++ b/src/projects/actions/projectAttachment.js @@ -9,13 +9,25 @@ import { import { ADD_PROJECT_ATTACHMENT, + DISCARD_PROJECT_ATTACHMENT, REMOVE_PROJECT_ATTACHMENT, UPDATE_PROJECT_ATTACHMENT, ADD_PRODUCT_ATTACHMENT, REMOVE_PRODUCT_ATTACHMENT, UPDATE_PRODUCT_ATTACHMENT, + CHANGE_ATTACHMENT_PERMISSION, + UPLOAD_PROJECT_ATTACHMENT_FILES, } from '../../config/constants' +export function uploadProjectAttachments(projectId, attachments) { + return dispatch => { + return dispatch({ + type: UPLOAD_PROJECT_ATTACHMENT_FILES, + payload: { attachments, projectId } + }) + } +} + export function addProjectAttachment(projectId, attachment) { return (dispatch) => { return dispatch({ @@ -25,6 +37,23 @@ export function addProjectAttachment(projectId, attachment) { } } +export function changeAttachmentPermission(userIds) { + return dispatch => { + return dispatch({ + type: CHANGE_ATTACHMENT_PERMISSION, + payload: userIds + }) + } +} + +export function discardAttachments() { + return (dispatch) => { + return dispatch({ + type: DISCARD_PROJECT_ATTACHMENT + }) + } +} + export function updateProjectAttachment(projectId, attachmentId, attachment) { return (dispatch) => { return dispatch({ diff --git a/src/projects/detail/components/FileListContainer.jsx b/src/projects/detail/components/FileListContainer.jsx index 4b1e9642e..17df85cde 100644 --- a/src/projects/detail/components/FileListContainer.jsx +++ b/src/projects/detail/components/FileListContainer.jsx @@ -5,14 +5,18 @@ import _ from 'lodash' import FileList from '../../../components/FileList/FileList' import AddFiles from '../../../components/FileList/AddFiles' import { getProjectRoleForCurrentUser } from '../../../helpers/projectHelper' +import { uploadProjectAttachments, discardAttachments, changeAttachmentPermission } from '../../actions/projectAttachment' +import AddFilePermission from '../../../components/FileList/AddFilePermissions' class FileListContainer extends Component { constructor(props) { super(props) this.processUploadedFiles = this.processUploadedFiles.bind(this) + this.onAddingAttachmentPermissions = this.onAddingAttachmentPermissions.bind(this) } processUploadedFiles(fpFiles, category) { + const attachments = [] fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles] _.forEach(fpFiles, f => { const attachment = { @@ -23,6 +27,24 @@ class FileListContainer extends Component { filePath: f.key, contentType: f.mimetype || 'application/unknown' } + attachments.push(attachment) + }) + this.onUploadAttachment(attachments) + } + + + onUploadAttachment(attachment) { + const { project } = this.props + this.props.uploadProjectAttachments(project.id, attachment) + } + + onAddingAttachmentPermissions(userIds) { + const { attachments } = this.props.pendingAttachments + _.forEach(attachments, f => { + const attachment = { + ...f, + userIds + } this.props.addAttachment(attachment) }) } @@ -38,11 +60,14 @@ class FileListContainer extends Component { files, category, allMembers, + loggedInUser, attachmentsStorePath, canManageAttachments, removeAttachment, updateAttachment, additionalClass, + pendingAttachments, + attachmentPermissions, } = this.props files.forEach(file => { @@ -57,18 +82,39 @@ class FileListContainer extends Component { return ( - + + + { + pendingAttachments && + + } ) } } -const mapDispatchToProps = {} +const mapDispatchToProps = { + uploadProjectAttachments, + onDiscardAttachments: discardAttachments, + changeAttachmentPermission +} -const mapStateToProps = ({ members }) => { +const mapStateToProps = ({ members, projectState, loadUser }) => { return { allMembers: members.members, + pendingAttachments: projectState.attachmentsAwaitingPermission, + attachmentPermissions: projectState.attachmentPermissions, + loggedInUser: loadUser.user, } } diff --git a/src/projects/detail/containers/ProjectInfoContainer.js b/src/projects/detail/containers/ProjectInfoContainer.js index 715c3a8bd..d4b165e48 100644 --- a/src/projects/detail/containers/ProjectInfoContainer.js +++ b/src/projects/detail/containers/ProjectInfoContainer.js @@ -14,7 +14,7 @@ import { PROJECT_ROLE_OWNER, PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER, DIRECT_PROJECT_URL, SALESFORCE_PROJECT_LEAD_LINK, PROJECT_STATUS_CANCELLED, PROJECT_ATTACHMENTS_FOLDER, PROJECT_FEED_TYPE_PRIMARY, PHASE_STATUS_DRAFT } from '../../../config/constants' import ProjectInfo from '../../../components/ProjectInfo/ProjectInfo' -import { addProjectAttachment } from '../../actions/projectAttachment' +import { addProjectAttachment, uploadProjectAttachments, discardAttachments, changeAttachmentPermission } from '../../actions/projectAttachment' class ProjectInfoContainer extends React.Component { @@ -29,7 +29,7 @@ class ProjectInfoContainer extends React.Component { this.onDeleteLink = this.onDeleteLink.bind(this) this.onEditLink = this.onEditLink.bind(this) this.onAddFile = this.onAddFile.bind(this) - this.onAddAttachment = this.onAddAttachment.bind(this) + this.onUploadAttachment = this.onUploadAttachment.bind(this) this.onSubmitForReview = this.onSubmitForReview.bind(this) } @@ -41,6 +41,8 @@ class ProjectInfoContainer extends React.Component { !_.isEqual(nextProps.phasesTopics, this.props.phasesTopics) || !_.isEqual(nextProps.isFeedsLoading, this.props.isFeedsLoading) || !_.isEqual(nextProps.isProjectProcessing, this.props.isProjectProcessing) || + !_.isEqual(nextProps.attachmentsAwaitingPermission, this.props.attachmentsAwaitingPermission) || + !_.isEqual(nextProps.attachmentPermissions, this.props.attachmentPermissions) || nextProps.activeChannelId !== this.props.activeChannelId } @@ -115,9 +117,9 @@ class ProjectInfoContainer extends React.Component { onAddFile() { } - onAddAttachment(attachment) { + onUploadAttachment(attachment) { const { project } = this.props - this.props.addProjectAttachment(project.id, attachment) + this.props.uploadProjectAttachments(project.id, attachment) } onSubmitForReview() { @@ -129,7 +131,9 @@ class ProjectInfoContainer extends React.Component { const { duration } = this.state const { project, currentMemberRole, isSuperUser, phases, feeds, hideInfo, hideLinks, hideMembers, onChannelClick, activeChannelId, productsTimelines, - isManageUser, phasesTopics, isProjectPlan, isProjectProcessing, projectTemplates } = this.props + isManageUser, phasesTopics, isProjectPlan, isProjectProcessing, projectTemplates, + attachmentsAwaitingPermission, addProjectAttachment, discardAttachments, attachmentPermissions, + changeAttachmentPermission, projectMembers, loggedInUser } = this.props let directLinks = null // check if direct links need to be added const isMemberOrCopilot = _.indexOf([PROJECT_ROLE_COPILOT, PROJECT_ROLE_MANAGER], currentMemberRole) > -1 @@ -246,7 +250,14 @@ class ProjectInfoContainer extends React.Component { title="Latest files" canAdd={enableFileUpload} onAddNewLink={this.onAddFile} - onAddAttachment={this.onAddAttachment} + onAddAttachment={addProjectAttachment} + onUploadAttachment={this.onUploadAttachment} + discardAttachments={discardAttachments} + onChangePermissions={changeAttachmentPermission} + selectedUsers={attachmentPermissions} + projectMembers={projectMembers} + pendingAttachments={attachmentsAwaitingPermission} + loggedInUser={loggedInUser} moreText="view all files" noDots attachmentsStorePath={attachmentsStorePath} @@ -284,10 +295,17 @@ ProjectInfoContainer.PropTypes = { isProjectProcessing: PropTypes.bool, } -const mapStateToProps = ({templates }) => ({ - projectTemplates : templates.projectTemplates, -}) +const mapStateToProps = ({ templates, projectState, members, loadUser }) => { + return ({ + projectTemplates : templates.projectTemplates, + attachmentsAwaitingPermission: projectState.attachmentsAwaitingPermission, + attachmentPermissions: projectState.attachmentPermissions, + projectMembers: members.members, + loggedInUser: loadUser.user + }) +} -const mapDispatchToProps = { updateProject, deleteProject, addProjectAttachment, loadDashboardFeeds, loadPhaseFeed } +const mapDispatchToProps = { updateProject, deleteProject, addProjectAttachment, + discardAttachments, uploadProjectAttachments, loadDashboardFeeds, loadPhaseFeed, changeAttachmentPermission } export default connect(mapStateToProps, mapDispatchToProps)(ProjectInfoContainer) diff --git a/src/projects/reducers/project.js b/src/projects/reducers/project.js index 0e559efb1..ca2f6aeae 100644 --- a/src/projects/reducers/project.js +++ b/src/projects/reducers/project.js @@ -19,7 +19,8 @@ import { INVITE_TOPCODER_MEMBER_SUCCESS, REMOVE_TOPCODER_MEMBER_INVITE_SUCCESS, INVITE_TOPCODER_MEMBER_PENDING, REMOVE_CUSTOMER_INVITE_PENDING, REMOVE_TOPCODER_MEMBER_INVITE_PENDING, REMOVE_TOPCODER_MEMBER_INVITE_FAILURE, REMOVE_CUSTOMER_INVITE_FAILURE, INVITE_CUSTOMER_FAILURE, INVITE_TOPCODER_MEMBER_FAILURE, INVITE_CUSTOMER_PENDING, - ACCEPT_OR_REFUSE_INVITE_SUCCESS, ACCEPT_OR_REFUSE_INVITE_FAILURE, ACCEPT_OR_REFUSE_INVITE_PENDING, + ACCEPT_OR_REFUSE_INVITE_SUCCESS, ACCEPT_OR_REFUSE_INVITE_FAILURE, ACCEPT_OR_REFUSE_INVITE_PENDING, + UPLOAD_PROJECT_ATTACHMENT_FILES, DISCARD_PROJECT_ATTACHMENT, CHANGE_ATTACHMENT_PERMISSION } from '../../config/constants' import _ from 'lodash' import update from 'react-addons-update' @@ -30,6 +31,8 @@ const initialState = { processingMembers: false, processingInvites: false, processingAttachments: false, + attachmentsAwaitingPermission: null, + attachmentPermissions: null, error: false, project: {}, projectNonDirty: {}, @@ -375,9 +378,29 @@ export const projectState = function (state=initialState, action) { return update(state, { processingAttachments: { $set : false }, project: { attachments: { $push: [action.payload] } }, - projectNonDirty: { attachments: { $push: [action.payload] } } + projectNonDirty: { attachments: { $push: [action.payload] } }, + attachmentsAwaitingPermission: { $set: null } }) + case UPLOAD_PROJECT_ATTACHMENT_FILES: + return { + ...state, + attachmentsAwaitingPermission: action.payload, + attachmentPermissions: null + } + + case DISCARD_PROJECT_ATTACHMENT: + return { + ...state, + attachmentsAwaitingPermission: null + } + + case CHANGE_ATTACHMENT_PERMISSION: + return { + ...state, + attachmentPermissions: action.payload + } + case UPDATE_PROJECT_ATTACHMENT_SUCCESS: { // get index const idx = _.findIndex(state.project.attachments, a => a.id === action.payload.id) diff --git a/src/reducers/alerts.js b/src/reducers/alerts.js index 1b87b5bd1..04f53479f 100644 --- a/src/reducers/alerts.js +++ b/src/reducers/alerts.js @@ -127,6 +127,17 @@ export default function(state = {}, action) { } return state + case ADD_PROJECT_ATTACHMENT_SUCCESS: + Alert.success('Added attachment to the project successfully') + return state + + case UPDATE_PROJECT_ATTACHMENT_SUCCESS: + Alert.success('Updated attachment succcessfully') + return state + case REMOVE_PROJECT_ATTACHMENT_SUCCESS: + Alert.success('Removed attachment successfully') + return state + case INVITE_TOPCODER_MEMBER_SUCCESS: case INVITE_CUSTOMER_SUCCESS: Alert.success('You\'ve successfully invited member(s).')