js_admin 0.0.1 → 0.1.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/README.md +179 -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 +28 -0
  9. data/app/controllers/js_admin/home_controller.rb +6 -0
  10. data/app/controllers/js_admin/resources_controller.rb +17 -4
  11. data/app/helpers/js_admin/application_helper.rb +286 -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 +3 -20
  15. data/app/views/js_admin/resources/_edit_content.html.erb +11 -0
  16. data/app/views/js_admin/resources/_form.html.erb +20 -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 +3 -11
  22. data/app/views/js_admin/resources/index.html.erb +3 -52
  23. data/app/views/js_admin/resources/new.html.erb +3 -10
  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/routes.rb +2 -1
  31. data/lib/generators/js_admin/install/install_generator.rb +63 -5
  32. data/lib/js_admin/configuration.rb +16 -3
  33. data/lib/js_admin/field.rb +4 -4
  34. data/lib/js_admin/resource.rb +12 -4
  35. data/lib/js_admin/version.rb +1 -1
  36. data/lib/js_admin.rb +6 -0
  37. metadata +37 -2
@@ -1,53 +1,245 @@
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")))
23
104
  when :enum_select
24
- form.select(field.name, field.enum_values, { include_blank: true }, class: SELECT_INPUT_CLASSES)
105
+ 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
106
  when :association_select
26
- form.select(field.name, field.association_options, { include_blank: true }, class: SELECT_INPUT_CLASSES)
107
+ 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
108
  else
28
- form.text_field(field.name, class: TEXT_INPUT_CLASSES)
109
+ 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
110
  end
30
111
  end
31
112
 
113
+ def js_admin_field_id(form, field)
114
+ "#{form.object_name}_#{field.name}".tr("][", "_").delete_suffix("_")
115
+ end
116
+
117
+ def js_admin_field_error_id(form, field)
118
+ "#{js_admin_field_id(form, field)}_error"
119
+ end
120
+
121
+ def js_admin_field_errors(record, field)
122
+ record.errors[field.name].map(&:to_s)
123
+ end
124
+
125
+ def js_admin_form_errors(record, resource)
126
+ editable_attributes = resource.editable_fields.map { |field| field.name.to_sym }
127
+
128
+ record.errors.filter_map do |error|
129
+ next if editable_attributes.include?(error.attribute.to_sym)
130
+
131
+ error.full_message
132
+ end
133
+ end
134
+
135
+ def js_admin_field_label_classes(record, field)
136
+ js_admin_field_errors(record, field).any? ? FIELD_LABEL_ERROR_CLASSES : FIELD_LABEL_CLASSES
137
+ end
138
+
139
+ def js_admin_field_error_text_classes
140
+ FIELD_ERROR_TEXT_CLASSES
141
+ end
142
+
143
+ def js_admin_form_error_classes
144
+ FORM_ERROR_CLASSES
145
+ end
146
+
147
+ def js_admin_content_frame_id
148
+ CONTENT_FRAME_ID
149
+ end
150
+
151
+ def js_admin_content_body_id
152
+ CONTENT_BODY_ID
153
+ end
154
+
155
+ def js_admin_flash_id
156
+ FLASH_ID
157
+ end
158
+
159
+ def js_admin_content_frame_data
160
+ {
161
+ turbo_action: "advance"
162
+ }
163
+ end
164
+
165
+ def js_admin_content_link_data
166
+ {
167
+ turbo_frame: js_admin_content_frame_id,
168
+ turbo_action: "advance"
169
+ }
170
+ end
171
+
32
172
  def js_admin_table_value(field, record)
33
173
  value = field.formatted_value(record)
34
- classes = value == "NULL" ? "text-slate-400" : "text-slate-700"
174
+ classes = value == "NULL" ? "text-slate-400 dark:text-slate-500" : "text-slate-700 dark:text-slate-200"
35
175
 
