Compare commits

..

14 Commits

Author SHA1 Message Date
Ahmed Ejaz
d1608a0288 Update all locales with the latest Transifex translations 2026-04-04 21:46:56 +05:00
Ahmed Ejaz
e4e7ef395b Merge pull request #14113 from gbathree/13817-fix-guest-order-cancellation
Fix guest order cancellation redirecting to home page
2026-04-04 21:42:44 +05:00
Ahmed Ejaz
ec24740c3b Merge pull request #14085 from dacook/admin-product-actions-fixes
[Admin Products] Action menu fixes
2026-04-04 21:42:12 +05:00
David Cook
15abea51ab Avoid unnecessary extra page visit
The second spec example below has to load the page after creating a record, so it's not helpful to load it here.
2026-04-01 14:48:18 +11:00
David Cook
cacb62f58c Style fix 2026-04-01 14:11:19 +11:00
David Cook
6048fcb053 Define function as member arrow function
This way it behaves as an instance method, and we don't have to pass in the object.
2026-04-01 14:11:19 +11:00
David Cook
6013b6be70 Remove transform at end of animation
During transform, any overflow on the element is clipped/hidden. This caused all dropdown menus to be clipped and unusable. Now, once the animation is complete, the overflow is visible, and menus are usable.

Mistral Vibe AI was used to find this solution. I tried to find a CSS solution last week but failed, then started to consider using JS to remove the class, but decided against it once I realised that the product clone JS was already doing that, and it didn't seem to solve the clipping issue.
So I asked Mistral Vibe and it suggested adding 'forwards' (before it had spent energy on evaluating the existing style rules). As you can see 'forwards' was already there, but removing it helped. So Mistral was wrong, but at least pointed me in the right direction, yay!
2026-04-01 14:11:19 +11:00
David Cook
22a1528ac7 Show unit in tooltip
Variants may have the same name, or no display_name at all. This helper method provides a more comprehensive way of describing the variant.
2026-04-01 14:11:19 +11:00
David Cook
ca3c0c98bf Don't group reviewdog output
Grouping is a nice feature, but it wasn't helpful here. If there's an error in rubocop for example, the rubocop section will be collapsed, and because we didn't close the group, the haml group was always open. So it wasn't clear where the error was.

Better to just show all the output, which isn't very long, so you can see where the problem is straight away.

Even better would be to add support for GitHub Actions annotations. I thought we used to have that turned on, not sure why it's not working now.
2026-04-01 14:11:19 +11:00
David Cook
19006d6c17 Close action menu when making a selection
But don't hide it immediately, because the user can't see if they made a selection, or accidentally closed it. Instead, fade slowly so that you can see the selected option momentarily (like system menus). This gives enough feedback while we wait for the selected action to perform.

I did attempt a blink on the item background colour, like my favourite OS does which is really helpful. But couldn't get the CSS to work.
2026-04-01 14:11:19 +11:00
David Cook
da69e2c383 Widen action menus slightly when needed 2026-04-01 14:11:19 +11:00
Greg Austic
cc608adddc Address review feedback: use referer in specs, remove demo screenshots
- Set HTTP_REFERER in cancel action specs so they test the redirect to
  the actual order page (not the cancel path, which was the implicit
  referer in controller specs)
- Keep response.body match pattern (consistent with checkout_controller_spec
  for CableReady redirects — redirect_to matcher does not work here since
  the cancel action uses cable_car.redirect_to, not a Rails redirect)
- Remove doc/demo screenshots; images to be added to PR description instead

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 19:03:47 -04:00
Greg Austic
a6f5f2c10d Add demo screenshots for guest order cancellation fix
Before/after screenshots showing the fix works end-to-end:
- step1: guest order confirmed, Cancel Order button visible
- step2: same page after cancellation, order shows Cancelled

These can be removed after the PR is reviewed and merged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:44:12 -04:00
Greg Austic
c72976b1e2 Fix guest order cancellation redirecting to home page
When a guest places an order and tries to cancel it from the order
confirmation page, the cancellation silently failed and redirected
to the home page. The guest was left unsure whether the order was
cancelled, and the hub received no cancellation notification.

