solidus_admin 0.0.0 → 0.0.2

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,36 @@
1
+ <div
2
+ class="
3
+ bg-white
4
+ rounded-lg
5
+ shadow-sm
6
+ border
7
+ border-gray-100
8
+ flex-col
9
+ justify-start
10
+ items-start
11
+ gap-6
12
+ inline-flex
13
+ w-full
14
+ py-6
15
+ "
16
+ data-controller="<%= stimulus_id %>"
17
+ >
18
+ <% if @title %>
19
+ <h2 class="mt-0 px-6 w-full">
20
+ <span class="body-title"><%= @title %></span>
21
+ <%= render component('ui/toggletip').new(text: @title_hint, position: :left) if @title_hint %>
22
+ </h2>
23
+ <% end %>
24
+
25
+ <% if content&.present? %>
26
+ <div class="px-6 w-full flex flex-col gap-6">
27
+ <%= content %>
28
+ </div>
29
+ <% end %>
30
+
31
+ <% if action? %>
32
+ <div class="flex gap-2 items-center border-t border-gray-100 px-6 pt-6 w-full">
33
+ <%= action %>
34
+ </div>
35
+ <% end %>
36
+ </div>
@@ -0,0 +1,14 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['output']
5
+
6
+ typed(event) {
7
+ this.text = event.currentTarget.value
8
+ this.render()
9
+ }
10
+
11
+ render() {
12
+ this.outputTarget.innerText = this.text
13
+ }
14
+ }
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::UI::Panel::Component < SolidusAdmin::BaseComponent
4
+ renders_one :action, ->(name:, href:, icon: 'add-box-fill', **args) {
5
+ link_to(
6
+ icon_tag(icon, class: 'w-[1.4em] h-[1.4em]') + name,
7
+ href,
8
+ **args,
9
+ class: 'flex gap-1 hover:underline'
10
+ )
11
+ }
12
+
13
+ # @param title [String] the title of the panel
14
+ # @param title_hint [String] the title hint of the panel
15
+ def initialize(title: nil, title_hint: nil)
16
+ @title = title
17
+ @title_hint = title_hint
18
+ end
19
+ end
@@ -0,0 +1,4 @@
1
+ # Add your component translations here.
2
+ # Use the translation in the example in your template with `t(".hello")`.
3
+ en:
4
+ hello: "Hello world!"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::UI::Tab::Component < SolidusAdmin::BaseComponent
4
+ SIZES = {
5
+ s: %w[h-7 px-1.5 body-small-bold],
6
+ m: %w[h-9 px-3 body-small-bold],
7
+ l: %w[h-12 px-4 body-text-bold],
8
+ }
9
+
10
+ def initialize(text:, size: :m, current: false, disabled: false, **attributes)
11
+ @text = text
12
+ @size = size
13
+ @attributes = attributes
14
+
15
+ @attributes[:'aria-current'] = current
16
+ @attributes[:'aria-disabled'] = disabled
17
+ @attributes[:class] = [
18
+ %w[
19
+ rounded justify-start items-center inline-flex py-1.5 cursor-pointer
20
+ bg-transparent text-gray-500
21
+
22
+ hover:bg-gray-75 hover:text-gray-700
23
+ focus:bg-gray-25 focus:text-gray-700
24
+
25
+ active:bg-gray-50 active:text-black
26
+ aria-current:bg-gray-50 aria-current:text-black
27
+
28
+ disabled:bg-gray-100 disabled:text-gray-400
29
+ aria-disabled:bg-gray-100 aria-disabled:text-gray-400
30
+ ],
31
+ SIZES.fetch(@size.to_sym),
32
+ @attributes.delete(:class),
33
+ ].join(" ")
34
+ end
35
+
36
+ def call
37
+ content_tag(
38
+ :a,
39
+ @text,
40
+ **@attributes
41
+ )
42
+ end
43
+ end
@@ -0,0 +1,170 @@
1
+ <div
2
+ class="
3
+ rounded-lg
4
+ border
5
+ border-gray-100
6
+ overflow-hidden
7
+ "
8
+ data-controller="<%= stimulus_id %>"
9
+ data-<%= stimulus_id %>-selected-row-class="bg-gray-15"
10
+ >
11
+ <% toolbar_classes = "h-14 p-2 bg-white border-b border-gray-100 justify-start items-center gap-2 visible:flex hidden:hidden" %>
12
+
13
+ <div role="search">
14
+ <div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="searchToolbar">
15
+ <%= form_with(
16
+ url: @search_url,
17
+ method: :get,
18
+ html: {
19
+ id: search_form_id,
20
+ class: 'flex-grow',
21
+ "data-turbo-frame": table_frame_id,
22
+ "data-turbo-action": "replace",
23
+ "data-#{stimulus_id}-target": "searchForm",
24
+ "data-action": "reset->#{stimulus_id}#search",
25
+ },
26
+ ) do |form| %>
27
+ <label class="items-center gap-1 p-0 inline-flex w-full justify-start relative">
28
+ <%= render component("ui/icon").new(name: 'search-line', class: "w-[1.4em] h-[1.4em] fill-gray-500 absolute ml-3") %>
29
+ <input
30
+ name="q[<%= @search_key %>]"
31
+ value="<%= params.dig(:q, @search_key) %>"
32
+ type="search"
33
+ placeholder="<%= t('.search_placeholder', resources: resource_plural_name) %>"
34
+ class="peer w-full placeholder:text-gray-400 py-1.5 px-10 bg-white rounded border border-gray-300 search-cancel:appearance-none"
35
+ data-<%= stimulus_id %>-target="searchField"
36
+ data-action="<%= stimulus_id %>#search"
37
+ aria-label="<%= t('.search_placeholder', resources: resource_plural_name) %>"
38
+ >
39
+ <button
40
+ class="absolute right-0 mr-3 peer-placeholder-shown:hidden"
41
+ data-action="<%= stimulus_id %>#clearSearch"
42
+ aria-label="<%= t('.clear') %>"
43
+ >
44
+ <%= render component("ui/icon").new(name: 'close-circle-fill', class: "w-[1.4em] h-[1.4em] fill-gray-500") %>
45
+ </button>
46
+ </label>
47
+ <% end %>
48
+
49
+ <div class="ml-4">
50
+ <%= render component("ui/button").new(
51
+ text: t('.cancel'),
52
+ scheme: :ghost,
53
+ "data-action": "#{stimulus_id}#cancelSearch",
54
+ ) %>
55
+ </div>
56
+ </div>
57
+
58
+ <% if @filters.any? %>
59
+ <div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="filterToolbar">
60
+ <div class="font-semibold text-gray-700 text-sm px-2"><%= t('.refine_search') %>:</div>
61
+ <% @filters.each do |filter| %>
62
+ <label class="flex gap-2 px-2">
63
+ <%= render component('ui/forms/checkbox').new(
64
+ name: filter[:name],
65
+ value: filter[:value],
66
+ size: :s,
67
+ form: search_form_id,
68
+ 'data-action': "#{stimulus_id}#search",
69
+ ) %>
70
+ <span class="text-gray-700 leading-none text-sm self-center"><%= filter[:label] %></span>
71
+ </label>
72
+ <% end %>
73
+ </div>
74
+ <% end %>
75
+
76
+ <div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="scopesToolbar">
77
+ <div class="flex-grow">
78
+ <%= render component("ui/tab").new(text: "All", current: true, href: "") %>
79
+ </div>
80
+
81
+ <%= render component("ui/button").new(
82
+ 'aria-label': t('.filter'),
83
+ icon: "filter-3-line",
84
+ scheme: :secondary,
85
+ "data-action": "#{stimulus_id}#showSearch",
86
+ ) %>
87
+ </div>
88
+ </div>
89
+
90
+ <div class="<%= toolbar_classes %>" data-<%= stimulus_id %>-target="batchToolbar" role="toolbar" aria-label="<%= t(".batch_actions") %>">
91
+ <%= form_tag '', id: batch_actions_form_id %>
92
+ <% @batch_actions.each do |batch_action| %>
93
+ <%= render_batch_action_button(batch_action) %>
94
+ <% end %>
95
+ </div>
96
+
97
+ <%= turbo_frame_tag table_frame_id, target: "_top" do %>
98
+ <table class="table-fixed w-full border-collapse">
99
+ <colgroup>
100
+ <% @columns.each do |column| %>
101
+ <col class="<%= column.class_name %>">
102
+ <% end %>
103
+ </colgroup>
104
+
105
+ <thead
106
+ class="bg-gray-15 text-gray-700 text-left text-small"
107
+ data-<%= stimulus_id %>-target="defaultHeader"
108
+ >
109
+ <tr>
110
+ <% @columns.each do |column| %>
111
+ <%= render_header_cell(column.header) %>
112
+ <% end %>
113
+ </tr>
114
+ </thead>
115
+
116
+ <% if @batch_actions %>
117
+ <thead
118
+ data-<%= stimulus_id %>-target="batchHeader"
119
+ class="bg-white color-black text-xs leading-none text-left"
120
+ hidden
121
+ >
122
+ <tr>
123
+ <%= render_header_cell(selectable_column.header) %>
124
+ <%= render_header_cell(content_tag(:div, safe_join([
125
+ content_tag(:span, "0", "data-#{stimulus_id}-target": "selectedRowsCount"),
126
+ " #{t('.rows_selected')}.",
127
+ ])), colspan: @columns.count - 1) %>
128
+ </div>
129
+ </thead>
130
+ <% end %>
131
+
132
+ <tbody class="bg-white text-3.5 line-[150%] text-black">
133
+ <% @rows.each do |row| %>
134
+ <tr class="<%= row_class_for(row) %>">
135
+ <% @columns.each do |column| %>
136
+ <%= render_data_cell(column.data, row) %>
137
+ <% end %>
138
+ </tr>
139
+ <% end %>
140
+
141
+ <% if @rows.empty? && @model_class %>
142
+ <tr>
143
+ <td
144
+ colspan="<%= @columns.size %>"
145
+ class="text-center py-4 text-3.5 line-[150%] text-black bg-white"
146
+ >
147
+ <%= t('.no_resources_found', resources: resource_plural_name) %>
148
+ </td>
149
+ </tr>
150
+ <% end %>
151
+ </tbody>
152
+
153
+ <% if @prev_page_link || @next_page_link %>
154
+ <tfoot>
155
+ <tr>
156
+ <td colspan="<%= @columns.size %>" class="py-4 bg-white">
157
+ <div class="flex justify-center">
158
+ <%= render component('ui/table/pagination').new(
159
+ prev_link: @prev_page_link,
160
+ next_link: @next_page_link
161
+ ) %>
162
+ </div>
163
+ </td>
164
+ </tr>
165
+ </tfoot>
166
+ <% end %>
167
+
168
+ </table>
169
+ <% end %>
170
+ </div>
@@ -0,0 +1,118 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+ import { debounce } from "solidus_admin/utils"
3
+
4
+ export default class extends Controller {
5
+ static targets = [
6
+ "checkbox",
7
+ "headerCheckbox",
8
+ "batchToolbar",
9
+ "scopesToolbar",
10
+ "searchToolbar",
11
+ "searchField",
12
+ "searchForm",
13
+ "filterToolbar",
14
+ "defaultHeader",
15
+ "batchHeader",
16
+ "selectedRowsCount",
17
+ ]
18
+
19
+ static classes = ["selectedRow"]
20
+ static values = {
21
+ mode: { type: String, default: "scopes" },
22
+ }
23
+
24
+ initialize() {
25
+ // Debounced search function.
26
+ // This method submits the search form after a delay of 200ms.
27
+ // If the function is called again within this delay, the previous call is cleared,
28
+ // effectively ensuring the form is only submitted 200ms after the last call (e.g., user stops typing).
29
+ this.search = debounce(this.search.bind(this), 200)
30
+ }
31
+
32
+ connect() {
33
+ if (this.searchFieldTarget.value !== "") this.modeValue = "search"
34
+
35
+ this.render()
36
+ }
37
+
38
+ showSearch(event) {
39
+ this.modeValue = "search"
40
+ this.render()
41
+ this.searchFieldTarget.focus()
42
+ }
43
+
44
+ search() {
45
+ this.searchFormTarget.requestSubmit()
46
+ }
47
+
48
+ clearSearch() {
49
+ this.searchFieldTarget.value = ''
50
+ this.search()
51
+ }
52
+
53
+ cancelSearch() {
54
+ this.clearSearch()
55
+
56
+ this.modeValue = "scopes"
57
+ this.render()
58
+ }
59
+
60
+ selectRow(event) {
61
+ if (this.checkboxTargets.some((checkbox) => checkbox.checked)) {
62
+ this.modeValue = "batch"
63
+ } else if (this.searchFieldTarget.value !== '') {
64
+ this.modeValue = "search"
65
+ } else {
66
+ this.modeValue = "scopes"
67
+ }
68
+
69
+ this.render()
70
+ }
71
+
72
+ selectAllRows(event) {
73
+ if (this.modeValue = event.target.checked) {
74
+ this.modeValue = "batch"
75
+ } else if (this.searchFieldTarget.value !== '') {
76
+ this.modeValue = "search"
77
+ } else {
78
+ this.modeValue = "scopes"
79
+ }
80
+
81
+ this.checkboxTargets.forEach((checkbox) => (checkbox.checked = event.target.checked))
82
+
83
+ this.render()
84
+ }
85
+
86
+ render() {
87
+ const selectedRows = this.checkboxTargets.filter((checkbox) => checkbox.checked)
88
+
89
+ this.searchToolbarTarget.toggleAttribute("hidden", this.modeValue !== "search")
90
+
91
+ if (this.hasFilterToolbarTarget) {
92
+ this.filterToolbarTarget.toggleAttribute("hidden", this.modeValue !== "search")
93
+ }
94
+
95
+ this.batchToolbarTarget.toggleAttribute("hidden", this.modeValue !== "batch")
96
+ this.batchHeaderTarget.toggleAttribute("hidden", this.modeValue !== "batch")
97
+ this.defaultHeaderTarget.toggleAttribute("hidden", this.modeValue === "batch")
98
+
99
+ this.scopesToolbarTarget.toggleAttribute("hidden", this.modeValue !== "scopes")
100
+
101
+ // Update the rows background color
102
+ this.checkboxTargets.filter((checkbox) =>
103
+ checkbox.closest("tr").classList.toggle(this.selectedRowClass, checkbox.checked),
104
+ )
105
+
106
+ // Update the selected rows count
107
+ this.selectedRowsCountTarget.textContent = `${selectedRows.length}`
108
+
109
+ // Update the header checkboxes
110
+ this.headerCheckboxTargets.forEach((checkbox) => {
111
+ checkbox.indeterminate = false
112
+ checkbox.checked = false
113
+
114
+ if (selectedRows.length === this.checkboxTargets.length) checkbox.checked = true
115
+ else if (selectedRows.length > 0) checkbox.indeterminate = true
116
+ })
117
+ }
118
+ }
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::UI::Table::Component < SolidusAdmin::BaseComponent
4
+ # @param id [String] A unique identifier for the table component.
5
+ # @param model_class [ActiveModel::Translation] The model class used for translations.
6
+ # @param rows [Array] The collection of objects that will be passed to columns for display.
7
+ # @param fade_row_proc [Proc, nil] A proc determining if a row should have a faded appearance.
8
+ # @param search_key [Symbol] The key for searching.
9
+ # @param search_url [String] The base URL for searching.
10
+ #
11
+ # @param columns [Array<Hash>] The array of column definitions.
12
+ # @option columns [Symbol|Proc|#to_s] :header The column header.
13
+ # @option columns [Symbol|Proc|#to_s] :data The data accessor for the column.
14
+ # @option columns [String] :class_name (optional) The class name for the column.
15
+ #
16
+ # @param batch_actions [Array<Hash>] The array of batch action definitions.
17
+ # @option batch_actions [String] :display_name The batch action display name.
18
+ # @option batch_actions [String] :icon The batch action icon.
19
+ # @option batch_actions [String] :action The batch action path.
20
+ # @option batch_actions [String] :method The batch action HTTP method for the provided path.
21
+ #
22
+ #
23
+ # @param filters [Array<Hash>] The array of filter definitions.
24
+ # @option filters [String] :name The filter's name.
25
+ # @option filters [Any] :value The filter's value.
26
+ # @option filters [String] :label The filter's label.
27
+ #
28
+ # @param prev_page_link [String, nil] The link to the previous page.
29
+ # @param next_page_link [String, nil] The link to the next page.
30
+ def initialize(
31
+ id:,
32
+ model_class:,
33
+ rows:,
34
+ search_key:,
35
+ search_url:,
36
+ fade_row_proc: nil,
37
+ columns: [],
38
+ batch_actions: [],
39
+ filters: [],
40
+ prev_page_link: nil,
41
+ next_page_link: nil
42
+ )
43
+ @columns = columns.map { Column.new(**_1) }
44
+ @batch_actions = batch_actions.map { BatchAction.new(**_1) }
45
+ @filters = filters.map { Filter.new(**_1) }
46
+ @id = id
47
+ @model_class = model_class
48
+ @rows = rows
49
+ @fade_row_proc = fade_row_proc
50
+ @search_key = search_key
51
+ @search_url = search_url
52
+ @prev_page_link = prev_page_link
53
+ @next_page_link = next_page_link
54
+
55
+ @columns.unshift selectable_column if batch_actions.present?
56
+ end
57
+
58
+ def resource_plural_name
59
+ @model_class.model_name.human.pluralize
60
+ end
61
+
62
+ def selectable_column
63
+ @selectable_column ||= Column.new(
64
+ header: -> {
65
+ component("ui/forms/checkbox").new(
66
+ form: batch_actions_form_id,
67
+ "data-action": "#{stimulus_id}#selectAllRows",
68
+ "data-#{stimulus_id}-target": "headerCheckbox",
69
+ "aria-label": t('.select_all'),
70
+ )
71
+ },
72
+ data: ->(data) {
73
+ component("ui/forms/checkbox").new(
74
+ name: "id[]",
75
+ form: batch_actions_form_id,
76
+ value: data.id,
77
+ "data-action": "#{stimulus_id}#selectRow",
78
+ "data-#{stimulus_id}-target": "checkbox",
79
+ "aria-label": t('.select_row'),
80
+ )
81
+ },
82
+ class_name: 'w-[52px]',
83
+ )
84
+ end
85
+
86
+ def batch_actions_form_id
87
+ @batch_actions_form_id ||= "#{stimulus_id}--batch-actions-#{@id}"
88
+ end
89
+
90
+ def table_frame_id
91
+ @table_frame_id ||= "#{stimulus_id}--table-frame-#{@id}"
92
+ end
93
+
94
+ def search_form_id
95
+ @search_form_id ||= "#{stimulus_id}--search-form-#{@id}"
96
+ end
97
+
98
+ def render_batch_action_button(batch_action)
99
+ render component("ui/button").new(
100
+ name: request_forgery_protection_token,
101
+ value: form_authenticity_token(form_options: {
102
+ action: batch_action.action,
103
+ method: batch_action.method,
104
+ }),
105
+ formaction: batch_action.action,
106
+ formmethod: batch_action.method,
107
+ form: batch_actions_form_id,
108
+ type: :submit,
109
+ icon: batch_action.icon,
110
+ text: batch_action.display_name,
111
+ scheme: :secondary,
112
+ )
113
+ end
114
+
115
+ def render_header_cell(cell, **attrs)
116
+ cell = cell.call if cell.respond_to?(:call)
117
+ cell = @model_class.human_attribute_name(cell) if cell.is_a?(Symbol)
118
+ cell = cell.render_in(self) if cell.respond_to?(:render_in)
119
+
120
+ content_tag(:th, cell, class: %{
121
+ border-b
122
+ border-gray-100
123
+ px-4
124
+ h-9
125
+ font-semibold
126
+ vertical-align-middle
127
+ leading-none
128
+ }, **attrs)
129
+ end
130
+
131
+ def render_data_cell(cell, data)
132
+ cell = cell.call(data) if cell.respond_to?(:call)
133
+ cell = data.public_send(cell) if cell.is_a?(Symbol)
134
+ cell = cell.render_in(self) if cell.respond_to?(:render_in)
135
+
136
+ content_tag(:td, content_tag(:div, cell, class: "flex items-center gap-1.5"), class: "py-2 px-4 h-10 vertical-align-middle leading-none")
137
+ end
138
+
139
+ def row_class_for(row)
140
+ classes = ['border-b', 'border-gray-100']
141
+ classes << ['bg-gray-15', 'text-gray-700'] if @fade_row_proc&.call(row)
142
+
143
+ classes.join(' ')
144
+ end
145
+
146
+ Column = Struct.new(:header, :data, :class_name, keyword_init: true)
147
+ BatchAction = Struct.new(:display_name, :icon, :action, :method, keyword_init: true) # rubocop:disable Lint/StructNewOverride
148
+ Filter = Struct.new(:name, :value, :label, keyword_init: true)
149
+ private_constant :Column, :BatchAction, :Filter
150
+ end
@@ -0,0 +1,11 @@
1
+ en:
2
+ no_resources_found: "No %{resources} found"
3
+ rows_selected: 'selected'
4
+ select_all: 'Select all'
5
+ select_row: 'Select row'
6
+ filter: 'Filter'
7
+ search_placeholder: 'Search all %{resources}'
8
+ refine_search: 'Refine Search'
9
+ batch_actions: Batch actions
10
+ clear: Clear
11
+ cancel: Cancel
@@ -0,0 +1,28 @@
1
+ <nav aria-label="pagination" class="flex items-center">
2
+ <%= render component("ui/button").new(
3
+ icon: 'arrow-left-s-line',
4
+ class: 'rounded-tr-none rounded-br-none border-r-0',
5
+ scheme: :secondary,
6
+ size: :s,
7
+ tag: :a,
8
+ href: @prev_link,
9
+ 'aria-disabled': @prev_link.blank?,
10
+ rel: 'prev',
11
+ text: content_tag(:span, t('.previous'), class: 'sr-only'),
12
+ "data-turbo-frame": "_self",
13
+ "data-turbo-action": "advance",
14
+ ) -%>
15
+ <%= render component("ui/button").new(
16
+ icon: 'arrow-right-s-line',
17
+ class: 'rounded-tl-none rounded-bl-none',
18
+ scheme: :secondary,
19
+ size: :s,
20
+ tag: :a,
21
+ href: @next_link,
22
+ 'aria-disabled': @next_link.blank?,
23
+ rel: 'next',
24
+ text: content_tag(:span, t('.next'), class: 'sr-only'),
25
+ "data-turbo-frame": "_self",
26
+ "data-turbo-action": "advance",
27
+ ) %>
28
+ </nav>
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::UI::Table::Pagination::Component < SolidusAdmin::BaseComponent
4
+ # @param prev_link [String] The link to the previous page.
5
+ # @param next_link [String] The link to the next page.
6
+ def initialize(prev_link: nil, next_link: nil)
7
+ @prev_link = prev_link
8
+ @next_link = next_link
9
+ end
10
+
11
+ def render?
12
+ @prev_link.present? || @next_link.present?
13
+ end
14
+ end
@@ -0,0 +1,3 @@
1
+ en:
2
+ next: Next
3
+ previous: Previous
@@ -0,0 +1,26 @@
1
+ <div
2
+ class="
3
+ rounded inline-block px-3 py-2
4
+ <%= SCHEMES.fetch(@scheme.to_sym).join(' ') %>
5
+ "
6
+ data-controller="<%= stimulus_id %>"
7
+ data-<%= stimulus_id %>-closing-class="transform opacity-0 transition duration-500"
8
+ data-<%= stimulus_id %>-transition-value="500"
9
+ role="dialog"
10
+ aria-label="<%= t(".#{@scheme}_label") %>"
11
+ aria-live="polite"
12
+ >
13
+ <%= icon_tag(@icon, class: 'inline-block w-[1.125rem] h-[1.125rem] mr-3 fill-current') if @icon %>
14
+
15
+ <p class="inline-block body-tiny-bold"><%= @text %></p>
16
+
17
+ <button
18
+ class="inline-block ml-3 align-text-bottom"
19
+ title="<%= t('.close_text') %>"
20
+ data-action="<%= stimulus_id %>#close"
21
+ aria-label="<%= t('.close_text') %>"
22
+ data-<%= stimulus_id %>-target="closeButton"
23
+ >
24
+ <%= icon_tag('close-line', class: "w-[1.125rem] h-[1.125rem] fill-current") %>
25
+ </button>
26
+ </div>
@@ -0,0 +1,17 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ export default class extends Controller {
4
+ static targets = ['closeButton']
5
+ static classes = ['closing']
6
+ static values = { transition: Number }
7
+
8
+ connect () {
9
+ // Give focus to the close button
10
+ this.closeButtonTarget.focus();
11
+ }
12
+
13
+ close () {
14
+ this.element.classList.add(...this.closingClasses);
15
+ setTimeout(() => this.element.remove(), this.transitionValue)
16
+ }
17
+ }
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class SolidusAdmin::UI::Toast::Component < SolidusAdmin::BaseComponent
4
+ SCHEMES = {
5
+ default: %w[
6
+ bg-gray-800 text-white
7
+ ],
8
+ error: %w[
9
+ bg-red-500 text-white
10
+ ],
11
+ }
12
+
13
+ def initialize(text:, icon: nil, scheme: :default)
14
+ @text = text
15
+ @icon = icon
16
+ @scheme = scheme.to_sym
17
+ end
18
+ end