Commit db839bf2 authored by Paul Slaughter's avatar Paul Slaughter

Merge branch '202125-update-removal-modal' into 'master'

Geo - Replace Browser Confirm with Modal

Closes #202125

See merge request gitlab-org/gitlab!24808
parents 7974883f 2a122af1
import Vue from 'vue'; import Vue from 'vue';
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
const mountConfirmModal = button => { const mountConfirmModal = () => {
const props = {
path: button.dataset.path,
method: button.dataset.method,
modalAttributes: JSON.parse(button.dataset.modalAttributes),
};
return new Vue({ return new Vue({
render(h) { data() {
return h(ConfirmModal, { props }); return {
path: '',
method: '',
modalAttributes: null,
showModal: false,
};
}, },
}).$mount(); mounted() {
}; document.querySelectorAll('.js-confirm-modal-button').forEach(button => {
export default () => {
document.getElementsByClassName('js-confirm-modal-button').forEach(button => {
button.addEventListener('click', e => { button.addEventListener('click', e => {
e.preventDefault(); e.preventDefault();
mountConfirmModal(button); this.path = button.dataset.path;
this.method = button.dataset.method;
this.modalAttributes = JSON.parse(button.dataset.modalAttributes);
this.showModal = true;
});
}); });
},
methods: {
dismiss() {
this.showModal = false;
},
},
render(h) {
return h(ConfirmModal, {
props: {
path: this.path,
method: this.method,
modalAttributes: this.modalAttributes,
showModal: this.showModal,
},
on: { dismiss: this.dismiss },
}); });
},
}).$mount();
}; };
export default () => mountConfirmModal();
...@@ -9,34 +9,43 @@ export default { ...@@ -9,34 +9,43 @@ export default {
props: { props: {
modalAttributes: { modalAttributes: {
type: Object, type: Object,
required: true, required: false,
default: () => {
return {};
},
}, },
path: { path: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
method: { method: {
type: String, type: String,
required: true, required: false,
default: '',
}, },
showModal: {
type: Boolean,
required: false,
default: false,
}, },
data() {
return {
isDismissed: false,
};
}, },
mounted() { watch: {
showModal(val) {
if (val) {
// Wait for v-if to render
this.$nextTick(() => {
this.openModal(); this.openModal();
});
}
},
}, },
methods: { methods: {
openModal() { openModal() {
this.$refs.modal.show(); this.$refs.modal.show();
}, },
submitModal() { submitModal() {
this.$refs.form.requestSubmit(); this.$refs.form.submit();
},
dismiss() {
this.isDismissed = true;
}, },
}, },
csrf, csrf,
...@@ -45,11 +54,11 @@ export default { ...@@ -45,11 +54,11 @@ export default {
<template> <template>
<gl-modal <gl-modal
v-if="!isDismissed" v-if="showModal"
ref="modal" ref="modal"
v-bind="modalAttributes" v-bind="modalAttributes"
@primary="submitModal" @primary="submitModal"
@canceled="dismiss" @canceled="$emit('dismiss')"
> >
<form ref="form" :action="path" method="post"> <form ref="form" :action="path" method="post">
<!-- Rails workaround for <form method="delete" /> <!-- Rails workaround for <form method="delete" />
......
import initVueAlerts from '~/vue_alerts'; import initVueAlerts from '~/vue_alerts';
import initConfirmModal from '~/confirm_modal';
import showToast from '~/vue_shared/plugins/global_toast'; import showToast from '~/vue_shared/plugins/global_toast';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initVueAlerts(); initVueAlerts();
initConfirmModal();
const toasts = document.querySelectorAll('.js-toast-message'); const toasts = document.querySelectorAll('.js-toast-message');
toasts.forEach(toast => showToast(toast.dataset.message)); toasts.forEach(toast => showToast(toast.dataset.message));
......
...@@ -153,5 +153,19 @@ module EE ...@@ -153,5 +153,19 @@ module EE
s_('Geo|Unknown state') s_('Geo|Unknown state')
end end
end end
def remove_tracking_entry_modal_data(path)
{
path: path,
method: 'delete',
modal_attributes: {
modalId: 'geo-entry-removal-modal',
title: s_('Geo|Remove tracking database entry'),
message: s_('Geo|Tracking database entry will be removed. Are you sure?'),
okVariant: 'danger',
okTitle: s_('Geo|Remove entry')
}
}
end
end end
end end
%strong.text-truncate.flex-fill %strong.text-truncate.flex-fill
= s_('Geo|Project (ID: %{project_id}) no longer exists on the primary. It is safe to remove this entry, as this will not remove any data on disk.') % { project_id: project_registry.project_id } = s_('Geo|Project (ID: %{project_id}) no longer exists on the primary. It is safe to remove this entry, as this will not remove any data on disk.') % { project_id: project_registry.project_id }
= link_to(admin_geo_project_path(project_registry), data: { confirm: s_('Geo|Tracking entry will be removed. Are you sure?')}, method: :delete, class: 'btn btn-inverted btn-remove btn-sm') do = button_tag s_('Geo|Remove'), type: "button", class: 'btn btn-danger btn-inverted js-confirm-modal-button', data: remove_tracking_entry_modal_data(admin_geo_project_path(project_registry))
= s_('Geo|Remove')
...@@ -4,8 +4,7 @@ ...@@ -4,8 +4,7 @@
%strong.text-truncate.flex-fill %strong.text-truncate.flex-fill
= upload_registry.file = upload_registry.file
- unless upload_registry.upload - unless upload_registry.upload
= link_to(admin_geo_upload_path(upload_registry), data: { confirm: s_('Geo|Tracking entry will be removed. Are you sure?')}, method: :delete, class: 'btn btn-inverted btn-remove btn-sm') do = button_tag s_('Geo|Remove'), type: "button", class: 'btn btn-danger btn-inverted js-confirm-modal-button', data: remove_tracking_entry_modal_data(admin_geo_upload_path(upload_registry))
= s_('Geo|Remove')
.card-body .card-body
.container.m-0.p-0 .container.m-0.p-0
.row .row
......
...@@ -157,4 +157,27 @@ describe 'admin Geo Projects', :js, :geo do ...@@ -157,4 +157,27 @@ describe 'admin Geo Projects', :js, :geo do
it_behaves_like 'shows tab specific projects and correct labels' it_behaves_like 'shows tab specific projects and correct labels'
end end
describe 'remove an orphaned Tracking Entry' do
before do
synced_registry.project.destroy!
visit(admin_geo_projects_path(sync_status: :synced))
wait_for_requests
end
it 'removes an existing Geo Project' do
card_count = page.all(:css, '.project-card').length
page.within(find('.project-card', match: :first)) do
page.click_button('Remove')
end
page.within('.modal') do
page.click_button('Remove entry')
end
# Wait for remove confirmation
expect(page.find('.gl-toast')).to have_text('removed')
expect(page.all(:css, '.project-card').length).to be(card_count - 1)
end
end
end end
# frozen_string_literal: true
require 'spec_helper'
describe 'admin Geo Uploads', :js, :geo do
let!(:geo_node) { create(:geo_node) }
let!(:synced_registry) { create(:geo_upload_registry, :with_file, :attachment, success: true) }
before do
allow(Gitlab::Geo).to receive(:license_allows?).and_return(true)
sign_in(create(:admin))
end
describe 'remove an orphaned Tracking Entry' do
before do
synced_registry.upload.destroy!
visit(admin_geo_uploads_path)
wait_for_requests
end
it 'removes an existing Geo Upload' do
card_count = page.all(:css, '.upload-card').length
page.within(find('.upload-card', match: :first)) do
page.click_button('Remove')
end
page.within('.modal') do
page.click_button('Remove entry')
end
# Wait for remove confirmation
expect(page.find('.gl-toast')).to have_text('removed')
expect(page.all(:css, '.upload-card').length).to be(card_count - 1)
end
end
end
...@@ -9250,6 +9250,12 @@ msgstr "" ...@@ -9250,6 +9250,12 @@ msgstr ""
msgid "Geo|Remove" msgid "Geo|Remove"
msgstr "" msgstr ""
msgid "Geo|Remove entry"
msgstr ""
msgid "Geo|Remove tracking database entry"
msgstr ""
msgid "Geo|Repository sync capacity" msgid "Geo|Repository sync capacity"
msgstr "" msgstr ""
...@@ -9301,13 +9307,13 @@ msgstr "" ...@@ -9301,13 +9307,13 @@ msgstr ""
msgid "Geo|This is a primary node" msgid "Geo|This is a primary node"
msgstr "" msgstr ""
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed." msgid "Geo|Tracking database entry will be removed. Are you sure?"
msgstr "" msgstr ""
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed." msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgstr "" msgstr ""
msgid "Geo|Tracking entry will be removed. Are you sure?" msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
msgstr "" msgstr ""
msgid "Geo|URL" msgid "Geo|URL"
......
HTMLFormElement.prototype.submit = jest.fn();
import './element_scroll_into_view'; import './element_scroll_into_view';
import './form_element';
import './get_client_rects'; import './get_client_rects';
import './inner_text'; import './inner_text';
import './window_scroll_to'; import './window_scroll_to';
......
...@@ -3,6 +3,8 @@ import { GlModal } from '@gitlab/ui'; ...@@ -3,6 +3,8 @@ import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue'; import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
describe('vue_shared/components/confirm_modal', () => { describe('vue_shared/components/confirm_modal', () => {
const testModalProps = { const testModalProps = {
path: `${TEST_HOST}/1`, path: `${TEST_HOST}/1`,
...@@ -39,45 +41,61 @@ describe('vue_shared/components/confirm_modal', () => { ...@@ -39,45 +41,61 @@ describe('vue_shared/components/confirm_modal', () => {
}); });
const findModal = () => wrapper.find(GlModal); const findModal = () => wrapper.find(GlModal);
const findForm = () => wrapper.find('form');
const findFormData = () =>
findForm()
.findAll('input')
.wrappers.map(x => ({ name: x.attributes('name'), value: x.attributes('value') }));
describe('template', () => { describe('template', () => {
describe('when showModal is false', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
it('calls openModal on mount', () => { it('does not render GlModal', () => {
expect(actionSpies.openModal).toHaveBeenCalled(); expect(findModal().exists()).toBeFalsy();
});
});
describe('when showModal is true', () => {
beforeEach(() => {
createComponent({ showModal: true });
}); });
it('renders GlModal', () => { it('renders GlModal', () => {
expect(findModal().exists()).toBeTruthy(); expect(findModal().exists()).toBeTruthy();
expect(findModal().attributes()).toEqual(
expect.objectContaining({
modalid: testModalProps.modalAttributes.modalId,
oktitle: testModalProps.modalAttributes.okTitle,
okvariant: testModalProps.modalAttributes.okVariant,
}),
);
});
}); });
}); });
describe('methods', () => { describe('methods', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({ showModal: true });
}); });
describe('submitModal', () => { it('does not submit form', () => {
beforeEach(() => { expect(findForm().element.submit).not.toHaveBeenCalled();
wrapper.vm.$refs.form.requestSubmit = jest.fn();
}); });
it('calls requestSubmit', () => { describe('when modal submitted', () => {
wrapper.vm.submitModal(); beforeEach(() => {
expect(wrapper.vm.$refs.form.requestSubmit).toHaveBeenCalled(); findModal().vm.$emit('primary');
});
}); });
describe('dismiss', () => { it('submits form', () => {
it('removes gl-modal', () => { expect(findFormData()).toEqual([
expect(findModal().exists()).toBeTruthy(); { name: '_method', value: testModalProps.method },
wrapper.vm.dismiss(); { name: 'authenticity_token', value: 'test-csrf-token' },
]);
return wrapper.vm.$nextTick(() => { expect(findForm().element.submit).toHaveBeenCalled();
expect(findModal().exists()).toBeFalsy();
});
}); });
}); });
}); });
......
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