solidus_admin 0.0.0 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +5 -5
  2. data/LICENSE +7 -0
  3. data/README.md +31 -0
  4. data/Rakefile +21 -0
  5. data/app/assets/config/solidus_admin_manifest.js +4 -0
  6. data/app/assets/images/solidus_admin/.keep +0 -0
  7. data/app/assets/images/solidus_admin/arrow_down_s_fill_gray_700.svg +3 -0
  8. data/app/assets/images/solidus_admin/arrow_down_s_fill_red_400.svg +3 -0
  9. data/app/assets/images/solidus_admin/arrow_right_up_line.svg +5 -0
  10. data/app/assets/images/solidus_admin/favicon.ico +0 -0
  11. data/app/assets/images/solidus_admin/remixicon.symbol.svg +11 -0
  12. data/app/assets/stylesheets/solidus_admin/application.css +3 -0
  13. data/app/assets/stylesheets/solidus_admin/application.tailwind.css.erb +35 -0
  14. data/app/components/solidus_admin/base_component.rb +42 -0
  15. data/app/components/solidus_admin/feedback/component.html.erb +11 -0
  16. data/app/components/solidus_admin/feedback/component.rb +4 -0
  17. data/app/components/solidus_admin/feedback/component.yml +5 -0
  18. data/app/components/solidus_admin/orders/index/component.html.erb +31 -0
  19. data/app/components/solidus_admin/orders/index/component.rb +118 -0
  20. data/app/components/solidus_admin/orders/index/component.yml +13 -0
  21. data/app/components/solidus_admin/products/index/component.html.erb +30 -0
  22. data/app/components/solidus_admin/products/index/component.rb +126 -0
  23. data/app/components/solidus_admin/products/index/component.yml +13 -0
  24. data/app/components/solidus_admin/products/show/component.html.erb +149 -0
  25. data/app/components/solidus_admin/products/show/component.js +9 -0
  26. data/app/components/solidus_admin/products/show/component.rb +26 -0
  27. data/app/components/solidus_admin/products/show/component.yml +17 -0
  28. data/app/components/solidus_admin/products/status/component.rb +31 -0
  29. data/app/components/solidus_admin/products/status/component.yml +3 -0
  30. data/app/components/solidus_admin/sidebar/account_nav/component.html.erb +67 -0
  31. data/app/components/solidus_admin/sidebar/account_nav/component.rb +15 -0
  32. data/app/components/solidus_admin/sidebar/account_nav/component.yml +3 -0
  33. data/app/components/solidus_admin/sidebar/component.html.erb +39 -0
  34. data/app/components/solidus_admin/sidebar/component.js +14 -0
  35. data/app/components/solidus_admin/sidebar/component.rb +21 -0
  36. data/app/components/solidus_admin/sidebar/component.yml +2 -0
  37. data/app/components/solidus_admin/sidebar/item/component.html.erb +26 -0
  38. data/app/components/solidus_admin/sidebar/item/component.rb +27 -0
  39. data/app/components/solidus_admin/skip_link/component.rb +24 -0
  40. data/app/components/solidus_admin/skip_link/component.yml +2 -0
  41. data/app/components/solidus_admin/ui/badge/component.rb +34 -0
  42. data/app/components/solidus_admin/ui/button/component.rb +101 -0
  43. data/app/components/solidus_admin/ui/forms/checkbox/component.rb +42 -0
  44. data/app/components/solidus_admin/ui/forms/field/component.html.erb +28 -0
  45. data/app/components/solidus_admin/ui/forms/field/component.rb +72 -0
  46. data/app/components/solidus_admin/ui/forms/input/component.js +16 -0
  47. data/app/components/solidus_admin/ui/forms/input/component.rb +99 -0
  48. data/app/components/solidus_admin/ui/forms/switch/component.rb +47 -0
  49. data/app/components/solidus_admin/ui/icon/component.rb +25 -0
  50. data/app/components/solidus_admin/ui/icon/names.txt +2494 -0
  51. data/app/components/solidus_admin/ui/panel/component.html.erb +36 -0
  52. data/app/components/solidus_admin/ui/panel/component.js +14 -0
  53. data/app/components/solidus_admin/ui/panel/component.rb +19 -0
  54. data/app/components/solidus_admin/ui/panel/component.yml +4 -0
  55. data/app/components/solidus_admin/ui/tab/component.rb +43 -0
  56. data/app/components/solidus_admin/ui/table/component.html.erb +170 -0
  57. data/app/components/solidus_admin/ui/table/component.js +118 -0
  58. data/app/components/solidus_admin/ui/table/component.rb +150 -0
  59. data/app/components/solidus_admin/ui/table/component.yml +11 -0
  60. data/app/components/solidus_admin/ui/table/pagination/component.html.erb +28 -0
  61. data/app/components/solidus_admin/ui/table/pagination/component.rb +14 -0
  62. data/app/components/solidus_admin/ui/table/pagination/component.yml +3 -0
  63. data/app/components/solidus_admin/ui/toast/component.html.erb +26 -0
  64. data/app/components/solidus_admin/ui/toast/component.js +17 -0
  65. data/app/components/solidus_admin/ui/toast/component.rb +18 -0
  66. data/app/components/solidus_admin/ui/toast/component.yml +4 -0
  67. data/app/components/solidus_admin/ui/toggletip/component.html.erb +53 -0
  68. data/app/components/solidus_admin/ui/toggletip/component.js +26 -0
  69. data/app/components/solidus_admin/ui/toggletip/component.rb +98 -0
  70. data/app/components/solidus_admin/ui/toggletip/component.yml +2 -0
  71. data/app/controllers/solidus_admin/accounts_controller.rb +11 -0
  72. data/app/controllers/solidus_admin/authentication_adapters/backend.rb +26 -0
  73. data/app/controllers/solidus_admin/base_controller.rb +21 -0
  74. data/app/controllers/solidus_admin/controller_helpers/authentication.rb +31 -0
  75. data/app/controllers/solidus_admin/controller_helpers/authorization.rb +29 -0
  76. data/app/controllers/solidus_admin/controller_helpers/locale.rb +32 -0
  77. data/app/controllers/solidus_admin/orders_controller.rb +21 -0
  78. data/app/controllers/solidus_admin/products_controller.rb +93 -0
  79. data/app/helpers/solidus_admin/components_helper.rb +9 -0
  80. data/app/helpers/solidus_admin/layout_helper.rb +18 -0
  81. data/app/javascript/solidus_admin/application.js +2 -0
  82. data/app/javascript/solidus_admin/controllers/application.js +9 -0
  83. data/app/javascript/solidus_admin/controllers/components.js +35 -0
  84. data/app/javascript/solidus_admin/controllers/hello_controller.js +7 -0
  85. data/app/javascript/solidus_admin/controllers/index.js +14 -0
  86. data/app/javascript/solidus_admin/utils.js +8 -0
  87. data/app/views/layouts/solidus_admin/application.html.erb +30 -0
  88. data/app/views/layouts/solidus_admin/preview.html.erb +10 -0
  89. data/app/views/solidus_admin/.keep +0 -0
  90. data/bin/rails +13 -0
  91. data/config/importmap.rb +13 -0
  92. data/config/locales/main_nav.en.yml +13 -0
  93. data/config/locales/orders.en.yml +4 -0
  94. data/config/locales/products.en.yml +10 -0
  95. data/config/routes.rb +13 -0
  96. data/config/solidus_admin/tailwind.config.js.erb +95 -0
  97. data/docs/customizing_main_navigation.md +42 -0
  98. data/docs/customizing_tailwind.md +78 -0
  99. data/docs/customizing_view_components.md +153 -0
  100. data/lib/generators/solidus_admin/component/USAGE +13 -0
  101. data/lib/generators/solidus_admin/component/component_generator.rb +130 -0
  102. data/lib/generators/solidus_admin/component/templates/component.html.erb.tt +3 -0
  103. data/lib/generators/solidus_admin/component/templates/component.js.tt +14 -0
  104. data/lib/generators/solidus_admin/component/templates/component.rb.tt +14 -0
  105. data/lib/generators/solidus_admin/component/templates/component.yml.tt +4 -0
  106. data/lib/generators/solidus_admin/component/templates/component_preview.rb.tt +15 -0
  107. data/lib/generators/solidus_admin/component/templates/component_preview_overview.html.erb +7 -0
  108. data/lib/generators/solidus_admin/component/templates/component_spec.rb.tt +16 -0
  109. data/lib/generators/solidus_admin/install/install_generator.rb +44 -0
  110. data/lib/generators/solidus_admin/install/templates/config/initializers/solidus_admin.rb +44 -0
  111. data/lib/solidus_admin/configuration.rb +217 -0
  112. data/lib/solidus_admin/engine.rb +67 -0
  113. data/lib/solidus_admin/importmap.rb +26 -0
  114. data/lib/solidus_admin/main_nav_item.rb +97 -0
  115. data/lib/solidus_admin/preview.rb +81 -0
  116. data/lib/solidus_admin/tailwindcss.rb +58 -0
  117. data/lib/solidus_admin/version.rb +5 -0
  118. data/lib/solidus_admin.rb +15 -0
  119. data/lib/tasks/importmap.rake +10 -0
  120. data/lib/tasks/tailwindcss.rake +55 -0
  121. data/solidus_admin.gemspec +35 -0
  122. metadata +255 -18
