-
-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add structured data fields to Spree stores #6113
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
<fieldset class="<%= stimulus_id %>" | ||
data-controller="<%= stimulus_id %>" | ||
> | ||
<div class="<%= stimulus_id %>--address-form flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(@name, :legal_name, object: @store) %> | ||
<%= render component("ui/forms/field").text_field(@name, :address1, object: @store) %> | ||
<%= render component("ui/forms/field").text_field(@name, :address2, object: @store) %> | ||
<div class="flex gap-4 w-full"> | ||
<%= render component("ui/forms/field").text_field(@name, :city, object: @store) %> | ||
<%= render component("ui/forms/field").text_field(@name, :zipcode, object: @store) %> | ||
</div> | ||
|
||
<%= render component("ui/forms/field").select( | ||
@name, | ||
:country_id, | ||
Spree::Country.pluck(:name, :id), | ||
object: @store, | ||
value: @store.country_id, | ||
"data-#{stimulus_id}-target": "country", | ||
"data-action": "change->#{stimulus_id}#loadStates" | ||
) %> | ||
<%= content_tag :div, | ||
class: "flex flex-col gap-2 w-full #{'hidden' if @store.country&.states_required}", | ||
data: { "#{stimulus_id}-target": "stateNameWrapper" } do %> | ||
<%= render component("ui/forms/field").text_field( | ||
@name, :state_name, | ||
object: @store, | ||
value: @store.state_name, | ||
data: { "#{stimulus_id}-target": "stateName" } | ||
) %> | ||
<% end %> | ||
|
||
<input autocomplete="off" type="hidden" name=<%= "#{@name}[state_id]" %>> | ||
|
||
<%= content_tag :div, | ||
class: "flex flex-col gap-2 w-full #{'hidden' unless @store.country&.states_required}", | ||
data: { "#{stimulus_id}-target": "stateWrapper" } do %> | ||
<%= render component("ui/forms/field").select( | ||
@name, :state_id, | ||
state_options, | ||
object: @store, | ||
value: @store.state_id, | ||
data: { "#{stimulus_id}-target": "state" } | ||
) %> | ||
<% end %> | ||
<%= render component("ui/forms/field").text_field(@name, :tax_id, object: @store) %> | ||
<%= render component("ui/forms/field").text_field(@name, :vat_id, object: @store, hint: t(".hint.vat_id").html_safe) %> | ||
</div> | ||
</fieldset> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Controller } from "@hotwired/stimulus" | ||
|
||
export default class extends Controller { | ||
static targets = ["country", "state", "stateName", "stateWrapper", "stateNameWrapper"] | ||
|
||
loadStates() { | ||
const countryId = this.countryTarget.value | ||
|
||
fetch(`/admin/countries/${countryId}/states`) | ||
.then((response) => response.json()) | ||
.then((data) => { | ||
this.updateStateOptions(data) | ||
}) | ||
} | ||
|
||
updateStateOptions(states) { | ||
if (states.length === 0) { | ||
this.toggleStateFields(false) | ||
} else { | ||
this.toggleStateFields(true) | ||
this.populateStateSelect(states) | ||
} | ||
} | ||
|
||
toggleStateFields(showSelect) { | ||
const stateWrapper = this.stateWrapperTarget | ||
const stateNameWrapper = this.stateNameWrapperTarget | ||
const stateSelect = this.stateTarget | ||
const stateName = this.stateNameTarget | ||
|
||
if (showSelect) { | ||
// Show state select dropdown. | ||
stateSelect.disabled = false | ||
stateName.value = "" | ||
stateWrapper.classList.remove("hidden") | ||
stateNameWrapper.classList.add("hidden") | ||
} else { | ||
// Show state name text input if no states to choose from. | ||
stateSelect.disabled = true | ||
stateWrapper.classList.add("hidden") | ||
stateNameWrapper.classList.remove("hidden") | ||
} | ||
} | ||
|
||
populateStateSelect(states) { | ||
const stateSelect = this.stateTarget | ||
stateSelect.innerHTML = "" | ||
|
||
states.forEach((state) => { | ||
const option = document.createElement("option") | ||
option.value = state.id | ||
option.innerText = state.name | ||
stateSelect.appendChild(option) | ||
}) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
# frozen_string_literal: true | ||
|
||
class SolidusAdmin::Stores::AddressForm::Component < SolidusAdmin::BaseComponent | ||
def initialize(store:) | ||
@name = "store" | ||
@store = store | ||
end | ||
|
||
def state_options | ||
country = @store.country | ||
return [] unless country && country.states_required | ||
|
||
country.states.pluck(:name, :id) | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
en: | ||
hint: | ||
vat_id: Enter your VAT-ID without the Country Prefix (eg IT for Italy or DE for Germany) but solely the identification string. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<%= page do %> | ||
<%= page_header do %> | ||
<%= page_header_back(solidus_admin.stores_path) %> | ||
<%= page_header_title(t(".title", store: @store&.name)) %> | ||
<%= page_header_actions do %> | ||
<div class="py-1.5 text-center"> | ||
<%= render component("ui/button").new(tag: :button, text: t(".update"), form: form_id) %> | ||
<%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.edit_store_path(@store), scheme: :secondary) %> | ||
</div> | ||
<% end %> | ||
<% end %> | ||
|
||
<%= form_for @store, url: solidus_admin.store_path(@store), html: { id: form_id } do |f| %> | ||
<%= page_with_sidebar do %> | ||
<%= page_with_sidebar_main do %> | ||
<%= render component("ui/panel").new(title: t(".store_settings")) do %> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why haven't you used the existing address form component? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It didn't seem indicated to invoke the address list for performance reasons. |
||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :name, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :url, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :code, required: true, hint: t(".hint.code").html_safe) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".regional_settings")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:default_currency, | ||
currency_options, | ||
include_blank: true, | ||
hint: t(".hint.default_currency").html_safe | ||
) %> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:cart_tax_country_iso, | ||
cart_tax_country_options, | ||
include_blank: t(".no_cart_tax_country"), | ||
hint: t(".hint.cart_tax_country_iso").html_safe | ||
) %> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:available_locales, | ||
localization_options, | ||
multiple: true, | ||
class: "select2", | ||
name: "store[available_locales][]", | ||
hint: t(".hint.available_locales").html_safe | ||
) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".email_settings")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :bcc_email) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".store_legal_addres")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<div class="js-addresses-form"> | ||
<%= render component("stores/address_form").new( | ||
store: @store, | ||
) %> | ||
</div> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".contact_options")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :contact_phone) %> | ||
<%= render component("ui/forms/field").text_field(f, :contact_email) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".content_on_storefront")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :seo_title) %> | ||
<%= render component("ui/forms/field").text_area(f, :description) %> | ||
<%= render component("ui/forms/field").text_field(f, :meta_keywords) %> | ||
<%= render component("ui/forms/field").text_area(f, :meta_description) %> | ||
</div> | ||
<% end %> | ||
<% end %> | ||
<% end %> | ||
<% end %> | ||
<% end %> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
# frozen_string_literal: true | ||
|
||
class SolidusAdmin::Stores::Edit::Component < SolidusAdmin::BaseComponent | ||
include SolidusAdmin::Layout::PageHelpers | ||
|
||
# Define the necessary attributes for the component | ||
attr_reader :store, :available_countries | ||
|
||
# Initialize the component with required data | ||
def initialize(store:) | ||
@store = store | ||
@available_countries = fetch_available_countries | ||
end | ||
|
||
def form_id | ||
@form_id ||= "#{stimulus_id}--form-#{@store.id}" | ||
end | ||
|
||
def currency_options | ||
Spree::Config.available_currencies.map(&:iso_code) | ||
end | ||
|
||
# Generates options for cart tax countries | ||
def cart_tax_country_options | ||
fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]).map do |country| | ||
[country.name, country.iso] | ||
end | ||
end | ||
|
||
# Generates available locales | ||
def localization_options | ||
Spree.i18n_available_locales.map do |locale| | ||
[ | ||
I18n.t('spree.i18n.this_file_language', locale: locale, default: locale.to_s), | ||
locale | ||
] | ||
end | ||
end | ||
|
||
# Fetch countries for the address form | ||
def available_country_options | ||
Spree::Country.order(:name).map { |country| [country.name, country.id] } | ||
end | ||
|
||
private | ||
|
||
# Fetch the available countries for the localization section | ||
def fetch_available_countries(restrict_to_zone: Spree::Config[:checkout_zone]) | ||
countries = Spree::Country.available(restrict_to_zone:) | ||
|
||
country_names = Carmen::Country.all.map do |country| | ||
[country.code, country.name] | ||
end.to_h | ||
|
||
country_names.update I18n.t('spree.country_names', default: {}).stringify_keys | ||
|
||
countries.collect do |country| | ||
country.name = country_names.fetch(country.iso, country.name) | ||
country | ||
end.sort_by { |country| country.name.parameterize } | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
en: | ||
address: Address | ||
cancel: Cancel | ||
contact_options: Contact Options | ||
content_on_storefront: Content on Storefront | ||
email_settings: Email Settings | ||
hint: | ||
available_locales: This determines which locales are available for your customers to choose from in the storefront. | ||
cart_tax_country_iso: 'This determines which country is used for taxes on carts (orders which don''t yet have an address).<br> Default: None.' | ||
code: An identifier for your store. Developers may need this value if you operate multiple storefronts. | ||
default_currency: This determines which currency will be used for the storefront's product prices. Please, be aware that changing this configuration, only products that have prices in the selected currency will be listed on your storefront. <br>This setting won't change the default currency used when you create a product. For that, only the global `Spree::Config.currency` is taken into account. | ||
regional_settings: Regional Settings | ||
store_details: Store Details | ||
store_legal_addres: Store Legal Address | ||
store_settings: Store Name & URL / URI Settings | ||
title: "%{store}" | ||
update: Update |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
<%= page do %> | ||
<%= page_header do %> | ||
<%= page_header_back(solidus_admin.stores_path) %> | ||
<%= page_header_title(t(".title")) %> | ||
<%= page_header_actions do %> | ||
<div class="py-1.5 text-center"> | ||
<%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %> | ||
<%= render component("ui/button").new(tag: :a, text: t(".cancel"), href: solidus_admin.new_store_path, scheme: :secondary) %> | ||
</div> | ||
<% end %> | ||
<% end %> | ||
|
||
<%= form_for @store, url: solidus_admin.stores_path, html: { id: form_id } do |f| %> | ||
<%= page_with_sidebar do %> | ||
<%= page_with_sidebar_main do %> | ||
<%= render component("ui/panel").new(title: t(".store_settings")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :name, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :url, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :code, required: true, hint: t(".hint.code").html_safe) %> | ||
</div> <!-- Closing div was missing --> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".regional_settings")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:default_currency, | ||
currency_options, | ||
include_blank: true, | ||
hint: t(".hint.default_currency").html_safe | ||
) %> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:cart_tax_country_iso, | ||
cart_tax_country_options, | ||
include_blank: t(".no_cart_tax_country"), | ||
hint: t(".hint.cart_tax_country_iso").html_safe | ||
) %> | ||
<%= render component("ui/forms/field").select( | ||
f, | ||
:available_locales, | ||
localization_options, | ||
multiple: true, | ||
class: "select2", | ||
name: "store[available_locales][]", | ||
hint: t(".hint.available_locales").html_safe | ||
) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".email_settings")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :mail_from_address, required: true) %> | ||
<%= render component("ui/forms/field").text_field(f, :bcc_email) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".store_legal_addres")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<div class="js-addresses-form"> | ||
<%= render component("stores/address_form").new( | ||
store: @store, | ||
) %> | ||
</div> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".contact_options")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :contact_phone) %> | ||
<%= render component("ui/forms/field").text_field(f, :contact_email) %> | ||
</div> | ||
<% end %> | ||
|
||
<%= render component("ui/panel").new(title: t(".content_on_storefront")) do %> | ||
<div class="flex flex-wrap gap-4 pb-4"> | ||
<%= render component("ui/forms/field").text_field(f, :seo_title) %> | ||
<%= render component("ui/forms/field").text_area(f, :description) %> | ||
<%= render component("ui/forms/field").text_field(f, :meta_keywords) %> | ||
<%= render component("ui/forms/field").text_area(f, :meta_description) %> | ||
</div> | ||
<% end %> | ||
<% end %> | ||
<% end %> | ||
<% end %> | ||
<% end %> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We already have a address form component that handles states. Can we use that one instead of implementing a second one?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we can look into reusing the logic to pick a state. Does it make sense if everything else should be detached from the address table?