diff options
author | Samantha Ming <sming@gitlab.com> | 2019-08-24 03:26:58 +0300 |
---|---|---|
committer | Samantha Ming <sming@gitlab.com> | 2019-09-14 06:03:08 +0300 |
commit | 7f3d2715e009eaf2dd7b2caf70faccb7394a5283 (patch) | |
tree | d2ebe8862ebd2c8f6f25412b694d52b85eb094b4 | |
parent | 4e5872eb4c3262849fa36acb175544eac88ee2c0 (diff) |
Add views of code owner approval
- squash 3c0eea75eb2 Add translation & consolidate bindEvents methods
- squash ee2b7846d51 Move selector to constructor & add changelog
Add translation & consolidate bindEvents methods
- Translation for protech branch table & form
- Move method under bindEvents and extract callback to
separate method
Move selector to constructor & add changelog
- move toggle button selector
- remove unused $ import
- add changelog
7 files changed, 365 insertions, 12 deletions
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/projects/protected_branches/shared/_branches_list.html.haml index 7615d60ba27..f41e20e873d 100644 --- a/app/views/projects/protected_branches/shared/_branches_list.html.haml +++ b/app/views/projects/protected_branches/shared/_branches_list.html.haml @@ -1,9 +1,9 @@ .protected-branches-list.js-protected-branches-list.qa-protected-branches-list - if @protected_branches.empty? .card-header.bg-white - Protected branch (#{@protected_branches_count}) + = (s_("ProtectedBranch|Protected branch (%{protected_branches_count})") % { protected_branches_count: @protected_branches_count }).html_safe %p.settings-message.text-center - There are currently no protected branches, protect a branch with the form above. + = s_("ProtectedBranch|There are currently no protected branches, protect a branch with the form above.") - else %table.table.table-bordered %colgroup diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml index 3644a623d2c..b1d8d423e90 100644 --- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml @@ -2,7 +2,7 @@ %input{ type: 'hidden', name: 'update_section', value: 'js-protected-branches-settings' } .card .card-header - Protect a branch + = s_("ProtectedBranch|Protect a branch") .card-body = form_errors(@protected_branch) .form-group.row @@ -11,22 +11,32 @@ .col-md-10 = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f } .form-text.text-muted - = link_to 'Wildcards', help_page_path('user/project/protected_branches', anchor: 'wildcard-protected-branches') - such as - %code *-stable - or - %code production/* - are supported + - wildcards_url = help_page_url('user/project/protected_branches', anchor: 'wildcard-protected-branches') + - wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url } + = (s_("ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported") % { wildcards_link_start: wildcards_link_start, wildcards_link_end: '</a>', code_tag_start: '<code>', code_tag_end: '</code>' }).html_safe .form-group.row %label.col-md-2.text-right{ for: 'merge_access_levels_attributes' } - Allowed to merge: + = s_("ProtectedBranch|Allowed to merge:") .col-md-10 = yield :merge_access_levels .form-group.row %label.col-md-2.text-right{ for: 'push_access_levels_attributes' } - Allowed to push: + = s_("ProtectedBranch|Allowed to push:") .col-md-10 = yield :push_access_levels + - if @project.merge_requests_require_code_owner_approval + .form-group.row + %label.col-md-2.text-right{ for: 'code_owner_approval_required' } + = s_("ProtectedBranch|Require approval from code owners:") + .col-md-10 + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle is-checked", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + .form-text.text-muted + = s_("ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected") .card-footer - = f.submit 'Protect', class: 'btn-success btn', disabled: true + = f.submit s_('ProtectedBranch|Protect'), class: 'btn-success btn', disabled: true diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/projects/protected_branches/shared/_protected_branch.html.haml index 81dcab1d1ab..71c8d616eb9 100644 --- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml +++ b/app/views/projects/protected_branches/shared/_protected_branch.html.haml @@ -19,6 +19,15 @@ = yield + - if @project.merge_requests_require_code_owner_approval + %td + %button{ type: 'button', + class: "js-project-feature-toggle project-feature-toggle mr-5 #{'is-checked' if protected_branch.code_owner_approval_required}", + "aria-label": s_("ProtectedBranch|Toggle code owner approval") } + %span.toggle-icon + = sprite_icon('status_success_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-checked') + = sprite_icon('status_failed_borderless', size: 16, css_class: 'toggle-icon-svg toggle-status-unchecked') + - if can_admin_project %td = link_to 'Unprotect', [@project.namespace.becomes(Namespace), @project, protected_branch, { update_section: 'js-protected-branches-settings' }], disabled: local_assigns[:disabled], data: { confirm: 'Branch will be writable for developers. Are you sure?' }, method: :delete, class: "btn btn-warning" diff --git a/ee/app/assets/javascripts/protected_branches/protected_branch_create.js b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js new file mode 100644 index 00000000000..e8b053b6103 --- /dev/null +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_create.js @@ -0,0 +1,127 @@ +import $ from 'jquery'; +import axios from '~/lib/utils/axios_utils'; +import AccessorUtilities from '~/lib/utils/accessor'; +import Flash from '~/flash'; +import CreateItemDropdown from '~/create_item_dropdown'; +import AccessDropdown from 'ee/projects/settings/access_dropdown'; +import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; +import { __ } from '~/locale'; + +export default class ProtectedBranchCreate { + constructor() { + this.$form = $('.js-new-protected-branch'); + this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe(); + this.currentProjectUserDefaults = {}; + this.buildDropdowns(); + this.$branchInput = this.$form.find('input[name="protected_branch[name]"]'); + this.$toggleButton = this.$form.find('.js-project-feature-toggle'); + this.bindEvents(); + } + + bindEvents() { + this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); + this.$form.on('submit', this.onFormSubmit.bind(this)); + } + + onToggleButtonClick() { + const toggleButton = this.$toggleButton; + toggleButton.toggleClass('is-checked'); + } + + buildDropdowns() { + const $allowedToMergeDropdown = this.$form.find('.js-allowed-to-merge'); + const $allowedToPushDropdown = this.$form.find('.js-allowed-to-push'); + + // Cache callback + this.onSelectCallback = this.onSelect.bind(this); + + // Allowed to Merge dropdown + this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({ + $dropdown: $allowedToMergeDropdown, + accessLevelsData: gon.merge_access_levels, + onSelect: this.onSelectCallback, + accessLevel: ACCESS_LEVELS.MERGE, + }); + + // Allowed to Push dropdown + this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({ + $dropdown: $allowedToPushDropdown, + accessLevelsData: gon.push_access_levels, + onSelect: this.onSelectCallback, + accessLevel: ACCESS_LEVELS.PUSH, + }); + + this.createItemDropdown = new CreateItemDropdown({ + $dropdown: this.$form.find('.js-protected-branch-select'), + defaultToggleLabel: __('Protected Branch'), + fieldName: 'protected_branch[name]', + onSelect: this.onSelectCallback, + getData: ProtectedBranchCreate.getProtectedBranches, + }); + } + + // Enable submit button after selecting an option + onSelect() { + 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"]').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.$form + .find('.js-project-feature-toggle') + .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/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js new file mode 100644 index 00000000000..af440dd028d --- /dev/null +++ b/ee/app/assets/javascripts/protected_branches/protected_branch_edit.js @@ -0,0 +1,159 @@ +import _ from 'underscore'; +import axios from '~/lib/utils/axios_utils'; +import Flash from '~/flash'; +import AccessDropdown from 'ee/projects/settings/access_dropdown'; +import { ACCESS_LEVELS, LEVEL_TYPES } from './constants'; +import { __ } from '~/locale'; + +export default class ProtectedBranchEdit { + constructor(options) { + 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.$toggleButton = this.$wrap.find('.js-project-feature-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() { + this.$toggleButton.on('click', this.onToggleButtonClick.bind(this)); + } + + onToggleButtonClick() { + this.$toggleButton.toggleClass('is-checked'); + this.$toggleButton.prop('disabled', true); + + const formData = { + code_owner_approval_required: this.$toggleButton.hasClass('is-checked'), + }; + + this.updateCodeOwnerApproval(formData); + } + + updateCodeOwnerApproval(formData) { + axios + .patch(this.$wrap.data('url'), { + protected_branch: formData, + }) + .then(() => { + this.$toggleButton.prop('disabled', false); + }) + .catch(() => { + Flash(__('Failed to update branch!')); + }); + } + + buildDropdowns() { + // Allowed to merge dropdown + this[`${ACCESS_LEVELS.MERGE}_dropdown`] = new AccessDropdown({ + accessLevel: ACCESS_LEVELS.MERGE, + accessLevelsData: gon.merge_access_levels, + $dropdown: this.$allowedToMergeDropdown, + onSelect: this.onSelectOption.bind(this), + onHide: this.onDropdownHide.bind(this), + }); + + // Allowed to push dropdown + this[`${ACCESS_LEVELS.PUSH}_dropdown`] = new AccessDropdown({ + accessLevel: ACCESS_LEVELS.PUSH, + accessLevelsData: gon.push_access_levels, + $dropdown: this.$allowedToPushDropdown, + 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_branch: 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`); + }); + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + }) + .catch(() => { + this.$allowedToMergeDropdown.enable(); + this.$allowedToPushDropdown.enable(); + 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 = _.findWhere(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/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml new file mode 100644 index 00000000000..3734c449de2 --- /dev/null +++ b/ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml @@ -0,0 +1,5 @@ +--- +title: Frontend Implementation of Code Owner Approval +merge_request: 15862 +author: +type: added diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7a0f10c83d2..7e4122eb1e5 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12364,7 +12364,50 @@ msgstr "" msgid "Protected Tag" msgstr "" +<<<<<<< HEAD msgid "Protected branches" +======= +msgid "ProtectedBranch|%{wildcards_link_start}Wildcards%{wildcards_link_end} such as %{code_tag_start}*-stable%{code_tag_end} or %{code_tag_start}production/*%{code_tag_end} are supported" +msgstr "" + +msgid "ProtectedBranch|Allowed to merge" +msgstr "" + +msgid "ProtectedBranch|Allowed to merge:" +msgstr "" + +msgid "ProtectedBranch|Allowed to push" +msgstr "" + +msgid "ProtectedBranch|Allowed to push:" +msgstr "" + +msgid "ProtectedBranch|Code owner approval" +msgstr "" + +msgid "ProtectedBranch|Last commit" +msgstr "" + +msgid "ProtectedBranch|Protect" +msgstr "" + +msgid "ProtectedBranch|Protect a branch" +msgstr "" + +msgid "ProtectedBranch|Protected branch (%{protected_branches_count})" +msgstr "" + +msgid "ProtectedBranch|Pushes that change filenames matched by the CODEOWNERS file will be rejected" +msgstr "" + +msgid "ProtectedBranch|Require approval from code owners:" +msgstr "" + +msgid "ProtectedBranch|There are currently no protected branches, protect a branch with the form above." +msgstr "" + +msgid "ProtectedBranch|Toggle code owner approval" +>>>>>>> cd182ca57e5... Add views of code owner approval msgstr "" msgid "ProtectedEnvironment|%{environment_name} will be writable for developers. Are you sure?" |