js_admin 0.0.1 → 0.2.0

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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +24 -0
  3. data/README.md +227 -19
  4. data/app/assets/javascripts/js_admin/application.js +13 -0
  5. data/app/assets/javascripts/js_admin/controllers/flash_controller.js +23 -0
  6. data/app/assets/javascripts/js_admin/controllers/theme_controller.js +62 -0
  7. data/app/assets/tailwind/js_admin/engine.css +4 -0
  8. data/app/controllers/js_admin/application_controller.rb +45 -1
  9. data/app/controllers/js_admin/home_controller.rb +6 -0
  10. data/app/controllers/js_admin/resources_controller.rb +21 -6
  11. data/app/helpers/js_admin/application_helper.rb +292 -19
  12. data/app/views/js_admin/home/index.html.erb +5 -0
  13. data/app/views/js_admin/models/_index_content.html.erb +20 -0
  14. data/app/views/js_admin/models/index.html.erb +4 -21
  15. data/app/views/js_admin/resources/_edit_content.html.erb +11 -0
  16. data/app/views/js_admin/resources/_form.html.erb +21 -13
  17. data/app/views/js_admin/resources/_index_content.html.erb +74 -0
  18. data/app/views/js_admin/resources/_new_content.html.erb +10 -0
  19. data/app/views/js_admin/resources/_show_content.html.erb +26 -0
  20. data/app/views/js_admin/resources/destroy.turbo_stream.erb +7 -0
  21. data/app/views/js_admin/resources/edit.html.erb +4 -12
  22. data/app/views/js_admin/resources/index.html.erb +3 -52
  23. data/app/views/js_admin/resources/new.html.erb +4 -11
  24. data/app/views/js_admin/resources/show.html.erb +3 -26
  25. data/app/views/layouts/js_admin/_content_frame.html.erb +7 -0
  26. data/app/views/layouts/js_admin/_flash.html.erb +3 -0
  27. data/app/views/layouts/js_admin/_flash_messages.html.erb +9 -0
  28. data/app/views/layouts/js_admin/_sidebar.html.erb +36 -0
  29. data/app/views/layouts/js_admin/application.html.erb +19 -17
  30. data/config/locales/js_admin.en.yml +65 -0
  31. data/config/locales/js_admin.ko.yml +65 -0
  32. data/config/routes.rb +2 -1
  33. data/lib/generators/js_admin/install/install_generator.rb +70 -5
  34. data/lib/js_admin/configuration.rb +82 -3
  35. data/lib/js_admin/configuration_error.rb +4 -0
  36. data/lib/js_admin/field.rb +21 -7
  37. data/lib/js_admin/registry.rb +4 -1
  38. data/lib/js_admin/resource.rb +17 -5
  39. data/lib/js_admin/version.rb +1 -1
  40. data/lib/js_admin.rb +8 -0
  41. metadata +40 -2
@@ -1,53 +1,251 @@
1
1
  module JSAdmin
2
2
  module ApplicationHelper
