diff options
8 files changed, 271 insertions, 62 deletions
diff --git a/app/assets/javascripts/pages/projects/branches/components/delete_branch_modal_component.vue b/app/assets/javascripts/pages/projects/branches/components/delete_branch_modal_component.vue new file mode 100644 index 00000000000..7a209dbc922 --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/components/delete_branch_modal_component.vue @@ -0,0 +1,151 @@ +<script> + import axios from '~/lib/utils/axios_utils'; + import modal from '~/vue_shared/components/modal.vue'; + import confirmationInput from '~/vue_shared/components/confirmation_input.vue'; + import { s__, sprintf } from '~/locale'; + import { redirectTo } from '~/lib/utils/url_utility'; + import Flash from '~/flash'; + import eventHub from '../shared/event_hub'; + + export default { + components: { + modal, + confirmationInput, + }, + props: { + id: { + type: String, + required: false, + default: 'delete-branch-modal', + }, + branchName: { + required: true, + type: String, + }, + deletePath: { + required: true, + type: String, + }, + isMerged: { + required: true, + type: Boolean, + }, + isProtected: { + required: true, + type: Boolean, + }, + rootRef: { + required: true, + type: String, + }, + redirectUrl: { + required: true, + type: String, + }, + }, + computed: { + title() { + const protectedTitle = sprintf(s__(`Branches| + Delete protected branch '%{branchName}'`), { branchName: this.branchName }); + const normalTitle = sprintf(s__(`Branches| + Delete branch '%{branchName}'`), { branchName: this.branchName }); + + return this.isProtected ? protectedTitle : normalTitle; + }, + text() { + const protectedBranch = sprintf(`<p>You’re about to permanently + delete the protected branch <strong>%{branchName}.</strong></p>`, + { + branchName: this.branchName, + }); + const whenNotMerged = sprintf(`<p>This branch hasn’t been merged into <span class='ref-name'>%{rootRef}</span> + To avoid data loss, consider merging this branch before deleting it.</p>`, + { + rootRef: this.rootRef, + }); + const finalWarning = sprintf(`<p>Once you confirm and press + <strong>%{buttonText}</strong>, it cannot be undone or recovered.</p>`, + { + buttonText: this.buttonText, + }); + let text = sprintf(s__(`Branches| + Deleting the '%{branchName}' branch cannot be undone. Are you sure?`), + { + branchName: this.branchName, + }); + + if (this.isProtected) { + text = !this.isMerged ? + sprintf(s__(`Branches| + %{protectedBranch} %{whenNotMerged} %{finalWarning}`), + { + protectedBranch, + whenNotMerged, + finalWarning, + }, + false) : + sprintf(s__(`Branches| + %{protectedBranch} %{finalWarning}`), + { + protectedBranch, + finalWarning, + }, + false); + } + + return text; + }, + buttonText() { + return this.isProtected ? s__('Branches|Delete protected branch') : s__('Branches|Delete branch'); + }, + canSubmit() { + let enableDeleteButton = this.$refs.confirmation && + this.$refs.confirmation.hasCorrectValue(); + + enableDeleteButton = this.isProtected ? enableDeleteButton : true; + return !enableDeleteButton; + }, + }, + methods: { + onSubmit() { + eventHub.$emit('deleteBranchModal.requestStarted', this.deletePath); + axios.delete(this.deletePath) + .then(() => { + eventHub.$emit('deleteBranchModal.requestFinished', { deletePath: this.deletePath, successful: true }); + redirectTo(this.redirectUrl); + }) + .catch((error) => { + eventHub.$emit('deleteBranchModal.requestFinished', { deletePath: this.deletePath, successful: false }); + Flash(sprintf(s__(`Branches| + An error has ocurred when deleting the branch %{branchName}`), { branchName: this.branchName })); + throw error; + }); + }, + }, + }; +</script> +<template> + <modal + id="delete-branch-modal" + :title="title" + :text="text" + kind="danger" + :primary-button-label="buttonText" + @submit="onSubmit" + :submit-disabled="canSubmit"> + + <template + slot="body" + slot-scope="props"> + <p v-html="props.text"></p> + + <confirmation-input + ref="confirmation" + :input-id="`${id}-input`" + confirmation-key="branch-name" + :confirmation-value="branchName" + v-show="isProtected" + /> + </template> + </modal> +</template> diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js index cee0f19bf2a..20d2bb44a96 100644 --- a/app/assets/javascripts/pages/projects/branches/index/index.js +++ b/app/assets/javascripts/pages/projects/branches/index/index.js @@ -1,7 +1,86 @@ -import AjaxLoadingSpinner from '~/ajax_loading_spinner'; -import DeleteModal from '~/branches/branches_delete_modal'; +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import { convertPermissionToBoolean } from '~/lib/utils/common_utils'; +import deleteBranchModal from '../components/delete_branch_modal_component.vue'; +import eventHub from '../shared/event_hub'; export default () => { - AjaxLoadingSpinner.init(); - new DeleteModal(); // eslint-disable-line no-new + Vue.use(Translate); + + const onRequestFinished = ({ branchUrl, successful }) => { + const button = document.querySelector(`.js-delete-branch[data-delete-path="${branchUrl}"]`); + + if (!successful) { + button.removeAttribute('disabled'); + } + }; + + const onRequestStarted = (branchUrl) => { + const button = document.querySelector(`.js-delete-branch[data-delete-path="${branchUrl}"]`); + button.setAttribute('disabled', ''); + eventHub.$once('deleteBranchModal.requestFinished', onRequestFinished); + }; + + const onDeleteButtonClick = (event) => { + const button = event.currentTarget; + const modalProps = { + deletePath: button.dataset.deletePath, + branchName: button.dataset.branchName, + isMerged: convertPermissionToBoolean(button.dataset.isMerged) || false, + isProtected: convertPermissionToBoolean(button.dataset.isProtected) || false, + rootRef: button.dataset.rootRef, + redirectUrl: button.dataset.redirectUrl, + }; + eventHub.$once('deleteBranchModal.requestStarted', onRequestStarted); + eventHub.$emit('deleteBranchModal.props', modalProps); + }; + + const deleteBranchButtons = document.querySelectorAll('.js-delete-branch'); + for (let i = 0; i < deleteBranchButtons.length; i += 1) { + const button = deleteBranchButtons[i]; + button.addEventListener('click', onDeleteButtonClick); + } + + eventHub.$once('deleteBranchModal.mounted', () => { + for (let i = 0; i < deleteBranchButtons.length; i += 1) { + const button = deleteBranchButtons[i]; + button.removeAttribute('disabled'); + } + }); + + return new Vue({ + el: '#delete-branch-modal', + components: { + deleteBranchModal, + }, + data() { + return { + modalProps: { + deletePath: '', + branchName: '', + isMerged: false, + isProtected: false, + rootRef: '', + redirectUrl: '', + }, + }; + }, + mounted() { + eventHub.$on('deleteBranchModal.props', this.setModalProps); + eventHub.$emit('deleteBranchModal.mounted'); + }, + beforeDestroy() { + eventHub.$off('deleteBranchModal.props', this.setModalProps); + }, + methods: { + setModalProps(modalProps) { + this.modalProps = modalProps; + }, + }, + render(createElement) { + return createElement('delete-branch-modal', { + props: this.modalProps, + }); + }, + }); }; diff --git a/app/assets/javascripts/pages/projects/branches/shared/event_hub.js b/app/assets/javascripts/pages/projects/branches/shared/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/pages/projects/branches/shared/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/vue_shared/components/confirmation_input.vue b/app/assets/javascripts/vue_shared/components/confirmation_input.vue index 1aa03ea6317..df967d0eb3a 100644 --- a/app/assets/javascripts/vue_shared/components/confirmation_input.vue +++ b/app/assets/javascripts/vue_shared/components/confirmation_input.vue @@ -22,6 +22,11 @@ default: true, }, }, + data() { + return { + enteredValue: '', + }; + }, computed: { inputLabel() { let value = this.confirmationValue; @@ -38,7 +43,7 @@ }, methods: { hasCorrectValue() { - return this.$refs.enteredValue.value === this.confirmationValue; + return this.enteredValue === this.confirmationValue; }, }, }; @@ -55,7 +60,7 @@ :id="inputId" :name="confirmationKey" type="text" - ref="enteredValue" + v-model="enteredValue" class="form-control" /> </div> diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml index 1da0e865a41..41f003455a8 100644 --- a/app/views/projects/branches/_branch.html.haml +++ b/app/views/projects/branches/_branch.html.haml @@ -39,31 +39,39 @@ %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, title: s_('Branches|The default branch cannot be deleted') } - = icon("trash-o") + = sprite_icon("remove") - elsif protected_branch?(@project, branch) - if can?(current_user, :delete_protected_branch, @project) - %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", + %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip js-delete-branch", title: s_('Branches|Delete protected branch'), data: { toggle: "modal", - target: "#modal-delete-branch", + target: "#delete-branch-modal", delete_path: project_branch_path(@project, branch.name), branch_name: branch.name, + container: 'body', + is_protected: "true", + redirect_url: project_branches_path(@project), + root_ref: @repository.root_ref, is_merged: ("true" if merged) } } - = icon("trash-o") + = sprite_icon("remove") - else %button{ class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip disabled", disabled: true, title: s_('Branches|Only a project master or owner can delete a protected branch') } - = icon("trash-o") + = sprite_icon("remove") - else - = link_to project_branch_path(@project, branch.name), - class: "btn btn-remove remove-row js-ajax-loading-spinner has-tooltip", - title: s_('Branches|Delete branch'), - method: :delete, - data: { confirm: s_("Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?") % { branch_name: branch.name } }, - remote: true, - 'aria-label' => s_('Branches|Delete branch') do - = icon("trash-o") + %button.btn.btn-remove.remove-row.js-ajax-loading-spinner.has-tooltip.js-delete-branch{ title: s_('Branches|Delete branch'), + data: { target: "#delete-branch-modal", + delete_path: project_branch_path(@project, branch.name), + branch_name: branch.name, + root_ref: @repository.root_ref, + redirect_url: project_branches_path(@project), + isProtected: "false", + is_merged: ("true" if merged), + toggle: "modal", + container: 'body' }, + 'aria-label': s_('Branches|Delete branch') } + = sprite_icon("remove") - if branch.name != @repository.root_ref .divergence-graph{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind), diff --git a/app/views/projects/branches/_delete_protected_modal.html.haml b/app/views/projects/branches/_delete_protected_modal.html.haml deleted file mode 100644 index e0008e322a0..00000000000 --- a/app/views/projects/branches/_delete_protected_modal.html.haml +++ /dev/null @@ -1,41 +0,0 @@ -#modal-delete-branch.modal{ tabindex: -1 } - .modal-dialog - .modal-content - .modal-header - %button.close{ data: { dismiss: 'modal' } } × - %h3.page-title - - title_branch_name = capture do - %span.js-branch-name.ref-name>[branch name] - = s_("Branches|Delete protected branch '%{branch_name}'?").html_safe % { branch_name: title_branch_name } - - .modal-body - %p - - branch_name = capture do - %strong.js-branch-name.ref-name>[branch name] - = s_('Branches|You’re about to permanently delete the protected branch %{branch_name}.').html_safe % { branch_name: branch_name } - %p.js-not-merged - - default_branch = capture do - %span.ref-name= @repository.root_ref - = s_('Branches|This branch hasn’t been merged into %{default_branch}.').html_safe % { default_branch: default_branch } - = s_('Branches|To avoid data loss, consider merging this branch before deleting it.') - %p - - delete_protected_branch = capture do - %strong - = s_('Branches|Delete protected branch') - = s_('Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered.').html_safe % { delete_protected_branch: delete_protected_branch } - %p - - branch_name_confirmation = capture do - %kbd.js-branch-name [branch name] - %strong - = s_('Branches|To confirm, type %{branch_name_confirmation}:').html_safe % { branch_name_confirmation: branch_name_confirmation } - - .form-group - = text_field_tag 'delete_branch_input', '', class: 'form-control js-delete-branch-input' - - .modal-footer - %button.btn{ data: { dismiss: 'modal' } } Cancel - = link_to s_('Branches|Delete protected branch'), '', - class: "btn btn-danger js-delete-branch", - title: s_('Branches|Delete branch'), - method: :delete, - 'aria-label' => s_('Branches|Delete branch') diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml index fb770764364..22f4424a31c 100644 --- a/app/views/projects/branches/index.html.haml +++ b/app/views/projects/branches/index.html.haml @@ -2,6 +2,7 @@ - page_title _('Branches') %div{ class: container_class } + #delete-branch-modal .top-area.adjust - if can?(current_user, :admin_project, @project) .nav-text @@ -43,5 +44,3 @@ - else .nothing-here-block = s_('Branches|No branches to show') - -= render 'projects/branches/delete_protected_modal' diff --git a/changelogs/unreleased/42603-refactor-the-delete-branch-and-delete-protected-branches-from-haml-to-vuejs.yml b/changelogs/unreleased/42603-refactor-the-delete-branch-and-delete-protected-branches-from-haml-to-vuejs.yml new file mode 100644 index 00000000000..3ab10d6303c --- /dev/null +++ b/changelogs/unreleased/42603-refactor-the-delete-branch-and-delete-protected-branches-from-haml-to-vuejs.yml @@ -0,0 +1,5 @@ +--- +title: Refactor the delete branch and delete protected branches modal to vue +merge_request: 16809 +author: +type: changed |