Commit fd0d60b8 authored by Jacques Erasmus's avatar Jacques Erasmus

Merge branch 'ek-explore-danger-modal' into 'master'

Add confirm_danger_modal component

See merge request gitlab-org/gitlab!71395
parents 52715b71 c012144b
...@@ -3,14 +3,14 @@ import { Rails } from '~/lib/utils/rails_ujs'; ...@@ -3,14 +3,14 @@ import { Rails } from '~/lib/utils/rails_ujs';
import { rstrip } from './lib/utils/common_utils'; import { rstrip } from './lib/utils/common_utils';
function openConfirmDangerModal($form, $modal, text) { function openConfirmDangerModal($form, $modal, text) {
const $input = $('.js-confirm-danger-input', $modal); const $input = $('.js-legacy-confirm-danger-input', $modal);
$input.val(''); $input.val('');
$('.js-confirm-text', $modal).text(text || ''); $('.js-confirm-text', $modal).text(text || '');
$modal.modal('show'); $modal.modal('show');
const confirmTextMatch = $('.js-confirm-danger-match', $modal).text(); const confirmTextMatch = $('.js-legacy-confirm-danger-match', $modal).text();
const $submit = $('.js-confirm-danger-submit', $modal); const $submit = $('.js-legacy-confirm-danger-submit', $modal);
$submit.disable(); $submit.disable();
$input.focus(); $input.focus();
...@@ -25,7 +25,7 @@ function openConfirmDangerModal($form, $modal, text) { ...@@ -25,7 +25,7 @@ function openConfirmDangerModal($form, $modal, text) {
}); });
// eslint-disable-next-line @gitlab/no-global-event-off // eslint-disable-next-line @gitlab/no-global-event-off
$('.js-confirm-danger-submit', $modal) $('.js-legacy-confirm-danger-submit', $modal)
.off('click') .off('click')
.on('click', () => { .on('click', () => {
if ($form.data('remote')) { if ($form.data('remote')) {
...@@ -47,7 +47,7 @@ function getModal($btn) { ...@@ -47,7 +47,7 @@ function getModal($btn) {
} }
export default function initConfirmDangerModal() { export default function initConfirmDangerModal() {
$(document).on('click', '.js-confirm-danger', (e) => { $(document).on('click', '.js-legacy-confirm-danger', (e) => {
const $btn = $(e.target); const $btn = $(e.target);
const checkFieldName = $btn.data('checkFieldName'); const checkFieldName = $btn.data('checkFieldName');
const checkFieldCompareValue = $btn.data('checkCompareValue'); const checkFieldCompareValue = $btn.data('checkCompareValue');
......
import Vue from 'vue';
import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
export default () => {
const el = document.querySelector('.js-confirm-danger');
if (!el) return null;
const { phrase, buttonText, confirmDangerMessage } = el.dataset;
return new Vue({
el,
render: (createElement) =>
createElement(ConfirmDanger, {
props: {
phrase,
buttonText,
},
provide: {
confirmDangerMessage,
},
}),
});
};
import { PROJECT_BADGE } from '~/badges/constants'; import { PROJECT_BADGE } from '~/badges/constants';
import initConfirmDangerModal from '~/confirm_danger_modal'; import initLegacyConfirmDangerModal from '~/confirm_danger_modal';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers'; import initFilePickers from '~/file_pickers';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings'; import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
...@@ -13,7 +13,7 @@ import initProjectPermissionsSettings from '../shared/permissions'; ...@@ -13,7 +13,7 @@ import initProjectPermissionsSettings from '../shared/permissions';
import initProjectLoadingSpinner from '../shared/save_project_loader'; import initProjectLoadingSpinner from '../shared/save_project_loader';
initFilePickers(); initFilePickers();
initConfirmDangerModal(); initLegacyConfirmDangerModal();
initSettingsPanels(); initSettingsPanels();
initProjectDeleteButton(); initProjectDeleteButton();
mountBadgeSettings(PROJECT_BADGE); mountBadgeSettings(PROJECT_BADGE);
......
<script>
import { GlButton, GlModalDirective } from '@gitlab/ui';
import { CONFIRM_DANGER_MODAL_ID } from './constants';
import ConfirmDangerModal from './confirm_danger_modal.vue';
export default {
name: 'ConfirmDanger',
components: {
GlButton,
ConfirmDangerModal,
},
directives: {
GlModal: GlModalDirective,
},
props: {
disabled: {
type: Boolean,
required: false,
default: false,
},
phrase: {
type: String,
required: true,
},
buttonText: {
type: String,
required: true,
},
},
modalId: CONFIRM_DANGER_MODAL_ID,
};
</script>
<template>
<div>
<gl-button
v-gl-modal="$options.modalId"
class="gl-button"
variant="danger"
:disabled="disabled"
data-testid="confirm-danger-button"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
:modal-id="$options.modalId"
:phrase="phrase"
@confirm="$emit('confirm')"
/>
</div>
</template>
<script>
import { GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
import {
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_TITLE,
CONFIRM_DANGER_PHRASE_TEXT,
CONFIRM_DANGER_WARNING,
} from './constants';
export default {
name: 'ConfirmDangerModal',
components: {
GlModal,
GlFormGroup,
GlFormInput,
GlSprintf,
},
inject: {
confirmDangerMessage: {
default: '',
},
confirmButtonText: {
default: CONFIRM_DANGER_MODAL_BUTTON,
},
},
props: {
modalId: {
type: String,
required: true,
},
phrase: {
type: String,
required: true,
},
},
data() {
return { confirmationPhrase: '' };
},
computed: {
isValid() {
return (
this.confirmationPhrase.length && this.equalString(this.confirmationPhrase, this.phrase)
);
},
actionPrimary() {
return {
text: this.confirmButtonText,
attributes: [{ variant: 'danger', disabled: !this.isValid }],
};
},
},
methods: {
equalString(a, b) {
return a.trim().toLowerCase() === b.trim().toLowerCase();
},
},
i18n: {
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_TITLE,
CONFIRM_DANGER_WARNING,
CONFIRM_DANGER_PHRASE_TEXT,
},
};
</script>
<template>
<gl-modal
ref="modal"
:modal-id="modalId"
:data-testid="modalId"
:title="$options.i18n.CONFIRM_DANGER_MODAL_TITLE"
:action-primary="actionPrimary"
@primary="$emit('confirm')"
>
<p v-if="confirmDangerMessage" class="text-danger" data-testid="confirm-danger-message">
{{ confirmDangerMessage }}
</p>
<p data-testid="confirm-danger-warning">{{ $options.i18n.CONFIRM_DANGER_WARNING }}</p>
<p data-testid="confirm-danger-phrase">
<gl-sprintf :message="$options.i18n.CONFIRM_DANGER_PHRASE_TEXT">
<template #phrase_code>
<code>{{ phrase }}</code>
</template>
</gl-sprintf>
</p>
<gl-form-group class="form-control" :state="isValid">
<gl-form-input v-model="confirmationPhrase" data-testid="confirm-danger-input" type="text" />
</gl-form-group>
</gl-modal>
</template>
import { __ } from '~/locale';
export const CONFIRM_DANGER_MODAL_ID = 'confirm-danger-modal';
export const CONFIRM_DANGER_MODAL_TITLE = __('Confirmation required');
export const CONFIRM_DANGER_MODAL_BUTTON = __('Confirm');
export const CONFIRM_DANGER_WARNING = __(
'This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.',
);
export const CONFIRM_DANGER_PHRASE_TEXT = __(
'Please type %{phrase_code} to proceed or close this modal to cancel.',
);
...@@ -4,4 +4,4 @@ ...@@ -4,4 +4,4 @@
.gl-alert-body .gl-alert-body
= html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe } = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
= button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' } = button_to _('Remove group'), '#', class: ['btn gl-button btn-danger js-legacy-confirm-danger', ('disabled' if group.paid?)], data: { 'confirm-danger-message' => remove_group_message(group), 'testid' => 'remove-group-button' }
...@@ -8,4 +8,4 @@ ...@@ -8,4 +8,4 @@
%p %p
%strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.') %strong= _('Once removed, the fork relationship cannot be restored. This project will no longer be able to receive or send merge requests to the source project or other forks.')
= link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer' = link_to _('Learn more.'), help_page_path('user/project/settings/index', anchor: 'removing-a-fork-relationship'), target: '_blank', rel: 'noopener noreferrer'
= button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) } = button_to _('Remove fork relationship'), '#', class: "gl-button btn btn-danger js-legacy-confirm-danger", data: { "confirm-danger-message" => remove_fork_project_warning_message(@project) }
...@@ -14,4 +14,4 @@ ...@@ -14,4 +14,4 @@
= label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold' = label_tag :new_namespace_id, _('Select a new namespace'), class: 'gl-font-weight-bold'
.form-group .form-group
= select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2' = select_tag :new_namespace_id, namespaces_options(nil), include_blank: true, class: 'select2'
= f.submit 'Transfer project', class: "gl-button btn btn-danger js-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) } = f.submit 'Transfer project', class: "gl-button btn btn-danger js-legacy-confirm-danger qa-transfer-button", data: { "confirm-danger-message" => transfer_project_message(@project) }
...@@ -20,10 +20,10 @@ ...@@ -20,10 +20,10 @@
%li %li
= _("Current forks will keep their visibility level.").html_safe = _("Current forks will keep their visibility level.").html_safe
%label{ for: "confirm_path_input" } %label{ for: "confirm_path_input" }
= _("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } } = _("To confirm, type %{phrase_code}").html_safe % { phrase_code: '<code class="js-legacy-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: @project.full_path } }
.form-group .form-group
= text_field_tag 'confirm_path_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input' = text_field_tag 'confirm_path_input', '', class: 'form-control js-legacy-confirm-danger-input qa-confirm-input'
.form-actions .form-actions
%button.btn.gl-button.btn-default.gl-mr-4{ type: "button", "data-dismiss": "modal" } %button.btn.gl-button.btn-default.gl-mr-4{ type: "button", "data-dismiss": "modal" }
= _('Cancel') = _('Cancel')
= submit_tag _('Reduce project visibility'), class: "btn gl-button btn-danger js-confirm-danger-submit qa-confirm-button", disabled: true = submit_tag _('Reduce project visibility'), class: "btn gl-button btn-danger js-legacy-confirm-danger-submit qa-confirm-button", disabled: true
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.js-project-permissions-form .js-project-permissions-form
- if show_visibility_confirm_modal?(@project) - if show_visibility_confirm_modal?(@project)
= render "visibility_modal" = render "visibility_modal"
= f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level } = f.submit _('Save changes'), class: "btn gl-button btn-confirm #{('js-legacy-confirm-danger' if show_visibility_confirm_modal?(@project))}", data: { qa_selector: 'visibility_features_permissions_save_button', check_field_name: ("project[visibility_level]" if show_visibility_confirm_modal?(@project)), check_compare_value: @project.visibility_level }
%section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } } %section.rspec-merge-request-settings.settings.merge-requests-feature.no-animate#js-merge-request-settings{ class: [('expanded' if expanded), ('hidden' if @project.project_feature.send(:merge_requests_access_level) == 0)], data: { qa_selector: 'merge_request_settings_content' } }
.settings-header .settings-header
......
...@@ -12,10 +12,10 @@ ...@@ -12,10 +12,10 @@
%p %p
%span.js-warning-text= _('This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.') %span.js-warning-text= _('This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.')
%br %br
- phrase_code = '<code class="js-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: phrase } - phrase_code = '<code class="js-legacy-confirm-danger-match">%{phrase_name}</code>'.html_safe % { phrase_name: phrase }
= _('Please type %{phrase_code} to proceed or close this modal to cancel.').html_safe % { phrase_code: phrase_code } = _('Please type %{phrase_code} to proceed or close this modal to cancel.').html_safe % { phrase_code: phrase_code }
.form-group .form-group
= text_field_tag 'confirm_name_input', '', class: 'form-control js-confirm-danger-input qa-confirm-input' = text_field_tag 'confirm_name_input', '', class: 'form-control js-legacy-confirm-danger-input qa-confirm-input'
.form-actions .form-actions
= submit_tag _('Confirm'), class: "gl-button btn btn-danger js-confirm-danger-submit qa-confirm-button" = submit_tag _('Confirm'), class: "gl-button btn btn-danger js-legacy-confirm-danger-submit qa-confirm-button"
...@@ -14,5 +14,5 @@ ...@@ -14,5 +14,5 @@
= _("Only proceed if you trust %{idp_url} to control your GitLab account sign in.") % { idp_url: @unauthenticated_group.saml_provider.sso_url } = _("Only proceed if you trust %{idp_url} to control your GitLab account sign in.") % { idp_url: @unauthenticated_group.saml_provider.sso_url }
.submit-container .submit-container
= button_to _("Transfer ownership"), '#', class: 'gl-button btn btn-danger js-confirm-danger', data: { 'confirm-danger-message' => transfer_ownership_message(@group_name), qa_selector: 'transfer_ownership_button' } = button_to _("Transfer ownership"), '#', class: 'gl-button btn btn-danger js-legacy-confirm-danger', data: { 'confirm-danger-message' => transfer_ownership_message(@group_name), qa_selector: 'transfer_ownership_button' }
= render 'shared/confirm_modal', phrase: current_user.username = render 'shared/confirm_modal', phrase: current_user.username
...@@ -22,7 +22,7 @@ RSpec.describe 'User changes public project visibility', :js do ...@@ -22,7 +22,7 @@ RSpec.describe 'User changes public project visibility', :js do
click_button 'Save changes' click_button 'Save changes'
end end
find('.js-confirm-danger-input').send_keys(project.path_with_namespace) find('.js-legacy-confirm-danger-input').send_keys(project.path_with_namespace)
page.within '.modal' do page.within '.modal' do
click_button 'Reduce project visibility' click_button 'Reduce project visibility'
......
import { GlModal, GlSprintf } from '@gitlab/ui';
import {
CONFIRM_DANGER_WARNING,
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_ID,
} from '~/vue_shared/components/confirm_danger/constants';
import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Confirm Danger Modal', () => {
const confirmDangerMessage = 'This is a dangerous activity';
const confirmButtonText = 'Confirm button text';
const phrase = 'You must construct additional pylons';
const modalId = CONFIRM_DANGER_MODAL_ID;
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmationPhrase = () => wrapper.findByTestId('confirm-danger-phrase');
const findConfirmationInput = () => wrapper.findByTestId('confirm-danger-input');
const findDefaultWarning = () => wrapper.findByTestId('confirm-danger-warning');
const findAdditionalMessage = () => wrapper.findByTestId('confirm-danger-message');
const findPrimaryAction = () => findModal().props('actionPrimary');
const findPrimaryActionAttributes = (attr) => findPrimaryAction().attributes[0][attr];
const createComponent = ({ provide = {} } = {}) =>
shallowMountExtended(ConfirmDangerModal, {
propsData: {
modalId,
phrase,
},
provide,
stubs: { GlSprintf },
});
beforeEach(() => {
wrapper = createComponent({ provide: { confirmDangerMessage, confirmButtonText } });
});
afterEach(() => {
wrapper.destroy();
});
it('renders the default warning message', () => {
expect(findDefaultWarning().text()).toBe(CONFIRM_DANGER_WARNING);
});
it('renders any additional messages', () => {
expect(findAdditionalMessage().text()).toBe(confirmDangerMessage);
});
it('renders the confirm button', () => {
expect(findPrimaryAction().text).toBe(confirmButtonText);
expect(findPrimaryActionAttributes('variant')).toBe('danger');
});
it('renders the correct confirmation phrase', () => {
expect(findConfirmationPhrase().text()).toBe(
`Please type ${phrase} to proceed or close this modal to cancel.`,
);
});
describe('without injected data', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('does not render any additional messages', () => {
expect(findAdditionalMessage().exists()).toBe(false);
});
it('renders the default confirm button', () => {
expect(findPrimaryAction().text).toBe(CONFIRM_DANGER_MODAL_BUTTON);
});
});
describe('with a valid confirmation phrase', () => {
beforeEach(() => {
wrapper = createComponent();
});
it('enables the confirm button', async () => {
expect(findPrimaryActionAttributes('disabled')).toBe(true);
await findConfirmationInput().vm.$emit('input', phrase);
expect(findPrimaryActionAttributes('disabled')).toBe(false);
});
it('emits a `confirm` event when the button is clicked', async () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
await findConfirmationInput().vm.$emit('input', phrase);
await findModal().vm.$emit('primary');
expect(wrapper.emitted('confirm')).not.toBeUndefined();
});
});
});
import { GlButton } from '@gitlab/ui';
import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
import ConfirmDangerModal from '~/vue_shared/components/confirm_danger/confirm_danger_modal.vue';
import { CONFIRM_DANGER_MODAL_ID } from '~/vue_shared/components/confirm_danger/constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Confirm Danger Modal', () => {
let wrapper;
const phrase = 'En Taro Adun';
const buttonText = 'Click me!';
const modalId = CONFIRM_DANGER_MODAL_ID;
const findBtn = () => wrapper.findComponent(GlButton);
const findModal = () => wrapper.findComponent(ConfirmDangerModal);
const findModalProps = () => findModal().props();
const createComponent = (props = {}) =>
shallowMountExtended(ConfirmDanger, {
propsData: {
buttonText,
phrase,
...props,
},
});
beforeEach(() => {
wrapper = createComponent();
});
afterEach(() => {
wrapper.destroy();
});
it('renders the button', () => {
expect(wrapper.html()).toContain(buttonText);
});
it('sets the modal properties', () => {
expect(findModalProps()).toMatchObject({
modalId,
phrase,
});
});
it('will disable the button if `disabled=true`', () => {
expect(findBtn().attributes('disabled')).toBeUndefined();
wrapper = createComponent({ disabled: true });
expect(findBtn().attributes('disabled')).toBe('true');
});
it('will emit `confirm` when the modal confirms', () => {
expect(wrapper.emitted('confirm')).toBeUndefined();
findModal().vm.$emit('confirm');
expect(wrapper.emitted('confirm')).not.toBeUndefined();
});
});
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