Commit 6bf9c407 authored by Clement Ho's avatar Clement Ho

Merge branch 'winh-protection-push-dropdowns' into 'master'

Shorten protected branch / tag access level dropdown text

Closes #5216

See merge request gitlab-org/gitlab-ee!5091
parents c79679f6 9c38fec6
......@@ -935,11 +935,6 @@ pre.light-well {
}
}
.dropdown-menu-toggle {
width: 100%;
max-width: 300px;
}
.flash-container {
padding: 0;
}
......
.panel.panel-default.protected-branches-list.js-protected-branches-list
.protected-branches-list.js-protected-branches-list
- if @protected_branches.empty?
.panel-heading
%h3.panel-title
......
.panel.panel-default.protected-tags-list.js-protected-tags-list
.protected-tags-list.js-protected-tags-list
- if @protected_tags.empty?
.panel-heading
%h3.panel-title
......
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import $ from 'jquery';
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import { n__ } from '~/locale';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedBranchAccessDropdown {
export default class AccessDropdown {
constructor(options) {
const {
$dropdown,
accessLevel,
accessLevelsData,
} = options;
const { $dropdown, accessLevel, accessLevelsData } = options;
this.options = options;
this.groups = [];
this.accessLevel = accessLevel;
this.accessLevelsData = accessLevelsData.roles;
this.$dropdown = $dropdown;
this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
this.$protectedTagsContainer = $('.js-protected-branches-container');
this.usersPath = '/autocomplete/users.json';
this.groupsPath = '/autocomplete/project_groups.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
......@@ -47,7 +42,7 @@ export default class ProtectedBranchAccessDropdown {
onHide();
}
},
clicked: (options) => {
clicked: options => {
const { $el, e } = options;
const item = options.selectedObj;
......@@ -56,7 +51,7 @@ export default class ProtectedBranchAccessDropdown {
if ($el.is('.is-active')) {
if (item.id === this.noOneObj.id) {
// remove all others selected items
this.accessLevelsData.forEach((level) => {
this.accessLevelsData.forEach(level => {
if (level.id !== item.id) {
this.removeSelectedItem(level);
}
......@@ -65,7 +60,9 @@ export default class ProtectedBranchAccessDropdown {
// remove selected item visually
this.$wrap.find(`.item-${item.type}`).removeClass('is-active');
} else {
const $noOne = this.$wrap.find(`.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`);
const $noOne = this.$wrap.find(
`.is-active.item-${item.type}[data-role-id="${this.noOneObj.id}"]`,
);
if ($noOne.length) {
$noOne.removeClass('is-active');
this.removeSelectedItem(this.noOneObj);
......@@ -86,6 +83,8 @@ export default class ProtectedBranchAccessDropdown {
}
},
});
this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
}
persistPreselectedItems() {
......@@ -95,7 +94,7 @@ export default class ProtectedBranchAccessDropdown {
return;
}
const persistedItems = itemsToPreselect.map((item) => {
const persistedItems = itemsToPreselect.map(item => {
const persistedItem = Object.assign({}, item);
persistedItem.persisted = true;
return persistedItem;
......@@ -120,7 +119,7 @@ export default class ProtectedBranchAccessDropdown {
getInputData() {
const selectedItems = this.getAllSelectedItems();
const accessLevels = selectedItems.map((item) => {
const accessLevels = selectedItems.map(item => {
const obj = {};
if (typeof item.id !== 'undefined') {
......@@ -222,14 +221,11 @@ export default class ProtectedBranchAccessDropdown {
return true;
}
if (item.type === LEVEL_TYPES.USER &&
item.user_id === itemToDelete.id) {
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) {
} 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) {
} else if (item.type === LEVEL_TYPES.GROUP && item.group_id === itemToDelete.id) {
index = i;
}
......@@ -256,34 +252,48 @@ export default class ProtectedBranchAccessDropdown {
toggleLabel() {
const currentItems = this.getSelectedItems();
const types = _.groupBy(currentItems, item => item.type);
let label = [];
const $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
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`;
if (currentItems.length === 0) {
$dropdownToggleText.addClass('is-default');
return this.defaultLabel;
}
return `${numberOfTypes} ${text}`;
});
} else {
label.push(this.defaultLabel);
$dropdownToggleText.removeClass('is-default');
if (currentItems.length === 1 && currentItems[0].type === LEVEL_TYPES.ROLE) {
const roleData = this.accessLevelsData.find(data => data.id === currentItems[0].access_level);
return roleData.text;
}
const labelPieces = [];
const counts = _.countBy(currentItems, item => item.type);
if (counts[LEVEL_TYPES.ROLE] > 0) {
labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
}
if (counts[LEVEL_TYPES.USER] > 0) {
labelPieces.push(n__('1 user', '%d users', counts[LEVEL_TYPES.USER]));
}
this.$dropdown.find('.dropdown-toggle-text').toggleClass('is-default', !currentItems.length);
if (counts[LEVEL_TYPES.GROUP] > 0) {
labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
}
return label.join(', ');
return labelPieces.join(', ');
}
getData(query, callback) {
Promise.all([
this.getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
]).then(([usersResponse, groupsResponse]) => {
])
.then(([usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
callback(this.consolidateData(usersResponse.data, groupsResponse.data));
}).catch(() => Flash('Failed to load groups & users.'));
})
.catch(() => Flash('Failed to load groups & users.'));
}
consolidateData(usersResponse, groupsResponse) {
......@@ -308,12 +318,15 @@ export default class ProtectedBranchAccessDropdown {
/*
* Build groups
*/
const groups = groupsResponse.map(group => ({ ...group, type: LEVEL_TYPES.GROUP }));
const groups = groupsResponse.map(group => ({
...group,
type: LEVEL_TYPES.GROUP,
}));
/*
* Build roles
*/
const roles = this.accessLevelsData.map((level) => {
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()
......@@ -327,7 +340,7 @@ export default class ProtectedBranchAccessDropdown {
/*
* Build users
*/
const users = selectedItems.filter(item => item.type === LEVEL_TYPES.USER).map((item) => {
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);
......@@ -342,7 +355,7 @@ export default class ProtectedBranchAccessDropdown {
// Has to be checked against server response
// because the selected item can be in filter results
usersResponse.forEach((response) => {
usersResponse.forEach(response => {
// Add is it has not been added
if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
const user = Object.assign({}, response);
......@@ -454,7 +467,9 @@ export default class ProtectedBranchAccessDropdown {
groupRowHtml(group, isActive) {
const isActiveClass = isActive || '';
const avatarEl = group.avatar_url ? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">` : '';
const avatarEl = group.avatar_url
? `<img src="${group.avatar_url}" class="avatar avatar-inline" width="30">`
: '';
return `
<li>
......
export const LEVEL_TYPES = {
ROLE: 'role',
USER: 'user',
GROUP: 'group',
};
export const LEVEL_ID_PROP = {
ROLE: 'access_level',
USER: 'user_id',
GROUP: 'group_id',
};
export const ACCESS_LEVEL_NONE = 0;
......@@ -3,8 +3,8 @@ import axios from '~/lib/utils/axios_utils';
import AccessorUtilities from '~/lib/utils/accessor';
import Flash from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
const PB_LOCAL_STORAGE_KEY = 'protected-branches-defaults';
......@@ -30,7 +30,7 @@ export default class ProtectedBranchCreate {
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Merge dropdown
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new ProtectedBranchAccessDropdown({
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToMergeDropdown,
accessLevelsData: gon.merge_access_levels,
onSelect: this.onSelectCallback,
......@@ -38,7 +38,7 @@ export default class ProtectedBranchCreate {
});
// Allowed to Push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new ProtectedBranchAccessDropdown({
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToPushDropdown,
accessLevelsData: gon.push_access_levels,
onSelect: this.onSelectCallback,
......@@ -82,12 +82,12 @@ export default class ProtectedBranchCreate {
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${accessLevel}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
selectedItems.forEach(item => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
......@@ -115,10 +115,14 @@ export default class ProtectedBranchCreate {
if (savedDefaults != null) {
this[`${ACCESS_LEVELS.MERGE}_dropdown`].setSelectedItems(savedDefaults.merge);
let updatedLabel = this[`${ACCESS_LEVELS.MERGE}_dropdown`].toggleLabel();
this[`${ACCESS_LEVELS.MERGE}_dropdown`].$dropdown.find('.dropdown-toggle-text').text(updatedLabel);
this[`${ACCESS_LEVELS.MERGE}_dropdown`].$dropdown
.find('.dropdown-toggle-text')
.text(updatedLabel);
this[`${ACCESS_LEVELS.PUSH}_dropdown`].setSelectedItems(savedDefaults.push);
updatedLabel = this[`${ACCESS_LEVELS.PUSH}_dropdown`].toggleLabel();
this[`${ACCESS_LEVELS.PUSH}_dropdown`].$dropdown.find('.dropdown-toggle-text').text(updatedLabel);
this[`${ACCESS_LEVELS.PUSH}_dropdown`].$dropdown
.find('.dropdown-toggle-text')
.text(updatedLabel);
}
}
}
......
......@@ -4,8 +4,8 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
constructor(options) {
......@@ -15,15 +15,19 @@ export default class ProtectedBranchEdit {
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.$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[`${ACCESS_LEVELS.MERGE}_dropdown`] = new ProtectedBranchAccessDropdown({
this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.MERGE,
accessLevelsData: gon.merge_access_levels,
$dropdown: this.$allowedToMergeDropdown,
......@@ -32,7 +36,7 @@ export default class ProtectedBranchEdit {
});
// Allowed to push dropdown
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new ProtectedBranchAccessDropdown({
this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.PUSH,
accessLevelsData: gon.push_access_levels,
$dropdown: this.$allowedToPushDropdown,
......@@ -64,12 +68,14 @@ export default class ProtectedBranchEdit {
return acc;
}, {});
axios.patch(this.$wrap.data('url'), {
axios
.patch(this.$wrap.data('url'), {
protected_branch: formData,
}).then(({ data }) => {
})
.then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
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
......@@ -77,7 +83,8 @@ export default class ProtectedBranchEdit {
});
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
}).catch(() => {
})
.catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
Flash('Failed to update branch!', null, $('.js-protected-branches-list'));
......@@ -85,12 +92,14 @@ export default class ProtectedBranchEdit {
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
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 });
const currentSelectedItem = _.findWhere(selectedItems, {
user_id: currentItem.user_id,
});
return {
id: currentItem.id,
......
/* eslint-disable no-underscore-dangle, class-methods-use-this */
import $ from 'jquery';
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVEL_NONE } from './constants';
export default class ProtectedTagAccessDropdown {
constructor(options) {
const {
$dropdown,
accessLevel,
accessLevelsData,
} = options;
this.options = options;
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 = '/autocomplete/users.json';
this.groupsPath = '/autocomplete/project_groups.json';
this.defaultLabel = this.$dropdown.data('defaultLabel');
this.setSelectedItems([]);
this.persistPreselectedItems();
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 (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 = {};
let index = -1;
let alreadyAdded = false;
const selectedItems = this.getAllSelectedItems();
// Compare IDs based on selectedItem.type
selectedItems.forEach((item, i) => {
let comparator;
switch (selectedItem.type) {
case LEVEL_TYPES.ROLE:
comparator = LEVEL_ID_PROP.ROLE;
// If the item already exists, just use it
if (item[comparator] === selectedItem.id) {
alreadyAdded = true;
}
break;
case LEVEL_TYPES.GROUP:
comparator = LEVEL_ID_PROP.GROUP;
break;
case LEVEL_TYPES.USER:
comparator = LEVEL_ID_PROP.USER;
break;
default:
break;
}
if (selectedItem.id === item[comparator]) {
index = i;
}
});
if (alreadyAdded) {
return;
}
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) {
Promise.all([
this.getUsers(query),
this.groupsData ? Promise.resolve(this.groupsData) : this.getGroups(),
]).then(([usersResponse, groupsResponse]) => {
this.groupsData = groupsResponse;
callback(this.consolidateData(usersResponse.data, groupsResponse.data));
}).catch(() => Flash('Failed to load groups & 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 axios.get(this.buildUrl(gon.relative_url_root, this.usersPath), {
params: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id,
push_code: true,
},
});
}
getGroups() {
return axios.get(this.buildUrl(gon.relative_url_root, this.groupsPath), {
params: {
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>
`;
}
}
import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import createFlash from '~/flash';
import CreateItemDropdown from '~/create_item_dropdown';
import { s__ } from '~/locale';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagCreate {
constructor() {
......@@ -24,7 +25,7 @@ export default class ProtectedTagCreate {
this.onSelectCallback = this.onSelect.bind(this);
// Allowed to Create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new AccessDropdown({
$dropdown: $allowedToCreateDropdown,
accessLevelsData: gon.create_access_levels,
onSelect: this.onSelectCallback,
......@@ -44,7 +45,9 @@ export default class ProtectedTagCreate {
// 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);
const toggle = !(
this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length
);
this.$form.find('input[type="submit"]').attr('disabled', toggle);
}
......@@ -61,12 +64,12 @@ export default class ProtectedTagCreate {
},
};
Object.keys(ACCESS_LEVELS).forEach((level) => {
Object.keys(ACCESS_LEVELS).forEach(level => {
const accessLevel = ACCESS_LEVELS[level];
const selectedItems = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems();
const levelAttributes = [];
selectedItems.forEach((item) => {
selectedItems.forEach(item => {
if (item.type === LEVEL_TYPES.USER) {
levelAttributes.push({
user_id: item.user_id,
......@@ -94,6 +97,7 @@ export default class ProtectedTagCreate {
axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData())
.then(() => {
location.reload();
}).catch(() => Flash('Failed to protect the tag'));
})
.catch(() => createFlash(s__('ProjectSettings|Failed to protect the tag')));
}
}
......@@ -3,9 +3,10 @@
import $ from 'jquery';
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { ACCESS_LEVELS, LEVEL_TYPES } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
constructor(options) {
......@@ -13,14 +14,16 @@ export default class ProtectedTagEdit {
this.$wrap = options.$wrap;
this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create');
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest('.create_access_levels-container');
this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest(
'.create_access_levels-container',
);
this.buildDropdowns();
}
buildDropdowns() {
// Allowed to create dropdown
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new ProtectedTagAccessDropdown({
this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new AccessDropdown({
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
$dropdown: this.$allowedToCreateDropdownButton,
......@@ -52,30 +55,35 @@ export default class ProtectedTagEdit {
return acc;
}, {});
axios.patch(this.$wrap.data('url'), {
axios
.patch(this.$wrap.data('url'), {
protected_tag: formData,
}).then(({ data }) => {
})
.then(({ data }) => {
this.hasChanges = false;
Object.keys(ACCESS_LEVELS).forEach((level) => {
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(data[accessLevelName], `${accessLevelName}_dropdown`);
});
}).catch(() => {
})
.catch(() => {
$.scrollTo(0);
Flash('Failed to update tag!');
createFlash(s__('ProjectSettings|Failed to update tag!'));
});
}
setSelectedItemsToDropdown(items = [], dropdownName) {
const itemsToAdd = items.map((currentItem) => {
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 });
const currentSelectedItem = _.findWhere(selectedItems, {
user_id: currentItem.user_id,
});
return {
id: currentItem.id,
......
- default_label = 'Select'
- dropdown_label = default_label
%div{ class: "#{input_basic_name}-container" }
- if access_levels.present?
- dropdown_label = [pluralize(level_frequencies[:role], 'role'), pluralize(level_frequencies[:user], 'user'), pluralize(level_frequencies[:group], 'group')].to_sentence
= dropdown_tag(dropdown_label, options: { toggle_class: "#{toggle_class} js-multiselect", dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { default_label: default_label, preselected_items: access_levels_data(access_levels) } })
%td
= render partial: 'projects/protected_branches/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' }
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.merge_access_levels, level_frequencies: access_level_frequencies(protected_branch.merge_access_levels), input_basic_name: 'merge_access_levels', toggle_class: 'js-allowed-to-merge' }
%td
= render partial: 'projects/protected_branches/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' }
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_branch: protected_branch, access_levels: protected_branch.push_access_levels, level_frequencies: access_level_frequencies(protected_branch.push_access_levels), input_basic_name: 'push_access_levels', toggle_class: 'js-allowed-to-push' }
- default_label = 'Select'
- dropdown_label = default_label
%div{ class: "#{input_basic_name}-container" }
- if access_levels.present?
- dropdown_label = [pluralize(level_frequencies[:role], 'role'), pluralize(level_frequencies[:user], 'user'), pluralize(level_frequencies[:group], 'group')].to_sentence
= dropdown_tag(dropdown_label, options: { toggle_class: "#{toggle_class} js-multiselect", dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true,
data: { default_label: default_label, preselected_items: access_levels_data(access_levels) } })
%td
= render partial: 'projects/protected_tags/ee/access_level_dropdown', locals: { protected_tag: protected_tag, access_levels: protected_tag.create_access_levels, level_frequencies: access_level_frequencies(protected_tag.create_access_levels), input_basic_name: 'create_access_levels', toggle_class: 'js-allowed-to-create' }
= render partial: 'projects/settings/ee/access_level_dropdown', locals: { protected_tag: protected_tag, access_levels: protected_tag.create_access_levels, level_frequencies: access_level_frequencies(protected_tag.create_access_levels), input_basic_name: 'create_access_levels', toggle_class: 'js-allowed-to-create' }
- default_label = s_('RepositorySettingsAccessLevel|Select')
%div{ class: "#{input_basic_name}-container" }
= dropdown_tag(default_label, options: { toggle_class: "#{toggle_class} js-multiselect", dropdown_class: 'dropdown-menu-user dropdown-menu-selectable', filter: true, data: { default_label: default_label, preselected_items: access_levels_data(access_levels) } })
---
title: Shorten protected branch / tag access level dropdown text
merge_request: 5091
author:
type: changed
......@@ -5,7 +5,9 @@ module EE
find(".js-allowed-to-#{operation}").click
wait_for_requests
within('.dropdown-content') do
Array(option).each { |opt| click_on(opt) }
end
find(".js-allowed-to-#{operation}").click # needed to submit form in some cases
end
......
......@@ -84,10 +84,10 @@ feature 'Protected Branches', :js do
within form do
page.within(".js-allowed-to-merge") do
expect(page.find(".dropdown-toggle-text")).to have_content("1 role, 0 users, 0 groups")
expect(page.find(".dropdown-toggle-text")).to have_content("No one")
end
page.within(".js-allowed-to-push") do
expect(page.find(".dropdown-toggle-text")).to have_content("1 role, 0 users, 0 groups")
expect(page.find(".dropdown-toggle-text")).to have_content("Developers + Masters")
end
end
end
......
......@@ -13,7 +13,9 @@ feature 'Protected Tags', :js do
find(".js-allowed-to-#{operation}").click
wait_for_requests
within('.dropdown-content') do
Array(option).each { |opt| click_on(opt) }
end
find(".js-allowed-to-#{operation}").click # needed to submit form in some cases
end
......
import $ from 'jquery';
import AccessDropdown from 'ee/projects/settings/access_dropdown';
import { LEVEL_TYPES } from 'ee/projects/settings/constants';
describe('AccessDropdown', () => {
const defaultLabel = 'dummy default label';
let dropdown;
beforeEach(() => {
setFixtures(`
<div id="dummy-dropdown">
<span class="dropdown-toggle-text"></span>
</div>
`);
const $dropdown = $('#dummy-dropdown');
$dropdown.data('defaultLabel', defaultLabel);
const options = {
$dropdown,
accessLevelsData: {
roles: [
{
id: 42,
text: 'Dummy Role',
},
],
},
};
dropdown = new AccessDropdown(options);
});
describe('toggleLabel', () => {
let $dropdownToggleText;
const dummyItems = [
{ type: LEVEL_TYPES.ROLE, access_level: 42 },
{ type: LEVEL_TYPES.USER },
{ type: LEVEL_TYPES.USER },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
{ type: LEVEL_TYPES.GROUP },
];
beforeEach(() => {
$dropdownToggleText = $('.dropdown-toggle-text');
});
it('displays number of items', () => {
dropdown.setSelectedItems(dummyItems);
$dropdownToggleText.addClass('is-default');
const label = dropdown.toggleLabel();
expect(label).toBe('1 role, 2 users, 3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
describe('without selected items', () => {
beforeEach(() => {
dropdown.setSelectedItems([]);
});
it('falls back to default label', () => {
const label = dropdown.toggleLabel();
expect(label).toBe(defaultLabel);
expect($dropdownToggleText).toHaveClass('is-default');
});
});
describe('with only role', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.ROLE));
$dropdownToggleText.addClass('is-default');
});
it('displays the role name', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('Dummy Role');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with only users', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.USER));
$dropdownToggleText.addClass('is-default');
});
it('displays number of users', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('2 users');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with only groups', () => {
beforeEach(() => {
dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.GROUP));
$dropdownToggleText.addClass('is-default');
});
it('displays number of groups', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
describe('with users and groups', () => {
beforeEach(() => {
const selectedTypes = [LEVEL_TYPES.GROUP, LEVEL_TYPES.USER];
dropdown.setSelectedItems(dummyItems.filter(item => selectedTypes.includes(item.type)));
$dropdownToggleText.addClass('is-default');
});
it('displays number of groups', () => {
const label = dropdown.toggleLabel();
expect(label).toBe('2 users, 3 groups');
expect($dropdownToggleText).not.toHaveClass('is-default');
});
});
});
});
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