3
- TEXT_INPUT_CLASSES = "w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
4
- SELECT_INPUT_CLASSES = "w-full rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-950 shadow-sm focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200"
5
- CHECKBOX_INPUT_CLASSES = "h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500"
3
+ CONTENT_FRAME_ID = "js_admin_content"
4
+ CONTENT_BODY_ID = "js_admin_content_body"
5
+ FLASH_ID = "js_admin_flash"
6
+ THEME_COOKIE_NAME = "js_admin_theme"
7
+
8
+ TEXT_INPUT_CLASSES = [
9
+ "w-full rounded-md border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-950 shadow-sm",
10
+ "placeholder:text-slate-400 focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200",
11
+ "dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500",
12
+ "dark:focus:border-sky-400 dark:focus:ring-sky-900"
13
+ ].join(" ")
14
+
15
+ SELECT_INPUT_CLASSES = [
16
+ "w-full rounded-md border border-slate-300 bg-white px-3.5 py-2.5 text-sm text-slate-950 shadow-sm",
17
+ "focus:border-sky-500 focus:outline-none focus:ring-2 focus:ring-sky-200",
18
+ "dark:border-slate-700 dark:bg-slate-900 dark:text-slate-100",
19
+ "dark:focus:border-sky-400 dark:focus:ring-sky-900"
20
+ ].join(" ")
21
+
22
+ CHECKBOX_INPUT_CLASSES = [
23
+ "h-4 w-4 rounded border-slate-300 text-sky-600 focus:ring-sky-500",
24
+ "dark:border-slate-700 dark:bg-slate-900 dark:focus:ring-sky-700"
25
+ ].join(" ")
26
+
27
+ TEXT_INPUT_ERROR_CLASSES = [
28
+ "w-full rounded-md border border-rose-400 bg-white px-3.5 py-2.5 text-sm text-slate-950 shadow-sm",
29
+ "placeholder:text-slate-400 focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-200",
30
+ "dark:border-rose-500 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500",
31
+ "dark:focus:border-rose-400 dark:focus:ring-rose-950"
32
+ ].join(" ")
33
+
34
+ SELECT_INPUT_ERROR_CLASSES = [
35
+ "w-full rounded-md border border-rose-400 bg-white px-3.5 py-2.5 text-sm text-slate-950 shadow-sm",
36
+ "focus:border-rose-500 focus:outline-none focus:ring-2 focus:ring-rose-200",
37
+ "dark:border-rose-500 dark:bg-slate-900 dark:text-slate-100",
38
+ "dark:focus:border-rose-400 dark:focus:ring-rose-950"
39
+ ].join(" ")
40
+
41
+ CHECKBOX_INPUT_ERROR_CLASSES = [
42
+ "h-4 w-4 rounded border-rose-400 text-rose-600 focus:ring-rose-500",
43
+ "dark:border-rose-500 dark:bg-slate-900 dark:focus:ring-rose-900"
44
+ ].join(" ")
45
+
46
+ FIELD_LABEL_CLASSES = "text-sm font-medium text-slate-700 dark:text-slate-300 sm:col-span-4"
47
+ FIELD_LABEL_ERROR_CLASSES = "text-sm font-medium text-rose-700 dark:text-rose-300 sm:col-span-4"
48
+ FIELD_ERROR_TEXT_CLASSES = "mt-2 text-sm text-rose-600 dark:text-rose-300"
49
+ FORM_ERROR_CLASSES = "rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-200"
50
+
51
+ SIDEBAR_LINK_BASE_CLASSES = [
52
+ "block rounded-md px-3 py-2 text-sm font-medium",
53
+ "hover:bg-slate-100 hover:text-slate-950",
54
+ "dark:hover:bg-slate-800 dark:hover:text-slate-100"
55
+ ].join(" ")
56
+
57
+ SIDEBAR_LINK_ACTIVE_CLASSES = "bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-200"
58
+ SIDEBAR_LINK_INACTIVE_CLASSES = "text-slate-700 dark:text-slate-300"
59
+
60
+ FLASH_BASE_CLASSES = [
61
+ "transform rounded-md border px-4 py-3 text-sm shadow-lg transition duration-300 ease-out"
62
+ ].join(" ")
63
+
64
+ FLASH_CLASSES = {
65
+ notice: "border-emerald-200 bg-emerald-50 text-emerald-800 dark:border-emerald-900 dark:bg-emerald-950 dark:text-emerald-200",
66
+ alert: "border-rose-200 bg-rose-50 text-rose-800 dark:border-rose-900 dark:bg-rose-950 dark:text-rose-200"
67
+ }.freeze
68
+
69
+ PAGINATION_LINK_CLASSES = [
70
+ "inline-flex h-9 min-w-9 items-center justify-center rounded-md border border-slate-300 bg-white px-3 text-sm font-medium",
71
+ "text-slate-700 shadow-sm hover:bg-slate-50",
72
+ "dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800"
73
+ ].join(" ")
74
+
75
+ PAGINATION_DISABLED_CLASSES = [
76
+ "inline-flex h-9 min-w-9 items-center justify-center rounded-md border border-slate-200 bg-slate-100 px-3 text-sm font-medium",
77
+ "text-slate-400 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-600"
78
+ ].join(" ")
79
+
80
+ PAGINATION_STATUS_CLASSES = [
81
+ "inline-flex h-9 min-w-28 items-center justify-center rounded-md border border-slate-200 bg-slate-50 px-3 text-sm font-medium",
82
+ "text-slate-700 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-200"
83
+ ].join(" ")
6
84
 
7
85
  def js_admin_field_input(form, field)
8
86
  case field.input_kind
9
87
  when :text_area
10
- form.text_area(field.name, rows: 5, class: TEXT_INPUT_CLASSES)
88
+ tag.textarea(js_admin_field_value(form, field), **js_admin_input_options(form, field, js_admin_text_input_classes(form, field)), rows: 5)
11
89
  when :check_box
12
90
  tag.div(class: "flex h-10 items-center") do
13
- form.check_box(field.name, class: CHECKBOX_INPUT_CLASSES)
91
+ safe_join([
92
+ tag.input(type: "hidden", name: js_admin_field_name(form, field), value: "0", autocomplete: "off"),
93
+ tag.input(**js_admin_input_options(form, field, js_admin_checkbox_input_classes(form, field)).merge(type: "checkbox", value: "1", checked: js_admin_checkbox_checked?(form, field)))
94
+ ])
14
95
  end
