Skip to content

Commit abc3c70

Browse files
committed
attachment permissions challenge
1 parent 13b8b13 commit abc3c70

File tree

14 files changed

+369
-26
lines changed

14 files changed

+369
-26
lines changed

src/api/projectAttachments.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,13 @@ export function addProjectAttachment(projectId, fileData) {
1313
}
1414

1515
export function updateProjectAttachment(projectId, attachmentId, attachment) {
16+
if (attachment && (!attachment.userIds || attachment.userIds.length === 0)) {
17+
attachment = {
18+
...attachment,
19+
userIds: null
20+
}
21+
}
22+
1623
return axios.patch(
1724
`${PROJECTS_API_URL}/v4/projects/${projectId}/attachments/${attachmentId}`,
1825
{ param: attachment })
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import Modal from 'react-modal'
4+
import { mapKeys, get } from 'lodash'
5+
6+
import UserAutoComplete from '../UserAutoComplete/UserAutoComplete'
7+
8+
import './AddFilePermissions.scss'
9+
import XMarkIcon from '../../assets/icons/icon-x-mark.svg'
10+
11+
const AddFilePermission = ({ onCancel, onSubmit, onChange, selectedUsers, projectMembers, loggedInUser }) => {
12+
selectedUsers = selectedUsers || ''
13+
const mapHandlesToUserIds = handles => {
14+
const projectMembersByHandle = mapKeys(projectMembers, value => value.handle)
15+
return handles.filter(handle => handle).map(h => get(projectMembersByHandle[h], 'userId'))
16+
}
17+
18+
return (
19+
<Modal
20+
isOpen
21+
className="project-dialog-conatiner"
22+
overlayClassName="management-dialog-overlay"
23+
contentLabel=""
24+
>
25+
<div className="project-dialog">
26+
<div className="dialog-title">
27+
Who do you want to share this file with?
28+
<span onClick={onCancel}><XMarkIcon /></span>
29+
</div>
30+
31+
{/* Share with all members */}
32+
<div className="dialog-body">
33+
<div styleName="btn-all-members">
34+
<button className="tc-btn tc-btn-primary tc-btn-md" onClick={() => onSubmit(null)}>All project members</button>
35+
</div>
36+
</div>
37+
38+
{/* Share with specific people */}
39+
<div className="input-container">
40+
<div className="hint">OR ONLY SPECIFIC PEOPLE</div>
41+
42+
<UserAutoComplete projectMembers={projectMembers} selectedUsers={selectedUsers} onUpdate={onChange} loggedInUser={loggedInUser} />
43+
44+
<div>
45+
<button className="tc-btn tc-btn-primary tc-btn-md"
46+
onClick={() => onSubmit(mapHandlesToUserIds(selectedUsers.split(',')))}
47+
disabled={!selectedUsers || selectedUsers.length === 0 }
48+
>Share with selected members</button>
49+
</div>
50+
</div>
51+
</div>
52+
</Modal>
53+
)
54+
}
55+
56+
AddFilePermission.propTypes = {
57+
onCancel: PropTypes.func.isRequired,
58+
onSubmit: PropTypes.func.isRequired,
59+
onChange: PropTypes.func.isRequired,
60+
selectedUsers: PropTypes.string,
61+
projectMembers: PropTypes.object,
62+
loggedInUser: PropTypes.object.isRequired
63+
}
64+
65+
export default AddFilePermission
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.btn-all-members {
2+
text-align: center;
3+
margin-top: 8px;
4+
}

src/components/FileList/FileList.jsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import uncontrollable from 'uncontrollable'
77
import FileDeletionConfirmModal from './FileDeletionConfirmModal'
88
import './FileList.scss'
99

10-
const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canModify}) => (
10+
const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canModify, projectMembers,
11+
loggedInUser }) => (
1112
<Panel className={cn('file-list', {'modal-active': deletingFile})}>
1213
{deletingFile && <div className="modal-overlay" />}
1314
{
@@ -34,6 +35,8 @@ const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canMod
3435
onDelete={ onDeleteIntent }
3536
onSave={ onSave }
3637
canModify={canModify}
38+
projectMembers={projectMembers}
39+
loggedInUser={loggedInUser}
3740
/>
3841
)
3942
})
@@ -42,7 +45,9 @@ const FileList = ({files, onDelete, onSave, deletingFile, onDeleteIntent, canMod
4245
)
4346

4447
FileList.propTypes = {
45-
canModify: PropTypes.bool.isRequired
48+
canModify: PropTypes.bool.isRequired,
49+
projectMembers: PropTypes.object.isRequired,
50+
loggedInUser: PropTypes.object.isRequired
4651
}
4752

4853
FileList.Item = FileListItem

src/components/FileList/FileListItem.jsx

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import TrashIcon from '../../assets/icons/icon-trash.svg'
1010
import CloseIcon from '../../assets/icons/icon-close.svg'
1111
import EditIcon from '../../assets/icons/icon-edit.svg'
1212
import SaveIcon from '../../assets/icons/icon-save.svg'
13+
import UserAutoComplete from '../UserAutoComplete/UserAutoComplete'
1314

1415

1516
export default class FileListItem extends React.Component {
@@ -19,6 +20,7 @@ export default class FileListItem extends React.Component {
1920
this.state = {
2021
title: props.title,
2122
description: props.description,
23+
userIds: props.userIds,
2224
isEditing: false
2325
}
2426
this.handleSave = this.handleSave.bind(this)
@@ -27,17 +29,19 @@ export default class FileListItem extends React.Component {
2729
this.validateForm = this.validateForm.bind(this)
2830
this.validateTitle = this.validateTitle.bind(this)
2931
this.onTitleChange = this.onTitleChange.bind(this)
32+
this.onUserIdChange = this.onUserIdChange.bind(this)
3033
}
3134

3235
onDelete() {
3336
this.props.onDelete(this.props.id)
3437
}
3538

3639
startEdit() {
37-
const {title, description} = this.props
40+
const {title, description, userIds} = this.props
3841
this.setState({
3942
title,
4043
description,
44+
userIds,
4145
isEditing: true
4246
})
4347
}
@@ -48,7 +52,7 @@ export default class FileListItem extends React.Component {
4852
if (!_.isEmpty(errors)) {
4953
this.setState({ errors })
5054
} else {
51-
this.props.onSave(this.props.id, {title, description: this.refs.desc.value}, e)
55+
this.props.onSave(this.props.id, {title, description: this.refs.desc.value, userIds: this.state.userIds}, e)
5256
this.setState({isEditing: false})
5357
}
5458
}
@@ -74,9 +78,28 @@ export default class FileListItem extends React.Component {
7478
this.setState({ errors })
7579
}
7680

81+
onUserIdChange(selectedHandles = '') {
82+
this.setState({
83+
userIds: this.handlesToUserIds(selectedHandles.split(','))
84+
})
85+
}
86+
87+
userIdsToHandles(userIds) {
88+
const { projectMembers } = this.props
89+
userIds = userIds || []
90+
return userIds.map(userId => _.get(projectMembers[userId], 'handle'))
91+
}
92+
93+
handlesToUserIds(handles) {
94+
const { projectMembers } = this.props
95+
const projectMembersByHandle = _.mapKeys(projectMembers, value => value.handle)
96+
handles = handles || []
97+
return handles.filter(handle => handle).map(handle => _.get(projectMembersByHandle[handle], 'userId'))
98+
}
99+
77100
renderEditing() {
78-
const {title, description} = this.props
79-
const { errors } = this.state
101+
const { title, description, projectMembers, loggedInUser } = this.props
102+
const { errors, userIds } = this.state
80103
const onExitEdit = () => this.setState({isEditing: false, errors: {} })
81104
return (
82105
<div>
@@ -90,6 +113,11 @@ export default class FileListItem extends React.Component {
90113
{ (errors && errors.title) && <div className="error-message">{ errors.title }</div> }
91114
<textarea defaultValue={description} ref="desc" maxLength={250} className="tc-textarea" />
92115
{ (errors && errors.desc) && <div className="error-message">{ errors.desc }</div> }
116+
<UserAutoComplete onUpdate={this.onUserIdChange}
117+
projectMembers={projectMembers}
118+
loggedInUser={loggedInUser}
119+
selectedUsers={this.userIdsToHandles(userIds).join(',')}
120+
/>
93121
</div>
94122
)
95123
}
@@ -156,6 +184,9 @@ FileListItem.propTypes = {
156184
createdAt: PropTypes.string.isRequired,
157185
updatedByUser: PropTypes.object,
158186
createdByUser: PropTypes.object.isRequired,
187+
projectMembers: PropTypes.object.isRequired,
188+
loggedInUser: PropTypes.object.isRequired,
189+
userIds: PropTypes.array,
159190

160191
/**
161192
* Callback fired when a save button is clicked

src/components/LinksMenu/FileLinksMenu.jsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {Link} from 'react-router-dom'
44
import './LinksMenu.scss'
55
import Panel from '../Panel/Panel'
66
import AddFiles from '../FileList/AddFiles'
7+
import AddFilePermission from '../FileList/AddFilePermissions'
78
import DeleteLinkModal from './DeleteLinkModal'
89
import EditLinkModal from './EditLinkModal'
910
import uncontrollable from 'uncontrollable'
@@ -35,7 +36,14 @@ const FileLinksMenu = ({
3536
withHash,
3637
attachmentsStorePath,
3738
category,
39+
selectedUsers,
3840
onAddAttachment,
41+
onUploadAttachment,
42+
discardAttachments,
43+
onChangePermissions,
44+
pendingAttachments,
45+
projectMembers,
46+
loggedInUser,
3947
}) => {
4048
const renderLink = (link) => {
4149
if (link.onClick) {
@@ -59,6 +67,7 @@ const FileLinksMenu = ({
5967
}
6068

6169
const processUploadedFiles = (fpFiles, category) => {
70+
const attachments = []
6271
onAddingNewLink(false)
6372
fpFiles = _.isArray(fpFiles) ? fpFiles : [fpFiles]
6473
_.forEach(fpFiles, f => {
@@ -70,7 +79,19 @@ const FileLinksMenu = ({
7079
filePath: f.key,
7180
contentType: f.mimetype || 'application/unknown'
7281
}
73-
onAddAttachment(attachment)
82+
attachments.push(attachment)
83+
})
84+
onUploadAttachment(attachments)
85+
}
86+
87+
const onAddingAttachmentPermissions = (userIds) => {
88+
const { attachments, projectId } = pendingAttachments
89+
_.forEach(attachments, f => {
90+
const attachment = {
91+
...f,
92+
userIds
93+
}
94+
onAddAttachment(projectId, attachment)
7495
})
7596
}
7697

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

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

114+
{
115+
pendingAttachments &&
116+
<AddFilePermission onCancel={discardAttachments}
117+
onSubmit={onAddingAttachmentPermissions}
118+
onChange={onChangePermissions}
119+
selectedUsers={selectedUsers}
120+
projectMembers={projectMembers}
121+
loggedInUser={loggedInUser}
122+
/>
123+
}
124+
93125
{isAddingNewLink &&
94126
<Modal onClose={onClose}>
95127
<Modal.Title>
96128
UPLOAD A FILE
97129
</Modal.Title>
130+
{
131+
pendingAttachments &&
132+
<AddFilePermission />
133+
}
98134
<AddFiles successHandler={processUploadedFiles.bind(this)}
99135
storePath={attachmentsStorePath}
100136
category={category}
@@ -201,13 +237,20 @@ FileLinksMenu.propTypes = {
201237
noDots: PropTypes.bool,
202238
limit: PropTypes.number,
203239
links: PropTypes.array.isRequired,
240+
selectedUsers: PropTypes.string,
241+
projectMembers: PropTypes.object,
242+
pendingAttachments: PropTypes.object,
243+
onUploadAttachment: PropTypes.func,
244+
discardAttachments: PropTypes.func,
245+
onChangePermissions: PropTypes.func,
204246
attachmentsStorePath: PropTypes.string.isRequired,
205247
moreText: PropTypes.string,
206248
onAddingNewLink: PropTypes.func,
207249
onAddNewLink: PropTypes.func,
208250
onChangeLimit: PropTypes.func,
209251
onDelete: PropTypes.func,
210252
title: PropTypes.string,
253+
loggedInUser: PropTypes.object.isRequired,
211254
}
212255

213256
FileLinksMenu.defaultProps = {
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import React from 'react'
2+
import PropTypes from 'prop-types'
3+
import { values } from 'lodash'
4+
5+
import './UserAutoComplete.scss'
6+
import Select from '../Select/Select'
7+
8+
/**
9+
* Render a searchable dropdown for selecting users
10+
* @param {Object} projectMembers - a map of userId to user object of project members. i.e., members.members from the store
11+
* @param {String} selectedUsers - currently selected user handles delimitted by ','
12+
* @param {Function} onUpdate - change handler. invoked when a user is added or removed from selection
13+
*/
14+
const UserAutoComplete = ({
15+
projectMembers,
16+
selectedUsers,
17+
onUpdate,
18+
loggedInUser
19+
}) => (
20+
<div styleName="user-select-wrapper" className="user-select-wrapper">
21+
<Select
22+
multi
23+
value={selectedUsers}
24+
placeholder="Enter user handles"
25+
onChange={selectedOptions => onUpdate(selectedOptions)}
26+
options={
27+
values(projectMembers || {})
28+
.filter(member => member.handle !== loggedInUser.handle)
29+
.map(member => ({ value: member.handle, label: member.handle }))
30+
}
31+
/>
32+
</div>
33+
)
34+
35+
UserAutoComplete.propTypes = {
36+
projectMembers: PropTypes.object,
37+
selectedUsers: PropTypes.string,
38+
onUpdate: PropTypes.func,
39+
loggedInUser: PropTypes.object
40+
}
41+
42+
export default UserAutoComplete
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
.user-select-wrapper {
2+
width: 100%;
3+
margin: 8px 0;
4+
}
5+
6+
:global {
7+
.management-dialog-overlay
8+
.project-dialog-conatiner
9+
.project-dialog
10+
.input-container
11+
.user-select-wrapper {
12+
input {
13+
margin: 0;
14+
}
15+
}
16+
}

src/config/constants.js

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -361,10 +361,13 @@ export const CLEAR_LOADED_PROJECT = 'CLEAR_LOADED_PROJECT'
361361

362362

363363
// Project attachments
364-
export const ADD_PROJECT_ATTACHMENT = 'ADD_PROJECT_ATTACHMENT'
365-
export const ADD_PROJECT_ATTACHMENT_PENDING = 'ADD_PROJECT_ATTACHMENT_PENDING'
366-
export const ADD_PROJECT_ATTACHMENT_SUCCESS = 'ADD_PROJECT_ATTACHMENT_SUCCESS'
367-
export const ADD_PROJECT_ATTACHMENT_FAILURE = 'ADD_PROJECT_ATTACHMENT_FAILURE'
364+
export const ADD_PROJECT_ATTACHMENT = 'ADD_PROJECT_ATTACHMENT'
365+
export const DISCARD_PROJECT_ATTACHMENT = 'DISCARD_PROJECT_ATTACHMENT'
366+
export const UPLOAD_PROJECT_ATTACHMENT_FILES = 'UPLOAD_PROJECT_ATTACHMENT_FILES'
367+
export const CHANGE_ATTACHMENT_PERMISSION = 'CHANGE_ATTACHMENT_PERMISSION'
368+
export const ADD_PROJECT_ATTACHMENT_PENDING = 'ADD_PROJECT_ATTACHMENT_PENDING'
369+
export const ADD_PROJECT_ATTACHMENT_SUCCESS = 'ADD_PROJECT_ATTACHMENT_SUCCESS'
370+
export const ADD_PROJECT_ATTACHMENT_FAILURE = 'ADD_PROJECT_ATTACHMENT_FAILURE'
368371

369372
export const REMOVE_PROJECT_ATTACHMENT = 'REMOVE_PROJECT_ATTACHMENT'
370373
export const REMOVE_PROJECT_ATTACHMENT_PENDING = 'REMOVE_PROJECT_ATTACHMENT_PENDING'

0 commit comments

Comments
 (0)