ShareProjectModalController.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309
  1. import _ from 'lodash'
  2. import App from '../../../base'
  3. App.controller('ShareProjectModalController', function(
  4. $scope,
  5. $modalInstance,
  6. $timeout,
  7. projectMembers,
  8. projectInvites,
  9. $modal,
  10. $http,
  11. ide,
  12. validateCaptcha,
  13. validateCaptchaV3,
  14. settings,
  15. // eslint-disable-next-line camelcase
  16. eventTracking
  17. ) {
  18. $scope.inputs = {
  19. privileges: 'readAndWrite',
  20. contacts: []
  21. }
  22. $scope.state = {
  23. error: null,
  24. errorReason: null,
  25. inflight: false,
  26. startedFreeTrial: false,
  27. invites: []
  28. }
  29. $modalInstance.opened.then(() =>
  30. $timeout(() => $scope.$broadcast('open'), 200)
  31. )
  32. const INFINITE_COLLABORATORS = -1
  33. $scope.refreshCanAddCollaborators = function() {
  34. const allowedNoOfMembers = $scope.project.features.collaborators
  35. $scope.canAddCollaborators =
  36. $scope.project.members.length + $scope.project.invites.length <
  37. allowedNoOfMembers || allowedNoOfMembers === INFINITE_COLLABORATORS
  38. }
  39. $scope.refreshCanAddCollaborators()
  40. $scope.$watch('canAddCollaborators', function() {
  41. if (!$scope.canAddCollaborators) {
  42. eventTracking.send(
  43. 'subscription-funnel',
  44. 'editor-click-feature',
  45. 'projectMembers'
  46. )
  47. }
  48. })
  49. $scope.$watch(
  50. '(project.members.length + project.invites.length)',
  51. _noOfMembers => $scope.refreshCanAddCollaborators()
  52. )
  53. $scope.autocompleteContacts = []
  54. if ($scope.isRestrictedTokenMember) {
  55. // Restricted token members are users who join via a read-only link.
  56. // They will not be able to invite any users, so skip the lookup of
  57. // their contacts. This request would result in a 403 for anonymous
  58. // users, which in turn would redirect them to the /login.
  59. } else {
  60. $http.get('/user/contacts').then(processContactsResponse)
  61. }
  62. function processContactsResponse(response) {
  63. const { data } = response
  64. $scope.autocompleteContacts = data.contacts || []
  65. for (let contact of $scope.autocompleteContacts) {
  66. if (contact.type === 'user') {
  67. if (
  68. contact.first_name === contact.email.split('@')[0] &&
  69. !contact.last_name
  70. ) {
  71. // User has not set their proper name so use email as canonical display property
  72. contact.display = contact.email
  73. } else {
  74. contact.name = `${contact.first_name} ${contact.last_name}`
  75. contact.display = `${contact.name} <${contact.email}>`
  76. }
  77. } else {
  78. // Must be a group
  79. contact.display = contact.name
  80. }
  81. }
  82. }
  83. const getCurrentMemberEmails = () =>
  84. ($scope.project.members || []).map(u => u.email)
  85. const getCurrentInviteEmails = () =>
  86. ($scope.project.invites || []).map(u => u.email)
  87. $scope.filterAutocompleteUsers = function($query) {
  88. const currentMemberEmails = getCurrentMemberEmails()
  89. return $scope.autocompleteContacts.filter(function(contact) {
  90. if (
  91. contact.email != null &&
  92. currentMemberEmails.includes(contact.email)
  93. ) {
  94. return false
  95. }
  96. for (let text of [contact.name, contact.email]) {
  97. if (
  98. text != null &&
  99. text.toLowerCase().indexOf($query.toLowerCase()) > -1
  100. ) {
  101. return true
  102. }
  103. }
  104. return false
  105. })
  106. }
  107. $scope.addMembers = function() {
  108. const addMembers = function() {
  109. if ($scope.inputs.contacts.length === 0) {
  110. return
  111. }
  112. const members = $scope.inputs.contacts
  113. $scope.inputs.contacts = []
  114. $scope.clearError()
  115. $scope.state.inflight = true
  116. if ($scope.project.invites == null) {
  117. $scope.project.invites = []
  118. }
  119. const currentMemberEmails = getCurrentMemberEmails()
  120. const currentInviteEmails = getCurrentInviteEmails()
  121. addNextMember()
  122. function addNextMember() {
  123. let email
  124. if (members.length === 0 || !$scope.canAddCollaborators) {
  125. $scope.state.inflight = false
  126. $scope.$apply()
  127. return
  128. }
  129. const member = members.shift()
  130. if (member.type === 'user') {
  131. email = member.email
  132. } else {
  133. // Not an auto-complete object, so email == display
  134. email = member.display
  135. }
  136. email = email.toLowerCase()
  137. if (currentMemberEmails.includes(email)) {
  138. // Skip this existing member
  139. return addNextMember()
  140. }
  141. // do v3 captcha to collect data only
  142. validateCaptchaV3('invite')
  143. // do v2 captcha
  144. const ExposedSettings = window.ExposedSettings
  145. validateCaptcha(function(response) {
  146. $scope.grecaptchaResponse = response
  147. const invites = $scope.project.invites || []
  148. const invite = _.find(invites, invite => invite.email === email)
  149. let request
  150. if (currentInviteEmails.includes(email) && invite) {
  151. request = projectInvites.resendInvite(invite._id)
  152. } else {
  153. request = projectInvites.sendInvite(
  154. email,
  155. $scope.inputs.privileges,
  156. $scope.grecaptchaResponse
  157. )
  158. }
  159. request
  160. .then(function(response) {
  161. const { data } = response
  162. if (data.error) {
  163. $scope.setError(data.error)
  164. $scope.state.inflight = false
  165. } else {
  166. if (data.invite) {
  167. const { invite } = data
  168. $scope.project.invites.push(invite)
  169. } else {
  170. const users =
  171. data.users != null
  172. ? data.users
  173. : data.user != null
  174. ? [data.user]
  175. : []
  176. $scope.project.members.push(...users)
  177. }
  178. }
  179. setTimeout(
  180. () =>
  181. // Give $scope a chance to update $scope.canAddCollaborators
  182. // with new collaborator information.
  183. addNextMember(),
  184. 0
  185. )
  186. })
  187. .catch(function(httpResponse) {
  188. const { data } = httpResponse
  189. $scope.state.inflight = false
  190. $scope.setError(data.errorReason)
  191. })
  192. }, ExposedSettings.recaptchaDisabled.invite)
  193. }
  194. }
  195. $timeout(addMembers, 50) // Give email list a chance to update
  196. }
  197. $scope.removeMember = function(member) {
  198. $scope.monitorRequest(
  199. projectMembers.removeMember(member).then(function() {
  200. const index = $scope.project.members.indexOf(member)
  201. if (index === -1) {
  202. return
  203. }
  204. $scope.project.members.splice(index, 1)
  205. })
  206. )
  207. }
  208. $scope.revokeInvite = function(invite) {
  209. $scope.monitorRequest(
  210. projectInvites.revokeInvite(invite._id).then(function() {
  211. const index = $scope.project.invites.indexOf(invite)
  212. if (index === -1) {
  213. return
  214. }
  215. $scope.project.invites.splice(index, 1)
  216. })
  217. )
  218. }
  219. $scope.resendInvite = function(invite, event) {
  220. $scope.monitorRequest(
  221. projectInvites
  222. .resendInvite(invite._id)
  223. .then(function() {
  224. event.target.blur()
  225. })
  226. .catch(function() {
  227. event.target.blur()
  228. })
  229. )
  230. }
  231. $scope.makeTokenBased = function() {
  232. $scope.project.publicAccesLevel = 'tokenBased'
  233. settings.saveProjectAdminSettings({ publicAccessLevel: 'tokenBased' })
  234. eventTracking.sendMB('project-make-token-based')
  235. }
  236. $scope.makePrivate = function() {
  237. $scope.project.publicAccesLevel = 'private'
  238. settings.saveProjectAdminSettings({ publicAccessLevel: 'private' })
  239. }
  240. $scope.$watch('project.tokens.readAndWrite', function(token) {
  241. if (token != null) {
  242. $scope.readAndWriteTokenLink = `${location.origin}/${token}`
  243. } else {
  244. $scope.readAndWriteTokenLink = null
  245. }
  246. })
  247. $scope.$watch('project.tokens.readOnly', function(token) {
  248. if (token != null) {
  249. $scope.readOnlyTokenLink = `${location.origin}/read/${token}`
  250. } else {
  251. $scope.readOnlyTokenLink = null
  252. }
  253. })
  254. $scope.done = () => $modalInstance.close()
  255. $scope.cancel = () => $modalInstance.dismiss()
  256. $scope.monitorRequest = function monitorRequest(request) {
  257. $scope.clearError()
  258. $scope.state.inflight = true
  259. return request
  260. .then(() => {
  261. $scope.state.inflight = false
  262. $scope.clearError()
  263. })
  264. .catch(err => {
  265. $scope.state.inflight = false
  266. $scope.setError(err.data && err.data.error)
  267. })
  268. }
  269. $scope.clearError = function clearError() {
  270. $scope.state.error = false
  271. }
  272. $scope.setError = function setError(reason) {
  273. $scope.state.error = true
  274. $scope.state.errorReason = reason
  275. }
  276. })