diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js index 380091a35014a2bbed0e009221383f83c81766ff..f64de693188f9c95fee29963d29f671a0ee3da25 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/form.js +++ b/app/assets/javascripts/pages/projects/settings/repository/form.js @@ -10,8 +10,8 @@ import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import initSettingsPanels from '~/settings_panels'; export default () => { - new ProtectedTagCreate(); - new ProtectedTagEditList(); + new ProtectedTagCreate({ hasLicense: false }); + new ProtectedTagEditList({ hasLicense: false }); initDeployKeys(); initSettingsPanels(); new ProtectedBranchCreate({ hasLicense: false }); diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js index 52b8e7e1cd5fb909a127db3f4e60769383ba5775..71c9e58042003a8eb9ed511c397e62d57cb75b35 100644 --- a/app/assets/javascripts/projects/settings/access_dropdown.js +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -469,6 +469,14 @@ export default class AccessDropdown { } } + if (this.accessLevel === ACCESS_LEVELS.CREATE && deployKeys.length) { + consolidatedData = consolidatedData.concat( + [{ type: 'divider' }], + [{ type: 'header', content: s__('AccessDropdown|Deploy Keys') }], + deployKeys, + ); + } + return consolidatedData; } @@ -506,7 +514,10 @@ export default class AccessDropdown { break; case LEVEL_TYPES.DEPLOY_KEY: groupRowEl = - this.accessLevel === ACCESS_LEVELS.PUSH ? this.deployKeyRowHtml(item, isActive) : ''; + this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE + ? this.deployKeyRowHtml(item, isActive) + : ''; + break; case LEVEL_TYPES.GROUP: groupRowEl = this.groupRowHtml(item, isActive); diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue index 627914ae2b1195399b94580e6b5c9f806f099f04..08a1c586f69576dbf9c078f2e9b2391caeaa0b95 100644 --- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue +++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue @@ -86,7 +86,10 @@ export default { return groupBy(this.preselectedItems, 'type'); }, showDeployKeys() { - return this.accessLevel === ACCESS_LEVELS.PUSH && this.deployKeys.length; + return ( + (this.accessLevel === ACCESS_LEVELS.PUSH || this.accessLevel === ACCESS_LEVELS.CREATE) && + this.deployKeys.length + ); }, toggleLabel() { const counts = Object.entries(this.selected).reduce((acc, [key, value]) => { diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js index 9cf1afd334fb2651d1b8dfb7f4fccb69da42773f..595cbc9c99138b146fc0db75599440a4416c3927 100644 --- a/app/assets/javascripts/projects/settings/constants.js +++ b/app/assets/javascripts/projects/settings/constants.js @@ -17,6 +17,7 @@ export const LEVEL_ID_PROP = { export const ACCESS_LEVELS = { MERGE: 'merge_access_levels', PUSH: 'push_access_levels', + CREATE: 'create_access_levels', }; export const ACCESS_LEVEL_NONE = 0; diff --git a/app/assets/javascripts/protected_tags/constants.js b/app/assets/javascripts/protected_tags/constants.js index 3e71ba62877c43fe87ea7cee58284a4c06dbcc1a..758b820c4c401cf53b8862b9dd5f32ed33d5cbf0 100644 --- a/app/assets/javascripts/protected_tags/constants.js +++ b/app/assets/javascripts/protected_tags/constants.js @@ -1,3 +1,14 @@ import { s__ } from '~/locale'; export const FAILED_TO_UPDATE_TAG_MESSAGE = s__('ProjectSettings|Failed to update tag!'); + +export const ACCESS_LEVELS = { + CREATE: 'create_access_levels', +}; + +export const LEVEL_TYPES = { + ROLE: 'role', + USER: 'user', + GROUP: 'group', + DEPLOY_KEY: 'deploy_key', +}; diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js index 75fd11cd0741c47dc9b038c5a09492fd265c44d2..365b9a3b142d59d4cfd973f946c4872c58f6eb65 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_create.js +++ b/app/assets/javascripts/protected_tags/protected_tag_create.js @@ -1,12 +1,21 @@ import $ from 'jquery'; -import { __ } from '~/locale'; -import CreateItemDropdown from '../create_item_dropdown'; -import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; +import CreateItemDropdown from '~/create_item_dropdown'; +import { createAlert } from '~/alert'; +import axios from '~/lib/utils/axios_utils'; +import { s__, __ } from '~/locale'; +import AccessDropdown from '~/projects/settings/access_dropdown'; +import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; export default class ProtectedTagCreate { - constructor() { + constructor({ hasLicense }) { + this.hasLicense = hasLicense; this.$form = $('.js-new-protected-tag'); this.buildDropdowns(); + this.bindEvents(); + } + + bindEvents() { + this.$form.on('submit', this.onFormSubmit.bind(this)); } buildDropdowns() { @@ -16,15 +25,14 @@ export default class ProtectedTagCreate { this.onSelectCallback = this.onSelect.bind(this); // Allowed to Create dropdown - this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + this.protectedTagAccessDropdown = new AccessDropdown({ $dropdown: $allowedToCreateDropdown, - data: gon.create_access_levels, + accessLevelsData: gon.create_access_levels, onSelect: this.onSelectCallback, + accessLevel: ACCESS_LEVELS.CREATE, + hasLicense: this.hasLicense, }); - // Select default - $allowedToCreateDropdown.data('deprecatedJQueryDropdown').selectRowAtIndex(0); - // Protected tag dropdown this.createItemDropdown = new CreateItemDropdown({ $dropdown: this.$form.find('.js-protected-tag-select'), @@ -39,7 +47,7 @@ export default class ProtectedTagCreate { onSelect() { // Enable submit button const $tagInput = this.$form.find('input[name="protected_tag[name]"]'); - const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes'); + const $allowedToCreateInput = this.protectedTagAccessDropdown.getSelectedItems(); this.$form .find('button[type="submit"]') @@ -49,4 +57,57 @@ export default class ProtectedTagCreate { static getProtectedTags(term, callback) { callback(gon.open_tags); } + + getFormData() { + const formData = { + authenticity_token: this.$form.find('input[name="authenticity_token"]').val(), + protected_tag: { + name: this.$form.find('input[name="protected_tag[name]"]').val(), + }, + }; + + Object.keys(ACCESS_LEVELS).forEach((level) => { + const accessLevel = ACCESS_LEVELS[level]; + const selectedItems = this.protectedTagAccessDropdown.getSelectedItems(); + const levelAttributes = []; + + selectedItems.forEach((item) => { + if (item.type === LEVEL_TYPES.USER) { + levelAttributes.push({ + user_id: item.user_id, + }); + } else if (item.type === LEVEL_TYPES.ROLE) { + levelAttributes.push({ + access_level: item.access_level, + }); + } else if (item.type === LEVEL_TYPES.GROUP) { + levelAttributes.push({ + group_id: item.group_id, + }); + } else if (item.type === LEVEL_TYPES.DEPLOY_KEY) { + levelAttributes.push({ + deploy_key_id: item.deploy_key_id, + }); + } + }); + + formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes; + }); + + return formData; + } + + onFormSubmit(e) { + e.preventDefault(); + + axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData()) + .then(() => { + window.location.reload(); + }) + .catch(() => + createAlert({ + message: s__('ProjectSettings|Failed to protect the tag'), + }), + ); + } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js index 392cf00d902fdbf0a7062f01acbfe44fa245e5b1..4fa3ac3be4b4ef9e508acaa93fd222005b4c1cf8 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js @@ -1,57 +1,115 @@ +import { find } from 'lodash'; import { createAlert } from '~/alert'; -import axios from '../lib/utils/axios_utils'; -import { FAILED_TO_UPDATE_TAG_MESSAGE } from './constants'; -import ProtectedTagAccessDropdown from './protected_tag_access_dropdown'; +import axios from '~/lib/utils/axios_utils'; +import AccessDropdown from '~/projects/settings/access_dropdown'; +import { ACCESS_LEVELS, LEVEL_TYPES, FAILED_TO_UPDATE_TAG_MESSAGE } from './constants'; export default class ProtectedTagEdit { constructor(options) { + this.hasLicense = options.hasLicense; + this.hasChanges = false; this.$wrap = options.$wrap; this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); - this.onSelectCallback = this.onSelect.bind(this); + + this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest( + '.create_access_levels-container', + ); this.buildDropdowns(); } buildDropdowns() { // Allowed to create dropdown - this.protectedTagAccessDropdown = new ProtectedTagAccessDropdown({ + this.protectedTagAccessDropdown = new AccessDropdown({ + accessLevel: ACCESS_LEVELS.CREATE, + accessLevelsData: gon.create_access_levels, $dropdown: this.$allowedToCreateDropdownButton, - data: gon.create_access_levels, - onSelect: this.onSelectCallback, + onSelect: this.onSelectOption.bind(this), + onHide: this.onDropdownHide.bind(this), + hasLicense: this.hasLicense, }); } - onSelect() { - const $allowedToCreateInput = this.$wrap.find( - `input[name="${this.$allowedToCreateDropdownButton.data('fieldName')}"]`, - ); + onSelectOption() { + this.hasChanges = true; + } - // Do not update if one dropdown has not selected any option - if (!$allowedToCreateInput.length) return; + onDropdownHide() { + if (!this.hasChanges) { + return; + } - this.$allowedToCreateDropdownButton.disable(); + this.hasChanges = true; + this.updatePermissions(); + } + + updatePermissions() { + const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => { + const accessLevelName = ACCESS_LEVELS[level]; + const inputData = this.protectedTagAccessDropdown.getInputData(accessLevelName); + acc[`${accessLevelName}_attributes`] = inputData; + + return acc; + }, {}); axios .patch(this.$wrap.data('url'), { - protected_tag: { - create_access_levels_attributes: [ - { - id: this.$allowedToCreateDropdownButton.data('accessLevelId'), - access_level: $allowedToCreateInput.val(), - }, - ], - }, + protected_tag: formData, }) - .then(() => { - this.$allowedToCreateDropdownButton.enable(); + .then(({ data }) => { + this.hasChanges = false; + + Object.keys(ACCESS_LEVELS).forEach((level) => { + const accessLevelName = ACCESS_LEVELS[level]; + + // The data coming from server will be the new persisted *state* for each dropdown + this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`); + }); }) .catch(() => { - this.$allowedToCreateDropdownButton.enable(); - window.scrollTo({ top: 0, behavior: 'smooth' }); createAlert({ message: FAILED_TO_UPDATE_TAG_MESSAGE, }); }); } + + setSelectedItemsToDropdown(items = []) { + 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.protectedTagAccessDropdown.getSelectedItems(); + const currentSelectedItem = find(selectedItems, { + user_id: currentItem.user_id, + }); + + return { + id: currentItem.id, + user_id: currentItem.user_id, + type: LEVEL_TYPES.USER, + persisted: true, + name: currentSelectedItem.name, + username: currentSelectedItem.username, + avatar_url: currentSelectedItem.avatar_url, + }; + } else if (currentItem.group_id) { + return { + id: currentItem.id, + group_id: currentItem.group_id, + type: LEVEL_TYPES.GROUP, + persisted: true, + }; + } + + return { + id: currentItem.id, + access_level: currentItem.access_level, + type: LEVEL_TYPES.ROLE, + persisted: true, + }; + }); + + this.protectedTagAccessDropdown.setSelectedItems(itemsToAdd); + } } diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js index b35bf4d4606da443535e9b124b9c4821afa3eec1..8ceb970bf0367cd4da01cbf362be0d94989c2362 100644 --- a/app/assets/javascripts/protected_tags/protected_tag_edit_list.js +++ b/app/assets/javascripts/protected_tags/protected_tag_edit_list.js @@ -4,7 +4,8 @@ import $ from 'jquery'; import ProtectedTagEdit from './protected_tag_edit'; export default class ProtectedTagEditList { - constructor() { + constructor(options) { + this.hasLicense = options.hasLicense; this.$wrap = $('.protected-tags-list'); this.initEditForm(); } @@ -13,6 +14,7 @@ export default class ProtectedTagEditList { this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { new ProtectedTagEdit({ $wrap: $(el), + hasLicense: this.hasLicense, }); }); } diff --git a/app/views/projects/protected_tags/_create_protected_tag.html.haml b/app/views/projects/protected_tags/_create_protected_tag.html.haml index d19a6401fc8295b705d0603e560ce5c7682b1daa..ef3974b04b5bc916f11fc001beb2b296ff1d01de 100644 --- a/app/views/projects/protected_tags/_create_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_create_protected_tag.html.haml @@ -1,9 +1,9 @@ - content_for :create_access_levels do .create_access_levels-container = dropdown_tag('Select', - options: { toggle_class: 'js-allowed-to-create wide', + options: { toggle_class: 'js-allowed-to-create js-multiselect wide', dropdown_class: 'dropdown-menu-selectable capitalize-header', - dropdown_qa_selector: 'access_levels_content', + dropdown_qa_selector: 'access_levels_content', dropdown_testid: 'allowed-to-create-dropdown', data: { field_name: 'protected_tag[create_access_levels_attributes][0][access_level]', input_id: 'create_access_levels_attributes', qa_selector: 'access_levels_dropdown' }}) = render 'projects/protected_tags/shared/create_protected_tag' diff --git a/app/views/projects/protected_tags/_protected_tag.html.haml b/app/views/projects/protected_tags/_protected_tag.html.haml index e0912bf39c0c58867ca25188a58f87d4e4d9ae6c..68e4a5e97a361f6c62a8bd6fdaeb13dc56d65290 100644 --- a/app/views/projects/protected_tags/_protected_tag.html.haml +++ b/app/views/projects/protected_tags/_protected_tag.html.haml @@ -1,4 +1,4 @@ = render layout: 'projects/protected_tags/shared/protected_tag', locals: { protected_tag: protected_tag } do - %td - = render 'projects/protected_tags/protected_tag_create_access_levels', protected_tag: protected_tag, create_access_level: protected_tag.create_access_levels.for_role.first + %td.create_access_levels-container + = render 'projects/protected_tags/protected_tag_create_access_levels', protected_tag: protected_tag, create_access_level: protected_tag.create_access_levels.for_role = render_if_exists 'projects/protected_tags/protected_tag_extra_create_access_levels', protected_tag: protected_tag diff --git a/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml b/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml index 1d4e95651568efda4ceb634afe715bd5a3a47d32..30b9e3e9005aa3a424e08e6963f5140f82dc64c3 100644 --- a/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml +++ b/app/views/projects/protected_tags/_protected_tag_create_access_levels.haml @@ -1,8 +1,8 @@ - protected_tag = local_assigns.fetch(:protected_tag) - create_access_level = local_assigns.fetch(:create_access_level) -- dropdown_label = create_access_level&.humanize || 'Select' +- dropdown_label = create_access_level.first&.humanize || 'Select' -= hidden_field_tag "allowed_to_create_#{protected_tag.id}", create_access_level&.access_level += hidden_field_tag "allowed_to_create_#{protected_tag.id}", create_access_level.first&.access_level = dropdown_tag(dropdown_label, - options: { toggle_class: 'js-allowed-to-create', dropdown_class: 'dropdown-menu-selectable capitalize-header js-allowed-to-create-container', - data: { field_name: "allowed_to_create_#{protected_tag.id}", access_level_id: create_access_level&.id }}) + options: { toggle_class: 'js-allowed-to-create js-multiselect', dropdown_class: 'dropdown-menu-selectable capitalize-header js-allowed-to-create-container', + data: { field_name: "allowed_to_create_#{protected_tag.id}", preselected_items: access_levels_data(create_access_level) }}) diff --git a/ee/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/ee/app/assets/javascripts/pages/projects/settings/repository/show/index.js index 91ed1f344b505fbcf5abf54468de8541d0055f5f..6373c72f5b5e6f1dc8b41e6bdadf3a904d8eef1d 100644 --- a/ee/app/assets/javascripts/pages/projects/settings/repository/show/index.js +++ b/ee/app/assets/javascripts/pages/projects/settings/repository/show/index.js @@ -1,14 +1,12 @@ /* eslint-disable no-new */ import ProtectedBranchEditList from 'ee/protected_branches/protected_branch_edit_list'; -import ProtectedTagCreate from 'ee/protected_tags/protected_tag_create'; -import ProtectedTagEditList from 'ee/protected_tags/protected_tag_edit_list'; import initDatePicker from '~/behaviors/date_picker'; import initDeployKeys from '~/deploy_keys'; import fileUpload from '~/lib/utils/file_upload'; import ProtectedBranchCreate from '~/protected_branches/protected_branch_create'; import CEProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list'; -import CEProtectedTagCreate from '~/protected_tags/protected_tag_create'; -import CEProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; +import ProtectedTagCreate from '~/protected_tags/protected_tag_create'; +import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list'; import initSearchSettings from '~/search_settings'; import initSettingsPanels from '~/settings_panels'; import UserCallout from '~/user_callout'; @@ -25,14 +23,13 @@ if (document.querySelector('.js-protected-refs-for-users')) { new ProtectedBranchCreate({ hasLicense: true }); new ProtectedBranchEditList(); - new ProtectedTagCreate(); - new ProtectedTagEditList(); + new ProtectedTagCreate({ hasLicense: true }); + new ProtectedTagEditList({ hasLicense: true }); } else { new ProtectedBranchCreate({ hasLicense: false }); new CEProtectedBranchEditList(); - - new CEProtectedTagCreate(); - new CEProtectedTagEditList(); + new ProtectedTagCreate({ hasLicense: false }); + new ProtectedTagEditList({ hasLicense: false }); } const pushPullContainer = document.querySelector('.js-mirror-settings'); diff --git a/ee/app/assets/javascripts/protected_tags/protected_tag_create.js b/ee/app/assets/javascripts/protected_tags/protected_tag_create.js deleted file mode 100644 index 5f5eed1240d266751ce86da89228a61fef6ef58b..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/protected_tags/protected_tag_create.js +++ /dev/null @@ -1,107 +0,0 @@ -import $ from 'jquery'; -import CreateItemDropdown from '~/create_item_dropdown'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import { s__, __ } from '~/locale'; -import AccessDropdown from '~/projects/settings/access_dropdown'; -import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; - -export default class ProtectedTagCreate { - constructor() { - this.$form = $('.js-new-protected-tag'); - this.buildDropdowns(); - this.$tagInput = this.$form.find('input[name="protected_tag[name]"]'); - this.bindEvents(); - } - - bindEvents() { - this.$form.on('submit', this.onFormSubmit.bind(this)); - } - - buildDropdowns() { - const $allowedToCreateDropdown = this.$form.find('.js-allowed-to-create'); - - // Cache callback - this.onSelectCallback = this.onSelect.bind(this); - - // Allowed to Create dropdown - this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new AccessDropdown({ - $dropdown: $allowedToCreateDropdown, - accessLevelsData: gon.create_access_levels, - onSelect: this.onSelectCallback, - accessLevel: ACCESS_LEVELS.CREATE, - }); - - // Protected tag dropdown - this.createItemDropdown = new CreateItemDropdown({ - $dropdown: this.$form.find('.js-protected-tag-select'), - defaultToggleLabel: __('Protected Tag'), - fieldName: 'protected_tag[name]', - onSelect: this.onSelectCallback, - getData: ProtectedTagCreate.getProtectedTags, - }); - } - - // Enable submit button after selecting an option - onSelect() { - const $allowedToCreate = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems(); - const toggle = !( - this.$form.find('input[name="protected_tag[name]"]').val() && $allowedToCreate.length - ); - - this.$form.find('button[type="submit"]').attr('disabled', toggle); - } - - static getProtectedTags(term, callback) { - callback(gon.open_tags); - } - - getFormData() { - const formData = { - authenticity_token: this.$form.find('input[name="authenticity_token"]').val(), - protected_tag: { - name: this.$form.find('input[name="protected_tag[name]"]').val(), - }, - }; - - Object.keys(ACCESS_LEVELS).forEach((level) => { - const accessLevel = ACCESS_LEVELS[level]; - const selectedItems = this[`${ACCESS_LEVELS.CREATE}_dropdown`].getSelectedItems(); - const levelAttributes = []; - - selectedItems.forEach((item) => { - if (item.type === LEVEL_TYPES.USER) { - levelAttributes.push({ - user_id: item.user_id, - }); - } else if (item.type === LEVEL_TYPES.ROLE) { - levelAttributes.push({ - access_level: item.access_level, - }); - } else if (item.type === LEVEL_TYPES.GROUP) { - levelAttributes.push({ - group_id: item.group_id, - }); - } - }); - - formData.protected_tag[`${accessLevel}_attributes`] = levelAttributes; - }); - - return formData; - } - - onFormSubmit(e) { - e.preventDefault(); - - axios[this.$form.attr('method')](this.$form.attr('action'), this.getFormData()) - .then(() => { - window.location.reload(); - }) - .catch(() => - createAlert({ - message: s__('ProjectSettings|Failed to protect the tag'), - }), - ); - } -} diff --git a/ee/app/assets/javascripts/protected_tags/protected_tag_edit.js b/ee/app/assets/javascripts/protected_tags/protected_tag_edit.js deleted file mode 100644 index 133d44621ebaad410c2a06cbd333782a5464f3a3..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/protected_tags/protected_tag_edit.js +++ /dev/null @@ -1,114 +0,0 @@ -import { find } from 'lodash'; -import { createAlert } from '~/alert'; -import axios from '~/lib/utils/axios_utils'; -import AccessDropdown from '~/projects/settings/access_dropdown'; -import { FAILED_TO_UPDATE_TAG_MESSAGE } from '~/protected_tags/constants'; -import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; - -export default class ProtectedTagEdit { - constructor(options) { - this.hasChanges = false; - this.$wrap = options.$wrap; - this.$allowedToCreateDropdownButton = this.$wrap.find('.js-allowed-to-create'); - - this.$allowedToCreateDropdownContainer = this.$allowedToCreateDropdownButton.closest( - '.create_access_levels-container', - ); - - this.buildDropdowns(); - } - - buildDropdowns() { - // Allowed to create dropdown - this[`${ACCESS_LEVELS.CREATE}_dropdown`] = new AccessDropdown({ - accessLevel: ACCESS_LEVELS.CREATE, - accessLevelsData: gon.create_access_levels, - $dropdown: this.$allowedToCreateDropdownButton, - onSelect: this.onSelectOption.bind(this), - onHide: this.onDropdownHide.bind(this), - }); - } - - onSelectOption() { - this.hasChanges = true; - } - - onDropdownHide() { - if (!this.hasChanges) { - return; - } - - this.hasChanges = true; - this.updatePermissions(); - } - - updatePermissions() { - const formData = Object.keys(ACCESS_LEVELS).reduce((acc, level) => { - const accessLevelName = ACCESS_LEVELS[level]; - const inputData = this[`${accessLevelName}_dropdown`].getInputData(accessLevelName); - acc[`${accessLevelName}_attributes`] = inputData; - - return acc; - }, {}); - - axios - .patch(this.$wrap.data('url'), { - protected_tag: formData, - }) - .then(({ data }) => { - this.hasChanges = false; - - Object.keys(ACCESS_LEVELS).forEach((level) => { - const accessLevelName = ACCESS_LEVELS[level]; - - // The data coming from server will be the new persisted *state* for each dropdown - this.setSelectedItemsToDropdown(data[accessLevelName], `${accessLevelName}_dropdown`); - }); - }) - .catch(() => { - window.scrollTo({ top: 0, behavior: 'smooth' }); - createAlert({ - message: FAILED_TO_UPDATE_TAG_MESSAGE, - }); - }); - } - - setSelectedItemsToDropdown(items = [], dropdownName) { - const itemsToAdd = items.map((currentItem) => { - if (currentItem.user_id) { - // Do this only for users for now - // get the current data for selected items - const selectedItems = this[dropdownName].getSelectedItems(); - const currentSelectedItem = find(selectedItems, { - user_id: currentItem.user_id, - }); - - return { - id: currentItem.id, - user_id: currentItem.user_id, - type: LEVEL_TYPES.USER, - persisted: true, - name: currentSelectedItem.name, - username: currentSelectedItem.username, - avatar_url: currentSelectedItem.avatar_url, - }; - } else if (currentItem.group_id) { - return { - id: currentItem.id, - group_id: currentItem.group_id, - type: LEVEL_TYPES.GROUP, - persisted: true, - }; - } - - return { - id: currentItem.id, - access_level: currentItem.access_level, - type: LEVEL_TYPES.ROLE, - persisted: true, - }; - }); - - this[dropdownName].setSelectedItems(itemsToAdd); - } -} diff --git a/ee/app/assets/javascripts/protected_tags/protected_tag_edit_list.js b/ee/app/assets/javascripts/protected_tags/protected_tag_edit_list.js deleted file mode 100644 index b35bf4d4606da443535e9b124b9c4821afa3eec1..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/protected_tags/protected_tag_edit_list.js +++ /dev/null @@ -1,19 +0,0 @@ -/* eslint-disable no-new */ - -import $ from 'jquery'; -import ProtectedTagEdit from './protected_tag_edit'; - -export default class ProtectedTagEditList { - constructor() { - this.$wrap = $('.protected-tags-list'); - this.initEditForm(); - } - - initEditForm() { - this.$wrap.find('.js-protected-tag-edit-form').each((i, el) => { - new ProtectedTagEdit({ - $wrap: $(el), - }); - }); - } -} diff --git a/ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml b/ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml index 5f52b66b85accba4fae0b3b615dc96e0c375c84a..0edf2a6a4f5fe46219595e2bda5e1cbd2455b2f2 100644 --- a/ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml +++ b/ee/app/views/projects/protected_tags/ee/_create_protected_tag.html.haml @@ -3,7 +3,7 @@ = dropdown_tag('Select', options: { toggle_class: 'js-allowed-to-create js-multiselect wide', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable capitalize-header', filter: true, - dropdown_qa_selector: 'access_levels_content', + dropdown_qa_selector: 'access_levels_content', dropdown_testid: 'allowed-to-create-dropdown', data: { input_id: 'create_access_levels_attributes', default_label: 'Select', qa_selector: 'access_levels_dropdown' } }) .form-text.text-muted = s_('ProtectedBranch|You can add only groups that have this project shared. %{learn_more_link}').html_safe % { learn_more_link: link_to(_('Learn more.'), help_page_path('user/project/members/share_project_with_groups')) } diff --git a/ee/spec/features/protected_tags_spec.rb b/ee/spec/features/protected_tags_spec.rb index 0df6c4a03ebac3ded99495f03612e114f63e58c7..ee0eb0e6db335fddf82b2ac31141b3842aac525a 100644 --- a/ee/spec/features/protected_tags_spec.rb +++ b/ee/spec/features/protected_tags_spec.rb @@ -53,4 +53,14 @@ end end end + + context 'when the users for protected tags feature is on' do + before do + stub_licensed_features(protected_refs_for_users: true) + end + + include_examples 'Deploy keys with protected tags' do + let(:all_dropdown_sections) { ['Roles', 'Users', 'Deploy Keys'] } + end + end end diff --git a/spec/features/protected_tags_spec.rb b/spec/features/protected_tags_spec.rb index c2058a5c3456e77c108dcaad0e1751d477e6afcb..45315f53fd6b0b4855596da8523f1777a294a408 100644 --- a/spec/features/protected_tags_spec.rb +++ b/spec/features/protected_tags_spec.rb @@ -16,8 +16,8 @@ it "allows creating explicit protected tags" do visit project_protected_tags_path(project) set_protected_tag_name('some-tag') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect within(".protected-tags-list") { expect(page).to have_content('some-tag') } expect(ProtectedTag.count).to eq(1) @@ -30,8 +30,8 @@ visit project_protected_tags_path(project) set_protected_tag_name('some-tag') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect within(".protected-tags-list") { expect(page).to have_content(commit.id[0..7]) } end @@ -39,8 +39,8 @@ it "displays an error message if the named tag does not exist" do visit project_protected_tags_path(project) set_protected_tag_name('some-tag') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect within(".protected-tags-list") { expect(page).to have_content('tag was removed') } end @@ -50,8 +50,8 @@ it "allows creating protected tags with a wildcard" do visit project_protected_tags_path(project) set_protected_tag_name('*-stable') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect within(".protected-tags-list") { expect(page).to have_content('*-stable') } expect(ProtectedTag.count).to eq(1) @@ -64,8 +64,8 @@ visit project_protected_tags_path(project) set_protected_tag_name('*-stable') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect within(".protected-tags-list") do expect(page).to have_content("Protected tags (2)") @@ -80,8 +80,8 @@ visit project_protected_tags_path(project) set_protected_tag_name('*-stable') - set_allowed_to('create') if Gitlab.ee? - click_on "Protect" + set_allowed_to('create') + click_on_protect visit project_protected_tags_path(project) click_on "2 matching tags" @@ -101,4 +101,14 @@ include_examples "protected tags > access control > CE" end + + context 'when the users for protected tags feature is off' do + before do + stub_licensed_features(protected_refs_for_users: false) + end + + include_examples 'Deploy keys with protected tags' do + let(:all_dropdown_sections) { ['Roles', 'Deploy Keys'] } + end + end end diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js index 26297d0c3ffaa6944367dc57da3af0edcb4de4f3..667399cc979e91866683ecc32892e70cc7d0f641 100644 --- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js +++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js @@ -98,6 +98,7 @@ describe('Access Level Dropdown', () => { const findAllDropdownItems = () => findDropdown().findAllComponents(GlDropdownItem); const findAllDropdownHeaders = () => findDropdown().findAllComponents(GlDropdownSectionHeader); const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + const findDeployKeyDropdownItem = () => wrapper.findByTestId('deploy_key-dropdown-item'); const findDropdownItemWithText = (items, text) => items.filter((item) => item.text().includes(text)).at(0); @@ -142,6 +143,21 @@ describe('Access Level Dropdown', () => { it('renders dropdown item for each access level type', () => { expect(findAllDropdownItems()).toHaveLength(12); }); + + it.each` + accessLevel | shouldRenderDeployKeyItems + ${ACCESS_LEVELS.PUSH} | ${true} + ${ACCESS_LEVELS.CREATE} | ${true} + ${ACCESS_LEVELS.MERGE} | ${false} + `( + 'conditionally renders deploy keys based on access levels', + async ({ accessLevel, shouldRenderDeployKeyItems }) => { + createComponent({ accessLevel }); + await waitForPromises(); + + expect(findDeployKeyDropdownItem().exists()).toBe(shouldRenderDeployKeyItems); + }, + ); }); describe('toggleLabel', () => { diff --git a/spec/support/protected_tags/access_control_ce_shared_examples.rb b/spec/support/protected_tags/access_control_ce_shared_examples.rb index 8666c19481c998cae870245e6ebd1a0c246acc41..6aa9647bcec9caa4dd2791f2ff4218f4ae8ef15b 100644 --- a/spec/support/protected_tags/access_control_ce_shared_examples.rb +++ b/spec/support/protected_tags/access_control_ce_shared_examples.rb @@ -6,18 +6,8 @@ visit project_protected_tags_path(project) set_protected_tag_name('master') - - within('.js-new-protected-tag') do - allowed_to_create_button = find(".js-allowed-to-create") - - unless allowed_to_create_button.text == access_type_name - allowed_to_create_button.click - find('.create_access_levels-container .dropdown-menu li', match: :first) - within('.create_access_levels-container .dropdown-menu') { click_on access_type_name } - end - end - - click_on "Protect" + set_allowed_to('create', access_type_name) + click_on_protect expect(ProtectedTag.count).to eq(1) expect(ProtectedTag.last.create_access_levels.map(&:access_level)).to eq([access_type_id]) @@ -27,19 +17,12 @@ visit project_protected_tags_path(project) set_protected_tag_name('master') - - click_on "Protect" + set_allowed_to('create', 'No one') + click_on_protect expect(ProtectedTag.count).to eq(1) - within(".protected-tags-list") do - find(".js-allowed-to-create").click - - within('.js-allowed-to-create-container') do - expect(first("li")).to have_content("Roles") - click_on access_type_name - end - end + set_allowed_to('create', access_type_name, form: '.protected-tags-list') wait_for_requests diff --git a/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb new file mode 100644 index 0000000000000000000000000000000000000000..cc0984b62262de7d0663fdc401ee27230657d45c --- /dev/null +++ b/spec/support/shared_examples/features/protected_tags_with_deploy_keys_examples.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'Deploy keys with protected tags' do + let(:dropdown_sections_minus_deploy_keys) { all_dropdown_sections - ['Deploy Keys'] } + + context 'when deploy keys are enabled to this project' do + let!(:deploy_key_1) { create(:deploy_key, title: 'title 1', projects: [project]) } + let!(:deploy_key_2) { create(:deploy_key, title: 'title 2', projects: [project]) } + + context 'when only one deploy key can push' do + before do + deploy_key_1.deploy_keys_projects.first.update!(can_push: true) + end + + it "shows all dropdown sections in the 'Allowed to create' main dropdown, with only one deploy key" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + expect(page).to have_content('title 1') + expect(page).not_to have_content('title 2') + end + end + + it "shows all sections in the 'Allowed to create' update dropdown" do + create(:protected_tag, :no_one_can_create, project: project, name: 'v1.0.0') + + visit project_protected_tags_path(project) + + within(".js-protected-tag-edit-form") do + find(".js-allowed-to-create").click + wait_for_requests + + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*all_dropdown_sections) + end + end + end + + context 'when no deploy key can push' do + it "just shows all sections but not deploy keys in the 'Allowed to create' dropdown" do + visit project_protected_tags_path(project) + + find(".js-allowed-to-create").click + wait_for_requests + + within('[data-testid="allowed-to-create-dropdown"]') do + dropdown_headers = page.all('.dropdown-header').map(&:text) + + expect(dropdown_headers).to contain_exactly(*dropdown_sections_minus_deploy_keys) + end + end + end + end +end