Commit ccac30cd authored by Bob Van Landuyt's avatar Bob Van Landuyt

Move in CE-js for protected branches.

And move the EE-specifics into a separate bundle
parent a02d0af0
/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, no-underscore-dangle, no-continue, no-restricted-syntax, guard-for-in, no-new, class-methods-use-this, consistent-return, max-len */
/* global Flash */
(global => {
global.gl = global.gl || {};
const PUSH_ACCESS_LEVEL = 'push_access_levels';
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchAccessDropdown = class {
constructor(options) {
const self = this;
const {
$dropdown,
onSelect,
onHide,
accessLevel,
accessLevelsData
} = options;
this.isAllowedToPushDropdown = false;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.usersPath = '/autocomplete/users.json';
this.groupsPath = '/autocomplete/project_groups.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
if (PUSH_ACCESS_LEVEL === this.accessLevel) {
this.isAllowedToPushDropdown = true;
this.noOneObj = this.accessLevelsData[2];
}
$dropdown.glDropdown({
selectable: true,
filterable: true,
filterRemote: true,
data: this.getData.bind(this),
multiSelect: $dropdown.hasClass('js-multiselect'),
renderRow: this.renderRow.bind(this),
toggleLabel: this.toggleLabel.bind(this),
hidden() {
if (onHide) {
onHide();
}
},
clicked(opts) {
const { $el, e } = opts;
const item = opts.selectedObj;
e.preventDefault();
if ($el.is('.is-active')) {
if (self.isAllowedToPushDropdown) {
if (item.id === self.noOneObj.id) {
// remove all others selected items
self.accessLevelsData.forEach((level) => {
if (level.id !== item.id) {
self.removeSelectedItem(level);
}
});
// remove selected item visually
self.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = self.$wrap.find(`.is-active.item-${item.type}:contains('No one')`);
if ($noOne.length) {
$noOne.removeClass('is-active');
self.removeSelectedItem(self.noOneObj);
}
}
// make element active right away
$el.addClass(`is-active item-${item.type}`);
}
// Add "No one"
self.addSelectedItem(item);
} else {
self.removeSelectedItem(item);
}
if (onSelect) {
onSelect(item, $el, self);
}
}
});
}
persistPreselectedItems() {
const itemsToPreselect = this.$dropdown.data('preselectedItems');
if (typeof itemsToPreselect === 'undefined' || !itemsToPreselect.length) {
return;
}
itemsToPreselect.forEach((item) => {
item.persisted = true;
});
this.setSelectedItems(itemsToPreselect);
}
setSelectedItems(items) {
this.items = items.length ? items : [];
}
getSelectedItems() {
return this.items.filter((item) => !item._destroy);
}
getAllSelectedItems() {
return this.items;
}
// Return dropdown as input data ready to submit
getInputData() {
const accessLevels = [];
const selectedItems = this.getAllSelectedItems();
selectedItems.forEach((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;
}
accessLevels.push(obj);
});
return accessLevels;
}
addSelectedItem(selectedItem) {
let itemToAdd = {};
// If the item already exists, just use it
let index = -1;
const selectedItems = this.getAllSelectedItems();
for (let i = 0; i < selectedItems.length; i += 1) {
if (selectedItem.id === selectedItems[i].access_level) {
index = i;
continue;
}
}
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
for (let i = 0; i < selectedItems.length; i += 1) {
const currentItem = selectedItems[i];
if (currentItem.type !== itemToDelete.type) {
continue;
}
if (currentItem.type === LEVEL_TYPES.USER && currentItem.user_id === itemToDelete.id) {
index = i;
} else if (currentItem.type === LEVEL_TYPES.ROLE && currentItem.access_level === itemToDelete.id) {
index = i;
} else if (currentItem.type === LEVEL_TYPES.GROUP && currentItem.group_id === itemToDelete.id) {
index = i;
}
if (index > -1) { break; }
}
// 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);
const label = [];
if (currentItems.length) {
for (const LEVEL_TYPE in LEVEL_TYPES) {
const typeName = LEVEL_TYPES[LEVEL_TYPE];
const numberOfTypes = types[typeName] ? types[typeName].length : 0;
const text = numberOfTypes === 1 ? typeName : `${typeName}s`;
label.push(`${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 users.');
});
}
consolidateData(usersResponse, groupsResponse) {
let consolidatedData = [];
const map = [];
let roles = [];
const users = [];
let groups = [];
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
*/
groups = groupsResponse.map((group) => {
group.type = LEVEL_TYPES.GROUP;
return group;
});
/*
* Build roles
*/
roles = this.accessLevelsData.map((level) => {
level.type = LEVEL_TYPES.ROLE;
return level;
});
/*
* Build users
*/
for (let x = 0; x < selectedItems.length; x += 1) {
const current = selectedItems[x];
if (current.type !== LEVEL_TYPES.USER) { continue; }
// Collect selected users
users.push({
id: current.user_id,
name: current.name,
username: current.username,
avatar_url: current.avatar_url,
type: LEVEL_TYPES.USER,
});
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + current.user_id);
}
// Has to be checked against server response
// because the selected item can be in filter results
for (let i = 0; i < usersResponse.length; i += 1) {
const u = usersResponse[i];
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + u.id) === -1) {
u.type = LEVEL_TYPES.USER;
users.push(u);
}
}
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(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(this.groupsPath),
data: {
project_id: gon.current_project_id
}
});
}
buildUrl(url) {
if (gon.relative_url_root != null) {
url = gon.relative_url_root.replace(/\/$/, '') + url;
}
return url;
}
renderRow(item) {
let criteria = {};
// Dectect if the current item is already saved so we can add
// the `is-active` class so the item looks as marked
if (item.type === LEVEL_TYPES.USER) {
criteria = { user_id: item.id };
} else if (item.type === LEVEL_TYPES.ROLE) {
criteria = { access_level: item.id };
} else if (item.type === LEVEL_TYPES.GROUP) {
criteria = { group_id: item.id };
}
const isActive = _.findWhere(this.getSelectedItems(), criteria) ? 'is-active' : '';
if (item.type === LEVEL_TYPES.USER) {
return this.userRowHtml(item, isActive);
} else if (item.type === LEVEL_TYPES.ROLE) {
return this.roleRowHtml(item, isActive);
} else if (item.type === LEVEL_TYPES.GROUP) {
return this.groupRowHtml(item, isActive);
}
}
userRowHtml(user, isActive) {
const avatarHtml = `<img src='${user.avatar_url}' class='avatar avatar-inline' width='30'>`;
const nameHtml = `<strong class='dropdown-menu-user-full-name'>${user.name}</strong>`;
const usernameHtml = `<span class='dropdown-menu-user-username'>${user.username}</span>`;
return `<li><a href='#' class='${isActive ? 'is-active' : ''}'>${avatarHtml} ${nameHtml} ${usernameHtml}</a></li>`;
}
groupRowHtml(group, isActive) {
const avatarHtml = group.avatar_url ? `<img src='${group.avatar_url}' class='avatar avatar-inline' width='30'>` : '';
const groupnameHtml = `<span class='dropdown-menu-group-groupname'>${group.name}</span>`;
return `<li><a href='#' class='${isActive ? 'is-active' : ''}'>${avatarHtml} ${groupnameHtml}</a></li>`;
}
roleRowHtml(role, isActive) {
return `<li><a href='#' class='${isActive ? 'is-active' : ''} item-${role.type}'>${role.text}</a></li>`;
}
};
})(window);
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, guard-for-in, no-restricted-syntax, max-len */
/* global ProtectedBranchDropdown */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchCreate = class {
constructor() {
this.$wrap = this.$form = $('#new_protected_branch');
this.buildDropdowns();
this.$branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
const $allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
const $allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
// Cache callback
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.MERGE
});
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.PUSH
});
// Protected branch dropdown
new window.ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback
});
}
// Enable submit button after selecting an option
onSelect() {
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(this.$wrap.find('input[name="protected_branch[name]"]').val() && $allowedToMerge.length && $allowedToPush.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_branch: {
name: this.$wrap.find('input[name="protected_branch[name]"]').val(),
}
};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const selectedItems = this[`${ACCESS_LEVELS[ACCESS_LEVEL]}_dropdown`].getSelectedItems();
const levelAttributes = [];
for (let i = 0; i < selectedItems.length; i += 1) {
const current = selectedItems[i];
if (current.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: selectedItems[i].user_id
});
} else if (current.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: selectedItems[i].access_level
});
} else if (current.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: selectedItems[i].group_id
});
}
}
formData.protected_branch[`${ACCESS_LEVELS[ACCESS_LEVEL]}_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 branch');
});
}
};
})(window);
/* eslint-disable comma-dangle, no-unused-vars */
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
this.$dropdown = options.$dropdown;
this.$dropdownContainer = this.$dropdown.parent();
this.$dropdownFooter = this.$dropdownContainer.find('.dropdown-footer');
this.$protectedBranch = this.$dropdownContainer.find('.js-create-new-protected-branch');
this.buildDropdown();
this.bindEvents();
// Hide footer
this.$dropdownFooter.addClass('hidden');
}
buildDropdown() {
this.$dropdown.glDropdown({
data: this.getProtectedBranches.bind(this),
filterable: true,
remote: false,
search: {
fields: ['title']
},
selectable: true,
toggleLabel(selected) {
return (selected && 'id' in selected) ? selected.title : 'Protected Branch';
},
fieldName: 'protected_branch[name]',
text(protectedBranch) {
return _.escape(protectedBranch.title);
},
id(protectedBranch) {
return _.escape(protectedBranch.id);
},
onFilter: this.toggleCreateNewButton.bind(this),
clicked: (options) => {
const { $el, e } = options;
e.preventDefault();
this.onSelect();
}
});
}
bindEvents() {
this.$protectedBranch.on('click', this.onClickCreateWildcard.bind(this));
}
onClickCreateWildcard(e) {
e.preventDefault();
// Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex();
}
getProtectedBranches(term, callback) {
if (this.selectedBranch) {
callback(gon.open_branches.concat(this.selectedBranch));
} else {
callback(gon.open_branches);
}
}
toggleCreateNewButton(branchName) {
this.selectedBranch = {
title: branchName,
id: branchName,
text: branchName
};
if (branchName) {
this.$dropdownContainer
.find('.js-create-new-protected-branch code')
.text(branchName);
}
this.$dropdownFooter.toggleClass('hidden', !branchName);
}
}
window.ProtectedBranchDropdown = ProtectedBranchDropdown;
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, dot-notation, no-unused-vars, no-restricted-syntax, guard-for-in, max-len */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchEdit = class {
constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(`.${ACCESS_LEVELS.MERGE}-container`);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(`.${ACCESS_LEVELS.PUSH}-container`);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to merge dropdown
this['merge_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
});
// Allowed to push dropdown
this['push_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
});
}
onSelectOption(item, $el, dropdownInstance) {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) return;
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = {};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL];
formData[`${accessLevelName}_attributes`] = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
}
return $.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_branch: formData
},
success: (response) => {
this.hasChanges = false;
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_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 branch!');
}
}).always(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = [];
for (let i = 0; i < items.length; i += 1) {
let itemToAdd;
const currentItem = items[i];
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 });
itemToAdd = {
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) {
itemToAdd = {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true
};
} else {
itemToAdd = {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true
};
}
itemsToAdd.push(itemToAdd);
}
this[dropdownName].setSelectedItems(itemsToAdd);
}
};
})(window);
/* eslint-disable arrow-parens, no-param-reassign, no-new, comma-dangle */
(global => {
global.gl = global.gl || {};
gl.ProtectedBranchEditList = class {
constructor() {
this.$wrap = $('.protected-branches-list');
// Build edit forms
this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => {
new gl.ProtectedBranchEdit({
$wrap: $(el)
});
});
}
};
})(window);
import './protected_branch_access_dropdown';
import './protected_branch_create';
import './protected_branch_dropdown';
import './protected_branch_edit';
import './protected_branch_edit_list';
/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, no-underscore-dangle, no-continue, no-restricted-syntax, guard-for-in, no-new, class-methods-use-this, consistent-return, max-len */
/* global Flash */
/* eslint-disable arrow-parens, no-param-reassign, object-shorthand, no-else-return, comma-dangle, max-len */
(global => {
global.gl = global.gl || {};
const PUSH_ACCESS_LEVEL = 'push_access_levels';
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchAccessDropdown = class {
constructor(options) {
const self = this;
const {
$dropdown,
onSelect,
onHide,
accessLevel,
accessLevelsData
} = options;
this.isAllowedToPushDropdown = false;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.usersPath = '/autocomplete/users.json';
this.groupsPath = '/autocomplete/project_groups.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
if (PUSH_ACCESS_LEVEL === this.accessLevel) {
this.isAllowedToPushDropdown = true;
this.noOneObj = this.accessLevelsData[2];
}
const { $dropdown, data, onSelect } = options;
$dropdown.glDropdown({
data: data,
selectable: true,
filterable: true,
filterRemote: true,
data: this.getData.bind(this),
multiSelect: $dropdown.hasClass('js-multiselect'),
renderRow: this.renderRow.bind(this),
toggleLabel: this.toggleLabel.bind(this),
hidden() {
if (onHide) {
onHide();
inputId: $dropdown.data('input-id'),
fieldName: $dropdown.data('field-name'),
toggleLabel(item, el) {
if (el.is('.is-active')) {
return item.text;
} else {
return 'Select';
}
},
clicked(opts) {
const { $el, e } = opts;
const item = opts.selectedObj;
const { e } = opts;
e.preventDefault();
if ($el.is('.is-active')) {
if (self.isAllowedToPushDropdown) {
if (item.id === self.noOneObj.id) {
// remove all others selected items
self.accessLevelsData.forEach((level) => {
if (level.id !== item.id) {
self.removeSelectedItem(level);
}
});
// remove selected item visually
self.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = self.$wrap.find(`.is-active.item-${item.type}:contains('No one')`);
if ($noOne.length) {
$noOne.removeClass('is-active');
self.removeSelectedItem(self.noOneObj);
}
}
// make element active right away
$el.addClass(`is-active item-${item.type}`);
}
// Add "No one"
self.addSelectedItem(item);
} else {
self.removeSelectedItem(item);
}
if (onSelect) {
onSelect(item, $el, self);
}
}
});
}
persistPreselectedItems() {
const itemsToPreselect = this.$dropdown.data('preselectedItems');
if (typeof itemsToPreselect === 'undefined' || !itemsToPreselect.length) {
return;
}
itemsToPreselect.forEach((item) => {
item.persisted = true;
});
this.setSelectedItems(itemsToPreselect);
}
setSelectedItems(items) {
this.items = items.length ? items : [];
}
getSelectedItems() {
return this.items.filter((item) => !item._destroy);
}
getAllSelectedItems() {
return this.items;
}
// Return dropdown as input data ready to submit
getInputData() {
const accessLevels = [];
const selectedItems = this.getAllSelectedItems();
selectedItems.forEach((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;
}
accessLevels.push(obj);
});
return accessLevels;
}
addSelectedItem(selectedItem) {
let itemToAdd = {};
// If the item already exists, just use it
let index = -1;
const selectedItems = this.getAllSelectedItems();
for (let i = 0; i < selectedItems.length; i += 1) {
if (selectedItem.id === selectedItems[i].access_level) {
index = i;
continue;
}
}
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
for (let i = 0; i < selectedItems.length; i += 1) {
const currentItem = selectedItems[i];
if (currentItem.type !== itemToDelete.type) {
continue;
}
if (currentItem.type === LEVEL_TYPES.USER && currentItem.user_id === itemToDelete.id) {
index = i;
} else if (currentItem.type === LEVEL_TYPES.ROLE && currentItem.access_level === itemToDelete.id) {
index = i;
} else if (currentItem.type === LEVEL_TYPES.GROUP && currentItem.group_id === itemToDelete.id) {
index = i;
onSelect();
}
if (index > -1) { break; }
}
// 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);
const label = [];
if (currentItems.length) {
for (const LEVEL_TYPE in LEVEL_TYPES) {
const typeName = LEVEL_TYPES[LEVEL_TYPE];
const numberOfTypes = types[typeName] ? types[typeName].length : 0;
const text = numberOfTypes === 1 ? typeName : `${typeName}s`;
label.push(`${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 users.');
});
}
consolidateData(usersResponse, groupsResponse) {
let consolidatedData = [];
const map = [];
let roles = [];
const users = [];
let groups = [];
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
*/
groups = groupsResponse.map((group) => {
group.type = LEVEL_TYPES.GROUP;
return group;
});
/*
* Build roles
*/
roles = this.accessLevelsData.map((level) => {
level.type = LEVEL_TYPES.ROLE;
return level;
});
/*
* Build users
*/
for (let x = 0; x < selectedItems.length; x += 1) {
const current = selectedItems[x];
if (current.type !== LEVEL_TYPES.USER) { continue; }
// Collect selected users
users.push({
id: current.user_id,
name: current.name,
username: current.username,
avatar_url: current.avatar_url,
type: LEVEL_TYPES.USER,
});
// Save identifiers for easy-checking more later
map.push(LEVEL_TYPES.USER + current.user_id);
}
// Has to be checked against server response
// because the selected item can be in filter results
for (let i = 0; i < usersResponse.length; i += 1) {
const u = usersResponse[i];
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + u.id) === -1) {
u.type = LEVEL_TYPES.USER;
users.push(u);
}
}
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(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(this.groupsPath),
data: {
project_id: gon.current_project_id
}
});
}
buildUrl(url) {
if (gon.relative_url_root != null) {
url = gon.relative_url_root.replace(/\/$/, '') + url;
}
return url;
}
renderRow(item) {
let criteria = {};
// Dectect if the current item is already saved so we can add
// the `is-active` class so the item looks as marked
if (item.type === LEVEL_TYPES.USER) {
criteria = { user_id: item.id };
} else if (item.type === LEVEL_TYPES.ROLE) {
criteria = { access_level: item.id };
} else if (item.type === LEVEL_TYPES.GROUP) {
criteria = { group_id: item.id };
}
const isActive = _.findWhere(this.getSelectedItems(), criteria) ? 'is-active' : '';
if (item.type === LEVEL_TYPES.USER) {
return this.userRowHtml(item, isActive);
} else if (item.type === LEVEL_TYPES.ROLE) {
return this.roleRowHtml(item, isActive);
} else if (item.type === LEVEL_TYPES.GROUP) {
return this.groupRowHtml(item, isActive);
}
}
userRowHtml(user, isActive) {
const avatarHtml = `<img src='${user.avatar_url}' class='avatar avatar-inline' width='30'>`;
const nameHtml = `<strong class='dropdown-menu-user-full-name'>${user.name}</strong>`;
const usernameHtml = `<span class='dropdown-menu-user-username'>${user.username}</span>`;
return `<li><a href='#' class='${isActive ? 'is-active' : ''}'>${avatarHtml} ${nameHtml} ${usernameHtml}</a></li>`;
}
groupRowHtml(group, isActive) {
const avatarHtml = group.avatar_url ? `<img src='${group.avatar_url}' class='avatar avatar-inline' width='30'>` : '';
const groupnameHtml = `<span class='dropdown-menu-group-groupname'>${group.name}</span>`;
return `<li><a href='#' class='${isActive ? 'is-active' : ''}'>${avatarHtml} ${groupnameHtml}</a></li>`;
}
roleRowHtml(role, isActive) {
return `<li><a href='#' class='${isActive ? 'is-active' : ''} item-${role.type}'>${role.text}</a></li>`;
}
};
})(window);
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, guard-for-in, no-restricted-syntax, max-len */
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global ProtectedBranchDropdown */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchCreate = class {
constructor() {
this.$wrap = this.$form = $('#new_protected_branch');
this.buildDropdowns();
this.$branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
this.bindEvents();
}
bindEvents() {
this.$form.on('submit', this.onFormSubmit.bind(this));
}
buildDropdowns() {
......@@ -36,87 +18,38 @@
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.MERGE
data: gon.merge_access_levels,
onSelect: this.onSelectCallback
});
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new gl.ProtectedBranchAccessDropdown({
new gl.ProtectedBranchAccessDropdown({
$dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
accessLevel: ACCESS_LEVELS.PUSH
data: gon.push_access_levels,
onSelect: this.onSelectCallback
});
// Select default
$allowedToPushDropdown.data('glDropdown').selectRowAtIndex(0);
$allowedToMergeDropdown.data('glDropdown').selectRowAtIndex(0);
// Protected branch dropdown
new window.ProtectedBranchDropdown({
new ProtectedBranchDropdown({
$dropdown: this.$wrap.find('.js-protected-branch-select'),
onSelect: this.onSelectCallback
});
}
// Enable submit button after selecting an option
// This will run after clicked callback
onSelect() {
const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems();
const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems();
const toggle = !(this.$wrap.find('input[name="protected_branch[name]"]').val() && $allowedToMerge.length && $allowedToPush.length);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
getFormData() {
const formData = {
authenticity_token: this.$form.find('input[name="authenticity_token"]').val(),
protected_branch: {
name: this.$wrap.find('input[name="protected_branch[name]"]').val(),
}
};
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const selectedItems = this[`${ACCESS_LEVELS[ACCESS_LEVEL]}_dropdown`].getSelectedItems();
const levelAttributes = [];
for (let i = 0; i < selectedItems.length; i += 1) {
const current = selectedItems[i];
if (current.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: selectedItems[i].user_id
});
} else if (current.type === LEVEL_TYPES.ROLE) {
levelAttributes.push({
access_level: selectedItems[i].access_level
});
} else if (current.type === LEVEL_TYPES.GROUP) {
levelAttributes.push({
group_id: selectedItems[i].group_id
});
}
}
// Enable submit button
const $branchInput = this.$wrap.find('input[name="protected_branch[name]"]');
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
formData.protected_branch[`${ACCESS_LEVELS[ACCESS_LEVEL]}_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 branch');
});
this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
};
})(window);
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, dot-notation, no-unused-vars, no-restricted-syntax, guard-for-in, max-len */
/* eslint-disable no-new, arrow-parens, no-param-reassign, comma-dangle, max-len */
/* global Flash */
(global => {
global.gl = global.gl || {};
const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
};
const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group'
};
gl.ProtectedBranchEdit = class {
constructor(options) {
this.$wraps = {};
this.hasChanges = false;
this.$wrap = options.$wrap;
this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge');
this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push');
this.$wraps[ACCESS_LEVELS.MERGE] = this.$allowedToMergeDropdown.closest(`.${ACCESS_LEVELS.MERGE}-container`);
this.$wraps[ACCESS_LEVELS.PUSH] = this.$allowedToPushDropdown.closest(`.${ACCESS_LEVELS.PUSH}-container`);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to merge dropdown
this['merge_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
new gl.ProtectedBranchAccessDropdown({
$dropdown: this.$allowedToMergeDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
data: gon.merge_access_levels,
onSelect: this.onSelect.bind(this)
});
// Allowed to push dropdown
this['push_access_levels_dropdown'] = new gl.ProtectedBranchAccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
new gl.ProtectedBranchAccessDropdown({
$dropdown: this.$allowedToPushDropdown,
onSelect: this.onSelectOption.bind(this),
onHide: this.onDropdownHide.bind(this)
data: gon.push_access_levels,
onSelect: this.onSelect.bind(this)
});
}
onSelectOption(item, $el, dropdownInstance) {
this.hasChanges = true;
}
onDropdownHide() {
if (!this.hasChanges) return;
this.hasChanges = true;
this.updatePermissions();
}
updatePermissions() {
const formData = {};
onSelect() {
const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL];
// Do not update if one dropdown has not selected any option
if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
formData[`${accessLevelName}_attributes`] = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName);
}
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
return $.ajax({
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_branch: formData
},
success: (response) => {
this.hasChanges = false;
for (const ACCESS_LEVEL in ACCESS_LEVELS) {
const accessLevelName = ACCESS_LEVELS[ACCESS_LEVEL];
// The data coming from server will be the new persisted *state* for each dropdown
this.setSelectedItemsToDropdown(response[accessLevelName], `${accessLevelName}_dropdown`);
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val()
}],
push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val()
}]
}
},
error() {
......@@ -97,49 +65,5 @@
this.$allowedToPushDropdown.enable();
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = [];
for (let i = 0; i < items.length; i += 1) {
let itemToAdd;
const currentItem = items[i];
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 });
itemToAdd = {
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) {
itemToAdd = {
id: currentItem.id,
group_id: currentItem.group_id,
type: LEVEL_TYPES.GROUP,
persisted: true
};
} else {
itemToAdd = {
id: currentItem.id,
access_level: currentItem.access_level,
type: LEVEL_TYPES.ROLE,
persisted: true
};
}
itemsToAdd.push(itemToAdd);
}
this[dropdownName].setSelectedItems(itemsToAdd);
}
};
})(window);
- expanded = Rails.env.test?
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('protected_branches')
- if @project.feature_available?(:ref_permissions_for_users, current_user)
= webpack_bundle_tag('ee_protected_branches')
- else
= webpack_bundle_tag('protected_branches')
%section.settings
.settings-header
......
......@@ -60,6 +60,7 @@ var config = {
profile: './profile/profile_bundle.js',
prometheus_metrics: './prometheus_metrics',
protected_branches: './protected_branches/protected_branches_bundle.js',
ee_protected_branches: './protected_branches/ee/protected_branches_bundle.js',
protected_tags: './protected_tags',
service_desk: './projects/settings_service_desk/service_desk_bundle.js',
sidebar: './sidebar/sidebar_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