15
96
  when :number_field
16
- form.number_field(field.name, step: field.number_step, class: TEXT_INPUT_CLASSES)
97
+ tag.input(**js_admin_input_options(form, field, js_admin_text_input_classes(form, field)).merge(type: "number", step: field.number_step, value: js_admin_field_value(form, field)))
17
98
  when :date_field
18
- form.date_field(field.name, class: TEXT_INPUT_CLASSES)
99
+ tag.input(**js_admin_input_options(form, field, js_admin_text_input_classes(form, field)).merge(type: "date", value: js_admin_formatted_field_value(form, field, "%Y-%m-%d")))
19
100
  when :datetime_field
20
- form.datetime_field(field.name, class: TEXT_INPUT_CLASSES)
101
+ tag.input(**js_admin_input_options(form, field, js_admin_text_input_classes(form, field)).merge(type: "datetime-local", value: js_admin_formatted_field_value(form, field, "%Y-%m-%dT%H:%M:%S")))
21
102
  when :time_field
22
- form.time_field(field.name, class: TEXT_INPUT_CLASSES)
103
+ tag.input(**js_admin_input_options(form, field, js_admin_text_input_classes(form, field)).merge(type: "time", value: js_admin_formatted_field_value(form, field, "%H:%M:%S")))
104
+ when :configured_select
105
+ select_tag(js_admin_field_name(form, field), options_for_select(field.configured_select_values, js_admin_field_value(form, field)), js_admin_select_options(form, field))
23
106
  when :enum_select
24
- form.select(field.name, field.enum_values, { include_blank: true }, class: SELECT_INPUT_CLASSES)
107
+ select_tag(js_admin_field_name(form, field), options_for_select(field.enum_values, js_admin_field_value(form, field)), js_admin_select_options(form, field))
25
108
  when :association_select
26
- form.select(field.name, field.association_options, { include_blank: true }, class: SELECT_INPUT_CLASSES)
109
+ select_tag(js_admin_field_name(form, field), options_for_select(field.association_options, js_admin_field_value(form, field)), js_admin_select_options(form, field))
27
110
  else
28
- form.text_field(field.name, class: TEXT_INPUT_CLASSES)
111
+ tag.input(**js_admin_input_options(form, field, js_admin_text_input_classes(form, field)).merge(type: "text", value: js_admin_field_value(form, field)))
29
112
  end
30
113
  end
31
114
 
115
+ def js_admin_field_id(form, field)
116
+ "#{form.object_name}_#{field.name}".tr("][", "_").delete_suffix("_")
117
+ end
118
+
119
+ def js_admin_field_error_id(form, field)
120
+ "#{js_admin_field_id(form, field)}_error"
121
+ end
122
+
123
+ def js_admin_field_errors(record, field)
124
+ record.errors[field.name].map(&:to_s)
125
+ end
126
+
127
+ def js_admin_form_errors(record, resource)
128
+ editable_attributes = resource.editable_fields.map { |field| field.name.to_sym }
129
+
130
+ record.errors.filter_map do |error|
131
+ next if editable_attributes.include?(error.attribute.to_sym)
132
+
133
+ error.full_message
134
+ end
135
+ end
136
+
137
+ def js_admin_field_label_classes(record, field)
138
+ js_admin_field_errors(record, field).any? ? FIELD_LABEL_ERROR_CLASSES : FIELD_LABEL_CLASSES
139
+ end
140
+
141
+ def js_admin_field_error_text_classes
142
+ FIELD_ERROR_TEXT_CLASSES
143
+ end
144
+
145
+ def js_admin_form_error_classes
146
+ FORM_ERROR_CLASSES
147
+ end
148
+
149
+ def js_admin_content_frame_id
150
+ CONTENT_FRAME_ID
151
+ end
152
+
153
+ def js_admin_content_body_id
154
+ CONTENT_BODY_ID
155
+ end
156
+
157
+ def js_admin_flash_id
158
+ FLASH_ID
159
+ end
160
+
161
+ def js_admin_content_frame_data
162
+ {
163
+ turbo_action: "advance"
164
+ }
165
+ end
166
+
167
+ def js_admin_content_link_data
168
+ {
169
+ turbo_frame: js_admin_content_frame_id,
170
+ turbo_action: "advance"
171
+ }
172
+ end
173
+
32
174
  def js_admin_table_value(field, record)
33
175
  value = field.formatted_value(record)
