Merge pull request #2239 from overleaf/em-collabs-frontend

Change collaborator permissions

GitOrigin-RevId: 3627181d201e6d96734f89a380703953424f0fdf
This commit is contained in:
Eric Mc Sween 2019-10-15 09:10:56 -04:00 committed by sharelatex
parent cc1de97df8
commit 6f966ceb3d
8 changed files with 211 additions and 226 deletions

View file

@ -49,23 +49,28 @@ async function getAllMembers(req, res, next) {
}
async function setCollaboratorInfo(req, res, next) {
const projectId = req.params.Project_id
const userId = req.params.user_id
const { privilegeLevel } = req.body
try {
const projectId = req.params.Project_id
const userId = req.params.user_id
const { privilegeLevel } = req.body
await CollaboratorsHandler.promises.setCollaboratorPrivilegeLevel(
projectId,
userId,
privilegeLevel
)
EditorRealTimeController.emitToRoom(
projectId,
'project:membership:changed',
{ members: true }
)
res.sendStatus(204)
} catch (err) {
if (err instanceof Errors.NotFoundError) {
throw new HttpErrors.NotFoundError({})
} else {
throw err
throw new HttpErrors.InternalServerError({}).withCause(err)
}
}
res.sendStatus(204)
}
async function _removeUserIdFromProject(projectId, userId) {

View file

@ -77,25 +77,45 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
) #{translate("make_private")}
.row.project-member
.col-xs-8 {{ project.owner.email }}
.text-left(
ng-class="{'col-xs-3': project.members.length > 0, 'col-xs-4': project.members.length == 0}"
) #{translate("owner")}
.row.project-member(ng-repeat="member in project.members")
.col-xs-8 {{ member.email }}
.col-xs-3.text-left
span(ng-show="member.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="member.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1(ng-show="isAdmin")
a(
href
tooltip=translate('remove_collaborator')
tooltip-placement="bottom"
ng-click="removeMember(member)"
)
i.fa.fa-times
.col-xs-7 {{ project.owner.email }}
.text-left.col-xs-3 #{translate("owner")}
form.form-horizontal(
ng-if="isAdmin"
ng-repeat="member in project.members"
ng-controller="ShareProjectModalMemberRowController"
)
.row.form-group.project-member
.col-xs-7.form-control-static {{ member.email }}
.col-xs-3
select.privileges.form-control.input-sm(name="privileges" ng-model="form.privileges")
option(value="readAndWrite") #{translate("can_edit")}
option(value="readOnly") #{translate("read_only")}
.col-xs-2.form-control-static.text-center(ng-hide="form.isModified()")
a(
href
tooltip=translate('remove_collaborator')
tooltip-placement="bottom"
ng-click="removeMember(member)"
aria-label=translate('remove_collaborator')
)
i.fa.fa-times
.col-xs-2.text-center(ng-show="form.isModified()")
button.btn.btn-sm.btn-success(
type="submit"
ng-click="form.submit()"
) #{translate("change")}
.text-sm
| #{translate("or")}
|
button.btn.btn-inline-link(ng-click="form.reset()") #{translate("cancel").toLowerCase()}
.row.project-member(ng-if="!isAdmin" ng-repeat="member in project.members")
.col-xs-7 {{ member.email }}
.col-xs-3
span(ng-if="member.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-if="member.privileges == 'readOnly'") #{translate("read_only")}
.row.project-invite(ng-repeat="invite in project.invites")
.col-xs-8 {{ invite.email }} 
.col-xs-7 {{ invite.email }} 
div.small
| #{translate("invite_not_accepted")}. 
button.btn.btn-inline-link(
@ -106,7 +126,7 @@ script(type='text/ng-template', id='shareProjectModalTemplate')
// todo: get invite privileges
span(ng-show="invite.privileges == 'readAndWrite'") #{translate("can_edit")}
span(ng-show="invite.privileges == 'readOnly'") #{translate("read_only")}
.col-xs-1(ng-show="isAdmin")
.col-xs-2.text-center(ng-if="isAdmin")
a(
href
tooltip=translate('revoke_invite')

View file

@ -1,31 +1,18 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], App =>
define(['base'], App => {
App.controller('ShareController', function(
$scope,
$modal,
ide,
projectInvites,
projectMembers,
// eslint-disable-next-line camelcase
event_tracking
) {
$scope.openShareProjectModal = function(isAdmin) {
$scope.isAdmin = isAdmin
event_tracking.sendMBOnce('ide-open-share-modal-once')
return $modal.open({
$modal.open({
templateUrl: 'shareProjectModalTemplate',
controller: 'ShareProjectModalController',
scope: $scope
@ -35,36 +22,35 @@ define(['base'], App =>
ide.socket.on('project:tokens:changed', data => {
if (data.tokens != null) {
ide.$scope.project.tokens = data.tokens
return $scope.$digest()
$scope.$digest()
}
})
return ide.socket.on('project:membership:changed', data => {
ide.socket.on('project:membership:changed', data => {
if (data.members) {
projectMembers
.getMembers()
.then(response => {
;({ data } = response)
if (data.members) {
return ($scope.project.members = data.members)
if (response.data.members) {
$scope.project.members = response.data.members
}
})
.catch(() => {
return console.error('Error fetching members for project')
console.error('Error fetching members for project')
})
}
if (data.invites) {
return projectInvites
projectInvites
.getInvites()
.then(response => {
;({ data } = response)
if (data.invites) {
return ($scope.project.invites = data.invites)
if (response.data.invites) {
$scope.project.invites = response.data.invites
}
})
.catch(() => {
return console.error('Error fetching invites for project')
console.error('Error fetching invites for project')
})
}
})
}))
})
})

View file

@ -1,22 +1,4 @@
/* eslint-disable
camelcase,
max-len,
no-return-assign,
no-undef,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS103: Rewrite code to no longer use __guard__
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], App =>
define(['base'], App => {
App.controller('ShareProjectModalController', function(
$scope,
$modalInstance,
@ -29,9 +11,9 @@ define(['base'], App =>
validateCaptcha,
validateCaptchaV3,
settings,
// eslint-disable-next-line camelcase
event_tracking
) {
let loadAutocompleteUsers
$scope.inputs = {
privileges: 'readAndWrite',
contacts: []
@ -52,9 +34,9 @@ define(['base'], App =>
$scope.refreshCanAddCollaborators = function() {
const allowedNoOfMembers = $scope.project.features.collaborators
return ($scope.canAddCollaborators =
$scope.canAddCollaborators =
$scope.project.members.length + $scope.project.invites.length <
allowedNoOfMembers || allowedNoOfMembers === INFINITE_COLLABORATORS)
allowedNoOfMembers || allowedNoOfMembers === INFINITE_COLLABORATORS
}
$scope.refreshCanAddCollaborators()
@ -74,34 +56,27 @@ define(['base'], App =>
)
$scope.autocompleteContacts = []
;(loadAutocompleteUsers = () =>
$http.get('/user/contacts').then(function(response) {
const { data } = response
$scope.autocompleteContacts = data.contacts || []
return (() => {
const result = []
for (let contact of Array.from($scope.autocompleteContacts)) {
if (contact.type === 'user') {
if (
contact.first_name === contact.email.split('@')[0] &&
!contact.last_name
) {
// User has not set their proper name so use email as canonical display property
result.push((contact.display = contact.email))
} else {
contact.name = `${contact.first_name} ${contact.last_name}`
result.push(
(contact.display = `${contact.name} <${contact.email}>`)
)
}
} else {
// Must be a group
result.push((contact.display = contact.name))
}
$http.get('/user/contacts').then(function(response) {
const { data } = response
$scope.autocompleteContacts = data.contacts || []
for (let contact of $scope.autocompleteContacts) {
if (contact.type === 'user') {
if (
contact.first_name === contact.email.split('@')[0] &&
!contact.last_name
) {
// User has not set their proper name so use email as canonical display property
contact.display = contact.email
} else {
contact.name = `${contact.first_name} ${contact.last_name}`
contact.display = `${contact.name} <${contact.email}>`
}
return result
})()
}))()
} else {
// Must be a group
contact.display = contact.name
}
}
})
const getCurrentMemberEmails = () =>
($scope.project.members || []).map(u => u.email)
@ -114,15 +89,14 @@ define(['base'], App =>
return $scope.autocompleteContacts.filter(function(contact) {
if (
contact.email != null &&
Array.from(currentMemberEmails).includes(contact.email)
currentMemberEmails.includes(contact.email)
) {
return false
}
for (let text of [contact.name, contact.email]) {
if (
(text != null
? text.toLowerCase().indexOf($query.toLowerCase())
: undefined) > -1
text != null &&
text.toLowerCase().indexOf($query.toLowerCase()) > -1
) {
return true
}
@ -133,15 +107,13 @@ define(['base'], App =>
$scope.addMembers = function() {
const addMembers = function() {
let addNextMember
if ($scope.inputs.contacts.length === 0) {
return
}
const members = $scope.inputs.contacts
$scope.inputs.contacts = []
$scope.state.error = false
$scope.state.errorReason = null
$scope.clearError()
$scope.state.inflight = true
if ($scope.project.invites == null) {
@ -150,7 +122,9 @@ define(['base'], App =>
const currentMemberEmails = getCurrentMemberEmails()
const currentInviteEmails = getCurrentInviteEmails()
return (addNextMember = function() {
addNextMember()
function addNextMember() {
let email
if (members.length === 0 || !$scope.canAddCollaborators) {
$scope.state.inflight = false
@ -160,14 +134,14 @@ define(['base'], App =>
const member = members.shift()
if (member.type === 'user') {
;({ email } = member)
email = member.email
} else {
// Not an auto-complete object, so email == display
email = member.display
}
email = email.toLowerCase()
if (Array.from(currentMemberEmails).includes(email)) {
if (currentMemberEmails.includes(email)) {
// Skip this existing member
return addNextMember()
}
@ -175,20 +149,13 @@ define(['base'], App =>
validateCaptchaV3('invite')
// do v2 captcha
const ExposedSettings = window.ExposedSettings
return validateCaptcha(function(response) {
let inviteId, request
validateCaptcha(function(response) {
$scope.grecaptchaResponse = response
if (
Array.from(currentInviteEmails).includes(email) &&
(inviteId = __guard__(
_.find(
$scope.project.invites || [],
invite => invite.email === email
),
x => x._id
))
) {
request = projectInvites.resendInvite(inviteId)
const invites = $scope.project.invites || []
const invite = _.find(invites, invite => invite.email === email)
let request
if (currentInviteEmails.includes(email) && invite) {
request = projectInvites.resendInvite(invite._id)
} else {
request = projectInvites.sendInvite(
email,
@ -197,31 +164,28 @@ define(['base'], App =>
)
}
return request
request
.then(function(response) {
const { data } = response
if (data.error) {
$scope.state.error = true
$scope.state.errorReason = `${data.error}`
$scope.setError(data.error)
$scope.state.inflight = false
} else {
if (data.invite) {
const { invite } = data
$scope.project.invites.push(invite)
} else {
let users
if (data.users != null) {
;({ users } = data)
} else if (data.user != null) {
users = [data.user]
} else {
users = []
}
$scope.project.members.push(...Array.from(users || []))
const users =
data.users != null
? data.users
: data.user != null
? [data.user]
: []
$scope.project.members.push(...users)
}
}
return setTimeout(
setTimeout(
() =>
// Give $scope a chance to update $scope.canAddCollaborators
// with new collaborator information.
@ -231,113 +195,106 @@ define(['base'], App =>
)
})
.catch(function(httpResponse) {
const { data, status, headers, config } = httpResponse
const { data } = httpResponse
$scope.state.inflight = false
$scope.state.error = true
if ((data != null ? data.errorReason : undefined) != null) {
return ($scope.state.errorReason =
data != null ? data.errorReason : undefined)
} else {
return ($scope.state.errorReason = null)
}
$scope.setError(data.errorReason)
})
}, ExposedSettings.recaptchaDisabled.invite)
})()
}
}
return $timeout(addMembers, 50) // Give email list a chance to update
$timeout(addMembers, 50) // Give email list a chance to update
}
$scope.removeMember = function(member) {
$scope.state.error = null
$scope.state.inflight = true
return projectMembers
.removeMember(member)
.then(function() {
$scope.state.inflight = false
$scope.monitorRequest(
projectMembers.removeMember(member).then(function() {
const index = $scope.project.members.indexOf(member)
if (index === -1) {
return
}
return $scope.project.members.splice(index, 1)
})
.catch(function() {
$scope.state.inflight = false
return ($scope.state.error = 'Sorry, something went wrong :(')
$scope.project.members.splice(index, 1)
})
)
}
$scope.revokeInvite = function(invite) {
$scope.state.error = null
$scope.state.inflight = true
return projectInvites
.revokeInvite(invite._id)
.then(function() {
$scope.state.inflight = false
$scope.monitorRequest(
projectInvites.revokeInvite(invite._id).then(function() {
const index = $scope.project.invites.indexOf(invite)
if (index === -1) {
return
}
return $scope.project.invites.splice(index, 1)
})
.catch(function() {
$scope.state.inflight = false
return ($scope.state.error = 'Sorry, something went wrong :(')
$scope.project.invites.splice(index, 1)
})
)
}
$scope.resendInvite = function(invite, event) {
$scope.state.error = null
$scope.state.inflight = true
return projectInvites
.resendInvite(invite._id)
.then(function() {
$scope.state.inflight = false
return event.target.blur()
})
.catch(function() {
$scope.state.inflight = false
$scope.state.error =
'Sorry, something went wrong resending the invite :('
return event.target.blur()
})
$scope.monitorRequest(
projectInvites
.resendInvite(invite._id)
.then(function() {
event.target.blur()
})
.catch(function() {
event.target.blur()
})
)
}
$scope.makeTokenBased = function() {
$scope.project.publicAccesLevel = 'tokenBased'
settings.saveProjectAdminSettings({ publicAccessLevel: 'tokenBased' })
return event_tracking.sendMB('project-make-token-based')
event_tracking.sendMB('project-make-token-based')
}
$scope.makePrivate = function() {
$scope.project.publicAccesLevel = 'private'
return settings.saveProjectAdminSettings({ publicAccessLevel: 'private' })
settings.saveProjectAdminSettings({ publicAccessLevel: 'private' })
}
$scope.$watch('project.tokens.readAndWrite', function(token) {
if (token != null) {
return ($scope.readAndWriteTokenLink = `${location.origin}/${token}`)
$scope.readAndWriteTokenLink = `${location.origin}/${token}`
} else {
return ($scope.readAndWriteTokenLink = null)
$scope.readAndWriteTokenLink = null
}
})
$scope.$watch('project.tokens.readOnly', function(token) {
if (token != null) {
return ($scope.readOnlyTokenLink = `${location.origin}/read/${token}`)
$scope.readOnlyTokenLink = `${location.origin}/read/${token}`
} else {
return ($scope.readOnlyTokenLink = null)
$scope.readOnlyTokenLink = null
}
})
$scope.done = () => $modalInstance.close()
return ($scope.cancel = () => $modalInstance.dismiss())
}))
$scope.cancel = () => $modalInstance.dismiss()
function __guard__(value, transform) {
return typeof value !== 'undefined' && value !== null
? transform(value)
: undefined
}
$scope.monitorRequest = function monitorRequest(request) {
$scope.clearError()
$scope.state.inflight = true
return request
.then(() => {
$scope.state.inflight = false
$scope.clearError()
})
.catch(err => {
$scope.state.inflight = false
$scope.setError(err.data && err.data.error)
})
}
$scope.clearError = function clearError() {
$scope.state.error = false
}
$scope.setError = function setError(reason) {
$scope.state.error = true
$scope.state.errorReason = reason
}
})
})

View file

@ -0,0 +1,31 @@
define(['base'], App => {
App.controller('ShareProjectModalMemberRowController', function(
$scope,
projectMembers
) {
$scope.form = {
privileges: $scope.member.privileges,
isModified() {
return this.privileges !== $scope.member.privileges
},
submit() {
const userId = $scope.member._id
const privilegeLevel = $scope.form.privileges
$scope.monitorRequest(
projectMembers
.setMemberPrivilegeLevel(userId, privilegeLevel)
.then(() => {
$scope.member.privileges = privilegeLevel
})
)
},
reset() {
this.privileges = $scope.member.privileges
$scope.clearError()
}
}
})
})

View file

@ -1,11 +1,7 @@
/* eslint-disable
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
define([
'ide/share/controllers/ShareController',
'ide/share/controllers/ShareProjectModalController',
'ide/share/controllers/ShareProjectModalMemberRowController',
'ide/share/services/projectMembers',
'ide/share/services/projectInvites'
], function() {})

View file

@ -1,14 +1,3 @@
/* eslint-disable
max-len,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], App =>
App.factory('projectInvites', (ide, $http) => ({
sendInvite(email, privileges, grecaptchaResponse) {

View file

@ -1,14 +1,3 @@
/* eslint-disable
camelcase,
no-undef,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
define(['base'], App =>
App.factory('projectMembers', (ide, $http) => ({
removeMember(member) {
@ -21,9 +10,9 @@ define(['base'], App =>
})
},
addGroup(group_id, privileges) {
addGroup(groupId, privileges) {
return $http.post(`/project/${ide.project_id}/group`, {
group_id,
group_id: groupId,
privileges,
_csrf: window.csrfToken
})
@ -36,5 +25,17 @@ define(['base'], App =>
'X-Csrf-Token': window.csrfToken
}
})
},
setMemberPrivilegeLevel(userId, privilegeLevel) {
return $http.put(
`/project/${ide.project_id}/users/${userId}`,
{ privilegeLevel },
{
headers: {
'X-Csrf-Token': window.csrfToken
}
}
)
}
})))