Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/projects/settings')
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js611
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js16
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue126
-rw-r--r--app/assets/javascripts/projects/settings/constants.js7
-rw-r--r--app/assets/javascripts/projects/settings/init_access_dropdown.js25
5 files changed, 131 insertions, 654 deletions
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
deleted file mode 100644
index 75d72f719e5..00000000000
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ /dev/null
@@ -1,611 +0,0 @@
-/* eslint-disable no-underscore-dangle, class-methods-use-this */
-import { escape, find, countBy } from 'lodash';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { createAlert } from '~/alert';
-import { n__, s__, __, sprintf } from '~/locale';
-import { renderAvatar } from '~/helpers/avatar_helper';
-import { getUsers, getGroups, getDeployKeys } from './api/access_dropdown_api';
-import { LEVEL_TYPES, LEVEL_ID_PROP, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from './constants';
-
-export default class AccessDropdown {
- constructor(options) {
- const { $dropdown, accessLevel, accessLevelsData, hasLicense = true } = options;
- this.options = options;
- this.hasLicense = hasLicense;
- this.groups = [];
- this.accessLevel = accessLevel;
- this.accessLevelsData = accessLevelsData.roles;
- this.$dropdown = $dropdown;
- this.$wrap = this.$dropdown.closest(`.${this.accessLevel}-container`);
- 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 { onSelect, onHide } = this.options;
- initDeprecatedJQueryDropdown(this.$dropdown, {
- 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;
- const fossWithMergeAccess = !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
-
- e.preventDefault();
-
- if (fossWithMergeAccess) {
- // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
- // remove all preselected items before selecting this item
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
- this.accessLevelsData.forEach((level) => {
- this.removeSelectedItem(level);
- });
- }
-
- if ($el.is('.is-active')) {
- if (this.noOneObj) {
- if (item.id === this.noOneObj.id && !fossWithMergeAccess) {
- // remove all others selected items
- this.accessLevelsData.forEach((level) => {
- if (level.id !== item.id) {
- this.removeSelectedItem(level);
- }
- });
-
- // 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}"]`,
- );
- if ($noOne.length) {
- $noOne.removeClass('is-active');
- this.removeSelectedItem(this.noOneObj);
- }
- }
- }
-
- // make element active right away
- $el.addClass(`is-active item-${item.type}`);
-
- // Add "No one"
- this.addSelectedItem(item);
- } else {
- this.removeSelectedItem(item);
- }
-
- if (onSelect) {
- onSelect(item, $el, this);
- }
- },
- });
-
- this.$dropdown.find('.dropdown-toggle-text').text(this.toggleLabel());
- }
-
- persistPreselectedItems() {
- const itemsToPreselect = this.$dropdown.data('preselectedItems');
-
- if (!itemsToPreselect || !itemsToPreselect.length) {
- return;
- }
-
- const persistedItems = itemsToPreselect.map((item) => {
- const persistedItem = { ...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;
- }
-
- // Return dropdown as input data ready to submit
- 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.DEPLOY_KEY) {
- obj.deploy_key_id = item.deploy_key_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.DEPLOY_KEY:
- comparator = LEVEL_ID_PROP.DEPLOY_KEY;
- 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,
- };
- } else if (selectedItem.type === LEVEL_TYPES.DEPLOY_KEY) {
- itemToAdd = {
- deploy_key_id: selectedItem.id,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- }
-
- 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) ||
- (item.type === LEVEL_TYPES.ROLE && item.access_level === itemToDelete.id) ||
- (item.type === LEVEL_TYPES.DEPLOY_KEY && item.deploy_key_id === itemToDelete.id) ||
- (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 $dropdownToggleText = this.$dropdown.find('.dropdown-toggle-text');
-
- if (currentItems.length === 0) {
- $dropdownToggleText.addClass('is-default');
- return 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]));
- }
-
- if (counts[LEVEL_TYPES.DEPLOY_KEY] > 0) {
- labelPieces.push(n__('1 deploy key', '%d deploy keys', counts[LEVEL_TYPES.DEPLOY_KEY]));
- }
-
- if (counts[LEVEL_TYPES.GROUP] > 0) {
- labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP]));
- }
-
- return labelPieces.join(', ');
- }
-
- getData(query, callback) {
- if (this.hasLicense) {
- Promise.all([
- getDeployKeys(query),
- getUsers(query),
- this.groupsData ? Promise.resolve(this.groupsData) : getGroups(),
- ])
- .then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.groupsData = groupsResponse;
- callback(
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data),
- );
- })
- .catch(() => {
- createAlert({ message: __('Failed to load groups, users and deploy keys.') });
- });
- } else {
- getDeployKeys(query)
- .then((deployKeysResponse) => callback(this.consolidateData(deployKeysResponse.data)))
- .catch(() => createAlert({ message: __('Failed to load deploy keys.') }));
- }
- }
-
- consolidateData(deployKeysResponse, usersResponse = [], groupsResponse = []) {
- let consolidatedData = [];
-
- // 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`
- //
- // For Deploy Keys
- // In dropdown: `id`
- // For submit: `deploy_key_id`
-
- /*
- * 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/merge_requests/1629#note_31285823
- level.type = LEVEL_TYPES.ROLE;
- return level;
- });
-
- if (roles.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Roles') }],
- roles,
- );
- }
-
- if (this.hasLicense) {
- const map = [];
- const selectedItems = this.getSelectedItems();
- /*
- * Build groups
- */
- const groups = groupsResponse.map((group) => ({
- ...group,
- type: LEVEL_TYPES.GROUP,
- }));
-
- /*
- * 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
- if (gon.current_project_id) {
- usersResponse.forEach((response) => {
- // Add is it has not been added
- if (map.indexOf(LEVEL_TYPES.USER + response.id) === -1) {
- const user = { ...response };
- user.type = LEVEL_TYPES.USER;
- users.push(user);
- }
- });
- }
-
- if (groups.length) {
- if (roles.length) {
- consolidatedData = consolidatedData.concat([{ type: 'divider' }]);
- }
-
- consolidatedData = consolidatedData.concat(
- [{ type: 'header', content: s__('AccessDropdown|Groups') }],
- groups,
- );
- }
-
- if (users.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Users') }],
- users,
- );
- }
- }
-
- const deployKeys = deployKeysResponse.map((response) => {
- const {
- id,
- fingerprint,
- fingerprint_sha256: fingerprintSha256,
- title,
- owner: { avatar_url, name, username },
- } = response;
-
- const availableFingerprint = fingerprintSha256 || fingerprint;
- const shortFingerprint = `(${availableFingerprint.substring(0, 14)}...)`;
-
- return {
- id,
- title: title.concat(' ', shortFingerprint),
- avatar_url,
- fullname: name,
- username,
- type: LEVEL_TYPES.DEPLOY_KEY,
- };
- });
-
- if (this.accessLevel === ACCESS_LEVELS.PUSH) {
- if (deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
- }
-
- if (this.accessLevel === ACCESS_LEVELS.CREATE && deployKeys.length) {
- consolidatedData = consolidatedData.concat(
- [{ type: 'divider' }],
- [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }],
- deployKeys,
- );
- }
-
- return consolidatedData;
- }
-
- renderRow(item) {
- let criteria = {};
- let groupRowEl;
-
- // Dectect 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.DEPLOY_KEY:
- criteria = { deploy_key_id: item.id };
- break;
- case LEVEL_TYPES.GROUP:
- criteria = { group_id: item.id };
- break;
- default:
- break;
- }
-
- const isActive = find(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.DEPLOY_KEY:
- groupRowEl =
- this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE
- ? this.deployKeyRowHtml(item, isActive)
- : '';
-
- break;
- case LEVEL_TYPES.GROUP:
- groupRowEl = this.groupRowHtml(item, isActive);
- break;
- default:
- groupRowEl = '';
- break;
- }
-
- return groupRowEl;
- }
-
- userRowHtml(user, isActive) {
- const isActiveClass = isActive || '';
- const avatarEl = renderAvatar(user, {
- sizeClass: 's32',
- });
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <div class="gl-avatar-labeled">
- ${avatarEl}
- <div>
- <strong class="dropdown-menu-user-full-name">${escape(user.name)}</strong>
- <span class="gl-avatar-labeled-sublabel dropdown-menu-user-username">@${
- user.username
- }</span>
- </div>
- </div>
- </a>
- </li>
- `;
- }
-
- deployKeyRowHtml(key, isActive) {
- const isActiveClass = isActive || '';
-
- return `
- <li>
- <a href="#" class="${isActiveClass}">
- <strong>${escape(key.title)}</strong>
- <p>
- ${sprintf(
- __('Owned by %{image_tag}'),
- {
- image_tag: `<img src="${key.avatar_url}" class="avatar avatar-inline s26" width="30">`,
- },
- false,
- )}
- <strong class="dropdown-menu-user-full-name gl-display-inline">${escape(
- key.fullname,
- )}</strong>
- <span class="dropdown-menu-user-username gl-display-inline">${key.username}</span>
- </p>
- </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}">
- ${escape(role.text)}
- </a>
- </li>
- `;
- }
-}
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index df99aac6b9e..b886bf43b57 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -1,7 +1,9 @@
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { ACCESS_LEVEL_DEVELOPER_INTEGER } from '~/access_level/constants';
-const USERS_PATH = '/-/autocomplete/users.json';
const GROUPS_PATH = '/-/autocomplete/project_groups.json';
+const USERS_PATH = '/-/autocomplete/users.json';
const DEPLOY_KEYS_PATH = '/-/autocomplete/deploy_keys_with_owners.json';
const buildUrl = (urlRoot, url) => {
@@ -26,10 +28,14 @@ export const getUsers = (query, states) => {
};
export const getGroups = () => {
- return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
- params: {
- project_id: gon.current_project_id,
- },
+ if (gon.current_project_id) {
+ return Api.projectGroups(gon.current_project_id, {
+ with_shared: true,
+ shared_min_access_level: ACCESS_LEVEL_DEVELOPER_INTEGER,
+ });
+ }
+ return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH)).then(({ data }) => {
+ return data;
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index a2e4827cbfa..ca24e948f69 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -12,13 +12,14 @@ import { debounce, intersectionWith, groupBy, differenceBy, intersectionBy } fro
import { createAlert } from '~/alert';
import { __, s__, n__ } from '~/locale';
import { getUsers, getGroups, getDeployKeys } from '../api/access_dropdown_api';
-import { LEVEL_TYPES, ACCESS_LEVELS } from '../constants';
+import { LEVEL_TYPES, ACCESS_LEVELS, ACCESS_LEVEL_NONE } from '../constants';
export const i18n = {
- selectUsers: s__('ProtectedEnvironment|Select users'),
+ defaultLabel: s__('AccessDropdown|Select'),
rolesSectionHeader: s__('AccessDropdown|Roles'),
groupsSectionHeader: s__('AccessDropdown|Groups'),
usersSectionHeader: s__('AccessDropdown|Users'),
+ noRole: s__('AccessDropdown|No role'),
deployKeysSectionHeader: s__('AccessDropdown|Deploy Keys'),
ownedBy: __('Owned by %{image_tag}'),
};
@@ -51,7 +52,7 @@ export default {
label: {
type: String,
required: false,
- default: i18n.selectUsers,
+ default: i18n.defaultLabel,
},
disabled: {
type: Boolean,
@@ -68,6 +69,31 @@ export default {
required: false,
default: () => [],
},
+ toggleClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ searchEnabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ block: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ testId: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ showUsers: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -96,6 +122,9 @@ export default {
this.deployKeys.length
);
},
+ isAccessesLevelNoneSelected() {
+ return this.selected.role[0].id === ACCESS_LEVEL_NONE;
+ },
toggleLabel() {
const counts = Object.entries(this.selected).reduce((acc, [key, value]) => {
acc[key] = value.length;
@@ -115,7 +144,11 @@ export default {
const labelPieces = [];
if (counts[LEVEL_TYPES.ROLE] > 0) {
- labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ if (this.isAccessesLevelNoneSelected) {
+ labelPieces.push(this.$options.i18n.noRole);
+ } else {
+ labelPieces.push(n__('1 role', '%d roles', counts[LEVEL_TYPES.ROLE]));
+ }
}
if (counts[LEVEL_TYPES.USER] > 0) {
@@ -132,8 +165,14 @@ export default {
return labelPieces.join(', ') || this.label;
},
- toggleClass() {
- return this.toggleLabel === this.label ? 'gl-text-gray-500!' : '';
+ fossWithMergeAccess() {
+ return !this.hasLicense && this.accessLevel === ACCESS_LEVELS.MERGE;
+ },
+ dropdownToggleClass() {
+ return {
+ 'gl-text-gray-500!': this.toggleLabel === this.label,
+ [this.toggleClass]: true,
+ };
},
selection() {
return [
@@ -180,7 +219,7 @@ export default {
);
},
focusInput() {
- this.$refs.search.focusInput();
+ this.$refs.search?.focusInput();
},
getData({ initial = false } = {}) {
this.initialLoading = initial;
@@ -190,10 +229,10 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length ? Promise.resolve(this.groups) : getGroups(),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
- this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
+ this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse);
this.setSelected({ initial });
})
.catch(() =>
@@ -224,13 +263,18 @@ export default {
if (this.hasLicense) {
this.groups = groupsResponse.map((group) => ({ ...group, type: LEVEL_TYPES.GROUP }));
- this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
- id,
- name,
- username,
- avatar_url,
- type: LEVEL_TYPES.USER,
- }));
+
+ // Has to be checked against server response
+ // because the selected item can be in filter results
+ if (this.showUsers) {
+ this.users = usersResponse.map(({ id, name, username, avatar_url }) => ({
+ id,
+ name,
+ username,
+ avatar_url,
+ type: LEVEL_TYPES.USER,
+ }));
+ }
}
this.deployKeys = deployKeysResponse.map((response) => {
@@ -328,14 +372,38 @@ export default {
return [...added, ...removed, ...preserved];
},
onItemClick(item) {
- this.toggleSelection(this.selected[item.type], item);
+ this.toggleSelection(item);
this.emitUpdate();
},
- toggleSelection(arr, item) {
- const itemIndex = arr.findIndex(({ id }) => id === item.id);
- if (itemIndex > -1) {
- arr.splice(itemIndex, 1);
- } else arr.push(item);
+ toggleSelection(item) {
+ if (item.id === ACCESS_LEVEL_NONE) {
+ this.selected[LEVEL_TYPES.ROLE] = [item];
+ return;
+ }
+
+ const itemSelected = this.isSelected(item);
+ if (itemSelected) {
+ this.selected[item.type] = this.selected[item.type].filter(({ id }) => id !== item.id);
+ return;
+ }
+
+ // We're not multiselecting quite yet in "Merge" access dropdown, on FOSS:
+ // remove all preselected items before selecting this item
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/37499
+ if (this.fossWithMergeAccess) this.clearSelection();
+ else if (item.type === LEVEL_TYPES.ROLE) this.unselectNone();
+
+ this.selected[item.type].push(item);
+ },
+ unselectNone() {
+ this.selected[LEVEL_TYPES.ROLE] = this.selected[LEVEL_TYPES.ROLE].filter(
+ ({ id }) => id !== ACCESS_LEVEL_NONE,
+ );
+ },
+ clearSelection() {
+ Object.values(LEVEL_TYPES).forEach((level) => {
+ this.selected[level] = [];
+ });
},
isSelected(item) {
return this.selected[item.type].some((selected) => selected.id === item.id);
@@ -346,6 +414,10 @@ export default {
onHide() {
this.$emit('hidden', this.selection);
},
+ onShown() {
+ this.$emit('shown');
+ this.focusInput();
+ },
},
};
</script>
@@ -354,13 +426,15 @@ export default {
<gl-dropdown
:disabled="disabled || initialLoading"
:text="toggleLabel"
- class="gl-min-w-20"
- :toggle-class="toggleClass"
+ :block="block"
+ class="gl-min-w-20 gl-p-0!"
+ :toggle-class="dropdownToggleClass"
aria-labelledby="allowed-users-label"
- @shown="focusInput"
+ :data-testid="testId"
+ @shown="onShown"
@hidden="onHide"
>
- <template #header>
+ <template v-if="searchEnabled" #header>
<gl-search-box-by-type ref="search" v-model.trim="query" :is-loading="loading" />
</template>
<template v-if="roles.length">
diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js
index 595cbc9c991..37a9fe0c741 100644
--- a/app/assets/javascripts/projects/settings/constants.js
+++ b/app/assets/javascripts/projects/settings/constants.js
@@ -7,13 +7,6 @@ export const LEVEL_TYPES = {
GROUP: 'group',
};
-export const LEVEL_ID_PROP = {
- ROLE: 'access_level',
- USER: 'user_id',
- DEPLOY_KEY: 'deploy_key_id',
- GROUP: 'group_id',
-};
-
export const ACCESS_LEVELS = {
MERGE: 'merge_access_levels',
PUSH: 'push_access_levels',
diff --git a/app/assets/javascripts/projects/settings/init_access_dropdown.js b/app/assets/javascripts/projects/settings/init_access_dropdown.js
index 941efaef3bc..67afbee3854 100644
--- a/app/assets/javascripts/projects/settings/init_access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/init_access_dropdown.js
@@ -7,8 +7,8 @@ export const initAccessDropdown = (el, options) => {
return null;
}
- const { accessLevelsData, accessLevel } = options;
- const { label, disabled, preselectedItems } = el.dataset;
+ const { accessLevelsData, ...props } = options;
+ const { label, disabled, preselectedItems } = el.dataset || {};
let preselected = [];
try {
preselected = JSON.parse(preselectedItems);
@@ -18,20 +18,35 @@ export const initAccessDropdown = (el, options) => {
return new Vue({
el,
+ name: 'AccessDropdownRoot',
+ data() {
+ return { preselected };
+ },
+ methods: {
+ setPreselectedItems(items) {
+ this.preselected = items;
+ },
+ },
render(createElement) {
const vm = this;
return createElement(AccessDropdown, {
props: {
- accessLevel,
- accessLevelsData: accessLevelsData.roles,
- preselectedItems: preselected,
label,
disabled,
+ accessLevelsData: accessLevelsData.roles,
+ preselectedItems: this.preselected,
+ ...props,
},
on: {
select(selected) {
vm.$emit('select', selected);
},
+ shown() {
+ vm.$emit('shown');
+ },
+ hidden() {
+ vm.$emit('hidden');
+ },
},
});
},