Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<div class="flex gap-4 items-end">
<% if (url = preview_url) %>
<%= render component("ui/thumbnail").new(src: url, alt: label) %>
<% end %>
<div class="grow">
<%= render component("ui/forms/field").file_field(@form, name, accept: Spree::Config.allowed_image_mime_types.join(",")) %>
</div>
</div>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

# A file field for one of the store's image attachments, with a thumbnail of
# the saved image.
class SolidusAdmin::Stores::Form::Attachment::Component < SolidusAdmin::BaseComponent
# @param form [ActionView::Helpers::FormBuilder] the store form
# @param attachment [ActiveStorage::Attached::One] the store's logo or favicon
def initialize(form:, attachment:)
@form = form
@attachment = attachment
end

def name
@attachment.name
end

def label
Spree::Store.human_attribute_name(name)
end

# Only a saved attachment has a URL: after a failed save the form is shown
# again with a new file assigned but not stored yet.
def preview_url
store = @attachment.record
return unless store.persisted? && store.errors.empty?

@attachment.url if @attachment.attached?
end
end
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<%= form_for @store, url: @url, html: { id: @id } do |f| %>
<%= form_for @store, url: @url, html: { id: @id, multipart: true } do |f| %>
<%= page_with_sidebar do %>
<%= page_with_sidebar_main do %>
<%= render component('ui/panel').new do |panel| %>
Expand All @@ -11,6 +11,15 @@
</div>
<% end %>
<% end %>
<%# solidus_admin also supports Solidus versions without store attachments, hence try. %>
<% if Spree::Store.try(:attachments_available?) %>
<%= render component('ui/panel').new(title: t('.branding')) do |panel| %>
<% panel.with_section(class: "flex flex-col gap-4") do %>
<%= render component("stores/form/attachment").new(form: f, attachment: @store.logo) %>
<%= render component("stores/form/attachment").new(form: f, attachment: @store.favicon) %>
<% end %>
<% end %>
<% end %>
<%# This check can be removed once we drop support for Solidus 4.7 and earlier. %>
<% if Spree::Store.reflect_on_association(:address) %>
<%= render component('ui/panel').new(title: t('.address')) do |panel| %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
en:
address: Address
branding: Logo and favicon
localization: Localization
plugins:
install: Install
Expand Down
19 changes: 19 additions & 0 deletions admin/app/components/solidus_admin/ui/forms/field/component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,25 @@ def self.text_field(form, method, object: nil, hint: nil, tip: nil, size: :m, **
)
end

def self.file_field(form, method, object: nil, hint: nil, tip: nil, size: :m, **attributes)
object_name, _object, label, errors = extract_form_details(form, object, method)

new(
label:,
hint:,
tip:,
error: errors,
input_attributes: {
name: "#{object_name}[#{method}]",
tag: :input,
type: :file,
size:,
error: errors&.to_sentence&.capitalize,
**attributes
}
)
end

def self.select(form, method, choices, object: nil, hint: nil, tip: nil, size: :m, **attributes)
object_name, object, label, errors = extract_form_details(form, object, method)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class SolidusAdmin::UI::Forms::Input::Component < SolidusAdmin::BaseComponent
search
time
hidden
file
]).freeze

def initialize(tag: :input, size: :m, error: nil, **attributes)
Expand Down
1 change: 1 addition & 0 deletions admin/app/controllers/solidus_admin/stores_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ def permitted_resource_params
]
# This check can be removed once we drop support for Solidus 4.7 and earlier.
permitted << {address_attributes: Spree::PermittedAttributes.address_attributes} if Spree::Store.reflect_on_association(:address)
permitted.push(:logo, :favicon) if Spree::Store.try(:attachments_available?)

params.require(:store).permit(*permitted).tap do |store_params|
without_address_parameter = store_params[:address_attributes]&.except(:country_id, :state_id, :reverse_charge_status)&.compact_blank&.empty?
Expand Down
44 changes: 44 additions & 0 deletions admin/spec/requests/solidus_admin/stores_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,4 +81,48 @@
end
end
end

describe "logo and favicon", if: Spree::Store.attachments_available? do
let(:admin_user) { create(:admin_user) }
let(:store) { create(:store, default: false) }
let(:image) { fixture_file_upload(Spree::Core::Engine.root.join("lib/spree/testing_support/fixtures/blank.jpg"), "image/jpeg") }

before { allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) }

it "renders a multipart form with a file field for each" do
get solidus_admin.edit_store_path(store)
expect(response.body).to include('enctype="multipart/form-data"')
expect(response.body).to include('name="store[logo]"').and include('name="store[favicon]"')
expect(response.body.scan('type="file"').size).to eq(2)
end

it "saves the uploaded images" do
patch solidus_admin.store_path(store), params: {store: {logo: image, favicon: image}}
expect(response).to have_http_status(:see_other)
expect(store.reload.logo).to be_attached
expect(store.favicon).to be_attached
end