@@ -0,0 +1,3 @@
1
+ /*
2
+ * = require solidus_admin/tailwind.css
3
+ */
@@ -0,0 +1,35 @@
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ @layer base {
6
+ .body-tiny {
7
+ @apply font-sans font-normal text-xs leading-5;
8
+ }
9
+
10
+ .body-tiny-bold {
11
+ @apply font-sans font-semibold text-xs leading-5;
12
+ }
13
+
14
+ .body-small {
15
+ @apply font-sans font-normal text-sm leading-6;
16
+ }
17
+
18
+ .body-small-bold {
19
+ @apply font-sans font-semibold text-sm leading-6;
20
+ }
21
+
22
+ .body-text {
23
+ @apply font-sans font-normal text-base leading-6;
24
+ }
25
+
26
+ .body-text-bold {
27
+ @apply font-sans font-semibold text-base leading-6;
28
+ }
29
+
30
+ .body-title {
31
+ @apply font-sans font-semibold text-xl leading-5;
32
+ }
33
+ }
34
+
35
+ <%= SolidusAdmin::Config.tailwind_stylesheets.map { File.read(_1) }.join("\n") %>
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SolidusAdmin
4
+ # BaseComponent is the base class for all components in Solidus Admin.
5
+ class BaseComponent < ViewComponent::Base
6
+ include SolidusAdmin::ComponentsHelper
7
+ include Turbo::FramesHelper
8
+
9
+ def icon_tag(name, **attrs)
10
+ render component("ui/icon").new(name: name, **attrs)
11
+ end
12
+
13
+ def missing_translation(key, options)
14
+ keys = I18n.normalize_keys(options[:locale] || I18n.locale, key, options[:scope])
15
+
16
+ logger.debug " [#{self.class}] Missing translation: #{keys.join('.')}"
17
+
18
+ if options[:locale] != :en
19
+ t(key, **options, locale: :en)
20
+ else
21
+ "translation missing: #{keys.join('.')}"
22
+ end
23
+ end
24
+
25
+ def self.stimulus_id
26
+ @stimulus_id ||= name.underscore
27
+ .sub(/^solidus_admin\/(.*)\/component$/, '\1')
28
+ .gsub("/", "--")
29
+ .tr("_", "-")
30
+ end
31
+
32
+ delegate :stimulus_id, to: :class
33
+
34
+ def spree
35
+ @spree ||= Spree::Core::Engine.routes.url_helpers
36
+ end
37
+
38
+ def solidus_admin
39
+ @solidus_admin ||= SolidusAdmin::Engine.routes.url_helpers
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,11 @@
1
+ <span class="max-w-xs text-xs text-right text-gray-600 hidden lg:block">
2
+ <%= t(".feedback_description") %>
3
+ </span>
4
+ <%= render component("ui/button").new(
5
+ tag: :a,
6
+ text: t(".give_feedback"),
7
+ href: "https://solidus.io/feedback?category=solidus-admin",
8
+ icon: "feedback-line",
9
+ scheme: :secondary,
10
+ target: :_blank,
11
+ ) %>
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::Feedback::Component < SolidusAdmin::BaseComponent
4
+ end
@@ -0,0 +1,5 @@
1
+ # Add your component translations here.
2
+ # Use the translation in the example in your template with `t(".hello")`.
3
+ en:
4
+ give_feedback: 'Give feedback'
5
+ feedback_description: 'We are constantly trying to improve. Please let us know what you think about this admin page.'
@@ -0,0 +1,31 @@
1
+ <div class="<%= stimulus_id %> px-4">
2
+ <header class="py-6 flex items-center">
3
+ <h1 class="body-title">
4
+ <%= title %>
5
+ </h1>
6
+
7
+ <div class="ml-auto flex gap-2 items-center">
8
+ <%= render component("feedback").new %>
9
+ <%= render component("ui/button").new(
10
+ tag: :a,
11
+ text: t('.create_order'),
12
+ href: spree.new_admin_order_path,
13
+ icon: "add-line",
14
+ ) %>
15
+ </div>
16
+ </header>
17
+
18
+ <%= render component('ui/table').new(
19
+ id: 'orders-list',
20
+ model_class: Spree::Order,
21
+ rows: @page.records,
22
+ fade_row_proc: fade_row_proc,
23
+ search_key: SolidusAdmin::Config[:order_search_key],
24
+ search_url: solidus_admin.orders_path,
25
+ batch_actions: batch_actions,
26
+ filters: filters,
27
+ columns: columns,
28
+ prev_page_link: prev_page_link,
29
+ next_page_link: next_page_link,
30
+ ) %>
31
+ </div>
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::Orders::Index::Component < SolidusAdmin::BaseComponent
4
+ def initialize(page:)
5
+ @page = page
6
+ end
7
+
8
+ class_attribute :fade_row_proc, default: ->(order) { order.paid? && order.shipped? }
9
+
10
+ def title
11
+ Spree::Order.model_name.human.pluralize
12
+ end
13
+
14
+ def prev_page_link
15
+ @page.first? ? nil : solidus_admin.url_for(host: request.host, port: request.port, **request.params, page: @page.number - 1)
16
+ end
17
+
18
+ def next_page_link
19
+ @page.last? ? nil : solidus_admin.url_for(host: request.host, port: request.port, **request.params, page: @page.next_param)
20
+ end
21
+
22
+ def batch_actions
23
+ []
24
+ end
25
+
26
+ def filters
27
+ [
28
+ {
29
+ name: 'q[completed_at_not_null]',
30
+ value: 1,
31
+ label: t('.filters.only_show_complete_orders'),
32
+ },
33
+ ]
34
+ end
35
+
36
+ def columns
37
+ [
38
+ number_column,
39
+ date_column,
40
+ customer_column,
41
+ total_column,
42
+ items_column,
43
+ payment_column,
44
+ shipment_column,
45
+ ]
46
+ end
47
+
48
+ def number_column
49
+ {
50
+ header: :order,
51
+ data: ->(order) do
52
+ order_path = spree.edit_admin_order_path(order)
53
+
54
+ if !fade_row_proc.call(order)
55
+ link_to order.number, order_path, class: 'font-semibold'
56
+ else
57
+ link_to order.number, order_path
58
+ end
59
+ end
60
+ }
61
+ end
62
+
63
+ def date_column
64
+ {
65
+ header: :date,
66
+ data: ->(order) do
67
+ content_tag :div, l(order.created_at, format: :short)
68
+ end
69
+ }
70
+ end
71
+
72
+ def customer_column
73
+ {
74
+ class_name: "w-[400px]",
75
+ header: :customer,
76
+ data: ->(order) do
77
+ customer_email = order.user&.email
78
+ content_tag :div, String(customer_email)
79
+ end
80
+ }
81
+ end
82
+
83
+ def total_column
84
+ {
85
+ header: :total,
86
+ data: ->(order) do
87
+ content_tag :div, number_to_currency(order.total)
88
+ end
89
+ }
90
+ end
91
+
92
+ def items_column
93
+ {
94
+ header: :items,
95
+ data: ->(order) do
96
+ content_tag :div, t('.columns.items', count: order.line_items.sum(:quantity))
97
+ end
98
+ }
99
+ end
100
+
101
+ def payment_column
102
+ {
103
+ header: :payment,
104
+ data: ->(order) do
105
+ component('ui/badge').new(name: order.payment_state&.humanize, color: order.paid? ? :green : :yellow)
106
+ end
107
+ }
108
+ end
109
+
110
+ def shipment_column
111
+ {
112
+ header: :shipment,
113
+ data: ->(order) do
114
+ component('ui/badge').new(name: order.shipment_state&.humanize, color: order.shipped? ? :green : :yellow)
115
+ end
116
+ }
117
+ end
118
+ end
@@ -0,0 +1,13 @@
1
+ # Add your component translations here.
2
+ # Use the translation in the example in your template with `t(".hello")`.
3
+ en:
4
+ create_order: 'Create Order'
5
+ columns:
6
+ items:
7
+ one: 1 Item
8
+ other: '%{count} Items'
9
+ filters:
10
+ only_show_complete_orders: Only show complete orders
11
+ date:
12
+ formats:
13
+ short: '%d %b %y'
@@ -0,0 +1,30 @@
1
+ <div class="<%= stimulus_id %> px-4">
2
+ <header class="py-6 flex items-center">
3
+ <h1 class="body-title">
4
+ <%= title %>
5
+ </h1>
6
+
7
+ <div class="ml-auto flex gap-2 items-center">
8
+ <%= render component("feedback").new %>
9
+ <%= render component("ui/button").new(
10
+ tag: :a,
11
+ text: t('.add_product'),
12
+ href: spree.new_admin_product_path,
13
+ icon: "add-line",
14
+ ) %>
15
+ </div>
16
+ </header>
17
+
18
+ <%= render component('ui/table').new(
19
+ id: 'products-list',
20
+ model_class: Spree::Product,
21
+ rows: @page.records,
22
+ search_key: SolidusAdmin::Config[:product_search_key],
23
+ search_url: solidus_admin.products_path,
24
+ batch_actions: batch_actions,
25
+ filters: filters,
26
+ columns: columns,
27
+ prev_page_link: prev_page_link,
28
+ next_page_link: next_page_link,
29
+ ) %>
30
+ </div>
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::Products::Index::Component < SolidusAdmin::BaseComponent
4
+ def initialize(page:)
5
+ @page = page
6
+ end
7
+
8
+ def title
9
+ Spree::Product.model_name.human.pluralize
10
+ end
11
+
12
+ def prev_page_link
13
+ @page.first? ? nil : solidus_admin.url_for(host: request.host, port: request.port, **request.params, page: @page.number - 1)
14
+ end
15
+
16
+ def next_page_link
17
+ @page.last? ? nil : solidus_admin.url_for(host: request.host, port: request.port, **request.params, page: @page.next_param)
18
+ end
19
+
20
+ def batch_actions
21
+ [
22
+ {
23
+ display_name: t('.batch_actions.delete'),
24
+ action: solidus_admin.products_path,
25
+ method: :delete,
26
+ icon: 'delete-bin-7-line',
27
+ },
28
+ {
29
+ display_name: t('.batch_actions.discontinue'),
30
+ action: solidus_admin.discontinue_products_path,
31
+ method: :put,
32
+ icon: 'pause-circle-line',
33
+ },
34
+ {
35
+ display_name: t('.batch_actions.activate'),
36
+ action: solidus_admin.activate_products_path,
37
+ method: :put,
38
+ icon: 'play-circle-line',
39
+ },
40
+ ]
41
+ end
42
+
43
+ def filters
44
+ [
45
+ {
46
+ name: 'q[with_discarded]',
47
+ value: true,
48
+ label: t('.filters.with_deleted'),
49
+ },
50
+ ]
51
+ end
52
+
53
+ def columns
54
+ [
55
+ image_column,
56
+ name_column,
57
+ status_column,
58
+ price_column,
59
+ stock_column,
60
+ ]
61
+ end
62
+
63
+ def image_column
64
+ {
65
+ class_name: "w-[72px]",
66
+ header: tag.span('aria-label': t('.product_image'), role: 'text'),
67
+ data: ->(product) do
68
+ image = product.gallery.images.first or return
69
+
70
+ link_to(
71
+ image_tag(image.url(:small), class: 'h-10 w-10 max-w-min rounded border border-gray-100', alt: product.name),
72
+ solidus_admin.product_path(product),
73
+ class: 'inline-flex overflow-hidden',
74
+ tabindex: -1,
75
+ )
76
+ end
77
+ }
78
+ end
79
+
80
+ def name_column
81
+ {
82
+ header: :name,
83
+ data: ->(product) do
84
+ link_to product.name, solidus_admin.product_path(product)
85
+ end
86
+ }
87
+ end
88
+
89
+ def status_column
90
+ {
91
+ header: :status,
92
+ data: ->(product) { component('products/status').new(product: product) }
93
+ }
94
+ end
95
+
96
+ def stock_column
97
+ {
98
+ header: :stock,
99
+ data: ->(product) do
100
+ stock_info =
101
+ case (on_hand = product.total_on_hand)
102
+ when Float::INFINITY
103
+ content_tag :span, t('.stock.in_stock', on_hand: t('.stock.infinity')), class: 'text-forest'
104
+ when 1..Float::INFINITY
105
+ content_tag :span, t('.stock.in_stock', on_hand: on_hand), class: 'text-forest'
106
+ else
107
+ content_tag :span, t('.stock.in_stock', on_hand: on_hand), class: 'text-red-500'
108
+ end
109
+
110
+ variant_info =
111
+ t('.for_variants', count: product.variants.count)
112
+
113
+ content_tag :div, safe_join([stock_info, variant_info], ' ')
114
+ end
115
+ }
116
+ end
117
+
118
+ def price_column
119
+ {
120
+ header: :price,
121
+ data: ->(product) do
122
+ content_tag :div, product.master.display_price.to_html
123
+ end
124
+ }
125
+ end
126
+ end
@@ -0,0 +1,13 @@
1
+ en:
2
+ product_image: 'Image'
3
+ add_product: 'Add Product'
4
+ stock:
5
+ infinity: '∞'
6
+ in_stock: '%{on_hand} in stock'
7
+ for_variants: 'for %{count} variants'
8
+ batch_actions:
9
+ delete: 'Delete'
10
+ discontinue: 'Discontinue'
11
+ activate: 'Activate'
12
+ filters:
13
+ with_deleted: Include deleted
@@ -0,0 +1,149 @@
1
+ <div class="px-4 relative" data-controller="<%= stimulus_id %>">
2
+ <header class="py-6 flex items-center gap-4">
3
+ <%= render component("ui/button").new(
4
+ tag: :a,
5
+ title: t(".back"),
6
+ icon: "arrow-left-line",
7
+ scheme: :secondary,
8
+ href: solidus_admin.products_path
9
+ ) %>
10
+ <h1 class="flex items-center gap-2">
11
+ <span class="body-title"><%= @product.name %></span>
12
+ <%= render component("products/status").new(product: @product) %>
13
+ </h1>
14
+
15
+ <div class="ml-auto flex gap-2 items-center">
16
+ <%= render component("feedback").new %>
17
+ <%= render component("ui/button").new(
18
+ tag: :a,
19
+ text: t(".duplicate"),
20
+ href: spree.clone_admin_product_path(@product),
21
+ scheme: :ghost
22
+ ) %>
23
+
24
+ <%= render component("ui/button").new(
25
+ tag: :a,
26
+ text: t(".view"),
27
+ href: SolidusAdmin::Config.storefront_product_path(@product),
28
+ target: :_blank,
29
+ scheme: :ghost
30
+ ) %>
31
+ <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %>
32
+ </div>
33
+ </header>
34
+
35
+ <%= form_for @product, url: solidus_admin.product_path(@product), html: { id: form_id } do |f| %>
36
+ <div class="flex gap-4 items-start pb-4">
37
+ <div class="justify-center items-start gap-4 flex flex-col w-full">
38
+ <%= render component('ui/panel').new do %>
39
+ <%= render component("ui/forms/field").text_field(f, :name) %>
40
+ <%= render component("ui/forms/field").text_field(f, :slug) %>
41
+ <%= render component("ui/forms/field").text_area(f, :description) %>
42
+ <% end %>
43
+
44
+ <%= render component('ui/panel').new(title: 'SEO', title_hint: 'Search Engine Optimization') do %>
45
+ <%= render component("ui/forms/field").text_field(f, :meta_title) %>
46
+ <%= render component("ui/forms/field").text_field(f, :meta_description) %>
47
+ <%= render component("ui/forms/field").text_area(f, :meta_keywords) %>
48
+ <% end %>
49
+
50
+ <%= render component('ui/panel').new(title: "Media") do |panel| %>
51
+ <% panel.with_action(
52
+ name: t(".manage_images"),
53
+ href: spree.admin_product_images_path(@product)
54
+ ) %>
55
+ <% end %>
56
+
57
+ <%= render component('ui/panel').new(title: 'Pricing') do %>
58
+ <%= render component("ui/forms/field").text_field(f, :price) %>
59
+ <div class="flex gap-4 justify-items-stretch">
60
+ <%= render component("ui/forms/field").text_field(f, :cost_price) %>
61
+ <%= render component("ui/forms/field").text_field(f, :cost_currency) %>
62
+ </div>
63
+ <% end %>
64
+
65
+ <%= render component('ui/panel').new(title: 'Stock') do |panel| %>
66
+ <%= render component("ui/forms/field").text_field(f, :sku) %>
67
+
68
+ <% panel.with_action(
69
+ name: t(".manage_stock"),
70
+ href: spree.admin_product_stock_path(@product)
71
+ ) %>
72
+ <% end %>
73
+
74
+ <%= render component('ui/panel').new(title: 'Shipping') do %>
75
+ <%= render component("ui/forms/field").select(
76
+ f,
77
+ :shipping_category_id,
78
+ [[t(".none"), nil]] + Spree::ShippingCategory.order(:name).pluck(:name, :id),
79
+ tip: t(".hints.shipping_category_html"),
80
+ ) %>
81
+ <%= render component("ui/forms/field").select(
82
+ f,
83
+ :tax_category_id,
84
+ [[t(".none"), nil]] + Spree::TaxCategory.order(:name).pluck(:name, :id),
85
+ tip: t(
86
+ ".hints.tax_category_html",
87
+ default_tax_category: Spree::TaxCategory.default&.name
88
+ ),
89
+ ) %>
90
+ <% end %>
91
+
92
+ <%= render component('ui/panel').new(title: "Options") do %>
93
+ <%= render component("ui/forms/field").select(
94
+ f,
95
+ :option_type_ids,
96
+ option_type_options,
97
+ multiple: true,
98
+ "size" => option_type_options.size,
99
+ ) %>
100
+ <% end %>
101
+
102
+ <%= render component('ui/panel').new(title: "Specifications") do |panel| %>
103
+ <% panel.with_action(
104
+ name: t(".manage_properties"),
105
+ href: spree.admin_product_product_properties_path(@product)
106
+ ) %>
107
+ <% end %>
108
+ </div>
109
+
110
+ <aside class="justify-center items-start gap-4 flex flex-col w-full max-w-sm">
111
+ <%= render component('ui/panel').new(title: "Publishing") do %>
112
+ <%= render component("ui/forms/field").text_field(f, :available_on, tip: t(".hints.available_on_html"), type: :date) %>
113
+ <%= render component("ui/forms/field").text_field(f, :discontinue_on, tip: t(".hints.discontinue_on_html"), type: :date) %>
114
+
115
+ <label class="flex gap-2 items-center">
116
+ <%= render component("ui/forms/checkbox").new(
117
+ name: "#{f.object_name}[promotionable]",
118
+ checked: f.object.promotionable
119
+ ) %>
120
+ <span class="body-text-sm"><%= Spree::Product.human_attribute_name :promotionable %></span>
121
+ <%= render component("ui/toggletip").new(
122
+ text: t(".hints.promotionable_html"),
123
+ position: :left
124
+ ) %>
125
+ </label>
126
+ <% end %>
127
+
128
+ <%= render component('ui/panel').new(title: "Product organization") do %>
129
+ <%= render component("ui/forms/field").select(f, :taxon_ids, taxon_options, multiple: true, "size" => taxon_options.size) %>
130
+ <% end %>
131
+ </aside>
132
+ </div>
133
+ <% end %>
134
+
135
+ <div class="mt-4 py-4 px-2 pb-8 border-t border-gray-100 flex">
136
+ <div class="flex gap-2 grow">
137
+ <%= form_for @product, url: solidus_admin.product_path(@product), method: :delete do %>
138
+ <%= render component("ui/button").new(
139
+ tag: :button,
140
+ text: t(".delete"),
141
+ scheme: :danger,
142
+ "data-action": "click->#{stimulus_id}#confirmDelete",
143
+ "data-#{stimulus_id}-message-param": t(".delete_confirmation"),
144
+ ) %>
145
+ <% end %>
146
+ </div>
147
+ <%= render component("ui/button").new(tag: :button, text: t(".save"), form: form_id) %>
148
+ </div>
149
+ </div>
@@ -0,0 +1,9 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ confirmDelete(event) {
5
+ if (!confirm(event.params.message)) {
6
+ event.preventDefault()
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::Products::Show::Component < SolidusAdmin::BaseComponent
4
+ def initialize(product:)
5
+ @product = product
6
+ end
7
+
8
+ def form_id
9
+ @form_id ||= "#{stimulus_id}--form-#{@product.id}"
10
+ end
11
+
12
+ private
13
+
14
+ def taxon_options
15
+ @taxon_options ||= Spree::Taxon.order(:lft).pluck(:name, :id, :lft, :depth).map do
16
+ name, id, _lft, depth = _1
17
+ ["#{'    ' * depth} → #{name}", id]
18
+ end
19
+ end
20
+
21
+ def option_type_options
22
+ @option_type_options ||= Spree::OptionType.order(:presentation).pluck(:presentation, :name, :id).map do
23
+ ["#{_1} (#{_2})", _3]
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,17 @@
1
+ en:
2
+ save: "Save"
3
+ back: "Back"
4
+ duplicate: "Duplicate"
5
+ view: "View online"
6
+ delete: "Delete"
7
+ none: "None"
8
+ manage_images: "Manage images"
9
+ manage_properties: "Manage product specifications"
10
+ manage_stock: "Manage stock"
11
+ delete_confirmation: "Are you sure you want to delete this product?"
12
+ hints:
13
+ available_on_html: This sets the availability date for the product. If this value is not set, or it is set to a date in the future, then the product is not available on the storefront.
14
+ discontinue_on_html: This sets the discontinue date for the product. If this value is set to a date, then the product is not available on the storefront from that day on anymore.
15
+ promotionable_html: 'This determines whether or not promotions can apply to this product.<br>Default: Checked'
16
+ shipping_category_html: 'This determines what kind of shipping this product requires.<br> Default: Default'
17
+ tax_category_html: 'This determines what kind of taxation is applied to this product.<br> Default: %{default_tax_category}'
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::Products::Status::Component < SolidusAdmin::BaseComponent
4
+ COLORS = {
5
+ available: :green,
6
+ discontinued: :red
7
+ }.freeze
8
+
9
+ # @param product [Spree::Product]
10
+ def initialize(product:)
11
+ @product = product
12
+ end
13
+
14
+ def call
15
+ render component('ui/badge').new(
16
+ name: t(".#{status}"),
17
+ color: COLORS.fetch(status)
18
+ )
19
+ end
20
+
21
+ # @return [Symbol]
22
+ # :available when the product is available
23
+ # :discontinued when the product is not available
24
+ def status
25
+ if @product.available?
26
+ :available
27
+ else
28
+ :discontinued
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ en:
2
+ available: 'Available'
3
+ discontinued: 'Discontinued'