Commit 4c4fd958 authored by Bob Van Landuyt's avatar Bob Van Landuyt

Add CE-protected-tag JS

And move the EE files into `EE` folder.
parent b1479d39
export { default as ProtectedTagCreate } from './protected_tag_create';
export { default as ProtectedTagEditList } from './protected_tag_edit_list';
/* eslint-disable no-underscore-dangle, class-methods-use-this */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedTagAccessDropdown {
constructor(options) {
const {
$dropdown,
accessLevel,
accessLevelsData,
} = options;
this.options = options;
this.isAllowedToCreateDropdown = false;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.$protectedTagsContainer = $('.js-protected-tags-container');
this.usersPath = this.$protectedTagsContainer.data('users-autocomplete');
this.groupsPath = this.$protectedTagsContainer.data('groups-autocomplete');
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
if (ACCESS_LEVELS.CREATE === this.accessLevel) {
this.isAllowedToCreateDropdown = true;
this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
}
this.initDropdown();
}
initDropdown() {
const self = this;
const { onSelect, onHide } = this.options;
this.$dropdown.glDropdown({
data: this.getData.bind(this),
selectable: true,
filterable: true,
filterRemote: true,
multiSelect: this.$dropdown.hasClass('js-multiselect'),
renderRow: this.renderRow.bind(this),
toggleLabel: this.toggleLabel.bind(this),
hidden() {
if (onHide) {
onHide();
}
},
clicked: (options) => {
const { $el, e } = options;
const item = options.selectedObj;
e.preventDefault();
if ($el.is('.is-active')) {
if (self.isAllowedToCreateDropdown) {
if (item.id === self.noOneObj.id) {
self.accessLevelsData.forEach((level) => {
if (level.id !== item.id) {
self.removeSelectedItem(level);
}
});
self.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = self.$wrap.find(`.is-active.item-${item.type}[data-role-id="${self.noOneObj.id}"]`);
if ($noOne.length) {
$noOne.removeClass('is-active');
self.removeSelectedItem(self.noOneObj);
}
}
$el.addClass(`is-active item-${item.type}`);
}
self.addSelectedItem(item);
} else {
self.removeSelectedItem(item);
}
if (onSelect) {
onSelect(item, $el, this);
}
},
});
}
persistPreselectedItems() {
const itemsToPreselect = this.$dropdown.data('preselectedItems');
if (!itemsToPreselect || !itemsToPreselect.length) {
return;
}
const persistedItems = itemsToPreselect.map((item) => {
const persistedItem = Object.assign({}, item);
persistedItem.persisted = true;
return persistedItem;
});
this.setSelectedItems(persistedItems);
}
setSelectedItems(items = []) {
this.items = items;
}
getSelectedItems() {
return this.items.filter(item => !item._destroy);
}
getAllSelectedItems() {
return this.items;
}
getInputData() {
const selectedItems = this.getAllSelectedItems();
const accessLevels = selectedItems.map((item) => {
const obj = {};
if (typeof item.id !== 'undefined') {
obj.id = item.id;
}
if (typeof item._destroy !== 'undefined') {
obj._destroy = item._destroy;
}
if (item.type === LEVEL_TYPES.ROLE) {
obj.access_level = item.access_level;
} else if (item.type === LEVEL_TYPES.USER) {
obj.user_id = item.user_id;
} else if (item.type === LEVEL_TYPES.GROUP) {
obj.group_id = item.group_id;
}
return obj;
});
return accessLevels;
}
addSelectedItem(selectedItem) {
let itemToAdd = {};
// If the item already exists, just use it
let index = -1;
const selectedItems = this.getAllSelectedItems();
selectedItems.forEach((item, i) => {
if (selectedItem.id === item.access_level) {
index = i;
}
});
if (index !== -1 && selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
return;
}
itemToAdd.type = selectedItem.type;
if (selectedItem.type === LEVEL_TYPES.USER) {
itemToAdd = {
user_id: selectedItem.id,
name: selectedItem.name || '-name1',
username: selectedItem.username || '-username1',
avatar_url: selectedItem.avatar_url || '-avatar_url1',
type: LEVEL_TYPES.USER,
};
} else if (selectedItem.type === LEVEL_TYPES.ROLE) {
itemToAdd = {
access_level: selectedItem.id,
type: LEVEL_TYPES.ROLE,
};
} else if (selectedItem.type === LEVEL_TYPES.GROUP) {
itemToAdd = {
group_id: selectedItem.id,
type: LEVEL_TYPES.GROUP,
};
}
this.items.push(itemToAdd);
}
removeSelectedItem(itemToDelete) {
let index = -1;
const selectedItems = this.getAllSelectedItems();
// To find itemToDelete on selectedItems, first we need the index
selectedItems.every((item, i) => {
if (item.type !== itemToDelete.type) {
return true;
}
if (item.type === LEVEL_TYPES.USER &&
item.user_id === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.ROLE &&
item.access_level === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.GROUP &&
item.group_id === itemToDelete.id) {
index = i;
}
// Break once we have index set
return !(index > -1);
});
// if ItemToDelete is not really selected do nothing
if (index === -1) {
return;
}
if (selectedItems[index].persisted) {
// If we toggle an item that has been already marked with _destroy
if (selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
} else {
selectedItems[index]._destroy = '1';
}
} else {
selectedItems.splice(index, 1);
}
}
toggleLabel() {
const currentItems = this.getSelectedItems();
const types = _.groupBy(currentItems, item => item.type);
let label = [];
if (currentItems.length) {
label = Object.keys(LEVEL_TYPES).map((levelType) => {
const typeName = LEVEL_TYPES[levelType];
const numberOfTypes = types[typeName] ? types[typeName].length : 0;
const text = numberOfTypes === 1 ? typeName : `${typeName}s`;
return `${numberOfTypes} ${text}`;
});
} else {
label.push(this.defaultLabel);
}
this.$dropdown.find('.dropdown-toggle-text').toggleClass('is-default', !currentItems.length);
return label.join(', ');
}
getData(query, callback) {
this.getUsers(query)
.done((usersResponse) => {
if (this.groups.length) {
callback(this.consolidateData(usersResponse, this.groups));
} else {
this.getGroups(query)
.done((groupsResponse) => {
// Cache groups to avoid multiple requests
this.groups = groupsResponse;
callback(this.consolidateData(usersResponse, groupsResponse));
})
.error(() => new Flash('Failed to load groups.'));
}
})
.error(() => new Flash('Failed to load users.'));
}
consolidateData(usersResponse, groupsResponse) {
let consolidatedData = [];
const map = [];
const selectedItems = this.getSelectedItems();
// ID property is handled differently locally from the server
//
// For Groups
// In dropdown: `id`
// For submit: `group_id`
//
// For Roles
// In dropdown: `id`
// For submit: `access_level`
//
// For Users
// In dropdown: `id`
// For submit: `user_id`
/*
* Build groups
*/
const groups = groupsResponse.map(group => ({ ...group, type: LEVEL_TYPES.GROUP }));
/*
* Build roles
*/
const roles = this.accessLevelsData.map((level) => {
/* eslint-disable no-param-reassign */
// This re-assignment is intentional as
// level.type property is being used in removeSelectedItem()
// for comparision, and accessLevelsData is provided by
// gon.create_access_levels which doesn't have `type` included.
// See this discussion https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1629#note_31285823
level.type = LEVEL_TYPES.ROLE;
return level;
});
/*
* Build users
*/
const users = selectedItems.filter(item => item.type === LEVEL_TYPES.USER).map((item) => {
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + item.user_id);
return {
id: item.user_id,
name: item.name,
username: item.username,
avatar_url: item.avatar_url,
type: LEVEL_TYPES.USER,
};
});
// Has to be checked against server response
// because the selected item can be in filter results
usersResponse.forEach((response) => {
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
const user = Object.assign({}, response);
user.type = LEVEL_TYPES.USER;
users.push(user);
}
});
if (roles.length) {
consolidatedData = consolidatedData.concat([{ header: 'Roles' }], roles);
}
if (groups.length) {
if (roles.length) {
consolidatedData = consolidatedData.concat(['divider']);
}
consolidatedData = consolidatedData.concat([{ header: 'Groups' }], groups);
}
if (users.length) {
consolidatedData = consolidatedData.concat(['divider'], [{ header: 'Users' }], users);
}
return consolidatedData;
}
getUsers(query) {
return $.ajax({
dataType: 'json',
url: this.buildUrl(gon.relative_url_root, this.usersPath),
data: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
getGroups() {
return $.ajax({
dataType: 'json',
url: this.buildUrl(gon.relative_url_root, this.groupsPath),
data: {
project_id: gon.current_project_id,
},
});
}
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot !== null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
}
renderRow(item) {
let criteria = {};
let groupRowEl;
// Detect if the current item is already saved so we can add
// the `is-active` class so the item looks as marked
switch (item.type) {
case LEVEL_TYPES.USER:
criteria = { user_id: item.id };
break;
case LEVEL_TYPES.ROLE:
criteria = { access_level: item.id };
break;
case LEVEL_TYPES.GROUP:
criteria = { group_id: item.id };
break;
default:
break;
}
const isActive = _.findWhere(this.getSelectedItems(), criteria) ? 'is-active' : '';
switch (item.type) {
case LEVEL_TYPES.USER:
groupRowEl = this.userRowHtml(item, isActive);
break;
case LEVEL_TYPES.ROLE:
groupRowEl = this.roleRowHtml(item, isActive);
break;
case LEVEL_TYPES.GROUP:
groupRowEl = this.groupRowHtml(item, isActive);
break;
default:
groupRowEl = '';
break;
}
return groupRowEl;
}
userRowHtml(user, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass}">
<img src="${user.avatar_url}" class="avatar avatar-inline" width="30">
<strong class="dropdown-menu-user-full-name">${user.name}</strong>
<span class="dropdown-menu-user-username">${user.username}</span>
</a>
</li>
`;
}
groupRowHtml(group, isActive) {
const isActiveClass = isActive || '';
const avatarEl = group.avatar_url ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">` : '';
return `
<li>
<a href="#" class="${isActiveClass}">
${avatarEl}
<span class="dropdown-menu-group-groupname">${group.name}</span>
</a>
</li>
`;
}
roleRowHtml(role, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
${role.text}
</a>
</li>
`;
}
}
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
accessLevelsData: gon.create_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
});
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
onSelect: this.onSelectCallback,
});
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_tag: {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
}
});
formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
$.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData(),
})
.success(() => {
location.reload();
})
.fail(() => new Flash('Failed to protect the tag'));
}
}
export default class ProtectedTagDropdown {
/**
* @param {Object} options containing
* `$dropdown` target element
* `onSelect` event callback
* $dropdown must be an element created using `dropdown_tag()` rails helper
*/
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedTag = this.$dropdownContainer.find('.js-create-new-protected-tag');
this.buildDropdown();
this.bindEvents();
// Hide footer
this.toggleFooter(true);
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedTags.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title'],
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Tag';
},
fieldName: 'protected_tag[name]',
text(protectedTag) {
return _.escape(protectedTag.title);
},
id(protectedTag) {
return _.escape(protectedTag.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
options.e.preventDefault();
this.onSelect();
},
});
}
bindEvents() {
this.$protectedTag.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
e.preventDefault();
}
getProtectedTags(term, callback) {
if (this.selectedTag) {
callback(gon.open_tags.concat(this.selectedTag));
} else {
callback(gon.open_tags);
}
}
toggleCreateNewButton(tagName) {
if (tagName) {
this.selectedTag = {
title: tagName,
id: tagName,
text: tagName,
};
this.$dropdownContainer
.find('.js-create-new-protected-tag code')
.text(tagName);
}
this.toggleFooter(!tagName);
}
toggleFooter(toggleState) {
this.$dropdownFooter.toggleClass('hidden', toggleState);
}
}
/* eslint-disable no-new */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
$dropdown: this.$allowedToCreateDropdownButton,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
});
}
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
/* eslint-disable no-param-reassign */
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
return acc;
}, {});
return $.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: formData,
},
success: (response) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
});
},
error() {
$.scrollTo(0);
new Flash('Failed to update tag!');
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = _.findWhere(selectedItems, { user_id: currentItem.user_id });
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
}
/* eslint-disable no-new */
import ProtectedTagEdit from './protected_tag_edit';
export default class ProtectedTagEditList {
constructor() {
this.$wrap = $('.protected-tags-list');
this.initEditForm();
}
initEditForm() {
this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => {
new ProtectedTagEdit({
$wrap: $(el),
});
});
}
}
/* eslint-disable no-underscore-dangle, class-methods-use-this */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedTagAccessDropdown {
constructor(options) {
const {
$dropdown,
accessLevel,
accessLevelsData,
} = options;
this.options = options;
this.isAllowedToCreateDropdown = false;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.$protectedTagsContainer = $('.js-protected-tags-container');
this.usersPath = this.$protectedTagsContainer.data('users-autocomplete');
this.groupsPath = this.$protectedTagsContainer.data('groups-autocomplete');
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
if (ACCESS_LEVELS.CREATE === this.accessLevel) {
this.isAllowedToCreateDropdown = true;
this.noOneObj = this.accessLevelsData.find(level => level.id === ACCESS_LEVEL_NONE);
}
this.initDropdown();
}
initDropdown() {
const self = this;
const { onSelect, onHide } = this.options;
this.$dropdown.glDropdown({
data: this.getData.bind(this),
const { onSelect } = this.options;
this.options.$dropdown.glDropdown({
data: this.options.data,
selectable: true,
filterable: true,
filterRemote: true,
multiSelect: this.$dropdown.hasClass('js-multiselect'),
renderRow: this.renderRow.bind(this),
toggleLabel: this.toggleLabel.bind(this),
hidden() {
if (onHide) {
onHide();
}
},
clicked: (options) => {
const { $el, e } = options;
const item = options.selectedObj;
e.preventDefault();
inputId: this.options.$dropdown.data('input-id'),
fieldName: this.options.$dropdown.data('field-name'),
toggleLabel(item, $el) {
if ($el.is('.is-active')) {
if (self.isAllowedToCreateDropdown) {
if (item.id === self.noOneObj.id) {
self.accessLevelsData.forEach((level) => {
if (level.id !== item.id) {
self.removeSelectedItem(level);
}
});
self.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = self.$wrap.find(`.is-active.item-${item.type}[data-role-id="${self.noOneObj.id}"]`);
if ($noOne.length) {
$noOne.removeClass('is-active');
self.removeSelectedItem(self.noOneObj);
}
}
$el.addClass(`is-active item-${item.type}`);
}
self.addSelectedItem(item);
} else {
self.removeSelectedItem(item);
}
if (onSelect) {
onSelect(item, $el, this);
return item.text;
}
return 'Select';
},
});
}
persistPreselectedItems() {
const itemsToPreselect = this.$dropdown.data('preselectedItems');
if (!itemsToPreselect || !itemsToPreselect.length) {
return;
}
const persistedItems = itemsToPreselect.map((item) => {
const persistedItem = Object.assign({}, item);
persistedItem.persisted = true;
return persistedItem;
});
this.setSelectedItems(persistedItems);
}
setSelectedItems(items = []) {
this.items = items;
}
getSelectedItems() {
return this.items.filter(item => !item._destroy);
}
getAllSelectedItems() {
return this.items;
}
getInputData() {
const selectedItems = this.getAllSelectedItems();
const accessLevels = selectedItems.map((item) => {
const obj = {};
if (typeof item.id !== 'undefined') {
obj.id = item.id;
}
if (typeof item._destroy !== 'undefined') {
obj._destroy = item._destroy;
}
if (item.type === LEVEL_TYPES.ROLE) {
obj.access_level = item.access_level;
} else if (item.type === LEVEL_TYPES.USER) {
obj.user_id = item.user_id;
} else if (item.type === LEVEL_TYPES.GROUP) {
obj.group_id = item.group_id;
}
return obj;
});
return accessLevels;
}
addSelectedItem(selectedItem) {
let itemToAdd = {};
// If the item already exists, just use it
let index = -1;
const selectedItems = this.getAllSelectedItems();
selectedItems.forEach((item, i) => {
if (selectedItem.id === item.access_level) {
index = i;
}
});
if (index !== -1 && selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
return;
}
itemToAdd.type = selectedItem.type;
if (selectedItem.type === LEVEL_TYPES.USER) {
itemToAdd = {
user_id: selectedItem.id,
name: selectedItem.name || '-name1',
username: selectedItem.username || '-username1',
avatar_url: selectedItem.avatar_url || '-avatar_url1',
type: LEVEL_TYPES.USER,
};
} else if (selectedItem.type === LEVEL_TYPES.ROLE) {
itemToAdd = {
access_level: selectedItem.id,
type: LEVEL_TYPES.ROLE,
};
} else if (selectedItem.type === LEVEL_TYPES.GROUP) {
itemToAdd = {
group_id: selectedItem.id,
type: LEVEL_TYPES.GROUP,
};
}
this.items.push(itemToAdd);
}
removeSelectedItem(itemToDelete) {
let index = -1;
const selectedItems = this.getAllSelectedItems();
// To find itemToDelete on selectedItems, first we need the index
selectedItems.every((item, i) => {
if (item.type !== itemToDelete.type) {
return true;
}
if (item.type === LEVEL_TYPES.USER &&
item.user_id === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.ROLE &&
item.access_level === itemToDelete.id) {
index = i;
} else if (item.type === LEVEL_TYPES.GROUP &&
item.group_id === itemToDelete.id) {
index = i;
}
// Break once we have index set
return !(index > -1);
});
// if ItemToDelete is not really selected do nothing
if (index === -1) {
return;
}
if (selectedItems[index].persisted) {
// If we toggle an item that has been already marked with _destroy
if (selectedItems[index]._destroy) {
delete selectedItems[index]._destroy;
} else {
selectedItems[index]._destroy = '1';
}
} else {
selectedItems.splice(index, 1);
}
}
toggleLabel() {
const currentItems = this.getSelectedItems();
const types = _.groupBy(currentItems, item => item.type);
let label = [];
if (currentItems.length) {
label = Object.keys(LEVEL_TYPES).map((levelType) => {
const typeName = LEVEL_TYPES[levelType];
const numberOfTypes = types[typeName] ? types[typeName].length : 0;
const text = numberOfTypes === 1 ? typeName : `${typeName}s`;
return `${numberOfTypes} ${text}`;
});
} else {
label.push(this.defaultLabel);
}
this.$dropdown.find('.dropdown-toggle-text').toggleClass('is-default', !currentItems.length);
return label.join(', ');
}
getData(query, callback) {
this.getUsers(query)
.done((usersResponse) => {
if (this.groups.length) {
callback(this.consolidateData(usersResponse, this.groups));
} else {
this.getGroups(query)
.done((groupsResponse) => {
// Cache groups to avoid multiple requests
this.groups = groupsResponse;
callback(this.consolidateData(usersResponse, groupsResponse));
})
.error(() => new Flash('Failed to load groups.'));
}
})
.error(() => new Flash('Failed to load users.'));
}
consolidateData(usersResponse, groupsResponse) {
let consolidatedData = [];
const map = [];
const selectedItems = this.getSelectedItems();
// ID property is handled differently locally from the server
//
// For Groups
// In dropdown: `id`
// For submit: `group_id`
//
// For Roles
// In dropdown: `id`
// For submit: `access_level`
//
// For Users
// In dropdown: `id`
// For submit: `user_id`
/*
* Build groups
*/
const groups = groupsResponse.map(group => ({ ...group, type: LEVEL_TYPES.GROUP }));
/*
* Build roles
*/
const roles = this.accessLevelsData.map((level) => {
/* eslint-disable no-param-reassign */
// This re-assignment is intentional as
// level.type property is being used in removeSelectedItem()
// for comparision, and accessLevelsData is provided by
// gon.create_access_levels which doesn't have `type` included.
// See this discussion https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/1629#note_31285823
level.type = LEVEL_TYPES.ROLE;
return level;
});
/*
* Build users
*/
const users = selectedItems.filter(item => item.type === LEVEL_TYPES.USER).map((item) => {
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + item.user_id);
return {
id: item.user_id,
name: item.name,
username: item.username,
avatar_url: item.avatar_url,
type: LEVEL_TYPES.USER,
};
});
// Has to be checked against server response
// because the selected item can be in filter results
usersResponse.forEach((response) => {
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
const user = Object.assign({}, response);
user.type = LEVEL_TYPES.USER;
users.push(user);
}
});
if (roles.length) {
consolidatedData = consolidatedData.concat([{ header: 'Roles' }], roles);
}
if (groups.length) {
if (roles.length) {
consolidatedData = consolidatedData.concat(['divider']);
}
consolidatedData = consolidatedData.concat([{ header: 'Groups' }], groups);
}
if (users.length) {
consolidatedData = consolidatedData.concat(['divider'], [{ header: 'Users' }], users);
}
return consolidatedData;
}
getUsers(query) {
return $.ajax({
dataType: 'json',
url: this.buildUrl(gon.relative_url_root, this.usersPath),
data: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
clicked(options) {
options.e.preventDefault();
onSelect();
},
});
}
getGroups() {
return $.ajax({
dataType: 'json',
url: this.buildUrl(gon.relative_url_root, this.groupsPath),
data: {
project_id: gon.current_project_id,
},
});
}
buildUrl(urlRoot, url) {
let newUrl;
if (urlRoot !== null) {
newUrl = urlRoot.replace(/\/$/, '') + url;
}
return newUrl;
}
renderRow(item) {
let criteria = {};
let groupRowEl;
// Detect if the current item is already saved so we can add
// the `is-active` class so the item looks as marked
switch (item.type) {
case LEVEL_TYPES.USER:
criteria = { user_id: item.id };
break;
case LEVEL_TYPES.ROLE:
criteria = { access_level: item.id };
break;
case LEVEL_TYPES.GROUP:
criteria = { group_id: item.id };
break;
default:
break;
}
const isActive = _.findWhere(this.getSelectedItems(), criteria) ? 'is-active' : '';
switch (item.type) {
case LEVEL_TYPES.USER:
groupRowEl = this.userRowHtml(item, isActive);
break;
case LEVEL_TYPES.ROLE:
groupRowEl = this.roleRowHtml(item, isActive);
break;
case LEVEL_TYPES.GROUP:
groupRowEl = this.groupRowHtml(item, isActive);
break;
default:
groupRowEl = '';
break;
}
return groupRowEl;
}
userRowHtml(user, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass}">
<img src="${user.avatar_url}" class="avatar avatar-inline" width="30">
<strong class="dropdown-menu-user-full-name">${user.name}</strong>
<span class="dropdown-menu-user-username">${user.username}</span>
</a>
</li>
`;
}
groupRowHtml(group, isActive) {
const isActiveClass = isActive || '';
const avatarEl = group.avatar_url ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">` : '';
return `
<li>
<a href="#" class="${isActiveClass}">
${avatarEl}
<span class="dropdown-menu-group-groupname">${group.name}</span>
</a>
</li>
`;
}
roleRowHtml(role, isActive) {
const isActiveClass = isActive || '';
return `
<li>
<a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
${role.text}
</a>
</li>
`;
}
}
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
import ProtectedTagDropdown from './protected_tag_dropdown';
......@@ -8,12 +5,6 @@ export default class ProtectedTagCreate {
constructor() {
this.$form = $('.js-new-protected-tag');
this.buildDropdowns();
this.$branchTag = this.$form.find('input[name="protected_tag[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
......@@ -23,13 +14,15 @@ export default class ProtectedTagCreate {
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: $allowedToCreateDropdown,
accessLevelsData: gon.create_access_levels,
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.CREATE,
});
// Select default
$allowedToCreateDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected tag dropdown
this.protectedTagDropdown = new ProtectedTagDropdown({
$dropdown: this.$form.find('.js-protected-tag-select'),
......@@ -37,60 +30,12 @@ export default class ProtectedTagCreate {
});
}
// Enable submit button after selecting an option
// This will run after clicked callback
onSelect() {
const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const toggle = !(this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_tag: {
name: this.$form.find('input[name="protected_tag[name]"]').val(),
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
});
} else if (item.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: item.access_level,
});
} else if (item.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: item.group_id,
});
}
});
formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes;
});
return formData;
}
onFormSubmit(e) {
e.preventDefault();
// Enable submit button
const $tagInput = this.$form.find('input[name="protected_tag[name]"]');
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
$.ajax({
url: this.$form.attr('action'),
method: this.$form.attr('method'),
data: this.getFormData(),
})
.success(() => {
location.reload();
})
.fail(() => new Flash('Failed to protect the tag'));
this.$form.find('input[type="submit"]').attr('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
}
/* eslint-disable no-new */
/* global Flash */
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.onSelectCallback = this.onSelect.bind(this);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({
$dropdown: this.$allowedToCreateDropdownButton,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this),
data: gon.create_access_levels,
onSelect: this.onSelectCallback,
});
}
onSelectOption() {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) {
return;
}
onSelect() {
const $allowedToCreateInput = this.$wrap.find(`input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`);
this.hasChanges = true;
this.updatePermissions();
}
// Do not update if one dropdown has not selected any option
if (!$allowedToCreateInput.length) return;
updatePermissions() {
const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => {
/* eslint-disable no-param-reassign */
const accessLevelName = ACCESS_LEVELS[level];
const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
acc[`${accessLevelName}_attributes`] = inputData;
this.$allowedToCreateDropdownButton.disable();
return acc;
}, {});
return $.ajax({
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: formData,
},
success: (response) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
const accessLevelName = ACCESS_LEVELS[level];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
});
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
},
error() {
$.scrollTo(0);
new Flash('Failed to update tag!');
new Flash('Failed to update tag!', null, $('.js-protected-tags-list'));
},
}).always(() => {
this.$allowedToCreateDropdownButton.enable();
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
if (currentItem.user_id) {
// Do this only for users for now
// get the current data for selected items
const selectedItems = this[dropdownName].getSelectedItems();
const currentSelectedItem = _.findWhere(selectedItems, { user_id: currentItem.user_id });
return {
id: currentItem.id,
user_id: currentItem.user_id,
type: LEVEL_TYPES.USER,
persisted: true,
name: currentSelectedItem.name,
username: currentSelectedItem.username,
avatar_url: currentSelectedItem.avatar_url,
};
} else if (currentItem.group_id) {
return {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true,
};
}
return {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true,
};
});
this[dropdownName].setSelectedItems(itemsToAdd);
}
}
......@@ -62,6 +62,7 @@ var config = {
protected_branches: './protected_branches/protected_branches_bundle.js',
ee_protected_branches: './protected_branches/ee/protected_branches_bundle.js',
protected_tags: './protected_tags',
ee_protected_tags: './protected_tags/ee',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
sidebar: './sidebar/sidebar_bundle.js',
schedule_form: './pipeline_schedules/pipeline_schedule_form_bundle.js',
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment