From a1a08ed1c7df15669b68c021e37f1ef21e8d14b9 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Wed, 27 Oct 2021 14:02:26 +0200 Subject: [PATCH 1/8] Use custom modal instead of browser confirm This overwrites `Rails.confirm` from `@rails/ujs` in order to show a modal instead of a Browser `window.confirm`. --- .../lib/utils/confirm_via_gl_modal.js | 46 +++++++++++++++++++ app/assets/javascripts/lib/utils/rails_ujs.js | 32 +++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 app/assets/javascripts/lib/utils/confirm_via_gl_modal.js diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js new file mode 100644 index 00000000000000..c3181328780894 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js @@ -0,0 +1,46 @@ +import Vue from 'vue'; +import { GlModal } from '@gitlab/ui'; +import { __ } from '~/locale'; + +export default function confirmViaGLModal(message) { + return new Promise((resolve) => { + let confirmed = false; + + const modal = new Vue({ + mounted() { + this.$refs.modal.show(); + }, + render(h) { + return h( + GlModal, + { + props: { + // TODO: The modal has no title and looks weird, we need a title! We should talk to UX + modalId: 'ConfirmationModal', + actionPrimary: { + // TODO: Idea: We could get the text and color of the button we clicked on and use those + // AND FALLBACK to `OK` for example + text: __('OK'), + }, + actionCancel: { + // TODO: Better word than cancel? + text: __('Cancel'), + }, + }, + ref: 'modal', + on: { + primary() { + confirmed = true; + }, + hidden() { + modal.$destroy(); + resolve(confirmed); + }, + }, + }, + [message], + ); + }, + }).$mount(); + }); +} diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index 8b40cc7bd11169..b6644dff7e4ae1 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -1,4 +1,36 @@ import Rails from '@rails/ujs'; +import confirmViaGlModal from './confirm_via_gl_modal'; + +/** + * This function is used to replace the `Rails.confirm` which uses `window.confirm` + * + * This function opens a confirmation modal which will resolve in a promise. + * Because the `Rails.confirm` API is synchronous, we go with a little hack here: + * + * 1. User clicks on something with `data-confirm` + * 2. We open the modal and return `false`, ending the "Rails" event chain + * 3. If the modal is closed and the user "confirmed" the action + * 1. replace the Rails.confirm with a function that always returns true + * 2. Click the same element programatically + * + * @param message {String} Message to be shown in the modal + * @param element {HTMLElement} Element that was clicked on + * @returns {boolean} + */ +function confirmViaModal(message, element) { + confirmViaGlModal(message) + .then((confirmed) => { + if (confirmed) { + Rails.confirm = () => true; + element.click(); + Rails.confirm = confirmViaModal; + } + }) + .catch(() => {}); + return false; +} + +Rails.confirm = confirmViaModal; export const initRails = () => { // eslint-disable-next-line no-underscore-dangle -- GitLab From 0619c0286a7cd34b2023e4011738066bfb1b4175 Mon Sep 17 00:00:00 2001 From: Lukas Eipert Date: Tue, 2 Nov 2021 16:34:39 +0100 Subject: [PATCH 2/8] Update initial implemention 1. Add a `bootstrap_confirmation_modals` feature flag and move the monkey patch behind it. 2. Move the confirm modal into a separate SFC Vue component in order to iterate quicker. That component is also loaded async. This means it will only be loaded when confirmViaGlModal is executed. 3. Copy styles and text of the button (in some use cases) 4. Remove the modal header and center the text vertically. --- .../lib/utils/confirm_via_gl_modal.js | 46 ------------- .../confirm_via_gl_modal/confirm_modal.vue | 40 ++++++++++++ .../confirm_via_gl_modal.js | 41 ++++++++++++ app/assets/javascripts/lib/utils/rails_ujs.js | 64 ++++++++++--------- .../bootstrap_confirmation_modals.yml | 8 +++ lib/gitlab/gon_helper.rb | 1 + 6 files changed, 125 insertions(+), 75 deletions(-) delete mode 100644 app/assets/javascripts/lib/utils/confirm_via_gl_modal.js create mode 100644 app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue create mode 100644 app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js create mode 100644 config/feature_flags/development/bootstrap_confirmation_modals.yml diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js deleted file mode 100644 index c3181328780894..00000000000000 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal.js +++ /dev/null @@ -1,46 +0,0 @@ -import Vue from 'vue'; -import { GlModal } from '@gitlab/ui'; -import { __ } from '~/locale'; - -export default function confirmViaGLModal(message) { - return new Promise((resolve) => { - let confirmed = false; - - const modal = new Vue({ - mounted() { - this.$refs.modal.show(); - }, - render(h) { - return h( - GlModal, - { - props: { - // TODO: The modal has no title and looks weird, we need a title! We should talk to UX - modalId: 'ConfirmationModal', - actionPrimary: { - // TODO: Idea: We could get the text and color of the button we clicked on and use those - // AND FALLBACK to `OK` for example - text: __('OK'), - }, - actionCancel: { - // TODO: Better word than cancel? - text: __('Cancel'), - }, - }, - ref: 'modal', - on: { - primary() { - confirmed = true; - }, - hidden() { - modal.$destroy(); - resolve(confirmed); - }, - }, - }, - [message], - ); - }, - }).$mount(); - }); -} diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue new file mode 100644 index 00000000000000..45a7b3c2edb2a1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -0,0 +1,40 @@ + + + diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js new file mode 100644 index 00000000000000..f7799b2ace73d1 --- /dev/null +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; + +export function confirmViaGlModal(message, element) { + return new Promise((resolve) => { + let confirmed = false; + + const props = {}; + + if (element.classList.contains('btn-danger')) { + props.primaryVariant = 'danger'; + } + + if (element.querySelector('.sr-only')) { + props.primaryText = element.querySelector('.sr-only').textContent; + } + + const component = new Vue({ + components: { + confirmModal: () => import('./confirm_modal.vue'), + }, + mounted() { + this.$root.$on('confirmModal::confirmed', () => { + confirmed = true; + }); + this.$root.$on('confirmModal::closed', () => { + component.$destroy(); + resolve(confirmed); + }); + }, + render(h) { + return h( + 'confirm-modal', + { + props, + }, + [message], + ); + }, + }).$mount(); + }); +} diff --git a/app/assets/javascripts/lib/utils/rails_ujs.js b/app/assets/javascripts/lib/utils/rails_ujs.js index b6644dff7e4ae1..272fee36f4ee7a 100644 --- a/app/assets/javascripts/lib/utils/rails_ujs.js +++ b/app/assets/javascripts/lib/utils/rails_ujs.js @@ -1,36 +1,42 @@ import Rails from '@rails/ujs'; -import confirmViaGlModal from './confirm_via_gl_modal'; +import { confirmViaGlModal } from './confirm_via_gl_modal/confirm_via_gl_modal'; -/** - * This function is used to replace the `Rails.confirm` which uses `window.confirm` - * - * This function opens a confirmation modal which will resolve in a promise. - * Because the `Rails.confirm` API is synchronous, we go with a little hack here: - * - * 1. User clicks on something with `data-confirm` - * 2. We open the modal and return `false`, ending the "Rails" event chain - * 3. If the modal is closed and the user "confirmed" the action - * 1. replace the Rails.confirm with a function that always returns true - * 2. Click the same element programatically - * - * @param message {String} Message to be shown in the modal - * @param element {HTMLElement} Element that was clicked on - * @returns {boolean} - */ -function confirmViaModal(message, element) { - confirmViaGlModal(message) - .then((confirmed) => { - if (confirmed) { - Rails.confirm = () => true; - element.click(); - Rails.confirm = confirmViaModal; - } - }) - .catch(() => {}); - return false; +function monkeyPatchConfirmModal() { + /** + * This function is used to replace the `Rails.confirm` which uses `window.confirm` + * + * This function opens a confirmation modal which will resolve in a promise. + * Because the `Rails.confirm` API is synchronous, we go with a little hack here: + * + * 1. User clicks on something with `data-confirm` + * 2. We open the modal and return `false`, ending the "Rails" event chain + * 3. If the modal is closed and the user "confirmed" the action + * 1. replace the Rails.confirm with a function that always returns true + * 2. Click the same element programmatically + * + * @param message {String} Message to be shown in the modal + * @param element {HTMLElement} Element that was clicked on + * @returns {boolean} + */ + function confirmViaModal(message, element) { + confirmViaGlModal(message, element) + .then((confirmed) => { + if (confirmed) { + Rails.confirm = () => true; + element.click(); + Rails.confirm = confirmViaModal; + } + }) + .catch(() => {}); + return false; + } + + Rails.confirm = confirmViaModal; } -Rails.confirm = confirmViaModal; +if (gon.features.bootstrapConfirmationModals) { + monkeyPatchConfirmModal(); +} export const initRails = () => { // eslint-disable-next-line no-underscore-dangle diff --git a/config/feature_flags/development/bootstrap_confirmation_modals.yml b/config/feature_flags/development/bootstrap_confirmation_modals.yml new file mode 100644 index 00000000000000..e67fd03fea658c --- /dev/null +++ b/config/feature_flags/development/bootstrap_confirmation_modals.yml @@ -0,0 +1,8 @@ +--- +name: bootstrap_confirmation_modals +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/73167 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344658 +milestone: '14.5' +type: development +group: group::foundations +default_enabled: false diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb index 6faf3575bc1b92..027551df15d044 100644 --- a/lib/gitlab/gon_helper.rb +++ b/lib/gitlab/gon_helper.rb @@ -58,6 +58,7 @@ def add_gon_variables push_frontend_feature_flag(:new_header_search, default_enabled: :yaml) push_frontend_feature_flag(:suppress_apollo_errors_during_navigation, current_user, default_enabled: :yaml) push_frontend_feature_flag(:configure_iac_scanning_via_mr, current_user, default_enabled: :yaml) + push_frontend_feature_flag(:bootstrap_confirmation_modals, default_enabled: :yaml) end # Exposes the state of a feature flag to the frontend code. -- GitLab From 6ad40791640f0d6c014eaf2f07179386a0e0920a Mon Sep 17 00:00:00 2001 From: Olena Horal-Koretska Date: Wed, 3 Nov 2021 18:14:29 +0200 Subject: [PATCH 3/8] Minor updates and specs --- .../confirm_via_gl_modal/confirm_modal.vue | 13 +++- .../confirm_via_gl_modal.js | 6 +- app/assets/javascripts/lib/utils/rails_ujs.js | 4 +- .../confirm_modal_spec.js | 62 +++++++++++++++++++ 4 files changed, 78 insertions(+), 7 deletions(-) create mode 100644 spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue index 45a7b3c2edb2a1..c77e2ad80178fc 100644 --- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue +++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue @@ -18,6 +18,12 @@ export default { default: 'confirm', }, }, + data() { + return { + primaryAction: { text: this.primaryText, attributes: { variant: this.primaryVariant } }, + cancelAction: { text: __('Cancel') }, + }; + }, mounted() { this.$refs.modal.show(); }, @@ -27,11 +33,12 @@ export default {