34
- classes = value == "NULL" ? "text-slate-400" : "text-slate-700"
176
+ classes = if field.value_for(record).nil?
177
+ "text-slate-400 dark:text-slate-500"
178
+ else
179
+ "text-slate-700 dark:text-slate-200"
180
+ end
35
181
 
36
182
  tag.span(truncate(value, length: 80), class: classes)
37
183
  end
38
184
 
185
+ def js_admin_sidebar_top_level_resources
186
+ js_admin_sidebar_resources.select { |resource| js_admin_resource_namespace(resource).blank? }
187
+ end
188
+
189
+ def js_admin_sidebar_namespace_groups
190
+ js_admin_sidebar_resources
191
+ .reject { |resource| js_admin_resource_namespace(resource).blank? }
192
+ .group_by { |resource| js_admin_resource_namespace(resource) }
193
+ .sort_by { |namespace, _resources| namespace }
194
+ .to_h
195
+ end
196
+
197
+ def js_admin_sidebar_resource_label(resource)
198
+ resource.singular_label
199
+ end
200
+
201
+ def js_admin_sidebar_namespace_label(namespace)
202
+ namespace.split("::").map { |name| name.underscore.humanize }.join(" / ")
203
+ end
204
+
205
+ def js_admin_sidebar_namespace_open?(resources)
206
+ resources.any? { |resource| js_admin_sidebar_resource_active?(resource) }
207
+ end
208
+
209
+ def js_admin_sidebar_link_classes(resource)
210
+ state_classes = if js_admin_sidebar_resource_active?(resource)
211
+ SIDEBAR_LINK_ACTIVE_CLASSES
212
+ else
213
+ SIDEBAR_LINK_INACTIVE_CLASSES
214
+ end
215
+
216
+ "#{SIDEBAR_LINK_BASE_CLASSES} #{state_classes}"
217
+ end
218
+
219
+ def js_admin_flash_classes(type)
220
+ [ FLASH_BASE_CLASSES, FLASH_CLASSES.fetch(type.to_sym) ].join(" ")
221
+ end
222
+
223
+ def js_admin_pagination_link_classes
224
+ PAGINATION_LINK_CLASSES
225
+ end
226
+
227
+ def js_admin_pagination_disabled_classes
228
+ PAGINATION_DISABLED_CLASSES
229
+ end
230
+
231
+ def js_admin_pagination_status_classes
232
+ PAGINATION_STATUS_CLASSES
233
+ end
234
+
39
235
  def js_admin_stylesheet_link_tag(logical_name)
40
236
  return unless js_admin_asset_exists?("#{logical_name}.css")
41
237
 
42
238
  stylesheet_link_tag logical_name, "data-turbo-track": "reload"
43
239
  end
44
240
 
45
- def js_admin_hotwire_javascript_tags
46
- if respond_to?(:javascript_importmap_tags) && Rails.root.join("config/importmap.rb").exist?
47
- javascript_importmap_tags
48
- elsif js_admin_asset_exists?("application.js")
49
- javascript_include_tag "application", type: "module", "data-turbo-track": "reload"
50
- end
241
+ def js_admin_javascript_tags
242
+ return unless respond_to?(:javascript_importmap_tags)
243
+
244
+ javascript_importmap_tags "js_admin"
245
+ end
246
+
247
+ def js_admin_html_classes
248
+ "dark" if cookies[THEME_COOKIE_NAME] == "dark"
51
249
  end
52
250
 
53
251
  def js_admin_asset_exists?(logical_path)
@@ -63,5 +261,80 @@ module JSAdmin
63
261
  rescue StandardError
64
262
  false
65
263
  end
