Commit 0eadb54e authored by Nathan Friend's avatar Nathan Friend

Merge branch...

Merge branch '254257-replace-bootstrap-modal-in-app-views-projects-branches-_branch-html-haml' into 'master'

Replace bootstrap modal for delete branch modal [RUN ALL RSPEC] [RUN AS-IF-FOSS]

See merge request gitlab-org/gitlab!56782
parents 9eb6b3b6 e006d7b3
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
name: 'DeleteBranchButton',
components: { GlButton },
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
branchName: {
type: String,
required: false,
default: '',
},
defaultBranchName: {
type: String,
required: false,
default: '',
},
deletePath: {
type: String,
required: false,
default: '',
},
tooltip: {
type: String,
required: false,
default: s__('Branches|Delete branch'),
},
disabled: {
type: Boolean,
required: false,
default: false,
},
isProtectedBranch: {
type: Boolean,
required: false,
default: false,
},
merged: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
variant() {
if (this.disabled) {
return 'default';
}
return 'danger';
},
title() {
if (this.isProtectedBranch && this.disabled) {
return s__('Branches|Only a project maintainer or owner can delete a protected branch');
} else if (this.isProtectedBranch) {
return s__('Branches|Delete protected branch');
}
return this.tooltip;
},
},
methods: {
openModal() {
eventHub.$emit('openModal', {
branchName: this.branchName,
defaultBranchName: this.defaultBranchName,
deletePath: this.deletePath,
isProtectedBranch: this.isProtectedBranch,
merged: this.merged,
});
},
},
};
</script>
<template>
<gl-button
v-gl-tooltip.hover
icon="remove"
class="js-delete-branch-button"
data-qa-selector="delete_branch_button"
:disabled="disabled"
:variant="variant"
:title="title"
:aria-label="title"
@click="openModal"
/>
</template>
<script>
import { GlButton, GlFormInput, GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import csrf from '~/lib/utils/csrf';
import { sprintf, s__ } from '~/locale';
import eventHub from '../event_hub';
export default {
csrf,
components: {
GlModal,
GlButton,
GlFormInput,
GlSprintf,
GlAlert,
},
data() {
return {
visible: false,
isProtectedBranch: false,
branchName: '',
defaultBranchName: '',
deletePath: '',
merged: false,
enteredBranchName: '',
modalId: 'delete-branch-modal',
};
},
computed: {
title() {
const modalTitle = this.isProtectedBranch
? this.$options.i18n.modalTitleProtectedBranch
: this.$options.i18n.modalTitle;
return sprintf(modalTitle, { branchName: this.branchName });
},
message() {
const modalMessage = this.isProtectedBranch
? this.$options.i18n.modalMessageProtectedBranch
: this.$options.i18n.modalMessage;
return sprintf(modalMessage, { branchName: this.branchName });
},
unmergedWarning() {
return sprintf(this.$options.i18n.unmergedWarning, {
defaultBranchName: this.defaultBranchName,
});
},
undoneWarning() {
return sprintf(this.$options.i18n.undoneWarning, {
buttonText: this.buttonText,
});
},
confirmationText() {
return sprintf(this.$options.i18n.confirmationText, {
branchName: this.branchName,
});
},
buttonText() {
return this.isProtectedBranch
? this.$options.i18n.deleteButtonTextProtectedBranch
: this.$options.i18n.deleteButtonText;
},
branchNameConfirmed() {
return this.enteredBranchName === this.branchName;
},
deleteButtonDisabled() {
return this.isProtectedBranch && !this.branchNameConfirmed;
},
},
mounted() {
eventHub.$on('openModal', (options) => {
this.openModal(options);
});
},
methods: {
openModal({ isProtectedBranch, branchName, defaultBranchName, deletePath, merged }) {
this.isProtectedBranch = isProtectedBranch;
this.branchName = branchName;
this.defaultBranchName = defaultBranchName;
this.deletePath = deletePath;
this.merged = merged;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
submitForm() {
this.$refs.form.submit();
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
},
i18n: {
modalTitle: s__('Branches|Delete branch. Are you ABSOLUTELY SURE?'),
modalTitleProtectedBranch: s__('Branches|Delete protected branch. Are you ABSOLUTELY SURE?'),
modalMessage: s__(
"Branches|You're about to permanently delete the branch %{strongStart}%{branchName}.%{strongEnd}",
),
modalMessageProtectedBranch: s__(
"Branches|You're about to permanently delete the protected branch %{strongStart}%{branchName}.%{strongEnd}",
),
unmergedWarning: s__(
'Branches|This branch hasn’t been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it.',
),
undoneWarning: s__(
'Branches|Once you confirm and press %{strongStart}%{buttonText},%{strongEnd} it cannot be undone or recovered.',
),
cancelButtonText: s__('Branches|Cancel, keep branch'),
confirmationText: s__(
'Branches|Deleting the %{strongStart}%{branchName}%{strongEnd} branch cannot be undone. Are you sure?',
),
confirmationTextProtectedBranch: s__('Branches|Please type the following to confirm:'),
deleteButtonText: s__('Branches|Yes, delete branch'),
deleteButtonTextProtectedBranch: s__('Branches|Yes, delete protected branch'),
},
};
</script>
<template>
<gl-modal :visible="visible" size="sm" :modal-id="modalId" :title="title">
<gl-alert class="gl-mb-5" variant="danger" :dismissible="false">
<div data-testid="modal-message">
<gl-sprintf :message="message">
<template #strong="{ content }">
<strong> {{ content }} </strong>
</template>
</gl-sprintf>
<p v-if="!merged" class="gl-mb-0 gl-mt-4">
{{ unmergedWarning }}
</p>
</div>
</gl-alert>
<form ref="form" :action="deletePath" method="post">
<div v-if="isProtectedBranch" class="gl-mt-4">
<p>
<gl-sprintf :message="undoneWarning">
<template #strong="{ content }">
<strong> {{ content }} </strong>
</template>
</gl-sprintf>
</p>
<p>
<gl-sprintf :message="$options.i18n.confirmationTextProtectedBranch">
<template #strong="{ content }">
{{ content }}
</template>
</gl-sprintf>
<code class="gl-white-space-pre-wrap"> {{ branchName }} </code>
<gl-form-input
v-model="enteredBranchName"
name="delete_branch_input"
type="text"
class="gl-mt-4"
aria-labelledby="input-label"
autocomplete="off"
/>
</p>
</div>
<div v-else>
<p class="gl-mt-4">
<gl-sprintf :message="confirmationText">
<template #strong="{ content }">
<strong>
{{ content }}
</strong>
</template>
</gl-sprintf>
</p>
</div>
<input ref="method" type="hidden" name="_method" value="delete" />
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
</form>
<template #modal-footer>
<div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
<gl-button @click="closeModal">
{{ $options.i18n.cancelButtonText }}
</gl-button>
<div class="gl-mr-3"></div>
<gl-button
ref="deleteBranchButton"
:disabled="deleteButtonDisabled"
variant="danger"
data-qa-selector="delete_branch_confirmation_button"
data-testid="delete_branch_confirmation_button"
@click="submitForm"
>{{ buttonText }}</gl-button
>
</div>
</template>
</gl-modal>
</template>
import createEventHub from '~/helpers/event_hub_factory';
export default createEventHub();
import Vue from 'vue';
import DeleteBranchButton from '~/branches/components/delete_branch_button.vue';
import { parseBoolean } from '~/lib/utils/common_utils';
export default function initDeleteBranchButton(el) {
if (!el) {
return false;
}
const {
branchName,
defaultBranchName,
deletePath,
tooltip,
disabled,
isProtectedBranch,
merged,
} = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(DeleteBranchButton, {
props: {
branchName,
defaultBranchName,
deletePath,
tooltip,
disabled: parseBoolean(disabled),
isProtectedBranch: parseBoolean(isProtectedBranch),
merged: parseBoolean(merged),
},
}),
});
}
import Vue from 'vue';
import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue';
export default function initDeleteBranchModal() {
const el = document.querySelector('.js-delete-branch-modal');
if (!el) {
return false;
}
return new Vue({
el,
render(createComponent) {
return createComponent(DeleteBranchModal);
},
});
}
...@@ -3,6 +3,8 @@ import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner'; ...@@ -3,6 +3,8 @@ import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import BranchSortDropdown from '~/branches/branch_sort_dropdown'; import BranchSortDropdown from '~/branches/branch_sort_dropdown';
import DeleteModal from '~/branches/branches_delete_modal'; import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph'; import initDiverganceGraph from '~/branches/divergence_graph';
import initDeleteBranchButton from '~/branches/init_delete_branch_button';
import initDeleteBranchModal from '~/branches/init_delete_branch_modal';
AjaxLoadingSpinner.init(); AjaxLoadingSpinner.init();
new DeleteModal(); // eslint-disable-line no-new new DeleteModal(); // eslint-disable-line no-new
...@@ -14,3 +16,9 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector( ...@@ -14,3 +16,9 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
initDiverganceGraph(divergingCountsEndpoint, defaultBranch); initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown(); BranchSortDropdown();
initDeprecatedRemoveRowBehavior(); initDeprecatedRemoveRowBehavior();
document
.querySelectorAll('.js-delete-branch-button')
.forEach((elem) => initDeleteBranchButton(elem));
initDeleteBranchModal();
...@@ -48,7 +48,10 @@ ...@@ -48,7 +48,10 @@
= render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top' = render 'projects/buttons/download', project: @project, ref: branch.name, pipeline: @refs_pipelines[branch.name], class: 'gl-vertical-align-top'
- if can?(current_user, :push_code, @project) - if Feature.enabled?(:delete_branch_confirmation_modals, @project, default_enabled: :yaml)
= render 'projects/branches/delete_branch_modal_button', project: @project, branch: branch, merged: merged
- elsif can?(current_user, :push_code, @project)
- if branch.name == @project.repository.root_ref - if branch.name == @project.repository.root_ref
- delete_default_branch_tooltip = s_('Branches|The default branch cannot be deleted') - delete_default_branch_tooltip = s_('Branches|The default branch cannot be deleted')
%span.gl-display-inline-block.has-tooltip{ title: delete_default_branch_tooltip } %span.gl-display-inline-block.has-tooltip{ title: delete_default_branch_tooltip }
......
- if branch.name == @project.repository.root_ref
.js-delete-branch-button{ data: { tooltip: s_('Branches|The default branch cannot be deleted'),
disabled: true.to_s } }
- elsif protected_branch?(@project, branch)
- if can?(current_user, :push_to_delete_protected_branch, @project)
.js-delete-branch-button{ data: { branch_name: branch.name,
is_protected_branch: true.to_s,
merged: merged.to_s,
default_branch_name: @project.repository.root_ref,
delete_path: project_branch_path(@project, branch.name) } }
- else
.js-delete-branch-button{ data: { is_protected_branch: true.to_s,
disabled: true.to_s } }
- else
.js-delete-branch-button{ data: { branch_name: branch.name,
merged: merged.to_s,
default_branch_name: @project.repository.root_ref,
delete_path: project_branch_path(@project, branch.name) } }
...@@ -54,4 +54,7 @@ ...@@ -54,4 +54,7 @@
.nothing-here-block .nothing-here-block
= s_('Branches|No branches to show') = s_('Branches|No branches to show')
= render 'projects/branches/delete_protected_modal' - if Feature.enabled?(:delete_branch_confirmation_modals, @project, default_enabled: :yaml) && can?(current_user, :push_code, @project)
.js-delete-branch-modal
- elsif can?(current_user, :push_code, @project)
= render 'projects/branches/delete_protected_modal'
---
name: delete_branch_confirmation_modals
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/56782
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329052
milestone: '13.12'
type: development
group: group::expansion
default_enabled: false
...@@ -5387,6 +5387,9 @@ msgstr "" ...@@ -5387,6 +5387,9 @@ msgstr ""
msgid "Branches|All" msgid "Branches|All"
msgstr "" msgstr ""
msgid "Branches|Cancel, keep branch"
msgstr ""
msgid "Branches|Cant find HEAD commit for this branch" msgid "Branches|Cant find HEAD commit for this branch"
msgstr "" msgstr ""
...@@ -5399,6 +5402,9 @@ msgstr "" ...@@ -5399,6 +5402,9 @@ msgstr ""
msgid "Branches|Delete branch" msgid "Branches|Delete branch"
msgstr "" msgstr ""
msgid "Branches|Delete branch. Are you ABSOLUTELY SURE?"
msgstr ""
msgid "Branches|Delete merged branches" msgid "Branches|Delete merged branches"
msgstr "" msgstr ""
...@@ -5408,6 +5414,12 @@ msgstr "" ...@@ -5408,6 +5414,12 @@ msgstr ""
msgid "Branches|Delete protected branch '%{branch_name}'?" msgid "Branches|Delete protected branch '%{branch_name}'?"
msgstr "" msgstr ""
msgid "Branches|Delete protected branch. Are you ABSOLUTELY SURE?"
msgstr ""
msgid "Branches|Deleting the %{strongStart}%{branchName}%{strongEnd} branch cannot be undone. Are you sure?"
msgstr ""
msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?" msgid "Branches|Deleting the '%{branch_name}' branch cannot be undone. Are you sure?"
msgstr "" msgstr ""
...@@ -5429,12 +5441,18 @@ msgstr "" ...@@ -5429,12 +5441,18 @@ msgstr ""
msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered." msgid "Branches|Once you confirm and press %{delete_protected_branch}, it cannot be undone or recovered."
msgstr "" msgstr ""
msgid "Branches|Once you confirm and press %{strongStart}%{buttonText},%{strongEnd} it cannot be undone or recovered."
msgstr ""
msgid "Branches|Only a project maintainer or owner can delete a protected branch" msgid "Branches|Only a project maintainer or owner can delete a protected branch"
msgstr "" msgstr ""
msgid "Branches|Overview" msgid "Branches|Overview"
msgstr "" msgstr ""
msgid "Branches|Please type the following to confirm:"
msgstr ""
msgid "Branches|Protected branches can be managed in %{project_settings_link}." msgid "Branches|Protected branches can be managed in %{project_settings_link}."
msgstr "" msgstr ""
...@@ -5468,6 +5486,9 @@ msgstr "" ...@@ -5468,6 +5486,9 @@ msgstr ""
msgid "Branches|The default branch cannot be deleted" msgid "Branches|The default branch cannot be deleted"
msgstr "" msgstr ""
msgid "Branches|This branch hasn’t been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it."
msgstr ""
msgid "Branches|This branch hasn’t been merged into %{default_branch}." msgid "Branches|This branch hasn’t been merged into %{default_branch}."
msgstr "" msgstr ""
...@@ -5480,6 +5501,18 @@ msgstr "" ...@@ -5480,6 +5501,18 @@ msgstr ""
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above." msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
msgstr "" msgstr ""
msgid "Branches|Yes, delete branch"
msgstr ""
msgid "Branches|Yes, delete protected branch"
msgstr ""
msgid "Branches|You're about to permanently delete the branch %{strongStart}%{branchName}.%{strongEnd}"
msgstr ""
msgid "Branches|You're about to permanently delete the protected branch %{strongStart}%{branchName}.%{strongEnd}"
msgstr ""
msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}." msgid "Branches|You’re about to permanently delete the protected branch %{branch_name}."
msgstr "" msgstr ""
......
...@@ -5,13 +5,23 @@ module QA ...@@ -5,13 +5,23 @@ module QA
module Project module Project
module Branches module Branches
class Show < Page::Base class Show < Page::Base
view 'app/assets/javascripts/branches/components/delete_branch_button.vue' do
element :delete_branch_button
end
view 'app/assets/javascripts/branches/components/delete_branch_modal.vue' do
element :delete_branch_confirmation_button
end
view 'app/views/projects/branches/_branch.html.haml' do view 'app/views/projects/branches/_branch.html.haml' do
element :remove_btn element :remove_btn
element :branch_name element :branch_name
end end
view 'app/views/projects/branches/_panel.html.haml' do view 'app/views/projects/branches/_panel.html.haml' do
element :all_branches element :all_branches
end end
view 'app/views/projects/branches/index.html.haml' do view 'app/views/projects/branches/index.html.haml' do
element :delete_merged_branches element :delete_merged_branches
end end
...@@ -19,12 +29,12 @@ module QA ...@@ -19,12 +29,12 @@ module QA
def delete_branch(branch_name) def delete_branch(branch_name)
within_element(:all_branches) do within_element(:all_branches) do
within(".js-branch-#{branch_name}") do within(".js-branch-#{branch_name}") do
accept_alert do click_element(:delete_branch_button)
click_element(:remove_btn)
end
end end
end end
click_element(:delete_branch_confirmation_button)
finished_loading? finished_loading?
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
module QA module QA
RSpec.describe 'Create' do RSpec.describe 'Create' do
describe 'Create, list, and delete branches via web' do describe 'Create, list, and delete branches via web', :requires_admin do
master_branch = nil master_branch = nil
second_branch = 'second-branch' second_branch = 'second-branch'
third_branch = 'third-branch' third_branch = 'third-branch'
...@@ -24,6 +24,8 @@ module QA ...@@ -24,6 +24,8 @@ module QA
proj.initialize_with_readme = true proj.initialize_with_readme = true
end end
Runtime::Feature.enable(:delete_branch_confirmation_modals, project: project)
master_branch = project.default_branch master_branch = project.default_branch
Git::Repository.perform do |repository| Git::Repository.perform do |repository|
......
...@@ -12,7 +12,7 @@ RSpec.describe "User deletes branch", :js do ...@@ -12,7 +12,7 @@ RSpec.describe "User deletes branch", :js do
sign_in(user) sign_in(user)
end end
it "deletes branch" do it "deletes branch", :js do
visit(project_branches_path(project)) visit(project_branches_path(project))
branch_search = find('input[data-testid="branch-search"]') branch_search = find('input[data-testid="branch-search"]')
...@@ -21,11 +21,38 @@ RSpec.describe "User deletes branch", :js do ...@@ -21,11 +21,38 @@ RSpec.describe "User deletes branch", :js do
branch_search.native.send_keys(:enter) branch_search.native.send_keys(:enter)
page.within(".js-branch-improve\\/awesome") do page.within(".js-branch-improve\\/awesome") do
accept_alert { click_link(title: 'Delete branch') } find('.js-delete-branch-button').click
end
page.within '.modal-footer' do
click_button 'Yes, delete branch'
end end
wait_for_requests wait_for_requests
expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden) expect(page).to have_content('Branch was deleted')
end
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
before do
stub_feature_flags(delete_branch_confirmation_modals: false)
end
it "deletes branch" do
visit(project_branches_path(project))
branch_search = find('input[data-testid="branch-search"]')
branch_search.set('improve/awesome')
branch_search.native.send_keys(:enter)
page.within(".js-branch-improve\\/awesome") do
accept_alert { click_link(title: 'Delete branch') }
end
wait_for_requests
expect(page).to have_css(".js-branch-improve\\/awesome", visible: :hidden)
end
end end
end end
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require "spec_helper" require "spec_helper"
RSpec.describe "User views branches" do RSpec.describe "User views branches", :js do
let_it_be(:project) { create(:project, :repository) } let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner } let_it_be(:user) { project.owner }
...@@ -10,9 +10,12 @@ RSpec.describe "User views branches" do ...@@ -10,9 +10,12 @@ RSpec.describe "User views branches" do
sign_in(user) sign_in(user)
end end
context "all branches" do context "all branches", :js do
before do before do
visit(project_branches_path(project)) visit(project_branches_path(project))
branch_search = find('input[data-testid="branch-search"]')
branch_search.set('master')
branch_search.native.send_keys(:enter)
end end
it "shows branches" do it "shows branches" do
...@@ -20,6 +23,10 @@ RSpec.describe "User views branches" do ...@@ -20,6 +23,10 @@ RSpec.describe "User views branches" do
expect(page.all(".graph-side")).to all( have_content(/\d+/) ) expect(page.all(".graph-side")).to all( have_content(/\d+/) )
end end
it "displays a disabled button with a tooltip for the default branch that cannot be deleted", :js do
expect(page).to have_button('The default branch cannot be deleted', disabled: true)
end
end end
context "protected branches" do context "protected branches" do
......
...@@ -88,10 +88,7 @@ RSpec.describe 'Branches' do ...@@ -88,10 +88,7 @@ RSpec.describe 'Branches' do
it 'shows filtered branches', :js do it 'shows filtered branches', :js do
visit project_branches_path(project) visit project_branches_path(project)
branch_search = find('input[data-testid="branch-search"]') search_for_branch('fix')
branch_search.set('fix')
branch_search.native.send_keys(:enter)
expect(page).to have_content('fix') expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1) expect(find('.all-branches')).to have_selector('li', count: 1)
...@@ -103,11 +100,10 @@ RSpec.describe 'Branches' do ...@@ -103,11 +100,10 @@ RSpec.describe 'Branches' do
visit project_branches_filtered_path(project, state: 'all') visit project_branches_filtered_path(project, state: 'all')
expect(all('.all-branches').last).to have_selector('li', count: 20) expect(all('.all-branches').last).to have_selector('li', count: 20)
accept_confirm do
within('.js-branch-item', match: :first) { click_link(title: 'Delete branch') }
end
expect(all('.all-branches').last).to have_selector('li', count: 19) delete_branch_and_confirm
expect(page).to have_content('Branch was deleted')
end end
end end
...@@ -153,10 +149,7 @@ RSpec.describe 'Branches' do ...@@ -153,10 +149,7 @@ RSpec.describe 'Branches' do
it 'shows filtered branches', :js do it 'shows filtered branches', :js do
visit project_branches_filtered_path(project, state: 'all') visit project_branches_filtered_path(project, state: 'all')
branch_search = find('input[data-testid="branch-search"]') search_for_branch('fix')
branch_search.set('fix')
branch_search.native.send_keys(:enter)
expect(page).to have_content('fix') expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1) expect(find('.all-branches')).to have_selector('li', count: 1)
...@@ -167,19 +160,39 @@ RSpec.describe 'Branches' do ...@@ -167,19 +160,39 @@ RSpec.describe 'Branches' do
it 'removes branch after confirmation', :js do it 'removes branch after confirmation', :js do
visit project_branches_filtered_path(project, state: 'all') visit project_branches_filtered_path(project, state: 'all')
branch_search = find('input[data-testid="branch-search"]') search_for_branch('fix')
branch_search.set('fix') expect(all('.all-branches').last).to have_selector('li', count: 1)
branch_search.native.send_keys(:enter)
expect(page).to have_content('fix') delete_branch_and_confirm
expect(find('.all-branches')).to have_selector('li', count: 1)
accept_confirm do expect(page).to have_content('Branch was deleted')
within('.js-branch-fix') { click_link(title: 'Delete branch') }
end page.refresh
search_for_branch('fix')
expect(page).not_to have_content('fix') expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0) expect(all('.all-branches').last).to have_selector('li', count: 0)
end
context 'when the delete_branch_confirmation_modals feature flag is disabled' do
it 'removes branch after confirmation', :js do
stub_feature_flags(delete_branch_confirmation_modals: false)
visit project_branches_filtered_path(project, state: 'all')
search_for_branch('fix')
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
accept_confirm do
within('.js-branch-item', match: :first) { click_link(title: 'Delete branch') }
end
expect(page).not_to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 0)
end
end end
end end
...@@ -327,4 +340,18 @@ RSpec.describe 'Branches' do ...@@ -327,4 +340,18 @@ RSpec.describe 'Branches' do
def create_file(message: 'message', branch_name:) def create_file(message: 'message', branch_name:)
repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name) repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name)
end end
def search_for_branch(name)
branch_search = find('input[data-testid="branch-search"]')
branch_search.set(name)
branch_search.native.send_keys(:enter)
end
def delete_branch_and_confirm
find('.js-delete-branch-button', match: :first).click
within '.modal-footer' do
click_button 'Yes, delete branch'
end
end
end end
...@@ -329,11 +329,11 @@ RSpec.describe 'Environment' do ...@@ -329,11 +329,11 @@ RSpec.describe 'Environment' do
expect(page).to have_button('Stop') expect(page).to have_button('Stop')
end end
it 'user deletes the branch with running environment' do it 'user deletes the branch with running environment', :js do
visit project_branches_filtered_path(project, state: 'all', search: 'feature') visit project_branches_filtered_path(project, state: 'all', search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do remove_branch_with_hooks(project, user, 'feature') do
within('.js-branch-feature') { click_link(title: 'Delete branch') } page.within('.js-branch-feature') { find('.js-delete-branch-button').click }
end end
visit_environment(environment) visit_environment(environment)
...@@ -341,6 +341,24 @@ RSpec.describe 'Environment' do ...@@ -341,6 +341,24 @@ RSpec.describe 'Environment' do
expect(page).not_to have_button('Stop') expect(page).not_to have_button('Stop')
end end
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
before do
stub_feature_flags(delete_branch_confirmation_modals: false)
end
it 'user deletes the branch with running environment' do
visit project_branches_filtered_path(project, state: 'all', search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
within('.js-branch-feature') { click_link(title: 'Delete branch') }
end
visit_environment(environment)
expect(page).not_to have_button('Stop')
end
end
## ##
# This is a workaround for problem described in #24543 # This is a workaround for problem described in #24543
# #
......
...@@ -27,7 +27,22 @@ RSpec.describe 'Protected Branches', :js do ...@@ -27,7 +27,22 @@ RSpec.describe 'Protected Branches', :js do
find('input[data-testid="branch-search"]').set('fix') find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter) find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_selector('button[data-testid="remove-protected-branch"][disabled]') expect(page).to have_button('Only a project maintainer or owner can delete a protected branch', disabled: true)
end
context 'when feature flag :delete_branch_confirmation_modals is disabled' do
before do
stub_feature_flags(delete_branch_confirmation_modals: false)
end
it 'does not allow developer to remove protected branch' do
visit project_branches_path(project)
find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_selector('button[data-testid="remove-protected-branch"][disabled]')
end
end end
end end
end end
...@@ -52,17 +67,44 @@ RSpec.describe 'Protected Branches', :js do ...@@ -52,17 +67,44 @@ RSpec.describe 'Protected Branches', :js do
expect(page).to have_content('fix') expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1) expect(find('.all-branches')).to have_selector('li', count: 1)
page.find('[data-target="#modal-delete-branch"]').click
expect(page).to have_css('.js-delete-branch[disabled]') expect(page).to have_button('Delete protected branch', disabled: false)
page.find('.js-delete-branch-button').click
fill_in 'delete_branch_input', with: 'fix' fill_in 'delete_branch_input', with: 'fix'
click_link 'Delete protected branch' click_button 'Yes, delete protected branch'
find('input[data-testid="branch-search"]').set('fix') find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter) find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('No branches to show') expect(page).to have_content('No branches to show')
end end
context 'when the feature flag :delete_branch_confirmation_modals is disabled' do
before do
stub_feature_flags(delete_branch_confirmation_modals: false)
end
it 'removes branch after modal confirmation' do
visit project_branches_path(project)
find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('fix')
expect(find('.all-branches')).to have_selector('li', count: 1)
page.find('[data-target="#modal-delete-branch"]').click
expect(page).to have_css('.js-delete-branch[disabled]')
fill_in 'delete_branch_input', with: 'fix'
click_link 'Delete protected branch'
find('input[data-testid="branch-search"]').set('fix')
find('input[data-testid="branch-search"]').native.send_keys(:enter)
expect(page).to have_content('No branches to show')
end
end
end end
end end
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Delete branch modal Deleting a protected branch (for owner or maintainer) renders the modal correctly 1`] = `
"<div visible=\\"visible\\">
<gl-alert-stub title=\\"\\" dismisslabel=\\"Dismiss\\" variant=\\"danger\\" primarybuttonlink=\\"\\" primarybuttontext=\\"\\" secondarybuttonlink=\\"\\" secondarybuttontext=\\"\\" class=\\"gl-mb-5\\">
<div data-testid=\\"modal-message\\">
<gl-sprintf-stub message=\\"You're about to permanently delete the protected branch %{strongStart}test_modal.%{strongEnd}\\"></gl-sprintf-stub>
<p class=\\"gl-mb-0 gl-mt-4\\">
This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it.
</p>
</div>
</gl-alert-stub>
<form action=\\"/path/to/branch\\" method=\\"post\\">
<div class=\\"gl-mt-4\\">
<p>
<gl-sprintf-stub message=\\"Once you confirm and press %{strongStart}Yes, delete protected branch,%{strongEnd} it cannot be undone or recovered.\\"></gl-sprintf-stub>
</p>
<p>
<gl-sprintf-stub message=\\"Please type the following to confirm:\\"></gl-sprintf-stub> <code class=\\"gl-white-space-pre-wrap\\"> test_modal </code>
<b-form-input-stub name=\\"delete_branch_input\\" value=\\"\\" autocomplete=\\"off\\" debounce=\\"0\\" type=\\"text\\" aria-labelledby=\\"input-label\\" class=\\"gl-form-input gl-mt-4\\"></b-form-input-stub>
</p>
</div> <input type=\\"hidden\\" name=\\"_method\\" value=\\"delete\\"> <input type=\\"hidden\\" name=\\"authenticity_token\\">
</form>
<div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0\\">
<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" class=\\"gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">
Cancel, keep branch
</span></b-button-stub>
<div class=\\"gl-mr-3\\"></div>
<b-button-stub disabled=\\"true\\" size=\\"md\\" variant=\\"danger\\" type=\\"button\\" tag=\\"button\\" data-qa-selector=\\"delete_branch_confirmation_button\\" data-testid=\\"delete_branch_confirmation_button\\" class=\\"gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Yes, delete protected branch</span></b-button-stub>
</div>
</div>"
`;
exports[`Delete branch modal Deleting a regular branch renders the modal correctly 1`] = `
"<div visible=\\"visible\\">
<gl-alert-stub title=\\"\\" dismisslabel=\\"Dismiss\\" variant=\\"danger\\" primarybuttonlink=\\"\\" primarybuttontext=\\"\\" secondarybuttonlink=\\"\\" secondarybuttontext=\\"\\" class=\\"gl-mb-5\\">
<div data-testid=\\"modal-message\\">
<gl-sprintf-stub message=\\"You're about to permanently delete the branch %{strongStart}test_modal.%{strongEnd}\\"></gl-sprintf-stub>
<p class=\\"gl-mb-0 gl-mt-4\\">
This branch hasn’t been merged into default. To avoid data loss, consider merging this branch before deleting it.
</p>
</div>
</gl-alert-stub>
<form action=\\"/path/to/branch\\" method=\\"post\\">
<div>
<p class=\\"gl-mt-4\\">
<gl-sprintf-stub message=\\"Deleting the %{strongStart}test_modal%{strongEnd} branch cannot be undone. Are you sure?\\"></gl-sprintf-stub>
</p>
</div> <input type=\\"hidden\\" name=\\"_method\\" value=\\"delete\\"> <input type=\\"hidden\\" name=\\"authenticity_token\\">
</form>
<div class=\\"gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0\\">
<b-button-stub size=\\"md\\" variant=\\"default\\" type=\\"button\\" tag=\\"button\\" class=\\"gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">
Cancel, keep branch
</span></b-button-stub>
<div class=\\"gl-mr-3\\"></div>
<b-button-stub size=\\"md\\" variant=\\"danger\\" type=\\"button\\" tag=\\"button\\" data-qa-selector=\\"delete_branch_confirmation_button\\" data-testid=\\"delete_branch_confirmation_button\\" class=\\"gl-button\\">
<!---->
<!----> <span class=\\"gl-button-text\\">Yes, delete branch</span></b-button-stub>
</div>
</div>"
`;
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteBranchButton from '~/branches/components/delete_branch_button.vue';
import eventHub from '~/branches/event_hub';
let wrapper;
let findDeleteButton;
const createComponent = (props = {}) => {
wrapper = shallowMount(DeleteBranchButton, {
propsData: {
branchName: 'test',
deletePath: '/path/to/branch',
defaultBranchName: 'main',
...props,
},
});
};
describe('Delete branch button', () => {
let eventHubSpy;
beforeEach(() => {
findDeleteButton = () => wrapper.findComponent(GlButton);
eventHubSpy = jest.spyOn(eventHub, '$emit');
});
afterEach(() => {
wrapper.destroy();
});
it('renders the button with correct tooltip, style, and icon', () => {
createComponent();
expect(findDeleteButton().attributes()).toMatchObject({
title: 'Delete branch',
variant: 'danger',
icon: 'remove',
});
});
it('renders a different tooltip for a protected branch', () => {
createComponent({ isProtectedBranch: true });
expect(findDeleteButton().attributes('title')).toBe('Delete protected branch');
});
it('emits the data to eventHub when button is clicked', () => {
createComponent({ merged: true });
findDeleteButton().vm.$emit('click');
expect(eventHubSpy).toHaveBeenCalledWith('openModal', {
branchName: 'test',
defaultBranchName: 'main',
deletePath: '/path/to/branch',
isProtectedBranch: false,
merged: true,
});
});
describe('#disabled', () => {
it('does not disable the button by default when mounted', () => {
createComponent();
expect(findDeleteButton().attributes('disabled')).not.toBe('true');
});
// Used for unallowed users and for the default branch.
it('disables the button when mounted for a disabled modal', () => {
createComponent({ disabled: true });
expect(findDeleteButton().attributes('disabled')).toBe('true');
});
});
});
import { GlButton, GlModal, GlFormInput } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import DeleteBranchModal from '~/branches/components/delete_branch_modal.vue';
let wrapper;
const branchName = 'test_modal';
const createComponent = (data = {}) => {
wrapper = shallowMount(DeleteBranchModal, {
data() {
return {
branchName,
deletePath: '/path/to/branch',
defaultBranchName: 'default',
...data,
};
},
attrs: {
visible: true,
},
stubs: {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
}),
GlButton,
GlFormInput,
},
});
};
const findDeleteButton = () => wrapper.find('[data-testid="delete_branch_confirmation_button"]');
const findFormInput = () => wrapper.findComponent(GlFormInput);
describe('Delete branch modal', () => {
afterEach(() => {
wrapper.destroy();
});
describe('Deleting a regular branch', () => {
beforeEach(() => {
createComponent();
});
it('renders the modal correctly', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('submits the form when clicked', () => {
const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
return wrapper.vm.$nextTick().then(() => {
findDeleteButton().trigger('click');
expect(submitFormSpy).toHaveBeenCalled();
});
});
});
describe('Deleting a protected branch (for owner or maintainer)', () => {
beforeEach(() => {
createComponent({ isProtectedBranch: true });
});
it('renders the modal correctly', () => {
expect(wrapper.html()).toMatchSnapshot();
});
it('disables the delete button when branch name input is unconfirmed', () => {
expect(findDeleteButton().attributes('disabled')).toBe('true');
});
it('enables the delete button when branch name input is confirmed', () => {
return wrapper.vm
.$nextTick()
.then(() => {
findFormInput().vm.$emit('input', branchName);
})
.then(() => {
expect(findDeleteButton()).not.toBeDisabled();
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment