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 ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
const mountConfirmModal = button => {
const props = {
path: button.dataset.path,
method: button.dataset.method,
modalAttributes: JSON.parse(button.dataset.modalAttributes),
};
const mountConfirmModal = () => {
return new Vue({
data() {
return {
path: '',
method: '',
modalAttributes: null,
showModal: false,
};
},
mounted() {
document.querySelectorAll('.js-confirm-modal-button').forEach(button => {
button.addEventListener('click', e => {
e.preventDefault();
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 });
return h(ConfirmModal, {
props: {
path: this.path,
method: this.method,
modalAttributes: this.modalAttributes,
showModal: this.showModal,
},
on: { dismiss: this.dismiss },
});
},
}).$mount();
};
export default () => {
document.getElementsByClassName('js-confirm-modal-button').forEach(button => {
button.addEventListener('click', e => {
e.preventDefault();
mountConfirmModal(button);
});
});
};
export default () => mountConfirmModal();
......@@ -9,34 +9,43 @@ export default {
props: {
modalAttributes: {
type: Object,
required: true,
required: false,
default: () => {
return {};
},
},
path: {
type: String,
required: true,
required: false,
default: '',
},
method: {
type: String,
required: true,
required: false,
default: '',
},
showModal: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
isDismissed: false,
};
},
mounted() {
this.openModal();
watch: {
showModal(val) {
if (val) {
// Wait for v-if to render
this.$nextTick(() => {
this.openModal();
});
}
},
},
methods: {
openModal() {
this.$refs.modal.show();
},
submitModal() {
this.$refs.form.requestSubmit();
},
dismiss() {
this.isDismissed = true;
this.$refs.form.submit();
},
},
csrf,
......@@ -45,11 +54,11 @@ export default {
<template>
<gl-modal
v-if="!isDismissed"
v-if="showModal"
ref="modal"
v-bind="modalAttributes"
@primary="submitModal"
@canceled="dismiss"
@canceled="$emit('dismiss')"
>
<form ref="form" :action="path" method="post">
<!-- Rails workaround for <form method="delete" />
......
import initVueAlerts from '~/vue_alerts';
import initConfirmModal from '~/confirm_modal';
import showToast from '~/vue_shared/plugins/global_toast';
document.addEventListener('DOMContentLoaded', () => {
initVueAlerts();
initConfirmModal();
const toasts = document.querySelectorAll('.js-toast-message');
toasts.forEach(toast => showToast(toast.dataset.message));
......
......@@ -153,5 +153,19 @@ module EE
s_('Geo|Unknown state')
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
%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 }
= 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
= s_('Geo|Remove')
= 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))
......@@ -4,8 +4,7 @@
%strong.text-truncate.flex-fill
= upload_registry.file
- 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
= s_('Geo|Remove')
= 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))
.card-body
.container.m-0.p-0
.row
......
......@@ -157,4 +157,27 @@ describe 'admin Geo Projects', :js, :geo do
it_behaves_like 'shows tab specific projects and correct labels'
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
# 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 ""
msgid "Geo|Remove"
msgstr ""
msgid "Geo|Remove entry"
msgstr ""
msgid "Geo|Remove tracking database entry"
msgstr ""
msgid "Geo|Repository sync capacity"
msgstr ""
......@@ -9301,13 +9307,13 @@ msgstr ""
msgid "Geo|This is a primary node"
msgstr ""
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgid "Geo|Tracking database entry will be removed. Are you sure?"
msgstr ""
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgstr ""
msgid "Geo|Tracking entry will be removed. Are you sure?"
msgid "Geo|Tracking entry for upload (%{type}/%{id}) was successfully removed."
msgstr ""
msgid "Geo|URL"
......
HTMLFormElement.prototype.submit = jest.fn();
import './element_scroll_into_view';
import './form_element';
import './get_client_rects';
import './inner_text';
import './window_scroll_to';
......
......@@ -3,6 +3,8 @@ import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import ConfirmModal from '~/vue_shared/components/confirm_modal.vue';
jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
describe('vue_shared/components/confirm_modal', () => {
const testModalProps = {
path: `${TEST_HOST}/1`,
......@@ -39,45 +41,61 @@ describe('vue_shared/components/confirm_modal', () => {
});
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', () => {
beforeEach(() => {
createComponent();
});
describe('when showModal is false', () => {
beforeEach(() => {
createComponent();
});
it('calls openModal on mount', () => {
expect(actionSpies.openModal).toHaveBeenCalled();
it('does not render GlModal', () => {
expect(findModal().exists()).toBeFalsy();
});
});
it('renders GlModal', () => {
expect(findModal().exists()).toBeTruthy();
describe('when showModal is true', () => {
beforeEach(() => {
createComponent({ showModal: true });
});
it('renders GlModal', () => {
expect(findModal().exists()).toBeTruthy();
expect(findModal().attributes()).toEqual(
expect.objectContaining({
modalid: testModalProps.modalAttributes.modalId,
oktitle: testModalProps.modalAttributes.okTitle,
okvariant: testModalProps.modalAttributes.okVariant,
}),
);
});
});
});
describe('methods', () => {
beforeEach(() => {
createComponent();
createComponent({ showModal: true });
});
describe('submitModal', () => {
beforeEach(() => {
wrapper.vm.$refs.form.requestSubmit = jest.fn();
});
it('calls requestSubmit', () => {
wrapper.vm.submitModal();
expect(wrapper.vm.$refs.form.requestSubmit).toHaveBeenCalled();
});
it('does not submit form', () => {
expect(findForm().element.submit).not.toHaveBeenCalled();
});
describe('dismiss', () => {
it('removes gl-modal', () => {
expect(findModal().exists()).toBeTruthy();
wrapper.vm.dismiss();
describe('when modal submitted', () => {
beforeEach(() => {
findModal().vm.$emit('primary');
});
return wrapper.vm.$nextTick(() => {
expect(findModal().exists()).toBeFalsy();
});
it('submits form', () => {
expect(findFormData()).toEqual([
{ name: '_method', value: testModalProps.method },
{ name: 'authenticity_token', value: 'test-csrf-token' },
]);
expect(findForm().element.submit).toHaveBeenCalled();
});
});
});
......
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