diff --git a/admin/app/components/solidus_admin/layout/confirm/component.html.erb b/admin/app/components/solidus_admin/layout/confirm/component.html.erb new file mode 100644 index 00000000000..68ebe250857 --- /dev/null +++ b/admin/app/components/solidus_admin/layout/confirm/component.html.erb @@ -0,0 +1,12 @@ +<%= render component("ui/modal").new( + title: t(".title"), + open: false, + id: "confirm" +) do |modal| %> + <% modal.with_actions do %> +
+ <%= render component("ui/button").new(scheme: :secondary, text: t(".cancel"), class: "confirm-cancel") %> +
+ <%= render component("ui/button").new(text: t(".confirm"), id: "confirm-accept", scheme: :danger) %> + <% end %> +<% end %> diff --git a/admin/app/components/solidus_admin/layout/confirm/component.rb b/admin/app/components/solidus_admin/layout/confirm/component.rb new file mode 100644 index 00000000000..ea5a692da75 --- /dev/null +++ b/admin/app/components/solidus_admin/layout/confirm/component.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# Component wrapper for confirmation dialog to use with rolemodel/turbo-confirm. +# +# The modal is rendered in the layout initially hidden. +# To have it open to confirm user's action, place the "data-turbo-confirm" on the submitter (or any other element that +# supports "data-turbo-confirm" attribute: form, link with "data-turbo-method") with the text you want to have in the +# modal title: +#
+# +#
+# +#
+#
+# +# You can add more details in the body of the modal using "data-confirm-details" attribute: +# +# +# To customize "Confirm" button text use "data-confirm-button" attribute: +# +# +# For more details see https://github.com/RoleModel/turbo-confirm. + +class SolidusAdmin::Layout::Confirm::Component < SolidusAdmin::BaseComponent +end diff --git a/admin/app/components/solidus_admin/layout/confirm/component.yml b/admin/app/components/solidus_admin/layout/confirm/component.yml new file mode 100644 index 00000000000..88d249a4706 --- /dev/null +++ b/admin/app/components/solidus_admin/layout/confirm/component.yml @@ -0,0 +1,4 @@ +en: + cancel: "Cancel" + confirm: "Confirm" + title: "Are you sure?" diff --git a/admin/app/components/solidus_admin/orders/cart/component.html.erb b/admin/app/components/solidus_admin/orders/cart/component.html.erb index cc4eef3281a..d34a6f3e9bb 100644 --- a/admin/app/components/solidus_admin/orders/cart/component.html.erb +++ b/admin/app/components/solidus_admin/orders/cart/component.html.erb @@ -62,8 +62,7 @@ size: :s, title: t("spree.delete"), icon: 'close-line', - "data-controller": "confirm", - "data-confirm-text-value": t("spree.are_you_sure"), + "data-turbo-confirm": t("spree.are_you_sure") ) %> <% end %> diff --git a/admin/app/components/solidus_admin/products/show/component.html.erb b/admin/app/components/solidus_admin/products/show/component.html.erb index f8095763f04..2614fba5b20 100644 --- a/admin/app/components/solidus_admin/products/show/component.html.erb +++ b/admin/app/components/solidus_admin/products/show/component.html.erb @@ -143,8 +143,7 @@ tag: :button, text: t(".delete"), scheme: :danger, - "data-action": "click->#{stimulus_id}#confirmDelete", - "data-#{stimulus_id}-message-param": t(".delete_confirmation"), + "data-turbo-confirm": t(".delete_confirmation") ) %> <% end %> <% end %> diff --git a/admin/app/components/solidus_admin/products/show/component.js b/admin/app/components/solidus_admin/products/show/component.js deleted file mode 100644 index fd490e3c1b7..00000000000 --- a/admin/app/components/solidus_admin/products/show/component.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - confirmDelete(event) { - if (!confirm(event.params.message)) { - event.preventDefault() - } - } -} diff --git a/admin/app/components/solidus_admin/ui/modal/component.html.erb b/admin/app/components/solidus_admin/ui/modal/component.html.erb index bf301c6cf51..4e6480dc5c9 100644 --- a/admin/app/components/solidus_admin/ui/modal/component.html.erb +++ b/admin/app/components/solidus_admin/ui/modal/component.html.erb @@ -11,7 +11,7 @@
-

