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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamantha Ming <sming@gitlab.com>2019-08-24 03:26:58 +0300
committerSamantha Ming <sming@gitlab.com>2019-09-14 06:03:08 +0300
commit7f3d2715e009eaf2dd7b2caf70faccb7394a5283 (patch)
treed2ebe8862ebd2c8f6f25412b694d52b85eb094b4
parent4e5872eb4c3262849fa36acb175544eac88ee2c0 (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
-rw-r--r--app/views/projects/protected_branches/shared/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/shared/_create_protected_branch.html.haml30
-rw-r--r--app/views/projects/protected_branches/shared/_protected_branch.html.haml9
-rw-r--r--ee/app/assets/javascripts/protected_branches/protected_branch_create.js127
-rw-r--r--ee/app/assets/javascripts/protected_branches/protected_branch_edit.js159
-rw-r--r--ee/changelogs/unreleased/13251-fe-require-approval-from-code-owners.yml5
-rw-r--r--locale/gitlab.pot43
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?"