Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ workflows:
- test
filters:
branches:
only: ['dev', 'dev-msinteg']
only: ['dev', 'dev-msinteg', 'feature/attachmentPermissions']
- deployTest02:
requires:
- test
Expand Down
7 changes: 7 additions & 0 deletions src/api/projectAttachments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down
65 changes: 65 additions & 0 deletions src/components/FileList/AddFilePermissions.jsx
Original file line number Diff line number Diff line change
@@ -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 (
<Modal
isOpen
className="project-dialog-conatiner"
overlayClassName="management-dialog-overlay"
contentLabel=""
>
<div className="project-dialog">
<div className="dialog-title">
Who do you want to share this file with?
<span onClick={onCancel}><XMarkIcon /></span>
</div>

{/* Share with all members */}
<div className="dialog-body">
<div styleName="btn-all-members">
<button className="tc-btn tc-btn-primary tc-btn-md" onClick={() => onSubmit(null)}>All project members</button>
</div>
</div>

{/* Share with specific people */}
<div className="input-container">
<div className="hint">OR ONLY SPECIFIC PEOPLE</div>

<UserAutoComplete projectMembers={projectMembers} selectedUsers={selectedUsers} onUpdate={onChange} loggedInUser={loggedInUser} />

<div>
<button className="tc-btn tc-btn-primary tc-btn-md"
onClick={() => onSubmit(mapHandlesToUserIds(selectedUsers.split(',')))}
disabled={!selectedUsers || selectedUsers.length === 0 }
>Share with selected members</button>
</div>
</div>
</div>
</Modal>
)
}

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
4 changes: 4 additions & 0 deletions src/components/FileList/AddFilePermissions.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.btn-all-members {
text-align: center;
margin-top: 8px;
}
9 changes: 7 additions & 2 deletions src/components/FileList/FileList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => (
<Panel className={cn('file-list', {'modal-active': deletingFile})}>
{deletingFile && <div className="modal-overlay" />}
{
Expand All @@ -34,6 +35,8 @@ const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canMod
onDelete={ onDeleteIntent }
onSave={ onSave }
canModify={canModify}
projectMembers={projectMembers}
loggedInUser={loggedInUser}
/>
)
})
Expand All @@ -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
Expand Down
39 changes: 35 additions & 4 deletions src/components/FileList/FileListItem.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -27,17 +29,19 @@ 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() {
this.props.onDelete(this.props.id)
}

startEdit() {
const {title, description} = this.props
const {title, description, userIds} = this.props
this.setState({
title,
description,
userIds,
isEditing: true
})
}
Expand All @@ -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})
}
}
Expand All @@ -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 (
<div>
Expand All @@ -90,6 +113,11 @@ export default class FileListItem extends React.Component {
{ (errors && errors.title) && <div className="error-message">{ errors.title }</div> }
<textarea defaultValue={description} ref="desc" maxLength={250} className="tc-textarea" />
{ (errors && errors.desc) && <div className="error-message">{ errors.desc }</div> }
<UserAutoComplete onUpdate={this.onUserIdChange}
projectMembers={projectMembers}
loggedInUser={loggedInUser}
selectedUsers={this.userIdsToHandles(userIds).join(',')}
/>
</div>
)
}
Expand Down Expand Up @@ -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
Expand Down
45 changes: 44 additions & 1 deletion src/components/LinksMenu/FileLinksMenu.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -35,7 +36,14 @@ const FileLinksMenu = ({
withHash,
attachmentsStorePath,
category,
selectedUsers,
onAddAttachment,
onUploadAttachment,
discardAttachments,
onChangePermissions,
pendingAttachments,
projectMembers,
loggedInUser,
}) => {
const renderLink = (link) => {
if (link.onClick) {
Expand All @@ -59,6 +67,7 @@ const FileLinksMenu = ({
}

const processUploadedFiles = (fpFiles, category) => {
const attachments = []
onAddingNewLink(false)
fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles]
_.forEach(fpFiles, f => {
Expand All @@ -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)
})
}

Expand All @@ -90,11 +111,26 @@ const FileLinksMenu = ({

{(isAddingNewLink || linkToDelete >= 0) && <div className="modal-overlay"/>}

{
pendingAttachments &&
<AddFilePermission onCancel={discardAttachments}
onSubmit={onAddingAttachmentPermissions}
onChange={onChangePermissions}
selectedUsers={selectedUsers}
projectMembers={projectMembers}
loggedInUser={loggedInUser}
/>
}

{isAddingNewLink &&
<Modal onClose={onClose}>
<Modal.Title>
UPLOAD A FILE
</Modal.Title>
{
pendingAttachments &&
<AddFilePermission />
}
<AddFiles successHandler={processUploadedFiles.bind(this)}
storePath={attachmentsStorePath}
category={category}
Expand Down Expand Up @@ -201,13 +237,20 @@ FileLinksMenu.propTypes = {
noDots: PropTypes.bool,
limit: PropTypes.number,
links: PropTypes.array.isRequired,
selectedUsers: PropTypes.string,
projectMembers: PropTypes.object,
pendingAttachments: PropTypes.object,
onUploadAttachment: PropTypes.func,
discardAttachments: PropTypes.func,
onChangePermissions: PropTypes.func,
attachmentsStorePath: PropTypes.string.isRequired,
moreText: PropTypes.string,
onAddingNewLink: PropTypes.func,
onAddNewLink: PropTypes.func,
onChangeLimit: PropTypes.func,
onDelete: PropTypes.func,
title: PropTypes.string,
loggedInUser: PropTypes.object.isRequired,
}

FileLinksMenu.defaultProps = {
Expand Down
42 changes: 42 additions & 0 deletions src/components/UserAutoComplete/UserAutoComplete.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react'
import PropTypes from 'prop-types'
import { values } from 'lodash'

import './UserAutoComplete.scss'
import Select from '../Select/Select'

/**
* Render a searchable dropdown for selecting users
* @param {Object} projectMembers - a map of userId to user object of project members. i.e., members.members from the store
* @param {String} selectedUsers - currently selected user handles delimitted by ','
* @param {Function} onUpdate - change handler. invoked when a user is added or removed from selection
*/
const UserAutoComplete = ({
projectMembers,
selectedUsers,
onUpdate,
loggedInUser
}) => (
<div styleName="user-select-wrapper" className="user-select-wrapper">
<Select
multi
value={selectedUsers}
placeholder="Enter user handles"
onChange={selectedOptions => onUpdate(selectedOptions)}
options={
values(projectMembers || {})
.filter(member => member.handle !== loggedInUser.handle)
.map(member => ({ value: member.handle, label: member.handle }))
}
/>
</div>
)

UserAutoComplete.propTypes = {
projectMembers: PropTypes.object,
selectedUsers: PropTypes.string,
onUpdate: PropTypes.func,
loggedInUser: PropTypes.object
}

export default UserAutoComplete
16 changes: 16 additions & 0 deletions src/components/UserAutoComplete/UserAutoComplete.scss
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
Loading