264
+
265
+ private
266
+
267
+ def js_admin_field_name(form, field)
268
+ "#{form.object_name}[#{field.name}]"
269
+ end
270
+
271
+ def js_admin_input_options(form, field, input_classes)
272
+ js_admin_field_html_options(form, field, input_classes).merge(name: js_admin_field_name(form, field))
273
+ end
274
+
275
+ def js_admin_select_options(form, field)
276
+ js_admin_field_html_options(form, field, js_admin_select_input_classes(form, field)).merge(include_blank: true)
277
+ end
278
+
279
+ def js_admin_field_html_options(form, field, input_classes)
280
+ options = {
281
+ id: js_admin_field_id(form, field),
282
+ class: input_classes
283
+ }
284
+
285
+ if js_admin_field_errors(form.object, field).any?
286
+ options[:aria] = {
287
+ invalid: "true",
288
+ describedby: js_admin_field_error_id(form, field)
289
+ }
290
+ end
291
+
292
+ options
293
+ end
294
+
295
+ def js_admin_text_input_classes(form, field)
296
+ js_admin_field_errors(form.object, field).any? ? TEXT_INPUT_ERROR_CLASSES : TEXT_INPUT_CLASSES
297
+ end
298
+
299
+ def js_admin_select_input_classes(form, field)
300
+ js_admin_field_errors(form.object, field).any? ? SELECT_INPUT_ERROR_CLASSES : SELECT_INPUT_CLASSES
301
+ end
302
+
303
+ def js_admin_checkbox_input_classes(form, field)
304
+ js_admin_field_errors(form.object, field).any? ? CHECKBOX_INPUT_ERROR_CLASSES : CHECKBOX_INPUT_CLASSES
305
+ end
306
+
307
+ def js_admin_field_value(form, field)
308
+ before_type_cast_method = "#{field.name}_before_type_cast"
309
+
310
+ if form.object.respond_to?(before_type_cast_method)
311
+ form.object.public_send(before_type_cast_method)
312
+ else
313
+ form.object.public_send(field.name)
314
+ end
315
+ end
316
+
317
+ def js_admin_formatted_field_value(form, field, format)
318
+ value = js_admin_field_value(form, field)
319
+ return value if value.blank? || !value.respond_to?(:strftime)
320
+
321
+ value.strftime(format)
322
+ end
323
+
324
+ def js_admin_checkbox_checked?(form, field)
325
+ ActiveModel::Type::Boolean.new.cast(js_admin_field_value(form, field))
326
+ end
327
+
328
+ def js_admin_sidebar_resources
329
+ JSAdmin.resources.sort_by { |resource| resource.model_class.name }
330
+ end
331
+
332
+ def js_admin_resource_namespace(resource)
333
+ resource.model_class.name.deconstantize
334
+ end
335
+
336
+ def js_admin_sidebar_resource_active?(resource)
337
+ params[:model_id] == resource.model_id
338
+ end
66
339
  end
67
340
  end
@@ -0,0 +1,5 @@
1
+ <% content_for :title, "JSAdmin" %>
2
+
3
+ <%= render layout: "layouts/js_admin/content_frame" do %>
4
+ <div data-js-admin-home></div>
5
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <div class="mb-8">
2
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100"><%= t("js_admin.models.heading") %></h1>
3
+ <p class="mt-2 text-sm text-slate-600 dark:text-slate-400"><%= t("js_admin.models.available", count: @resources.size, formatted_count: number_with_delimiter(@resources.size)) %></p>
4
+ </div>
5
+
6
+ <div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
7
+ <ul class="divide-y divide-slate-200 dark:divide-slate-800">
8
+ <% @resources.each do |resource| %>
9
+ <li>
10
+ <%= link_to resources_path(resource.model_id), class: "flex items-center justify-between gap-6 px-5 py-5 hover:bg-slate-50 dark:hover:bg-slate-800/70 sm:px-6" do %>
11
+ <span>
12
+ <span class="block text-sm font-medium text-slate-950 dark:text-slate-100"><%= resource.label %></span>
13
+ <span class="mt-1.5 block text-xs text-slate-500 dark:text-slate-400"><%= resource.model_class.name %></span>
14
+ </span>
15
+ <span class="text-sm text-slate-500 dark:text-slate-400"><%= number_with_delimiter(resource.count) %></span>
16
+ <% end %>
17
+ </li>
18
+ <% end %>
19
+ </ul>
20
+ </div>
@@ -1,22 +1,5 @@
1
- <% content_for :title, "JSAdmin Models" %>
1
+ <% content_for :title, t("js_admin.titles.models") %>
2
2
 