+

@@ -23,9 +23,7 @@
-
- <%= content %> -
+ <% if actions? %>
diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.js b/admin/app/components/solidus_admin/users/edit/api_access/component.js deleted file mode 100644 index 910294c5462..00000000000 --- a/admin/app/components/solidus_admin/users/edit/api_access/component.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - confirm(event) { - if (!confirm(event.params.message)) { - event.preventDefault() - } - } -} diff --git a/admin/app/components/solidus_admin/users/edit/api_access/component.yml b/admin/app/components/solidus_admin/users/edit/api_access/component.yml index e865ba47c30..820fa4832c2 100644 --- a/admin/app/components/solidus_admin/users/edit/api_access/component.yml +++ b/admin/app/components/solidus_admin/users/edit/api_access/component.yml @@ -6,5 +6,11 @@ en: clear_key: Clear key regenerate_key: Regenerate key hidden: Hidden - confirm_clear_key: Are you sure you want to clear this user's API key? It will invalidate the existing key. - confirm_regenerate_key: Are you sure you want to regenerate this user's API key? It will invalidate the existing key. + confirm: + title: Are you sure? + clear: + details: Are you sure you want to clear this user's API key? It will invalidate the existing key. + button: Clear + regenerate: + details: Are you sure you want to regenerate this user's API key? It will invalidate the existing key. + button: Regenerate diff --git a/admin/app/javascript/solidus_admin/application.js b/admin/app/javascript/solidus_admin/application.js index 7e7a85ebe4a..6e5c0a253f5 100644 --- a/admin/app/javascript/solidus_admin/application.js +++ b/admin/app/javascript/solidus_admin/application.js @@ -2,3 +2,4 @@ import "@hotwired/turbo-rails" import "vendor/custom_elements" import "solidus_admin/controllers" import "solidus_admin/web_components/solidus_select" +import "solidus_admin/turbo-confirm" diff --git a/admin/app/javascript/solidus_admin/controllers/confirm_controller.js b/admin/app/javascript/solidus_admin/controllers/confirm_controller.js deleted file mode 100644 index 60e5f3c0a6f..00000000000 --- a/admin/app/javascript/solidus_admin/controllers/confirm_controller.js +++ /dev/null @@ -1,21 +0,0 @@ -import { Controller } from "@hotwired/stimulus" - -export default class extends Controller { - static values = {"text": String} - - connect() { - this.element.addEventListener("click", this) - this.element.addEventListener("submit", this) - } - - disconnect() { - this.element.removeEventListener("click", this) - this.element.removeEventListener("submit", this) - } - - handleEvent(event) { - if (!confirm(this.textValue)) { - event.preventDefault() - } - } -} diff --git a/admin/app/javascript/solidus_admin/rolemodel/turbo-confirm.js b/admin/app/javascript/solidus_admin/rolemodel/turbo-confirm.js new file mode 100644 index 00000000000..fbbabad5c49 --- /dev/null +++ b/admin/app/javascript/solidus_admin/rolemodel/turbo-confirm.js @@ -0,0 +1,16 @@ +import TC from "@rolemodel/turbo-confirm" + +TC.start({ + animationDuration: 0, + messageSlotSelector: ".modal-title", + contentSlots: { + body: { + contentAttribute: "confirm-details", + slotSelector: ".modal-body" + }, + acceptText: { + contentAttribute: "confirm-button", + slotSelector: "#confirm-accept" + } + } +}); diff --git a/admin/app/javascript/vendor/@rolemodel--turbo-confirm.js b/admin/app/javascript/vendor/@rolemodel--turbo-confirm.js new file mode 100644 index 00000000000..b040dde8c5d --- /dev/null +++ b/admin/app/javascript/vendor/@rolemodel--turbo-confirm.js @@ -0,0 +1,18 @@ +// @rolemodel/turbo-confirm@2.1.1 downloaded from https://ga.jspm.io/npm:@rolemodel/turbo-confirm@2.1.1/src/index.js + +const dispatch=(t,e=document,{bubbles:o=true,cancelable:n=true,prefix:i="rms",detail:r}={})=>{const s=new CustomEvent(`${i}:${t}`,{bubbles:o,cancelable:n,detail:r});e.dispatchEvent(s);return!s.defaultPrevented};class TurboConfirmError extends Error{name="TurboConfirmError";static missingDialog(t,e){return new this(`No element matching dialogSelector: '${t}'`,{cause:e})}static noTurbo(){return new this('Turbo is not defined. Be sure to import "@hotwired/turbo-rails" before calling the `start()` function')}}class ConfirmationController{initialContent;#t;constructor(t){this.delegate=t;this.accept=this.accept.bind(this);this.deny=this.deny.bind(this)}showConfirm(t){this.#e();for(const[e,o]of Object.entries(t)){const t=this.element.querySelector(e);t&&o&&(t.innerHTML=o)}this.#o();this.delegate.showConfirm(this.element);return new Promise((t=>this.#t=t))}accept(){this.#t(true);this.#n()}deny(){this.#t(false);this.#n()}get acceptButtons(){return this.element.querySelectorAll(this.delegate.acceptSelector)}get denyButtons(){return this.element.querySelectorAll(this.delegate.denySelector)}get element(){return document.querySelector(this.delegate.dialogSelector)}#n(){this.#t=null;this.delegate.hideConfirm(this.element);this.#i();setTimeout(this.#r.bind(this),this.delegate.animationDuration)}#o(){this.acceptButtons.forEach((t=>t.addEventListener("click",this.accept)));this.denyButtons.forEach((t=>t.addEventListener("click",this.deny)));this.element.addEventListener("cancel",this.deny)}#i(){this.acceptButtons.forEach((t=>t.removeEventListener("click",this.accept)));this.denyButtons.forEach((t=>t.removeEventListener("click",this.deny)));this.element.removeEventListener("cancel",this.deny)}#e(){try{this.initialContent=this.element.innerHTML}catch(t){throw TurboConfirmError.missingDialog(this.delegate.dialogSelector,t)}}#r(){try{this.element.innerHTML=this.initialContent}catch{}}}class TurboConfirm{#s;#c={dialogSelector:"#confirm",activeClass:"modal--active",acceptSelector:"#confirm-accept",denySelector:".confirm-cancel",animationDuration:300,showConfirmCallback:t=>t.showModal&&t.showModal(),hideConfirmCallback:t=>t.close&&t.close(),messageSlotSelector:"#confirm-title",contentSlots:{body:{contentAttribute:"confirm-details",slotSelector:"#confirm-body"},acceptText:{contentAttribute:"confirm-button",slotSelector:"#confirm-accept"}}};constructor(t={}){for(const[e,o]of Object.entries(t))this.#c[e]=o;this.#s=new ConfirmationController(this)} +/** + * Present a confirmation challenge to the user. + * @public + * @param {string} [message] - The main challenge message; Value of `data-turbo-confirm` attribute. + * @param {HTMLFormElement} [_formElement] - (ignored) `form` element that contains the submitter. + * @param {HTMLElement} [submitter] - button of input of type submit that triggered the form submission. + * @returns {Promise} - A promise that resolves to true if the user accepts the challenge or false if they deny it. + */confirm(t,e,o){const n=this.#l(o);const i=this.#a(t,n);return this.confirmWithContent(i)} +/** + * Present a confirmation challenge to the user. + * @public + * @param {Object} contentMap - A map of CSS selectors to HTML content to be inserted into the dialog. + * @returns {Promise} - A promise that resolves to true if the user accepts the challenge or false if they deny it. + */confirmWithContent(t){return this.#s.showConfirm(t)}showConfirm(t){t.classList.add(this.#c.activeClass);typeof this.#c.showConfirmCallback==="function"&&this.#c.showConfirmCallback(t)}hideConfirm(t){t.classList.remove(this.#c.activeClass);typeof this.#c.hideConfirmCallback==="function"&&this.#c.hideConfirmCallback(t)}get dialogSelector(){return this.#c.dialogSelector}get acceptSelector(){return this.#c.acceptSelector}get denySelector(){return this.#c.denySelector}get animationDuration(){return this.#c.animationDuration}#a(t,e){const o={};t&&(o[this.#c.messageSlotSelector]=t);if(e)for(const t of Object.keys(this.#c.contentSlots))o[this.#h(t)]=this.#f(t,e);return o}#h(t){return this.#c.contentSlots[t].slotSelector}#f(t,e){return e.getAttribute(`data-${this.#c.contentSlots[t].contentAttribute}`)}#l(t){const e=t??document.activeElement;return e.closest("[data-turbo-confirm]")}}const start=t=>{if(!window.Turbo)throw TurboConfirmError.noTurbo();const e=new TurboConfirm(t);const confirmationHandler=async(t,o,n)=>{const i=await e.confirm(t,o,n);dispatch(i?"confirm-accept":"confirm-reject",n);return i};window.Turbo.config?window.Turbo.config.forms.confirm=confirmationHandler:window.Turbo.setConfirmMethod(confirmationHandler)};var t={start:start};export{TurboConfirm,t as default}; + diff --git a/admin/app/views/layouts/solidus_admin/application.html.erb b/admin/app/views/layouts/solidus_admin/application.html.erb index 6755eea3c1d..924d44cc321 100644 --- a/admin/app/views/layouts/solidus_admin/application.html.erb +++ b/admin/app/views/layouts/solidus_admin/application.html.erb @@ -36,5 +36,7 @@ <%= render component("ui/toast").new(text: message, scheme: key.to_sym == :error ? :error : :default) %> <% end %> + + <%= render component("layout/confirm").new %> diff --git a/admin/config/importmap.rb b/admin/config/importmap.rb index 09ec1d8f7a5..32d6cbb194e 100644 --- a/admin/config/importmap.rb +++ b/admin/config/importmap.rb @@ -21,3 +21,6 @@ pin "tom-select", to: "https://ga.jspm.io/npm:tom-select@2.4.3/dist/esm/tom-select.complete.js" pin "@orchidjs/sifter", to: "https://ga.jspm.io/npm:@orchidjs/sifter@1.1.0/dist/esm/sifter.js" pin "@orchidjs/unicode-variants", to: "https://ga.jspm.io/npm:@orchidjs/unicode-variants@1.1.2/dist/esm/index.js" + +pin "@rolemodel/turbo-confirm", to: "vendor/@rolemodel--turbo-confirm.js" # @2.1.1 +pin "solidus_admin/turbo-confirm", to: "solidus_admin/rolemodel/turbo-confirm.js" diff --git a/admin/lib/solidus_admin/testing_support/feature_helpers.rb b/admin/lib/solidus_admin/testing_support/feature_helpers.rb index 6f28ad553cb..a9774492432 100644 --- a/admin/lib/solidus_admin/testing_support/feature_helpers.rb +++ b/admin/lib/solidus_admin/testing_support/feature_helpers.rb @@ -61,6 +61,12 @@ def clear_search find('button[aria-label="Clear"]').click end end + + def accept_turbo_confirm(title) + yield + dialog = find("dialog", text: title) + within(dialog) { find_button(id: "confirm-accept").click } + end end end end diff --git a/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview.rb b/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview.rb new file mode 100644 index 00000000000..2743c734aad --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# @component "ui/modal" +class SolidusAdmin::Layout::Confirm::ComponentPreview < ViewComponent::Preview + include SolidusAdmin::Preview + + # @param title text + # @param body text + # @param button text + def overview(title: "Are you sure?", body: "You are about to delete something. This cannot be undone.", button: "Confirm") + render_with_template(locals: { title:, body:, button: }) + end +end diff --git a/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview/overview.html.erb b/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview/overview.html.erb new file mode 100644 index 00000000000..25a117dfd02 --- /dev/null +++ b/admin/spec/components/previews/solidus_admin/layout/confirm/component_preview/overview.html.erb @@ -0,0 +1,18 @@ +
+
+
+ <%= render component("ui/button").new( + type: :submit, + scheme: :secondary, + text: "Summon confirmation modal", + data: { + "turbo-confirm": title, + "confirm-details": body, + "confirm-button": button + } + ) %> +
+
+ + <%= render current_component.new %> +
diff --git a/admin/spec/components/previews/solidus_admin/ui/modal/component_preview.rb b/admin/spec/components/previews/solidus_admin/ui/modal/component_preview.rb index 5ee9f4709a2..abb4773c007 100644 --- a/admin/spec/components/previews/solidus_admin/ui/modal/component_preview.rb +++ b/admin/spec/components/previews/solidus_admin/ui/modal/component_preview.rb @@ -11,8 +11,4 @@ def with_text def with_form render_with_template end - - def with_actions - render_with_template - end end diff --git a/admin/spec/components/previews/solidus_admin/ui/modal/component_preview/with_actions.html.erb b/admin/spec/components/previews/solidus_admin/ui/modal/component_preview/with_actions.html.erb deleted file mode 100644 index 28ef8b9fc58..00000000000 --- a/admin/spec/components/previews/solidus_admin/ui/modal/component_preview/with_actions.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -
-
- With Actions -
- - <%= render current_component.new(title: 'Delete view?', open: true) do |component| %> -

