3 var _ = require('lodash');
4 var Backbone = require('backbone');
5 var Marionette = require('backbone.marionette');
6 var $ = require('jquery');
7 var fuzzysearch = require('fuzzysearch');
8 var urlJoin = require('url-join');
9 var avatars = require('gitter-web-avatars');
10 var toggleClass = require('../../utils/toggle-class');
11 var apiClient = require('../../components/api-client');
12 const isGitlabSecurityDescriptorType = require('gitter-web-shared/is-gitlab-security-descriptor-type');
14 var ModalView = require('./modal');
15 var Typeahead = require('../controls/typeahead');
16 var userSearchModels = require('../../collections/user-search');
17 var userSearchItemTemplate = require('../app/tmpl/userSearchItem.hbs');
18 var PermissionsPeopleListView = require('./permissions/permissions-people-list-view');
19 var requestingSecurityDescriptorStatusConstants = require('./permissions/requesting-security-descriptor-status-constants');
20 var submitSecurityDescriptorStatusConstants = require('./permissions/requesting-security-descriptor-status-constants');
21 var template = require('./tmpl/permissions-view.hbs');
23 require('../behaviors/isomorphic');
25 var PermissionsView = Marionette.LayoutView.extend({
29 peopleInput: '.js-permissions-people-input',
30 permissionsOptionsWrapper: '.js-permissions-options-wrapper',
31 permissionsOptionsSelect: '.js-permissions-options-select',
32 permissionsOptionsSpinner: '.js-permissions-options-spinner',
33 permissionsOptionsErrorIcon: '.js-permissions-options-error-icon',
34 permissionsOptionsGithubIcon: '.js-permissions-options-github-icon',
35 permissionsOptionsGitlabIcon: '.js-permissions-options-gitlab-icon',
36 permissionsOptionsGitterIcon: '.js-permissions-options-gitter-icon',
37 extraAdminsNote: '.js-permissions-extra-admins-note',
38 extraAdminsList: '.js-permissions-admin-list',
39 sdWarning: '.js-permissions-sd-warning',
40 modelError: '.js-permissions-model-error',
41 submissionError: '.js-permissions-submission-error'
46 adminListView: { el: '.js-permissions-admin-list-root', init: 'initAdminListView' }
50 initAdminListView: function(optionsForRegion) {
51 this.adminListView = new PermissionsPeopleListView(
53 collection: this.model.adminCollection
57 this.listenTo(this.adminListView, 'user:remove', this.onAdminRemoved, this);
58 return this.adminListView;
62 'change @ui.permissionsOptionsSelect': 'onPermissionsOptionsSelectChange'
66 'change:entity': 'onEntityChange',
67 'change:securityDescriptor': 'onSecurityDescriptorChange',
68 'change:requestingSecurityDescriptorStatus': 'onRequestingSecurityDescriptorStatusChange',
69 'change:submitSecurityDescriptorStatus': 'onSubmitSecurityDescriptorStatusChange'
72 initialize: function(/*attrs, options*/) {
73 this.initializeForEntity();
74 this.listenTo(this, 'menuItemClicked', this.menuItemClicked);
75 this.listenTo(this.model.adminCollection, 'add remove', this.onAdminCollectionChange);
76 this.listenTo(this.model.groupCollection, 'add remove', this.onGroupCollectionChange);
79 menuItemClicked: function(button) {
81 case 'switch-to-group-entity':
82 var entity = this.model.get('entity');
83 var groupId = entity && entity.get('groupId');
85 var group = entity && entity.get('group');
87 group = this.model.groupCollection.get(groupId);
95 this.submitNewSecurityDescriptor();
100 serializeData: function() {
101 var data = this.model.toJSON();
102 data.entity = data.entity && data.entity.toJSON();
104 data.groupAvatarUrl = avatars.getForGroupId(data.entity.groupId || data.entity.id);
105 data.permissionOpts = this.getPermissionOptions();
110 onRender: function() {
111 this.typeahead = new Typeahead({
112 collection: new userSearchModels.Collection(),
113 itemTemplate: userSearchItemTemplate,
114 el: this.ui.peopleInput[0],
115 autoSelector: function(input) {
117 var displayName = (m.get('displayName') || '').toLowerCase();
118 var username = (m.get('username') || '').toLowerCase();
121 fuzzysearch(input.toLowerCase(), displayName) ||
122 fuzzysearch(input.toLowerCase(), username)
126 fetch: function(input, collection, fetchSuccess) {
127 this.collection.fetch(
138 success: fetchSuccess
144 this.listenTo(this.typeahead, 'selected', this.onAdminSelected);
146 this.onRequestingSecurityDescriptorStatusChange();
147 this.onAdminCollectionChange();
149 this.rendered = true;
152 onPermissionsOptionsSelectChange: function() {
153 var currentSd = this.model.get('securityDescriptor');
154 var selectValue = this.ui.permissionsOptionsSelect.val();
157 securityDescriptor: _.extend({}, currentSd, {
158 type: selectValue === 'null' ? null : selectValue
163 onGroupCollectionChange: function() {
165 this.updatePermissionSelect();
169 onAdminCollectionChange: function() {
170 toggleClass(this.ui.extraAdminsList[0], 'hidden', this.model.adminCollection.length === 0);
171 this.updateModelErrors();
174 onAdminSelected: function(user) {
175 this.ui.peopleInput.val('');
176 this.model.adminCollection.add([user]);
177 this.typeahead.dropdown.hide();
180 onAdminRemoved: function(user) {
181 this.model.adminCollection.remove(user);
184 initializeForEntity: function() {
185 this.fetchSecurityDescriptor();
186 this.fetchAdminUsers();
189 onEntityChange: function() {
191 securityDescriptor: null
193 this.model.adminCollection.reset();
194 this.initializeForEntity();
195 this.updatePermissionSelect();
198 // eslint-disable-next-line complexity
199 onSecurityDescriptorChange: function() {
200 var sd = this.model.get('securityDescriptor');
201 var sdType = sd && sd.type;
202 toggleClass(this.ui.extraAdminsNote[0], 'hidden', !sdType);
204 var sdWarningString = '';
205 var initialSdType = this.model.get('initialSecurityDescriptorType');
206 var isInitialSdTypeNonReversable =
207 initialSdType === 'GL_GROUP' ||
208 initialSdType === 'GL_PROJECT' ||
209 initialSdType === 'GL_USER' ||
210 initialSdType === 'GH_ORG' ||
211 initialSdType === 'GH_REPO' ||
212 initialSdType === 'GH_USER';
213 if (isInitialSdTypeNonReversable && initialSdType !== sdType) {
214 let backendString = '';
216 initialSdType === 'GH_ORG' ||
217 initialSdType === 'GH_REPO' ||
218 initialSdType === 'GH_USER'
220 backendString = 'GitHub';
221 } else if (isGitlabSecurityDescriptorType(initialSdType)) {
222 backendString = 'GitLab';
225 sdWarningString = `Warning, switching away from ${backendString}-based administrators is permanent. Once you have applied these changes, you cannot go back to ${backendString} based administrators.`;
227 this.ui.sdWarning.text(sdWarningString);
228 toggleClass(this.ui.sdWarning[0], 'hidden', sdWarningString.length === 0);
230 this.updatePermissionSelect();
231 this.updatePermissionOptionsIcons();
232 this.updateModelErrors();
235 onRequestingSecurityDescriptorStatusChange: function() {
236 this.updatePermissionOptionsIcons();
239 onSubmitSecurityDescriptorStatusChange: function() {
240 var status = this.model.get('submitSecurityDescriptorStatus');
241 var statusString = '';
243 if (status === submitSecurityDescriptorStatusConstants.ERROR) {
244 statusString = 'Problem submitting security descriptor';
247 this.ui.submissionError.text(statusString);
248 toggleClass(this.ui.submissionError[0], 'hidden', statusString.length === 0);
251 updateModelErrors: function() {
252 var modelIsValid = this.model.isValid();
253 var errors = !modelIsValid && this.model.validationError;
255 var errorStrings = (errors || []).map(function(error) {
256 return error.message;
258 var errorMessage = errorStrings.join('\n');
260 this.ui.modelError.text(errorMessage);
261 toggleClass(this.ui.modelError[0], 'hidden', errorMessage.length === 0);
264 updatePermissionSelect: function() {
265 var sd = this.model.get('securityDescriptor');
266 this.model.set('initialSecurityDescriptorType', sd && sd.type);
268 var permissionOpts = this.getPermissionOptions();
270 this.ui.permissionsOptionsSelect.html('');
271 permissionOpts.forEach(
273 var optionEl = $('<option></option>');
274 optionEl.text(opt.label);
275 optionEl.attr('value', opt.value);
277 optionEl.attr('selected', opt.selected);
279 optionEl.appendTo(this.ui.permissionsOptionsSelect);
284 updatePermissionOptionsIcons: function() {
285 var state = this.model.get('requestingSecurityDescriptorStatus');
286 var isSpinnerHidden = state !== requestingSecurityDescriptorStatusConstants.PENDING;
287 var isErrorIconHidden = state !== requestingSecurityDescriptorStatusConstants.ERROR;
288 toggleClass(this.ui.permissionsOptionsSpinner[0], 'hidden', isSpinnerHidden);
289 toggleClass(this.ui.permissionsOptionsErrorIcon[0], 'hidden', isErrorIconHidden);
291 var sd = this.model.get('securityDescriptor');
292 var sdType = sd && sd.type;
294 if (isSpinnerHidden && isErrorIconHidden) {
296 this.ui.permissionsOptionsGithubIcon[0],
298 sdType !== 'GH_ORG' && sdType !== 'GH_REPO' && sdType !== 'GH_USER'
301 this.ui.permissionsOptionsGitlabIcon[0],
303 !isGitlabSecurityDescriptorType(sdType)
305 toggleClass(this.ui.permissionsOptionsGitterIcon[0], 'hidden', sdType !== 'GROUP' && sdType);
309 // eslint-disable-next-line complexity
310 getPermissionOptions: function() {
311 var entity = this.model.get('entity');
312 var sd = this.model.get('securityDescriptor');
313 var permissionOpts = [];
315 if (sd && sd.type === 'GH_ORG') {
316 permissionOpts.push({
318 label: 'Any member of the ' + sd.linkPath + ' organization on GitHub',
319 selected: sd.type === 'GH_ORG'
321 } else if (sd && sd.type === 'GH_REPO') {
322 permissionOpts.push({
324 label: 'Anyone with push access to the ' + sd.linkPath + ' repo on GitHub',
325 selected: sd.type === 'GH_REPO'
327 } else if (sd && sd.type === 'GH_USER') {
328 permissionOpts.push({
330 label: `The @${sd.linkPath} user on GitHub`,
331 selected: sd.type === 'GL_PROJECT'
333 } else if (sd && sd.type === 'GL_GROUP') {
334 permissionOpts.push({
336 label: `Anyone with maintainer access to the ${sd.linkPath} group on GitLab`,
337 selected: sd.type === 'GL_GROUP'
339 } else if (sd && sd.type === 'GL_PROJECT') {
340 permissionOpts.push({
342 label: `Anyone with maintainer access to the ${sd.linkPath} project on GitLab`,
343 selected: sd.type === 'GL_PROJECT'
345 } else if (sd && sd.type === 'GL_USER') {
346 permissionOpts.push({
348 label: `The @${sd.linkPath} user on GitLab`,
349 selected: sd.type === 'GL_PROJECT'
353 var hasGitHubOpts = permissionOpts.length > 0;
355 var groupId = entity && entity.get('groupId');
356 var group = entity && entity.get('group');
358 group = this.model.groupCollection.get(groupId);
361 permissionOpts.push({
364 'Any administrator of the ' +
365 (group ? group.get('name') + ' ' : '') +
366 'community on Gitter',
367 selected: !hasGitHubOpts && sd.type === 'GROUP'
372 permissionOpts.push({
374 label: 'Only the users listed below',
375 // This accounts for undefined or null
380 return permissionOpts;
383 getBaseApiEndpointForEntity: function() {
384 var entity = this.model.get('entity');
385 var baseEntityApiUrl = '/v1/groups';
386 // TODO: Better way to tell if it is a room or how to determine associated endpoint???
387 var isRoom = entity && entity.get('groupId');
389 baseEntityApiUrl = '/v1/rooms';
392 return baseEntityApiUrl;
395 getApiEndpointForEntity: function() {
396 var entity = this.model.get('entity');
398 return urlJoin(this.getBaseApiEndpointForEntity(), entity.get('id'));
402 fetchSecurityDescriptor: function() {
404 'requestingSecurityDescriptorStatus',
405 requestingSecurityDescriptorStatusConstants.PENDING
407 var securityApiUrl = urlJoin(this.getApiEndpointForEntity(), 'security');
413 securityDescriptor: sd,
414 requestingSecurityDescriptorStatus: requestingSecurityDescriptorStatusConstants.COMPLETE
419 'requestingSecurityDescriptorStatus',
420 requestingSecurityDescriptorStatusConstants.ERROR
425 fetchAdminUsers: function() {
426 var entity = this.model.get('entity');
429 this.model.adminCollection.url = urlJoin(
430 this.getApiEndpointForEntity(),
431 'security/extraAdmins'
433 this.model.adminCollection.fetch();
437 submitNewSecurityDescriptor: function() {
438 var modelIsValid = this.model.isValid();
439 var errors = !modelIsValid && this.model.validationError;
442 this.updateModelErrors();
446 var securityApiUrl = urlJoin(this.getApiEndpointForEntity(), 'security');
447 var sd = this.model.get('securityDescriptor');
448 sd.extraAdmins = this.model.adminCollection.map(function(user) {
449 return user.get('id');
453 'submitSecurityDescriptorStatus',
454 submitSecurityDescriptorStatusConstants.PENDING
457 .put(securityApiUrl, sd)
459 .then(function(updatedSd) {
461 securityDescriptor: updatedSd,
462 submitSecurityDescriptorStatus: submitSecurityDescriptorStatusConstants.COMPLETE
471 'submitSecurityDescriptorStatus',
472 submitSecurityDescriptorStatusConstants.ERROR
478 var Modal = ModalView.extend({
479 disableAutoFocus: true,
482 'change:entity': 'onEntityChange'
485 initialize: function(options) {
486 options = options || {};
487 options.title = this.getModalTitle();
488 this.options = options;
490 options.menuItems = new Backbone.Collection(
491 this.generateMenuItems().concat(options.menuItems || [])
494 ModalView.prototype.initialize.call(this, options);
495 this.view = new PermissionsView(
496 _.extend({}, options, {
497 menuItemCollection: options.menuItems
502 onEntityChange: function() {
503 this.updateModalTitle();
504 this.updateMenuItems();
507 updateModalTitle: function() {
508 this.ui.title.text(this.getModalTitle());
511 updateMenuItems: function() {
512 this.options.menuItems.set(this.generateMenuItems(), { merge: true });
515 getModalTitle: function() {
516 var model = this.model;
517 var entity = model && model.get('entity');
519 var title = 'Community Permissions';
522 // TODO: Better way to tell if it is a room or how to determine associated endpoint???
523 var isRoom = entity && entity.get('groupId');
525 title = entity.get('name') + ' ' + (isRoom ? 'Room' : 'Community') + ' Permissions';
531 generateMenuItems: function() {
532 var model = this.model;
533 var entity = model && model.get('entity');
538 var groupId = entity.get('groupId');
541 action: 'switch-to-group-entity',
543 text: 'Edit Community Permissions',
544 className: 'modal--default__footer__link'
553 className: 'modal--default__footer__btn'
561 View: PermissionsView,