3
- <div class="mb-6">
4
- <h1 class="text-2xl font-semibold text-slate-950">Models</h1>
5
- <p class="mt-1 text-sm text-slate-600"><%= pluralize(@resources.size, "model") %> available</p>
6
- </div>
7
-
8
- <div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm">
9
- <ul class="divide-y divide-slate-200">
10
- <% @resources.each do |resource| %>
11
- <li>
12
- <%= link_to resources_path(resource.model_id), class: "flex items-center justify-between px-4 py-4 hover:bg-slate-50" do %>
13
- <span>
14
- <span class="block text-sm font-medium text-slate-950"><%= resource.label %></span>
15
- <span class="mt-1 block text-xs text-slate-500"><%= resource.model_class.name %></span>
16
- </span>
17
- <span class="text-sm text-slate-500"><%= number_with_delimiter(resource.count) %></span>
18
- <% end %>
19
- </li>
20
- <% end %>
21
- </ul>
22
- </div>
3
+ <%= render layout: "layouts/js_admin/content_frame" do %>
4
+ <%= render "index_content" %>
5
+ <% end %>
@@ -0,0 +1,11 @@
1
+ <div class="mb-8">
2
+ <div class="mb-2 text-sm text-slate-500 dark:text-slate-400">
3
+ <%= link_to t("js_admin.navigation.home"), root_path, class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
4
+ <%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
5
+ <%= link_to @resource.display_name(@record), resource_path(@resource.model_id, @record), class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
6
+ <%= t("js_admin.resources.actions.edit") %>
7
+ </div>
8
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100"><%= t("js_admin.resources.headings.edit", record: @resource.display_name(@record)) %></h1>
9
+ </div>
10
+
11
+ <%= render "form", record: @record, resource: @resource, url: resource_path(@resource.model_id, @record) %>
@@ -1,22 +1,29 @@
1
- <%= form_with model: record, url: url, class: "space-y-6" do |form| %>
2
- <% if record.errors.any? %>
3
- <div class="rounded-md border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-800">
4
- <p class="font-medium"><%= pluralize(record.errors.count, "error") %> prevented this record from being saved.</p>
5
- <ul class="mt-2 list-disc space-y-1 pl-5">
6
- <% record.errors.full_messages.each do |message| %>
1
+ <%= form_with model: record, url: url, class: "space-y-7" do |form| %>
2
+ <% form_errors = js_admin_form_errors(record, resource) %>
3
+
4
+ <% if form_errors.any? %>
5
+ <div class="<%= js_admin_form_error_classes %>" role="alert">
6
+ <ul class="list-disc space-y-1 pl-5">
7
+ <% form_errors.each do |message| %>
7
8
  <li><%= message %></li>
8
9
  <% end %>
9
10
  </ul>
10
11
  </div>
11
12
  <% end %>
12
13
 
13
- <div class="rounded-lg border border-slate-200 bg-white shadow-sm">
14
- <div class="divide-y divide-slate-200">
14
+ <div class="rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
15
+ <div class="divide-y divide-slate-200 dark:divide-slate-800">
15
16
  <% resource.editable_fields.each do |field| %>
16
- <div class="grid gap-2 px-4 py-4 sm:grid-cols-3">
17
- <%= form.label field.name, field.human_name, class: "text-sm font-medium text-slate-700" %>
18
- <div class="sm:col-span-2">
17
+ <% field_errors = js_admin_field_errors(record, field) %>
18
+
19
+ <div class="grid gap-3 px-5 py-5 sm:grid-cols-12 sm:gap-6 sm:px-6">
20
+ <%= tag.label field.human_name, for: js_admin_field_id(form, field), class: js_admin_field_label_classes(record, field) %>
21
+ <div class="sm:col-span-8">
19
22
  <%= js_admin_field_input(form, field) %>
23
+
24
+ <% if field_errors.any? %>
25
+ <p id="<%= js_admin_field_error_id(form, field) %>" class="<%= js_admin_field_error_text_classes %>"><%= field_errors.to_sentence %></p>
26
+ <% end %>
20
27
  </div>
21
28
  </div>
22
29
  <% end %>
@@ -24,7 +31,8 @@
24
31
  </div>
25
32
 
26
33
  <div class="flex items-center gap-2">
27
- <%= form.submit class: "inline-flex items-center justify-center rounded-md bg-sky-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-sky-700" %>
28
- <%= link_to "Cancel", record.persisted? ? resource_path(resource.model_id, record) : resources_path(resource.model_id), class: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50" %>
34
+ <% submit_label = record.persisted? ? t("js_admin.resources.actions.update", resource: resource.singular_label) : t("js_admin.resources.actions.create", resource: resource.singular_label) %>
35
+ <%= form.submit submit_label, class: "inline-flex items-center justify-center rounded-md bg-sky-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-sky-400" %>
36
+ <%= link_to t("js_admin.resources.actions.cancel"), record.persisted? ? resource_path(resource.model_id, record) : resources_path(resource.model_id), class: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800" %>
29
37
  </div>
30
38
  <% end %>