36
176
  tag.span(truncate(value, length: 80), class: classes)
37
177
  end
38
178
 
179
+ def js_admin_sidebar_top_level_resources
180
+ js_admin_sidebar_resources.select { |resource| js_admin_resource_namespace(resource).blank? }
181
+ end
182
+
183
+ def js_admin_sidebar_namespace_groups
184
+ js_admin_sidebar_resources
185
+ .reject { |resource| js_admin_resource_namespace(resource).blank? }
186
+ .group_by { |resource| js_admin_resource_namespace(resource) }
187
+ .sort_by { |namespace, _resources| namespace }
188
+ .to_h
189
+ end
190
+
191
+ def js_admin_sidebar_resource_label(resource)
192
+ resource.model_class.name.demodulize.underscore.humanize
193
+ end
194
+
195
+ def js_admin_sidebar_namespace_label(namespace)
196
+ namespace.split("::").map { |name| name.underscore.humanize }.join(" / ")
197
+ end
198
+
199
+ def js_admin_sidebar_namespace_open?(resources)
200
+ resources.any? { |resource| js_admin_sidebar_resource_active?(resource) }
201
+ end
202
+
203
+ def js_admin_sidebar_link_classes(resource)
204
+ state_classes = if js_admin_sidebar_resource_active?(resource)
205
+ SIDEBAR_LINK_ACTIVE_CLASSES
206
+ else
207
+ SIDEBAR_LINK_INACTIVE_CLASSES
208
+ end
209
+
210
+ "#{SIDEBAR_LINK_BASE_CLASSES} #{state_classes}"
211
+ end
212
+
213
+ def js_admin_flash_classes(type)
214
+ [ FLASH_BASE_CLASSES, FLASH_CLASSES.fetch(type.to_sym) ].join(" ")
215
+ end
216
+
217
+ def js_admin_pagination_link_classes
218
+ PAGINATION_LINK_CLASSES
219
+ end
220
+
221
+ def js_admin_pagination_disabled_classes
222
+ PAGINATION_DISABLED_CLASSES
223
+ end
224
+
225
+ def js_admin_pagination_status_classes
226
+ PAGINATION_STATUS_CLASSES
227
+ end
228
+
39
229
  def js_admin_stylesheet_link_tag(logical_name)
40
230
  return unless js_admin_asset_exists?("#{logical_name}.css")
41
231
 
42
232
  stylesheet_link_tag logical_name, "data-turbo-track": "reload"
43
233
  end
44
234
 
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
235
+ def js_admin_javascript_tags
236
+ return unless respond_to?(:javascript_importmap_tags)
237
+
238
+ javascript_importmap_tags "js_admin"
239
+ end
240
+
241
+ def js_admin_html_classes
242
+ "dark" if cookies[THEME_COOKIE_NAME] == "dark"
51
243
  end
52
244
 
53
245
  def js_admin_asset_exists?(logical_path)
@@ -63,5 +255,80 @@ module JSAdmin
63
255
  rescue StandardError
64
256
  false
65
257
  end
