solidus_admin 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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'