@@ -0,0 +1,74 @@
1
+ <div class="mb-8 flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
2
+ <div>
3
+ <div class="mb-2 text-sm text-slate-500 dark:text-slate-400"><%= link_to t("js_admin.navigation.home"), root_path, class: "hover:text-slate-950 dark:hover:text-slate-100" %> / <%= @resource.label %></div>
4
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100"><%= @resource.label %></h1>
5
+ <p class="mt-2 text-sm text-slate-600 dark:text-slate-400"><%= t("js_admin.resources.records", count: @total_count, formatted_count: number_with_delimiter(@total_count)) %></p>
6
+ </div>
7
+
8
+ <%= link_to t("js_admin.resources.actions.new", resource: @resource.singular_label), new_resource_path(@resource.model_id), class: "inline-flex items-center justify-center rounded-md bg-sky-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-sky-700 dark:bg-sky-500 dark:hover:bg-sky-400" %>
9
+ </div>
10
+
11
+ <div class="overflow-x-auto rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
12
+ <table class="min-w-full divide-y divide-slate-200 dark:divide-slate-800">
13
+ <thead class="bg-slate-50 dark:bg-slate-900">
14
+ <tr>
15
+ <% @resource.list_fields.each do |field| %>
16
+ <th scope="col" class="whitespace-nowrap px-5 py-4 text-left text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"><%= field.human_name %></th>
17
+ <% end %>
18
+ <th scope="col" class="w-44 px-5 py-4 text-right text-xs font-semibold uppercase tracking-wide text-slate-500 dark:text-slate-400"><%= t("js_admin.resources.table.actions") %></th>
19
+ </tr>
20
+ </thead>
21
+ <tbody class="divide-y divide-slate-200 bg-white dark:divide-slate-800 dark:bg-slate-900">
22
+ <% @records.each do |record| %>
23
+ <tr class="hover:bg-slate-50 dark:hover:bg-slate-800/70">
24
+ <% @resource.list_fields.each do |field| %>
25
+ <td class="whitespace-nowrap px-5 py-4 text-sm"><%= js_admin_table_value(field, record) %></td>
26
+ <% end %>
27
+ <td class="whitespace-nowrap px-5 py-4 text-right text-sm">
28
+ <div class="flex items-center justify-end gap-3">
29
+ <%= link_to t("js_admin.resources.actions.view"), resource_path(@resource.model_id, record), class: "font-medium text-sky-700 hover:text-sky-900 dark:text-sky-300 dark:hover:text-sky-200" %>
30
+ <%= link_to t("js_admin.resources.actions.edit"), edit_resource_path(@resource.model_id, record), class: "font-medium text-slate-700 hover:text-slate-950 dark:text-slate-300 dark:hover:text-slate-100" %>
31
+ <%= button_to t("js_admin.resources.actions.delete"), resource_path(@resource.model_id, record), method: :delete, form: { class: "inline", data: { turbo_confirm: t("js_admin.resources.delete_confirmation") } }, class: "font-medium text-rose-700 hover:text-rose-900 dark:text-rose-300 dark:hover:text-rose-200" %>
32
+ </div>
33
+ </td>
34
+ </tr>
35
+ <% end %>
36
+
37
+ <% if @records.empty? %>
38
+ <tr>
39
+ <td colspan="<%= @resource.list_fields.size + 1 %>" class="px-5 py-12 text-center text-sm text-slate-500 dark:text-slate-400"><%= t("js_admin.resources.table.no_records") %></td>
40
+ </tr>
41
+ <% end %>
42
+ </tbody>
43
+ </table>
44
+ </div>
45
+
46
+ <div class="mt-6 flex justify-end">
47
+ <nav class="flex flex-wrap items-center justify-end gap-2" aria-label="<%= t("js_admin.resources.pagination.label") %>">
48
+ <% if @page > 1 %>
49
+ <%= link_to t("js_admin.resources.pagination.first"), resources_path(@resource.model_id, page: 1), class: js_admin_pagination_link_classes %>
50
+ <% else %>
51
+ <span class="<%= js_admin_pagination_disabled_classes %>" aria-disabled="true"><%= t("js_admin.resources.pagination.first") %></span>
52
+ <% end %>
53
+
54
+ <% if @resource.previous_page?(@page) %>
55
+ <%= link_to t("js_admin.resources.pagination.previous"), resources_path(@resource.model_id, page: @page - 1), class: js_admin_pagination_link_classes %>
56
+ <% else %>
57
+ <span class="<%= js_admin_pagination_disabled_classes %>" aria-disabled="true"><%= t("js_admin.resources.pagination.previous") %></span>
58
+ <% end %>
59
+
60
+ <span class="<%= js_admin_pagination_status_classes %>" aria-current="page" aria-label="<%= t("js_admin.resources.pagination.status", page: @page, total_pages: @total_pages) %>"><%= @page %> / <%= @total_pages %></span>
61
+
62
+ <% if @resource.next_page?(@page) %>
63
+ <%= link_to t("js_admin.resources.pagination.next"), resources_path(@resource.model_id, page: @page + 1), class: js_admin_pagination_link_classes %>
64
+ <% else %>
65
+ <span class="<%= js_admin_pagination_disabled_classes %>" aria-disabled="true"><%= t("js_admin.resources.pagination.next") %></span>
66
+ <% end %>
67
+
68
+ <% if @page < @total_pages %>
69
+ <%= link_to t("js_admin.resources.pagination.last"), resources_path(@resource.model_id, page: @total_pages), class: js_admin_pagination_link_classes %>
70
+ <% else %>
71
+ <span class="<%= js_admin_pagination_disabled_classes %>" aria-disabled="true"><%= t("js_admin.resources.pagination.last") %></span>
72
+ <% end %>
73
+ </nav>
74
+ </div>
@@ -0,0 +1,10 @@
1
+ <div class="mb-8">
2
+ <div class="mb-2 text-sm text-slate-500 dark:text-slate-400">
3
+ <%= link_to t("js_admin.navigation.home"), root_path, class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
4
+ <%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
5
+ <%= t("js_admin.resources.actions.new", resource: @resource.singular_label) %>
6
+ </div>
7
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100"><%= t("js_admin.resources.headings.new", resource: @resource.singular_label) %></h1>
8
+ </div>
9
+
10
+ <%= render "form", record: @record, resource: @resource, url: resources_path(@resource.model_id) %>
@@ -0,0 +1,26 @@
1
+ <div class="mb-8 flex flex-col gap-5 sm:flex-row sm:items-center sm:justify-between">
2
+ <div>
3
+ <div class="mb-2 text-sm text-slate-500 dark:text-slate-400">
4
+ <%= link_to t("js_admin.navigation.home"), root_path, class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
5
+ <%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950 dark:hover:text-slate-100" %> /
6
+ <%= @resource.display_name(@record) %>
7
+ </div>
8
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100"><%= @resource.display_name(@record) %></h1>
9
+ </div>
10
+
11
+ <div class="flex gap-2">
12
+ <%= link_to t("js_admin.resources.actions.edit"), edit_resource_path(@resource.model_id, @record), class: "inline-flex items-center justify-center rounded-md border border-slate-300 bg-white px-4 py-2.5 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800" %>
13
+ <%= button_to t("js_admin.resources.actions.delete"), resource_path(@resource.model_id, @record), method: :delete, form: { data: { turbo_confirm: t("js_admin.resources.delete_confirmation") } }, class: "inline-flex items-center justify-center rounded-md bg-rose-600 px-4 py-2.5 text-sm font-medium text-white shadow-sm hover:bg-rose-700 dark:bg-rose-500 dark:hover:bg-rose-400" %>
14
+ </div>
15
+ </div>
16
+
17
+ <div class="overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
18
+ <dl class="divide-y divide-slate-200 dark:divide-slate-800">
19
+ <% @resource.fields.each do |field| %>
20
+ <div class="grid gap-3 px-5 py-5 sm:grid-cols-12 sm:gap-6 sm:px-6">
21
+ <dt class="text-sm font-medium text-slate-600 dark:text-slate-300 sm:col-span-4"><%= field.human_name %></dt>
22
+ <dd class="text-sm text-slate-950 dark:text-slate-100 sm:col-span-8"><%= js_admin_table_value(field, @record) %></dd>
23
+ </div>
24
+ <% end %>
25
+ </dl>
26
+ </div>
@@ -0,0 +1,7 @@
1
+ <%= turbo_stream.update js_admin_flash_id do %>
2
+ <%= render "layouts/js_admin/flash_messages", notice: notice, alert: alert %>
3
+ <% end %>
4
+
5
+ <%= turbo_stream.update js_admin_content_body_id do %>
6
+ <%= render "index_content" %>
7
+ <% end %>
@@ -1,13 +1,5 @@
1
- <% content_for :title, "Edit #{@resource.display_name(@record)}" %>
1
+ <% content_for :title, t("js_admin.titles.edit", record: @resource.display_name(@record)) %>
2
2
 
3
- <div class="mb-6">
4
- <div class="mb-2 text-sm text-slate-500">
5
- <%= link_to "Models", root_path, class: "hover:text-slate-950" %> /
6
- <%= link_to @resource.label, resources_path(@resource.model_id), class: "hover:text-slate-950" %> /
7
- <%= link_to @resource.display_name(@record), resource_path(@resource.model_id, @record), class: "hover:text-slate-950" %> /
8
- Edit
9
- </div>
10
- <h1 class="text-2xl font-semibold text-slate-950">Edit <%= @resource.display_name(@record) %></h1>
11
- </div>
12
-
13
- <%= render "form", record: @record, resource: @resource, url: resource_path(@resource.model_id, @record) %>
3
+ <%= render layout: "layouts/js_admin/content_frame" do %>
4
+ <%= render "edit_content" %>
5
+ <% end %>