- This can't be undone. T-shirt SM view will no longer be available in your - admin! -

- <% component.with_actions do %> - <%= render component("ui/button").new(text: t('.close'), scheme: :secondary) %> - <%= render component("ui/button").new(scheme: :primary, text: "Delete") %> - <% end %> - <% end %> -
diff --git a/admin/spec/components/solidus_admin/layout/confirm/component_spec.rb b/admin/spec/components/solidus_admin/layout/confirm/component_spec.rb new file mode 100644 index 00000000000..664d7d227f2 --- /dev/null +++ b/admin/spec/components/solidus_admin/layout/confirm/component_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require "spec_helper" + +RSpec.describe SolidusAdmin::Layout::Confirm::Component, type: :component do + it "renders the overview preview" do + render_preview(:overview) + end +end diff --git a/admin/spec/components/solidus_admin/ui/modal/component_spec.rb b/admin/spec/components/solidus_admin/ui/modal/component_spec.rb index 6d27081c71f..399cbb13afa 100644 --- a/admin/spec/components/solidus_admin/ui/modal/component_spec.rb +++ b/admin/spec/components/solidus_admin/ui/modal/component_spec.rb @@ -6,6 +6,5 @@ it "renders the overview preview" do render_preview(:with_text) render_preview(:with_form) - render_preview(:with_actions) end end diff --git a/admin/spec/features/orders/show_spec.rb b/admin/spec/features/orders/show_spec.rb index 610bee167c2..55f70774585 100644 --- a/admin/spec/features/orders/show_spec.rb +++ b/admin/spec/features/orders/show_spec.rb @@ -131,7 +131,7 @@ expect(Spree::Order.last.line_items.last.quantity).to eq(4) - accept_confirm("Are you sure?") { click_on "Delete" } + accept_turbo_confirm("Are you sure?") { click_on "Delete" } expect(page).to have_content("Line item removed successfully", wait: 5) expect(Spree::Order.last.line_items.count).to eq(0) diff --git a/admin/spec/features/products_spec.rb b/admin/spec/features/products_spec.rb index 6efb41af821..f3c99fe3112 100644 --- a/admin/spec/features/products_spec.rb +++ b/admin/spec/features/products_spec.rb @@ -30,7 +30,7 @@ visit "/admin/products" select_row("Just a product") - accept_confirm("Are you sure you want to delete 1 product?") do + accept_turbo_confirm("Are you sure you want to delete 1 product?") do click_button("Delete", wait: 5) end @@ -48,7 +48,7 @@ visit "/admin/products" find('main tbody tr:nth-child(2)').find('input').check - accept_confirm("Are you sure you want to discontinue 1 product?") do + accept_turbo_confirm("Are you sure you want to discontinue 1 product?") do click_button "Discontinue" end @@ -66,7 +66,7 @@ find('main tbody tr:nth-child(2)').find('input').check - accept_confirm("Are you sure you want to activate 1 product?") do + accept_turbo_confirm("Are you sure you want to activate 1 product?") do click_button "Activate" end diff --git a/admin/spec/features/users_spec.rb b/admin/spec/features/users_spec.rb index cc9c6a5d7fc..e62d3f70cdf 100644 --- a/admin/spec/features/users_spec.rb +++ b/admin/spec/features/users_spec.rb @@ -82,11 +82,11 @@ expect(page).to have_content("Key generated") expect(page).to have_content("(hidden)") - click_on "Regenerate key" + accept_turbo_confirm("Are you sure?") { click_on "Regenerate key" } expect(page).to have_content("Key generated") expect(page).to have_content("(hidden)") - click_on "Clear key" + accept_turbo_confirm("Are you sure?") { click_on "Clear key" } expect(page).to have_content("Key cleared") expect(page).to have_content("No key")