Root cause: two missing pieces for guest (token-based) authorization:

1. The `:cancel` ability in Ability#add_shopping_abilities only checked
   `order.user == user`, ignoring the guest token. The `:read` and
   `:update` abilities already support `order.token && token == order.token`
   as a fallback — `:cancel` now does the same.

2. The `cancel` action called `authorize! :cancel, @order` without
   passing `session[:access_token]`, so even with the corrected ability
   the token was never evaluated.

Fixes #13817

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 09:05:47 -04:00
13 changed files with 91 additions and 28 deletions

View File

@@ -19,7 +19,9 @@
background-color: white;
box-shadow: $box-shadow;
border-radius: 3px;
width: max-content;
min-width: 80px;
max-width: 110px;
display: none;
z-index: 100;
@@ -27,6 +29,15 @@
display: block;
}
// Fade out so user can see which option was selected
&.selected {
transition:
opacity 0.2s linear,
visibility 0.2s linear;
opacity: 0;
visibility: hidden;
}
& > a {
display: block;
padding: 5px 10px;

View File

@@ -6,6 +6,9 @@ export default class extends Controller {
connect() {
super.connect();
window.addEventListener("click", this.#hideIfClickedOutside);
// Close menu when making a selection
this.contentTarget.addEventListener("click", this.#selected);
}
disconnect() {
@@ -13,17 +16,22 @@ export default class extends Controller {
}
toggle() {
this.contentTarget.classList.toggle("show");
this.#toggleShow();
}
#selected = () => {
this.contentTarget.classList.add("selected");
};
#hideIfClickedOutside = (event) => {
if (this.element.contains(event.target)) {
return;
}
this.#hide();
this.#toggleShow(false);
};
#hide() {
this.contentTarget.classList.remove("show");
#toggleShow(force = undefined) {
this.contentTarget.classList.toggle("show", force);
this.contentTarget.classList.remove("selected");
}
}

View File

@@ -107,7 +107,7 @@ module Spree
def cancel
@order = Spree::Order.find_by!(number: params[:id])
authorize! :cancel, @order
authorize! :cancel, @order, session[:access_token]
if Orders::CustomerCancellationService.new(@order).call
flash[:success] = I18n.t(:orders_your_order_has_been_cancelled)

View File

@@ -113,7 +113,11 @@ module Spree
item.order.changes_allowed?
end
can [:cancel, :bulk_cancel], Spree::Order do |order|
can :cancel, Spree::Order do |order, token|
order.user == user || (order.token && token == order.token)
end
can :bulk_cancel, Spree::Order do |order|
order.user == user
end

View File

@@ -4,7 +4,7 @@
%td.col-image
-# empty
- variant.source_variants.each do |source_variant|
= content_tag(:span, "🔗", title: t('admin.products_page.variant_row.sourced_from', source_name: source_variant.display_name, source_id: source_variant.id, hub_name: variant.hub&.name))
= content_tag(:span, "🔗", title: t('admin.products_page.variant_row.sourced_from', source_name: source_variant.full_name, source_id: source_variant.id, hub_name: variant.hub&.name))
%td.col-name.field.naked_inputs
= f.hidden_field :id
= f.text_field :display_name, 'aria-label': t('admin.products_page.columns.name'), placeholder: variant.product.name

View File

@@ -375,6 +375,6 @@
.slide-in {
transform-origin: top;
animation: slideInTop 0.5s forwards;
animation: slideInTop 0.5s;
}
}

View File

