diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-27 18:09:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-07-27 18:09:54 +0000 |
commit | 03fbe61813666e96cd9b2dec9a22ab39f358f542 (patch) | |
tree | 786d9e6ad0e3ddf56709dad4dfabe963be378ebd /app/assets | |
parent | e69aae81ead38d4740771a5c8f0f33a4f248a312 (diff) | |
download | gitlab-ce-03fbe61813666e96cd9b2dec9a22ab39f358f542.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets')
13 files changed, 870 insertions, 187 deletions
diff --git a/app/assets/javascripts/pages/projects/settings/repository/form.js b/app/assets/javascripts/pages/projects/settings/repository/form.js index 3e02893f24c..eff45bad603 100644 --- a/app/assets/javascripts/pages/projects/settings/repository/form.js +++ b/app/assets/javascripts/pages/projects/settings/repository/form.js @@ -14,7 +14,7 @@ export default () => { new ProtectedTagEditList(); initDeployKeys(); initSettingsPanels(); - new ProtectedBranchCreate(); + new ProtectedBranchCreate({ hasLicense: false }); new ProtectedBranchEditList(); new DueDateSelectors(); fileUpload('.js-choose-file', '.js-object-map-input'); diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js new file mode 100644 index 00000000000..b9d75340f8b --- /dev/null +++ b/app/assets/javascripts/projects/settings/access_dropdown.js @@ -0,0 +1,524 @@ +/* eslint-disable no-underscore-dangle, class-methods-use-this */ +import { escape, find, countBy } from 'lodash'; +import axios from '~/lib/utils/axios_utils'; +import Flash from '~/flash'; +import { n__, s__, __ } from '~/locale'; +import { LEVEL_TYPES, LEVEL_ID_PROP, 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.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 { 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 (!this.hasLicense) { + // We're not multiselecting quite yet with 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 && this.hasLicense) { + // 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.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 $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.GROUP] > 0) { + labelPieces.push(n__('1 group', '%d groups', counts[LEVEL_TYPES.GROUP])); + } + + return labelPieces.join(', '); + } + + getData(query, callback) { + if (this.hasLicense) { + 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.'))); + } else { + callback(this.consolidateData()); + } + } + + consolidateData(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` + + /* + * 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 + 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, + ); + } + } + + 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; + + // 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.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.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">${escape(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> + `; + } +} diff --git a/app/assets/javascripts/projects/settings/constants.js b/app/assets/javascripts/projects/settings/constants.js new file mode 100644 index 00000000000..fadb1f4f178 --- /dev/null +++ b/app/assets/javascripts/projects/settings/constants.js @@ -0,0 +1,13 @@ +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; diff --git a/app/assets/javascripts/protected_branches/constants.js b/app/assets/javascripts/protected_branches/constants.js new file mode 100644 index 00000000000..a17ae6811b7 --- /dev/null +++ b/app/assets/javascripts/protected_branches/constants.js @@ -0,0 +1,18 @@ +export const ACCESS_LEVELS = { + MERGE: 'merge_access_levels', + PUSH: 'push_access_levels', +}; + +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; diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js deleted file mode 100644 index 41e295387ae..00000000000 --- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js +++ /dev/null @@ -1,28 +0,0 @@ -import { __ } from '~/locale'; - -export default class ProtectedBranchAccessDropdown { - constructor(options) { - this.options = options; - this.initDropdown(); - } - - initDropdown() { - const { $dropdown, data, onSelect } = this.options; - $dropdown.glDropdown({ - data, - selectable: true, - inputId: $dropdown.data('inputId'), - fieldName: $dropdown.data('fieldName'), - toggleLabel(item, $el) { - if ($el.is('.is-active')) { - return item.text; - } - return __('Select'); - }, - clicked(options) { - options.e.preventDefault(); - onSelect(); - }, - }); - } -} diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js index 16ecd5523d6..af1d1f1b31a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_create.js +++ b/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -1,41 +1,62 @@ import $ from 'jquery'; -import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; -import CreateItemDropdown from '../create_item_dropdown'; -import AccessorUtilities from '../lib/utils/accessor'; +import AccessDropdown from '~/projects/settings/access_dropdown'; +import axios from '~/lib/utils/axios_utils'; +import AccessorUtilities from '~/lib/utils/accessor'; +import Flash from '~/flash'; +import CreateItemDropdown from '~/create_item_dropdown'; +import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; import { __ } from '~/locale'; export default class ProtectedBranchCreate { - constructor() { + constructor(options) { + this.hasLicense = options.hasLicense; + this.$form = $('.js-new-protected-branch'); this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); this.currentProjectUserDefaults = {}; this.buildDropdowns(); + this.$codeOwnerToggle = this.$form.find('.js-code-owner-toggle'); + this.bindEvents(); + } + + bindEvents() { + if (this.hasLicense) { + this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this)); + } + this.$form.on('submit', this.onFormSubmit.bind(this)); + } + + onCodeOwnerToggleClick() { + this.$codeOwnerToggle.toggleClass('is-checked'); } buildDropdowns() { const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); - const $protectedBranchDropdown = this.$form.find('.js-protected-branch-select'); // Cache callback this.onSelectCallback = this.onSelect.bind(this); // Allowed to Merge dropdown - this.protectedBranchMergeAccessDropdown = new ProtectedBranchAccessDropdown({ + this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({ $dropdown: $allowedToMergeDropdown, - data: gon.merge_access_levels, + accessLevelsData: gon.merge_access_levels, onSelect: this.onSelectCallback, + accessLevel: ACCESS_LEVELS.MERGE, + hasLicense: this.hasLicense, }); // Allowed to Push dropdown - this.protectedBranchPushAccessDropdown = new ProtectedBranchAccessDropdown({ + this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({ $dropdown: $allowedToPushDropdown, - data: gon.push_access_levels, + accessLevelsData: gon.push_access_levels, onSelect: this.onSelectCallback, + accessLevel: ACCESS_LEVELS.PUSH, + hasLicense: this.hasLicense, }); this.createItemDropdown = new CreateItemDropdown({ - $dropdown: $protectedBranchDropdown, + $dropdown: this.$form.find('.js-protected-branch-select'), defaultToggleLabel: __('Protected Branch'), fieldName: 'protected_branch[name]', onSelect: this.onSelectCallback, @@ -43,26 +64,66 @@ export default class ProtectedBranchCreate { }); } - // This will run after clicked callback + // Enable submit button after selecting an option onSelect() { - // Enable submit button - const $branchInput = this.$form.find('input[name="protected_branch[name]"]'); - const $allowedToMergeInput = this.$form.find( - 'input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]', - ); - const $allowedToPushInput = this.$form.find( - 'input[name="protected_branch[push_access_levels_attributes][0][access_level]"]', - ); - const completedForm = !( - $branchInput.val() && - $allowedToMergeInput.length && - $allowedToPushInput.length + const $allowedToMerge = this[`${ACCESS_LEVELS.MERGE}_dropdown`].getSelectedItems(); + const $allowedToPush = this[`${ACCESS_LEVELS.PUSH}_dropdown`].getSelectedItems(); + const toggle = !( + this.$form.find('input[name="protected_branch[name]"]').val() && + $allowedToMerge.length && + $allowedToPush.length ); - this.$form.find('input[type="submit"]').prop('disabled', completedForm); + this.$form.find('input[type="submit"]').attr('disabled', toggle); } static getProtectedBranches(term, callback) { callback(gon.open_branches); } + + getFormData() { + const formData = { + authenticity_token: this.$form.find('input[name="authenticity_token"]').val(), + protected_branch: { + name: this.$form.find('input[name="protected_branch[name]"]').val(), + code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'), + }, + }; + + Object.keys(ACCESS_LEVELS).forEach(level => { + const accessLevel = ACCESS_LEVELS[level]; + const selectedItems = this[`${accessLevel}_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_branch[`${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(() => Flash(__('Failed to protect the branch'))); + } } diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js index 08d8c9919dd..239bd8e543a 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -1,78 +1,165 @@ -import flash from '../flash'; -import axios from '../lib/utils/axios_utils'; -import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown'; +import { find } from 'lodash'; +import AccessDropdown from '~/projects/settings/access_dropdown'; +import axios from '~/lib/utils/axios_utils'; +import Flash from '~/flash'; +import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; import { __ } from '~/locale'; export default class ProtectedBranchEdit { constructor(options) { + this.hasLicense = options.hasLicense; + + this.$wraps = {}; + this.hasChanges = false; this.$wrap = options.$wrap; this.$allowedToMergeDropdown = this.$wrap.find('.js-allowed-to-merge'); this.$allowedToPushDropdown = this.$wrap.find('.js-allowed-to-push'); - this.onSelectCallback = this.onSelect.bind(this); + this.$codeOwnerToggle = this.$wrap.find('.js-code-owner-toggle'); + + 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(); + this.bindEvents(); + } + + bindEvents() { + if (this.hasLicense) { + this.$codeOwnerToggle.on('click', this.onCodeOwnerToggleClick.bind(this)); + } + } + + onCodeOwnerToggleClick() { + this.$codeOwnerToggle.toggleClass('is-checked'); + this.$codeOwnerToggle.prop('disabled', true); + + const formData = { + code_owner_approval_required: this.$codeOwnerToggle.hasClass('is-checked'), + }; + + this.updateCodeOwnerApproval(formData); + } + + updateCodeOwnerApproval(formData) { + axios + .patch(this.$wrap.data('url'), { + protected_branch: formData, + }) + .then(() => { + this.$codeOwnerToggle.prop('disabled', false); + }) + .catch(() => { + Flash(__('Failed to update branch!')); + }); } buildDropdowns() { // Allowed to merge dropdown - this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ + this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({ + accessLevel: ACCESS_LEVELS.MERGE, + accessLevelsData: gon.merge_access_levels, $dropdown: this.$allowedToMergeDropdown, - data: gon.merge_access_levels, - onSelect: this.onSelectCallback, + onSelect: this.onSelectOption.bind(this), + onHide: this.onDropdownHide.bind(this), + hasLicense: this.hasLicense, }); // Allowed to push dropdown - this.protectedBranchAccessDropdown = new ProtectedBranchAccessDropdown({ + this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({ + accessLevel: ACCESS_LEVELS.PUSH, + accessLevelsData: gon.push_access_levels, $dropdown: this.$allowedToPushDropdown, - data: gon.push_access_levels, - onSelect: this.onSelectCallback, + onSelect: this.onSelectOption.bind(this), + onHide: this.onDropdownHide.bind(this), + hasLicense: this.hasLicense, }); } - onSelect() { - const $allowedToMergeInput = this.$wrap.find( - `input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`, - ); - const $allowedToPushInput = this.$wrap.find( - `input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`, - ); + onSelectOption() { + this.hasChanges = true; + } - // Do not update if one dropdown has not selected any option - if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return; + onDropdownHide() { + if (!this.hasChanges) { + return; + } - this.$allowedToMergeDropdown.disable(); - this.$allowedToPushDropdown.disable(); + 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_branch: { - merge_access_levels_attributes: [ - { - id: this.$allowedToMergeDropdown.data('accessLevelId'), - access_level: $allowedToMergeInput.val(), - }, - ], - push_access_levels_attributes: [ - { - id: this.$allowedToPushDropdown.data('accessLevelId'), - access_level: $allowedToPushInput.val(), - }, - ], - }, + protected_branch: formData, }) - .then(() => { + .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`); + }); this.$allowedToMergeDropdown.enable(); this.$allowedToPushDropdown.enable(); }) .catch(() => { this.$allowedToMergeDropdown.enable(); this.$allowedToPushDropdown.enable(); - - flash( - __('Failed to update branch!'), - 'alert', - document.querySelector('.js-protected-branches-list'), - ); + Flash(__('Failed to update branch!')); }); } + + 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/app/assets/javascripts/protected_branches/protected_branch_edit_list.js b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js index 10253c0febc..6ab9a126e76 100644 --- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js +++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js @@ -13,6 +13,7 @@ export default class ProtectedBranchEditList { this.$wrap.find('.js-protected-branch-edit-form').each((i, el) => { new ProtectedBranchEdit({ $wrap: $(el), + hasLicense: false, }); }); } diff --git a/app/assets/javascripts/static_site_editor/components/app.vue b/app/assets/javascripts/static_site_editor/components/app.vue index 98240aef810..365fc7ce6e9 100644 --- a/app/assets/javascripts/static_site_editor/components/app.vue +++ b/app/assets/javascripts/static_site_editor/components/app.vue @@ -1,3 +1,13 @@ +<script> +export default { + props: { + mergeRequestsIllustrationPath: { + type: String, + required: true, + }, + }, +}; +</script> <template> - <router-view /> + <router-view :merge-requests-illustration-path="mergeRequestsIllustrationPath" /> </template> diff --git a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue b/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue deleted file mode 100644 index dd907570114..00000000000 --- a/app/assets/javascripts/static_site_editor/components/saved_changes_message.vue +++ /dev/null @@ -1,79 +0,0 @@ -<script> -import { isString } from 'lodash'; - -import { GlLink, GlButton } from '@gitlab/ui'; - -const validateUrlAndLabel = value => isString(value.label) && isString(value.url); - -export default { - components: { - GlLink, - GlButton, - }, - props: { - branch: { - type: Object, - required: true, - validator: validateUrlAndLabel, - }, - commit: { - type: Object, - required: true, - validator: validateUrlAndLabel, - }, - mergeRequest: { - type: Object, - required: true, - validator: validateUrlAndLabel, - }, - returnUrl: { - type: String, - required: false, - default: '', - }, - }, -}; -</script> - -<template> - <div> - <div class="border-bottom pb-4"> - <h3>{{ s__('StaticSiteEditor|Success!') }}</h3> - <p> - {{ - s__( - 'StaticSiteEditor|Your changes have been submitted and a merge request has been created. The changes won’t be visible on the site until the merge request has been accepted.', - ) - }} - </p> - <div class="d-flex justify-content-end"> - <gl-button v-if="returnUrl" ref="returnToSiteButton" :href="returnUrl">{{ - s__('StaticSiteEditor|Return to site') - }}</gl-button> - <gl-button ref="mergeRequestButton" class="ml-2" :href="mergeRequest.url" variant="success"> - {{ s__('StaticSiteEditor|View merge request') }} - </gl-button> - </div> - </div> - - <div class="pt-2"> - <h4>{{ s__('StaticSiteEditor|Summary of changes') }}</h4> - <ul> - <li> - {{ s__('StaticSiteEditor|You created a new branch:') }} - <gl-link ref="branchLink" :href="branch.url">{{ branch.label }}</gl-link> - </li> - <li> - {{ s__('StaticSiteEditor|You created a merge request:') }} - <gl-link ref="mergeRequestLink" :href="mergeRequest.url">{{ - mergeRequest.label - }}</gl-link> - </li> - <li> - {{ s__('StaticSiteEditor|You added a commit:') }} - <gl-link ref="commitLink" :href="commit.url">{{ commit.label }}</gl-link> - </li> - </ul> - </div> - </div> -</template> diff --git a/app/assets/javascripts/static_site_editor/index.js b/app/assets/javascripts/static_site_editor/index.js index 12aa301e02f..b7e5ea4eee3 100644 --- a/app/assets/javascripts/static_site_editor/index.js +++ b/app/assets/javascripts/static_site_editor/index.js @@ -5,7 +5,14 @@ import createRouter from './router'; import createApolloProvider from './graphql'; const initStaticSiteEditor = el => { - const { isSupportedContent, path: sourcePath, baseUrl, namespace, project } = el.dataset; + const { + isSupportedContent, + path: sourcePath, + baseUrl, + namespace, + project, + mergeRequestsIllustrationPath, + } = el.dataset; const { current_username: username } = window.gon; const returnUrl = el.dataset.returnUrl || null; @@ -26,7 +33,11 @@ const initStaticSiteEditor = el => { App, }, render(createElement) { - return createElement('app'); + return createElement('app', { + props: { + mergeRequestsIllustrationPath, + }, + }); }, }); }; diff --git a/app/assets/javascripts/static_site_editor/pages/success.vue b/app/assets/javascripts/static_site_editor/pages/success.vue index 123683b2833..f0d597d7c9b 100644 --- a/app/assets/javascripts/static_site_editor/pages/success.vue +++ b/app/assets/javascripts/static_site_editor/pages/success.vue @@ -1,12 +1,21 @@ <script> +import { GlEmptyState, GlButton } from '@gitlab/ui'; +import { s__, __, sprintf } from '~/locale'; + import savedContentMetaQuery from '../graphql/queries/saved_content_meta.query.graphql'; import appDataQuery from '../graphql/queries/app_data.query.graphql'; -import SavedChangesMessage from '../components/saved_changes_message.vue'; import { HOME_ROUTE } from '../router/constants'; export default { components: { - SavedChangesMessage, + GlEmptyState, + GlButton, + }, + props: { + mergeRequestsIllustrationPath: { + type: String, + required: true, + }, }, apollo: { savedContentMeta: { @@ -16,20 +25,65 @@ export default { query: appDataQuery, }, }, + computed: { + updatedFileDescription() { + const { sourcePath } = this.appData; + + return sprintf(s__('Update %{sourcePath} file'), { sourcePath }); + }, + }, created() { if (!this.savedContentMeta) { this.$router.push(HOME_ROUTE); } }, + title: s__('StaticSiteEditor|Your merge request has been created'), + primaryButtonText: __('View merge request'), + returnToSiteBtnText: s__('StaticSiteEditor|Return to site'), + mergeRequestInstructionsHeading: s__( + 'StaticSiteEditor|To see your changes live you will need to do the following things:', + ), + addTitleInstruction: s__('StaticSiteEditor|1. Add a clear title to describe the change.'), + addDescriptionInstruction: s__( + 'StaticSiteEditor|2. Add a description to explain why the change is being made.', + ), + assignMergeRequestInstruction: s__( + 'StaticSiteEditor|3. Assign a person to review and accept the merge request.', + ), }; </script> <template> - <div v-if="savedContentMeta" class="container"> - <saved-changes-message - :branch="savedContentMeta.branch" - :commit="savedContentMeta.commit" - :merge-request="savedContentMeta.mergeRequest" - :return-url="appData.returnUrl" - /> + <div + v-if="savedContentMeta" + class="container gl-flex-grow-1 gl-display-flex gl-flex-direction-column" + > + <div class="gl-fixed gl-left-0 gl-right-0 gl-border-b-solid gl-border-b-1 gl-border-b-gray-100"> + <div class="container gl-py-4"> + <gl-button + v-if="appData.returnUrl" + ref="returnToSiteButton" + class="gl-mr-5" + :href="appData.returnUrl" + >{{ $options.returnToSiteBtnText }}</gl-button + > + <strong> + {{ updatedFileDescription }} + </strong> + </div> + </div> + <gl-empty-state + class="gl-my-9" + :primary-button-text="$options.primaryButtonText" + :title="$options.title" + :primary-button-link="savedContentMeta.mergeRequest.url" + :svg-path="mergeRequestsIllustrationPath" + > + <template #description> + <p>{{ $options.mergeRequestInstructionsHeading }}</p> + <p>{{ $options.addTitleInstruction }}</p> + <p>{{ $options.addDescriptionInstruction }}</p> + <p>{{ $options.assignMergeRequestInstruction }}</p> + </template> + </gl-empty-state> </div> </template> diff --git a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js index 9ecae87c1a9..b70f093e930 100644 --- a/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js +++ b/app/assets/javascripts/vue_shared/components/file_icon/file_icon_map.js @@ -586,5 +586,16 @@ const fileNameIcons = { }; export default function getIconForFile(name) { - return fileNameIcons[name] || fileExtensionIcons[name ? name.split('.').pop() : ''] || ''; + return ( + fileNameIcons[name] || + fileExtensionIcons[ + name + ? name + .split('.') + .pop() + .toLowerCase() + : '' + ] || + '' + ); } |