it "shows the saved images on the form" do
store.update!(logo: image, favicon: image)
# These specs draw the app's routes without Active Storage's, so its URLs can't be generated.
allow_any_instance_of(ActiveStorage::Blob).to receive(:url).and_return("/stored.jpg")

get solidus_admin.edit_store_path(store)
expect(response.body).to include('alt="Logo"').and include('alt="Favicon"')
end
end

describe "logo and favicon without Active Storage", unless: Spree::Store.attachments_available? do
let(:admin_user) { create(:admin_user) }
let(:store) { create(:store, default: false) }

before { allow_any_instance_of(SolidusAdmin::BaseController).to receive(:spree_current_user).and_return(admin_user) }

it "renders the form without file fields" do
get solidus_admin.edit_store_path(store)
expect(response).to have_http_status(:ok)
expect(response.body).not_to include('type="file"')
end
end
end
24 changes: 24 additions & 0 deletions core/app/models/spree/store.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ module Spree
# hosted by a single Solidus implementation can be built.
#
class Store < Spree::Base
# The logo and favicon are stored with Active Storage, so a store has them
# only when images use the Active Storage attachment adapter.
#
# @return [Boolean]
def self.attachments_available?
Spree::Image.include?(Spree::Image::ActiveStorageAttachment)
end

if attachments_available?
has_one_attached :logo
has_one_attached :favicon
validate :logo_and_favicon_are_supported_images
end

has_many :store_payment_methods, inverse_of: :store
has_many :payment_methods, through: :store_payment_methods

Expand Down Expand Up @@ -68,6 +82,16 @@ def default_cart_tax_location

private

def logo_and_favicon_are_supported_images
[:logo, :favicon].each do |name|
attachment = public_send(name)
next unless attachment.attached?
next if attachment.content_type.in?(Spree::Config.allowed_image_mime_types)

errors.add(name, :content_type_not_supported)
end
end

def ensure_default_exists_and_is_unique
if default
Spree::Store.where.not(id:).update_all(default: false)
Expand Down
2 changes: 2 additions & 0 deletions core/config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ en:
code: Slug
default: Default
default_currency: Default Currency
favicon: Favicon
logo: Logo
mail_from_address: Mail From Address
meta_description: Meta Description
meta_keywords: Meta Keywords
Expand Down
1 change: 1 addition & 0 deletions core/lib/spree/permitted_attributes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ module PermittedAttributes
:meta_description, :default_currency,
:mail_from_address, :cart_tax_country_iso,
:bcc_email, :reverse_charge_status,
:logo, :favicon,
address_attributes:]

@@taxonomy_attributes = [:name]
Expand Down
41 changes: 41 additions & 0 deletions core/spec/models/spree/store_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,47 @@
RSpec.describe Spree::Store, type: :model do
it { is_expected.to respond_to(:cart_tax_country_iso) }

describe "#logo and #favicon", if: described_class.attachments_available? do
let(:store) { create(:store) }

def upload(name, content_type)
Rack::Test::UploadedFile.new(Spree::Core::Engine.root.join("lib/spree/testing_support/fixtures", name), content_type)
end

it "attaches each image separately" do
store.update!(logo: upload("blank.jpg", "image/jpeg"))

expect(store.reload.logo).to be_attached
expect(store.favicon).not_to be_attached
end

it "accepts the allowed image types" do
store.favicon.attach(upload("blank.jpg", "image/jpeg"))

expect(store).to be_valid
end

it "rejects files that are not one of the allowed image types" do
store.logo.attach(io: StringIO.new("not an image"), filename: "logo.txt", content_type: "text/plain")
store.favicon.attach(io: StringIO.new("not an image"), filename: "favicon.txt", content_type: "text/plain")

expect(store).not_to be_valid
expect(store.errors.details[:logo]).to include(error: :content_type_not_supported)
expect(store.errors.details[:favicon]).to include(error: :content_type_not_supported)
end
end

describe ".attachments_available?" do
it "is true only with the Active Storage image adapter" do
expect(described_class.attachments_available?).to eq(Spree::Config.image_attachment_module == Spree::Image::ActiveStorageAttachment)
end

it "defines the logo and favicon only when available" do
expect(described_class.new.respond_to?(:logo)).to eq(described_class.attachments_available?)
expect(described_class.new.respond_to?(:favicon)).to eq(described_class.attachments_available?)
end
end

describe ".default" do
it "should ensure saved store becomes default if one doesn't exist yet" do
expect(Spree::Store.where(default: true).count).to eq(0)
Expand Down
14 changes: 14 additions & 0 deletions storefront/templates/app/helpers/layout_helper.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,18 @@
module LayoutHelper
# @return [String, nil] the URL of the current store's logo, if it has one
def store_logo_url
return unless Spree::Store.attachments_available?