@@ -193,6 +193,7 @@ de_DE:
not_available_to_shop: "ist nicht verfügbar für %{shop}"
card_details: "Kreditkartendaten"
card_type: "Kreditkartentyp"
use_new_cc: "Eine neue Kreditkarte verwenden"
cardholder_name: "Kreditkarteninhaber"
community_forum_url: "URL des Community-Forums"
customer_instructions: "Informationen für Kunden"
@@ -665,6 +666,7 @@ de_DE:
bill_address: "Rechnungsadresse"
ship_address: "Lieferadresse"
balance: "Saldo"
credit: "Verfügbares Guthaben"
update_address_success: "Adresse wurde erfolgreich aktualisiert."
update_address_error: "Bitte füllen Sie alle erforderlichen Felder aus."
edit_bill_address: "Rechnungsadresse bearbeiten"
@@ -683,6 +685,7 @@ de_DE:
has_associated_subscriptions: "Löschen fehlgeschlagen: Dieser Kunde hat aktive Abonnements. Bitte kündigen Sie diese zuerst."
customer_account_transaction:
index:
available_credit: "Verfügbares Guthaben: %{available_credit}"
description: Beschreibung
amount: Summe
running_balance: Fortlaufender Saldo
@@ -2203,6 +2206,7 @@ de_DE:
order_total: 'Gesamtsumme:'
order_payment: "Zahlungsart:"
no_payment_required: "Keine Zahlung erforderlich."
credit_used: "Verwendetes Guthaben: %{amount}"
customer_credit: Guthaben
order_billing_address: Rechnungsadresse
order_delivery_on: Lieferung am
@@ -4363,7 +4367,7 @@ de_DE:
date_picker:
flatpickr_date_format: "d.m.Y"
flatpickr_datetime_format: "d.m.Y H:i"
today: "heute"
today: "Heute"
now: "Jetzt"
close: "Schließen"
orders:

View File

@@ -208,6 +208,10 @@ hu:
no_default_card: "^Ennél az ügyfélnél nem áll rendelkezésre alapértelmezett kártya"
shipping_method:
not_available_to_shop: "nem érhető el a %{shop} számára"
user_invitation:
attributes:
email:
is_already_manager: már menedzser
card_details: "A kártya adatai"
card_type: "Kártyatípus"
card_type_is: "A kártya típusa: "
@@ -513,7 +517,7 @@ hu:
create: "Létrehozás"
cancel: "Mégsem"
cancel_order: "Törlés"
resume: "Összefoglaló"
resume: "Visszaállítás"
save: "Mentés"
edit: "Szerkesztés"
update: "Frissítés"
@@ -570,8 +574,11 @@ hu:
delete: Törlés
remove: Eltávolítás
preview: Előnézet
create_linked_variant: Kapcsolt termékváltozat létrehozása
image:
edit: Szerkesztés
variant_row:
sourced_from: "Forrás: %{source_name} ( %{source_id} ); Elosztó: %{hub_name}"
product_preview:
product_preview: Termék előnézet
shop_tab: Átvételi pont
@@ -1051,7 +1058,7 @@ hu:
back_to_my_inventory: Vissza a leltárhoz
orders:
edit:
order_sure_want_to: Biztosan %{event} akarod ezt a megrendelést?
order_sure_want_to: 'Biztosan végrehajtod a rendelésen ezt a műveletet? %{event} '
voucher_tax_included_in_price: "%{label}(a kupon tartalmazza az adót)"
tax_on_fees: "Díjak adója"
invoice_email_sent: 'Számla e-mail elküldve'
@@ -1561,7 +1568,7 @@ hu:
coordinator: Koordinátor
orders_close: A rendelések lezárulnak
row:
suppliers: beszállítók
suppliers: beszállító
distributors: elosztó
variants: változat
simple_form:

View File

@@ -6,7 +6,7 @@
set -o pipefail
echo "::group:: Running prettier with reviewdog 🐶 ..."
echo -e "\nRunning prettier with reviewdog 🐶 ..."
"$(npm root)/.bin/prettier" --check . 2>&1 | sed --regexp-extended 's/(\[warn\].*)$/\1 File is not properly formatted./' \
| reviewdog \
@@ -25,7 +25,7 @@ echo "::group:: Running prettier with reviewdog 🐶 ..."
prettier=$?
echo "::group:: Running rubocop with reviewdog 🐶 ..."
echo -e "\nRunning rubocop with reviewdog 🐶 ..."
bundle exec rubocop \
--fail-level info \
@@ -39,7 +39,7 @@ bundle exec rubocop \
rubocop=$?
echo "::group:: Running haml-lint with reviewdog 🐶 ..."
echo -e "\nRunning haml-lint with reviewdog 🐶 ..."
bundle exec haml-lint \
--fail-level warning \

View File