258
+
259
+ private
260
+
261
+ def js_admin_field_name(form, field)
262
+ "#{form.object_name}[#{field.name}]"
263
+ end
264
+
265
+ def js_admin_input_options(form, field, input_classes)
266
+ js_admin_field_html_options(form, field, input_classes).merge(name: js_admin_field_name(form, field))
267
+ end
268
+
269
+ def js_admin_select_options(form, field)
270
+ js_admin_field_html_options(form, field, js_admin_select_input_classes(form, field)).merge(include_blank: true)
271
+ end
272
+
273
+ def js_admin_field_html_options(form, field, input_classes)
274
+ options = {
275
+ id: js_admin_field_id(form, field),
276
+ class: input_classes
277
+ }
278
+
279
+ if js_admin_field_errors(form.object, field).any?
280
+ options[:aria] = {
281
+ invalid: "true",
282
+ describedby: js_admin_field_error_id(form, field)
283
+ }
284
+ end
285
+
286
+ options
287
+ end
288
+
289
+ def js_admin_text_input_classes(form, field)
290
+ js_admin_field_errors(form.object, field).any? ? TEXT_INPUT_ERROR_CLASSES : TEXT_INPUT_CLASSES
291
+ end
292
+
293
+ def js_admin_select_input_classes(form, field)
294
+ js_admin_field_errors(form.object, field).any? ? SELECT_INPUT_ERROR_CLASSES : SELECT_INPUT_CLASSES
295
+ end
296
+
297
+ def js_admin_checkbox_input_classes(form, field)
298
+ js_admin_field_errors(form.object, field).any? ? CHECKBOX_INPUT_ERROR_CLASSES : CHECKBOX_INPUT_CLASSES
299
+ end
300
+
301
+ def js_admin_field_value(form, field)
302
+ before_type_cast_method = "#{field.name}_before_type_cast"
303
+
304
+ if form.object.respond_to?(before_type_cast_method)
305
+ form.object.public_send(before_type_cast_method)
306
+ else
307
+ form.object.public_send(field.name)
308
+ end
309
+ end
310
+
311
+ def js_admin_formatted_field_value(form, field, format)
312
+ value = js_admin_field_value(form, field)
313
+ return value if value.blank? || !value.respond_to?(:strftime)
314
+
315
+ value.strftime(format)
316
+ end
317
+
318
+ def js_admin_checkbox_checked?(form, field)
319
+ ActiveModel::Type::Boolean.new.cast(js_admin_field_value(form, field))
320
+ end
321
+
322
+ def js_admin_sidebar_resources
323
+ JSAdmin.resources.sort_by { |resource| resource.model_class.name }
324
+ end
325
+
326
+ def js_admin_resource_namespace(resource)
327
+ resource.model_class.name.deconstantize
328
+ end
329
+
330
+ def js_admin_sidebar_resource_active?(resource)
331
+ params[:model_id] == resource.model_id
332
+ end
66
333
  end
67
334
  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">Models</h1>
3
+ <p class="mt-2 text-sm text-slate-600 dark:text-slate-400"><%= pluralize(@resources.size, "model") %> available</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
1
  <% content_for :title, "JSAdmin 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 "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
+ Edit
7
+ </div>
8
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100">Edit <%= @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,7 @@
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
+ <%= form.submit 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" %>
35
+ <%= 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.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
36
  </div>
30
37
  <% 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 "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"><%= number_with_delimiter(@total_count) %> records</p>
6
+ </div>
7
+
8
+ <%= link_to "New #{@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">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 "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 "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 "Delete", resource_path(@resource.model_id, record), method: :delete, form: { class: "inline", data: { turbo_confirm: "Delete this record?" } }, 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">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="Pagination">
48
+ <% if @page > 1 %>
49
+ <%= link_to "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">First</span>
52
+ <% end %>
53
+
54
+ <% if @resource.previous_page?(@page) %>
55
+ <%= link_to "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">Previous</span>
58
+ <% end %>
59
+
60
+ <span class="<%= js_admin_pagination_status_classes %>" aria-current="page" aria-label="Page <%= @page %> of <%= @total_pages %>"><%= @page %> / <%= @total_pages %></span>
61
+
62
+ <% if @resource.next_page?(@page) %>
63
+ <%= link_to "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">Next</span>
66
+ <% end %>
67
+
68
+ <% if @page < @total_pages %>
69
+ <%= link_to "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">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 "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
+ New
6
+ </div>
7
+ <h1 class="text-2xl font-semibold text-slate-950 dark:text-slate-100">New <%= @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 "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 "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 "Delete", resource_path(@resource.model_id, @record), method: :delete, form: { data: { turbo_confirm: "Delete this record?" } }, 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
1
  <% content_for :title, "Edit #{@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 %>