url_for(current_store.logo) if current_store&.logo&.attached?
end

# @return [String, nil] the URL of the current store's favicon, if it has one
def store_favicon_url
return unless Spree::Store.attachments_available?

url_for(current_store.favicon) if current_store&.favicon&.attached?
end

# Generates a simple canonical tag based on the request path, preserving allowed
# parameters. For collection actions, a trailing slash is added to the href.
# For more advanced use cases, consider using the `canonical-rails` gem.
Expand Down
8 changes: 6 additions & 2 deletions storefront/templates/app/views/layouts/_footer.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@
<div class="col-span-full border-t-[1px] border-t-black dark:border-t-white py-16">
<div class="text-center col-span-full md:flex md:items-center md:justify-between md:col-start-2 md:col-span-10">
<div class="col-span-full mb-14 md:mb-0">
<%= image_tag "logo_full_with_label.svg", class: "mx-auto w-52 mb-7 dark:hidden" %>
<%= image_tag "logo_full_with_label_white.svg", class: "mx-auto w-52 mb-7 hidden dark:block" %>
<% if (logo_url = store_logo_url) %>
<%= image_tag logo_url, alt: current_store.name, class: "mx-auto w-52 mb-7" %>
<% else %>
<%= image_tag "logo_full_with_label.svg", class: "mx-auto w-52 mb-7 dark:hidden" %>
<%= image_tag "logo_full_with_label_white.svg", class: "mx-auto w-52 mb-7 hidden dark:block" %>
<% end %>
<%= render partial: "shared/call_to_action", :locals => { :label => "Visit Solidus.io", :url => 'https://solidus.io', :target => '_blank', :additional_classes => "block mx-auto" } %>
</div>
<div class="col-span-full">
Expand Down
6 changes: 5 additions & 1 deletion storefront/templates/app/views/layouts/_header.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@
</div>
<div class="flex items-center gap-x-4 xl:gap-x-8 w-full justify-center lg:justify-start">
<%= link_to root_path, class: 'absolute lg:relative left-1/2 lg:left-0 lg:-translate-x-0 -translate-x-1/2 flex items-center space-x-3' do %>
<%= image_tag "logo_small_no_label.svg" %>
<% if (logo_url = store_logo_url) %>
<%= image_tag logo_url, alt: current_store.name, class: "h-8 w-auto" %>
<% else %>
<%= image_tag "logo_small_no_label.svg" %>
<% end %>
<span class="uppercase font-sans font-semibold mb-0 dark:text-sand text-body lg:text-body-sm xl:text-body-sm"><%= Spree::Store.default.name %></span>
<% end %>

Expand Down
8 changes: 6 additions & 2 deletions storefront/templates/app/views/layouts/storefront.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@

<%= javascript_importmap_tags %>

<%= favicon_link_tag '/storefront_favicon.svg', type: 'image/svg+xml', rel: 'icon' %>
<%= favicon_link_tag '/storefront_favicon.ico', rel: 'icon', type: 'image/ico' %>
<% if (favicon_url = store_favicon_url) %>
<%= favicon_link_tag favicon_url, rel: 'icon', type: nil %>
<% else %>
<%= favicon_link_tag '/storefront_favicon.svg', type: 'image/svg+xml', rel: 'icon' %>
<%= favicon_link_tag '/storefront_favicon.ico', rel: 'icon', type: 'image/ico' %>
<% end %>

<script>
const lsTheme = localStorage.getItem('theme') || 'light'
Expand Down
25 changes: 25 additions & 0 deletions storefront/templates/spec/system/template_rendering_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,29 @@
find("link[rel=canonical]")[:href]
).to eql("http://spreestore.example.com/t/solidus-brand/accessories?page=2")
end

it "uses the default logo and favicon when the store has none" do
visit root_path

expect(page).to have_css("header img[src*='logo_small_no_label']")
expect(page).to have_css("footer img[src*='logo_full_with_label']", count: 2)
expect(page).to have_css("link[rel=icon][href*='storefront_favicon']", count: 2)
end

it "uses the store's logo and favicon when it has them" do
image = Spree::Core::Engine.root.join("lib/spree/testing_support/fixtures/blank.jpg")
Spree::Store.default.update!(
logo: {io: File.open(image), filename: "logo.jpg"},
favicon: {io: File.open(image), filename: "favicon.jpg"}
)

visit root_path

expect(page).to have_css("header img[alt='My Spree Store']")
expect(page).to have_css("footer img[alt='My Spree Store']", count: 1)
expect(page).not_to have_css("img[src*='logo_small_no_label'], img[src*='logo_full_with_label']")
expect(page).to have_css("header img[src*='/rails/active_storage/']")
expect(page).to have_css("link[rel=icon][href*='/rails/active_storage/']", count: 1)
expect(page).not_to have_css("link[rel=icon][href*='storefront_favicon']")
end
end
Loading