@@ -461,14 +461,34 @@ RSpec.describe Spree::OrdersController do
end
end
context "when a guest user has the order token in session" do
let(:order) {
create(:completed_order_with_totals, user: nil, email: "guest@example.com",
distributor: create(:distributor_enterprise))
}
before do
allow(controller).to receive(:spree_current_user) { nil }
session[:access_token] = order.token
end
it "cancels the order and redirects to the order page" do
request.env['HTTP_REFERER'] = order_path(order)
spree_put :cancel, params
expect(response.body).to match(order_path(order)).and match("redirect")
expect(flash[:success]).to eq 'Your order has been cancelled'
end
end
context "when the user has permission to cancel the order" do
before { allow(controller).to receive(:spree_current_user) { user } }
context "when the order is not yet complete" do
it "responds with forbidden" do
request.env['HTTP_REFERER'] = order_path(order)
spree_put :cancel, params
expect(response).to have_http_status(:found)
expect(response.body).to match(order_path(order)).and match("redirect")
expect(flash[:error]).to eq 'Sorry, the order could not be cancelled'
end
@@ -481,9 +501,9 @@ RSpec.describe Spree::OrdersController do
}
it "responds with success" do
request.env['HTTP_REFERER'] = order_path(order)
spree_put :cancel, params
expect(response).to have_http_status(:found)
expect(response.body).to match(order_path(order)).and match("redirect")
expect(flash[:success]).to eq 'Your order has been cancelled'
end

View File

@@ -16,12 +16,13 @@ describe("VerticalEllipsisMenuController test", () => {
<div data-controller="vertical-ellipsis-menu" id="root">
<div data-action="click->vertical-ellipsis-menu#toggle" id="button">...</div>
<div data-vertical-ellipsis-menu-target="content" id="content">
<a href="#" id="item"></a>
</div>
</div>
`;
const button = document.getElementById("button");
const content = document.getElementById("content");
const item = document.getElementById("item");
});
it("add show class to content when toggle is called", () => {
@@ -43,4 +44,15 @@ describe("VerticalEllipsisMenuController test", () => {
document.body.click();
expect(content.classList.contains("show")).toBe(false);
});
it("adds selected class to content when clicking a menu item", () => {
button.click();
expect(content.classList.contains("selected")).toBe(false);
item.click();
expect(content.classList.contains("selected")).toBe(true);
// and removes it again when clicking button again
button.click();
expect(content.classList.contains("selected")).toBe(false);
});
});

View File

@@ -295,12 +295,11 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
# Close action menu (shouldn't need this, it should close itself)
last_box.click
# And I can perform actions on the new product
# And I can perform actions on the new variant
within last_box do
page.find(".vertical-ellipsis-menu").click
expect(page).to have_link "Edit"
# expect(page).to have_link "Clone" # tofix: menu is partially obscured
# expect(page).to have_link "Delete" # it's not a proper link
expect(page).to have_selector "a", text: "Delete" # it's not a proper link
fill_in "Name", with: "My copy of Apples"
end
@@ -339,12 +338,10 @@ RSpec.describe 'As an enterprise user, I can perform actions on the products scr
}
describe "Actions columns (delete)" do
before do
visit admin_products_url
end
it "shows an actions menu with a delete link when clicking on icon for product. " \
"doesn't show delete link for the single variant" do
visit admin_products_url
within product_selector do
page.find(".vertical-ellipsis-menu").click
expect(page).to have_css(delete_option_selector)

View File

@@ -5151,9 +5151,9 @@ mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
mini-css-extract-plugin@^2.9.4:
version "2.10.2"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz#5c85ec9450c05d26e32531b465a15a08c3a57253"
integrity sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==
version "2.10.1"
resolved "https://registry.yarnpkg.com/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.1.tgz#a7f0bb890f4e1ce6dfc124bd1e6d6fcd3b359844"
integrity sha512-k7G3Y5QOegl380tXmZ68foBRRjE9Ljavx835ObdvmZjQ639izvZD8CS7BkWw1qKPPzHsGL/JDhl0uyU1zc2rJw==
dependencies:
schema-utils "^4.0.0"
tapable "^2.2.1"