diff options
Diffstat (limited to 'app/assets/javascripts/projects/settings')
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'); + }, }, }); }, |