Commit 2a122af1 authored by Zack Cuddy's avatar Zack Cuddy

Replace browser confirm with GlModal (Geo)

Based on our new design specs we want to replace default browser
behavior with our Pajamas Design System.

This required us to replace the confirm with a modal confirm.
GlModal information can be found here:
https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-modal--default

This was not currently feasible so this work is built on top of
a new JS Confirm Modal that will be pulled into its own MR.
The issue tracking that info can be found here:
https://gitlab.com/gitlab-org/gitlab/issues/205900
parent 422c6d97
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
......@@ -9191,6 +9191,12 @@ msgstr ""
msgid "Geo|Remove"
msgstr ""
msgid "Geo|Remove entry"
msgstr ""
msgid "Geo|Remove tracking database entry"
msgstr ""
msgid "Geo|Repository sync capacity"
msgstr ""
......@@ -9242,13 +9248,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