plutonium 0.15.6 → 0.15.7

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/plutonium.css +1 -1
  3. data/app/assets/plutonium.js +25 -11
  4. data/app/assets/plutonium.js.map +2 -2
  5. data/app/assets/plutonium.min.js +4 -4
  6. data/app/assets/plutonium.min.js.map +3 -3
  7. data/app/views/layouts/rodauth.html.erb +2 -2
  8. data/docs/guide/getting-started/installation.md +2 -1
  9. data/docs/guide/getting-started/resources.md +8 -12
  10. data/docs/public/templates/plutonium.rb +8 -0
  11. data/lib/generators/pu/core/assets/assets_generator.rb +1 -1
  12. data/lib/generators/pu/eject/layout/layout_generator.rb +3 -3
  13. data/lib/generators/pu/eject/shell/shell_generator.rb +3 -3
  14. data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
  15. data/lib/generators/pu/gem/letter_opener/letter_opener_generator.rb +21 -0
  16. data/lib/generators/pu/gem/redis/redis_generator.rb +0 -2
  17. data/lib/generators/pu/gem/standard/standard_generator.rb +19 -0
  18. data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -1
  19. data/lib/generators/pu/res/conn/conn_generator.rb +1 -1
  20. data/lib/plutonium/definition/actions.rb +6 -2
  21. data/lib/plutonium/definition/base.rb +1 -0
  22. data/lib/plutonium/definition/nested_inputs.rb +19 -0
  23. data/lib/plutonium/resource/controller.rb +1 -1
  24. data/lib/plutonium/resource/controllers/interactive_actions.rb +1 -1
  25. data/lib/plutonium/resource/controllers/presentable.rb +1 -5
  26. data/lib/plutonium/ui/block.rb +13 -0
  27. data/lib/plutonium/ui/component/kit.rb +10 -0
  28. data/lib/plutonium/ui/display/resource.rb +29 -11
  29. data/lib/plutonium/ui/display/theme.rb +1 -1
  30. data/lib/plutonium/ui/dyna_frame/content.rb +2 -2
  31. data/lib/plutonium/ui/dyna_frame/host.rb +20 -0
  32. data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +282 -0
  33. data/lib/plutonium/ui/form/resource.rb +39 -29
  34. data/lib/plutonium/ui/form/theme.rb +1 -1
  35. data/lib/plutonium/ui/frame_navigator_panel.rb +53 -0
  36. data/lib/plutonium/ui/panel.rb +63 -0
  37. data/lib/plutonium/ui/skeleton_table.rb +29 -0
  38. data/lib/plutonium/ui/table/resource.rb +1 -1
  39. data/lib/plutonium/version.rb +1 -1
  40. data/src/js/controllers/frame_navigator_controller.js +25 -8
  41. data/src/js/controllers/nested_resource_form_fields_controller.js +2 -2
  42. metadata +11 -3
  43. data/lib/generators/pu/gem/redis/templates/.keep +0 -0
@@ -0,0 +1,282 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Plutonium
4
+ module UI
5
+ module Form
6
+ module Concerns
7
+ # Handles rendering of nested resource fields in forms
8
+ # @api private
9
+ module RendersNestedResourceFields
10
+ extend ActiveSupport::Concern
11
+
12
+ DEFAULT_NESTED_LIMIT = 10
13
+ NESTED_OPTION_KEYS = [:allow_destroy, :update_only, :macro, :class].freeze
14
+ SINGULAR_MACROS = %i[belongs_to has_one].freeze
15
+
16
+ class NestedInputsDefinition
17
+ include Plutonium::Definition::DefineableProps
18
+
19
+ defineable_props :field, :input
20
+ end
21
+
22
+ # Template object for new nested records
23
+ class NotPersisted
24
+ def persisted?
25
+ false
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Renders a nested resource field with associated inputs
32
+ # @param [Symbol] name The name of the nested resource field
33
+ # @raise [ArgumentError] if the nested input definition is missing required configuration
34
+ def render_nested_resource_field(name)
35
+ context = NestedFieldContext.new(
36
+ name: name,
37
+ definition: build_nested_definition(name),
38
+ resource_class: resource_class,
39
+ resource_definition: resource_definition
40
+ )
41
+
42
+ render_nested_field_container(context) do
43
+ render_nested_field_header(context)
44
+ render_nested_field_content(context)
45
+ render_nested_add_button(context)
46
+ end
47
+ end
48
+
49
+ private
50
+
51
+ class NestedFieldContext
52
+ attr_reader :name, :definition, :options, :permitted_fields
53
+
54
+ def initialize(name:, definition:, resource_class:, resource_definition:)
55
+ @name = name
56
+ @definition = definition
57
+ @resource_definition = resource_definition
58
+ @resource_class = resource_class
59
+ @options = build_options
60
+ @permitted_fields = build_permitted_fields
61
+ end
62
+
63
+ def nested_attribute_options
64
+ @nested_attribute_options ||= @resource_class.all_nested_attributes_options[@name] || {}
65
+ end
66
+
67
+ def nested_input_param
68
+ @options[:as] || :"#{@name}_attributes"
69
+ end
70
+
71
+ def multiple?
72
+ @options[:multiple]
73
+ end
74
+
75
+ private
76
+
77
+ def build_options
78
+ options = @resource_definition.defined_nested_inputs[@name][:options].dup || {}
79
+ merge_nested_options(options)
80
+ set_nested_limits(options)
81
+ options
82
+ end
83
+
84
+ def merge_nested_options(options)
85
+ NESTED_OPTION_KEYS.each do |key|
86
+ options.fetch(key) { options[key] = nested_attribute_options[key] }
87
+ end
88
+ end
89
+
90
+ def set_nested_limits(options)
91
+ options.fetch(:limit) do
92
+ options[:limit] = if SINGULAR_MACROS.include?(nested_attribute_options[:macro])
93
+ 1
94
+ else
95
+ nested_attribute_options[:limit] || DEFAULT_NESTED_LIMIT
96
+ end
97
+ end
98
+
99
+ options.fetch(:multiple) do
100
+ options[:multiple] = !SINGULAR_MACROS.include?(nested_attribute_options[:macro])
101
+ end
102
+ end
103
+
104
+ def build_permitted_fields
105
+ @options[:fields] || @definition.defined_inputs.keys
106
+ end
107
+ end
108
+
109
+ def build_nested_definition(name)
110
+ nested_input_definition = resource_definition.defined_nested_inputs[name]
111
+
112
+ if nested_input_definition[:options]&.fetch(:using, nil)
113
+ nested_input_definition[:options][:using]
114
+ elsif nested_input_definition[:block]
115
+ build_definition_from_block(nested_input_definition[:block])
116
+ else
117
+ raise_missing_nested_definition_error(name)
118
+ end
119
+ end
120
+
121
+ def build_definition_from_block(block)
122
+ definition = NestedInputsDefinition.new
123
+ block.call(definition)
124
+ definition
125
+ end
126
+
127
+ def render_nested_field_container(context, &)
128
+ div(
129
+ class: "col-span-full space-y-2 my-4",
130
+ data: {
131
+ controller: "nested-resource-form-fields",
132
+ nested_resource_form_fields_limit_value: context.options[:limit]
133
+ },
134
+ &
135
+ )
136
+ end
137
+
138
+ def render_nested_field_header(context)
139
+ div do
140
+ h2(class: "text-lg font-semibold text-gray-900 dark:text-white") { context.name.to_s.humanize }
141
+ render_description(context.options[:description]) if context.options[:description]
142
+ end
143
+ end
144
+
145
+ def render_description(description)
146
+ p(class: "text-md font-normal text-gray-500 dark:text-gray-400") { description }
147
+ end
148
+
149
+ def render_nested_field_content(context)
150
+ if context.multiple?
151
+ render_multiple_nested_fields(context)
152
+ else
153
+ render_single_nested_field(context)
154
+ end
155
+
156
+ div(data_nested_resource_form_fields_target: :target, hidden: true)
157
+ end
158
+
159
+ def render_multiple_nested_fields(context)
160
+ render_template_for_nested_fields(context, collection: {NEW_RECORD: NotPersisted.new})
161
+ render_existing_nested_fields(context)
162
+ end
163
+
164
+ def render_single_nested_field(context)
165
+ render_template_for_nested_fields(context, object: NotPersisted.new)
166
+ render_existing_nested_fields(context, single: true)
167
+ end
168
+
169
+ def render_template_for_nested_fields(context, field_options)
170
+ template_tag data_nested_resource_form_fields_target: "template" do
171
+ nesting_method = field_options[:collection] ? :nest_many : :nest_one
172
+ send(nesting_method, context.name, as: context.nested_input_param, template: true, **field_options) do |nested|
173
+ render_fieldset(nested, context)
174
+ end
175
+ end
176
+ end
177
+
178
+ def render_existing_nested_fields(context, single: false)
179
+ nesting_method = single ? :nest_one : :nest_many
180
+ send(nesting_method, context.name, as: context.nested_input_param) do |nested|
181
+ render_fieldset(nested, context)
182
+ end
183
+ end
184
+
185
+ def render_fieldset(nested, context)
186
+ fieldset(
187
+ data_new_record: !nested.object&.persisted?,
188
+ class: "nested-resource-form-fields border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-4 relative"
189
+ ) do
190
+ render_fieldset_content(nested, context)
191
+ render_delete_button(nested, context.options)
192
+ end
193
+ end
194
+
195
+ def render_fieldset_content(nested, context)
196
+ div(class: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense") do
197
+ render_hidden_fields(nested, context)
198
+ render_input_fields(nested, context)
199
+ end
200
+ end
201
+
202
+ def render_hidden_fields(nested, context)
203
+ if !context.options[:update_only] && context.options[:class]&.respond_to?(:primary_key)
204
+ render nested.field(context.options[:class].primary_key).hidden_tag
205
+ end
206
+ render nested.field(:_destroy).hidden_tag if context.options[:allow_destroy]
207
+ end
208
+
209
+ def render_input_fields(nested, context)
210
+ context.permitted_fields.each do |input|
211
+ render_simple_resource_field(input, context.definition, nested)
212
+ end
213
+ end
214
+
215
+ def render_delete_button(nested, options)
216
+ return unless !nested.object&.persisted? || options[:allow_destroy]
217
+
218
+ render_delete_button_content
219
+ end
220
+
221
+ def render_delete_button_content
222
+ div(class: "flex items-center justify-end") do
223
+ label(class: "inline-flex items-center text-md font-medium text-red-900 cursor-pointer") do
224
+ plain "Delete"
225
+ render_delete_checkbox
226
+ end
227
+ end
228
+ end
229
+
230
+ def render_delete_checkbox
231
+ input(
232
+ type: :checkbox,
233
+ class: "w-4 h-4 ms-2 text-red-600 bg-red-100 border-red-300 rounded focus:ring-red-500 dark:focus:ring-red-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer",
234
+ data_action: "nested-resource-form-fields#remove"
235
+ )
236
+ end
237
+
238
+ def render_nested_add_button(context)
239
+ div do
240
+ button(
241
+ type: :button,
242
+ class: "inline-block",
243
+ data: {
244
+ action: "nested-resource-form-fields#add",
245
+ nested_resource_form_fields_target: "addButton"
246
+ }
247
+ ) do
248
+ render_add_button_content(context.name)
249
+ end
250
+ end
251
+ end
252
+
253
+ def render_add_button_content(name)
254
+ span(class: "bg-secondary-700 text-white hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800 flex items-center justify-center px-4 py-1.5 text-sm font-medium rounded-lg focus:outline-none focus:ring-4") do
255
+ render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
256
+ span { "Add #{name.to_s.singularize.humanize}" }
257
+ end
258
+ end
259
+
260
+ def raise_missing_nested_definition_error(name)
261
+ raise ArgumentError, %(
262
+ `nested_input :#{name}` is missing a definition
263
+
264
+ you can either pass in a block:
265
+ ```ruby
266
+ nested_input :#{name} do |definition|
267
+ input :city
268
+ input :country
269
+ end
270
+ ```
271
+
272
+ or pass in options:
273
+ ```ruby
274
+ nested_input :#{name}, using: #{name.to_s.classify}Definition, fields: %i[city country]
275
+ ```
276
+ )
277
+ end
278
+ end
279
+ end
280
+ end
281
+ end
282
+ end
@@ -4,6 +4,8 @@ module Plutonium
4
4
  module UI
5
5
  module Form
6
6
  class Resource < Base
7
+ include Plutonium::UI::Form::Concerns::RendersNestedResourceFields
8
+
7
9
  attr_reader :resource_fields, :resource_definition
8
10
 
9
11
  def initialize(*, resource_fields:, resource_definition:, **, &)
@@ -35,37 +37,45 @@ module Plutonium
35
37
 
36
38
  def render_resource_field(name)
37
39
  when_permitted(name) do
38
- # field :name, as: :string
39
- # input :name, as: :string
40
- # input :description, class: "col-span-full"
41
- # input :age, tag: {class: "max-h-fit"}
42
- # input :dob do |f|
43
- # f.date_tag
44
- # end
45
-
46
- field_options = resource_definition.defined_fields[name] ? resource_definition.defined_fields[name][:options] : {}
47
-
48
- input_definition = resource_definition.defined_inputs[name] || {}
49
- input_options = input_definition[:options] || {}
50
-
51
- tag = field_options[:as] || input_options[:as]
52
- tag_attributes = input_options[:tag] || {}
53
- tag_block = input_definition[:block] || ->(f) {
54
- tag ||= f.inferred_field_component
55
- f.send(:"#{tag}_tag", **tag_attributes)
56
- }
57
-
58
- field_options = field_options.except(:as)
59
- wrapper_options = input_options.except(:tag, :as)
60
- if !wrapper_options[:class] || !wrapper_options[:class].include?("col-span")
61
- # temp hack to allow col span overrides
62
- # TODO: remove once we complete theming, which will support merges
63
- wrapper_options[:class] = tokens("col-span-full", wrapper_options[:class])
40
+ if resource_definition.defined_nested_inputs[name]
41
+ render_nested_resource_field(name)
42
+ else
43
+ render_simple_resource_field(name, resource_definition, self)
64
44
  end
45
+ end
46
+ end
65
47
 
66
- render field(name, **field_options).wrapped(**wrapper_options) do |f|
67
- render tag_block.call(f)
68
- end
48
+ def render_simple_resource_field(name, definition, form)
49
+ # field :name, as: :string
50
+ # input :name, as: :string
51
+ # input :description, class: "col-span-full"
52
+ # input :age, tag: {class: "max-h-fit"}
53
+ # input :dob do |f|
54
+ # f.date_tag
55
+ # end
56
+
57
+ field_options = definition.defined_fields[name] ? definition.defined_fields[name][:options] : {}
58
+
59
+ input_definition = definition.defined_inputs[name] || {}
60
+ input_options = input_definition[:options] || {}
61
+
62
+ tag = field_options[:as] || input_options[:as]
63
+ tag_attributes = input_options[:tag] || {}
64
+ tag_block = input_definition[:block] || ->(f) {
65
+ tag ||= f.inferred_field_component
66
+ f.send(:"#{tag}_tag", **tag_attributes)
67
+ }
68
+
69
+ field_options = field_options.except(:as)
70
+ wrapper_options = input_options.except(:tag, :as)
71
+ if !wrapper_options[:class] || !wrapper_options[:class].include?("col-span")
72
+ # temp hack to allow col span overrides
73
+ # TODO: remove once we complete theming, which will support merges
74
+ wrapper_options[:class] = tokens("col-span-full", wrapper_options[:class])
75
+ end
76
+
77
+ render form.field(name, **field_options).wrapped(**wrapper_options) do |f|
78
+ render tag_block.call(f)
69
79
  end
70
80
  end
71
81
 
@@ -7,7 +7,7 @@ module Plutonium
7
7
  def self.theme
8
8
  super.merge({
9
9
  base: "relative bg-white dark:bg-gray-800 shadow-md sm:rounded-lg my-3 p-6 space-y-6",
10
- fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-6 grid-flow-row-dense",
10
+ fields_wrapper: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense",
11
11
  actions_wrapper: "flex justify-end space-x-2",
12
12
  wrapper: nil,
13
13
  inner_wrapper: "w-full",
@@ -0,0 +1,53 @@
1
+ module Plutonium
2
+ module UI
3
+ class FrameNavigatorPanel < Plutonium::UI::Component::Base
4
+ class PanelItem < Plutonium::UI::Component::Base
5
+ def initialize(label:, icon:, **attributes)
6
+ @label = label
7
+ @icon = icon
8
+ @attributes = attributes
9
+ end
10
+
11
+ def view_template
12
+ button(
13
+ title: @label,
14
+ style: "display: none",
15
+ class: "text-gray-600 dark:text-gray-300",
16
+ **@attributes
17
+ ) {
18
+ render @icon.new(class: "w-6 h-6")
19
+ }
20
+ end
21
+ end
22
+
23
+ class PanelContent < Plutonium::UI::Component::Base
24
+ def initialize(src:)
25
+ @src = src
26
+ end
27
+
28
+ def view_template
29
+ DynaFrameHost src: @src, loading: :lazy, data: {"frame-navigator-target": "frame"} do
30
+ SkeletonTable()
31
+ end
32
+ end
33
+ end
34
+
35
+ def initialize(title:, src:)
36
+ @title = title
37
+ @src = src
38
+ end
39
+
40
+ def view_template
41
+ div(data: {controller: %w[has-many-panel frame-navigator]}) do
42
+ Panel do |panel|
43
+ panel.with_title @title
44
+ panel.with_item PanelItem.new(label: "Home", icon: Phlex::TablerIcons::Home2, data_frame_navigator_target: %(homeButton))
45
+ panel.with_item PanelItem.new(label: "Back", icon: Phlex::TablerIcons::ChevronLeft, data_frame_navigator_target: %(backButton))
46
+ panel.with_item PanelItem.new(label: "Refresh", icon: Phlex::TablerIcons::RefreshDot, data_frame_navigator_target: %(refreshButton))
47
+ panel.with_content PanelContent.new(src: @src)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,63 @@
1
+ module Plutonium
2
+ module UI
3
+ class Panel < Plutonium::UI::Component::Base
4
+ include Phlex::DeferredRender
5
+
6
+ def initialize
7
+ @items = []
8
+ end
9
+
10
+ def with_title(title)
11
+ @title = title
12
+ end
13
+
14
+ def with_item(item)
15
+ @items << item
16
+ end
17
+
18
+ def with_content(content)
19
+ @content = content
20
+ end
21
+
22
+ def view_template
23
+ wrapped do
24
+ render_toolbar if render_toolbar?
25
+ render_content if render_content?
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def wrapped(&)
32
+ div(class: "mt-6", &)
33
+ end
34
+
35
+ def render_toolbar
36
+ div(class: %(flex justify-between items-center mb-4)) do
37
+ if @title.present?
38
+ h5(class: %(text-2xl font-bold tracking-tight text-gray-900 dark:text-white)) do
39
+ @title
40
+ end
41
+ end
42
+ div(class: "flex space-x-4") do
43
+ @items.each do |item|
44
+ render item
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ def render_content
51
+ render @content
52
+ end
53
+
54
+ def render_toolbar?
55
+ @title || @items.present?
56
+ end
57
+
58
+ def render_content?
59
+ @content
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,29 @@
1
+ module Plutonium
2
+ module UI
3
+ class SkeletonTable < Plutonium::UI::Component::Base
4
+ def view_template
5
+ div(
6
+ role: "status",
7
+ class:
8
+ "p-4 space-y-4 border border-gray-200 divide-y divide-gray-200 rounded shadow motion-safe:animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700"
9
+ ) do
10
+ div(class: "flex items-center justify-between") do
11
+ div do
12
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5")
13
+ div(class: "w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700")
14
+ end
15
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12")
16
+ end
17
+ div(class: "flex items-center justify-between pt-4") do
18
+ div do
19
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5")
20
+ div(class: "w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700")
21
+ end
22
+ div(class: "h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12")
23
+ end
24
+ span(class: "sr-only") { "Loading..." }
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -94,7 +94,7 @@ module Plutonium
94
94
  end
95
95
 
96
96
  def render_footer
97
- div(class: "sticky bottom-[-2px] p-4 pb-6 w-full z-50 bg-gray-50 dark:bg-gray-900") {
97
+ div(class: "sticky bottom-[-2px] mt-1 p-4 pb-6 w-full z-50 bg-gray-50 dark:bg-gray-900") {
98
98
  TableInfo(pagy_instance)
99
99
  TablePagination(pagy_instance)
100
100
  }
@@ -1,5 +1,5 @@
1
1
  module Plutonium
2
- VERSION = "0.15.6"
2
+ VERSION = "0.15.7"
3
3
  NEXT_MAJOR_VERSION = VERSION.split(".").tap { |v|
4
4
  v[1] = v[1].to_i + 1
5
5
  v[2] = 0
@@ -6,6 +6,7 @@ export default class extends Controller {
6
6
 
7
7
  connect() {
8
8
  console.log(`frame-navigator connected: ${this.element}`)
9
+ this.#loadingStarted()
9
10
 
10
11
  this.srcHistory = []
11
12
  this.originalFrameSrc = this.frameTarget.src
@@ -32,6 +33,9 @@ export default class extends Controller {
32
33
  this.frameLoading = this.frameLoading.bind(this);
33
34
  this.frameTarget.addEventListener("turbo:click", this.frameLoading);
34
35
  this.frameTarget.addEventListener("turbo:submit-start", this.frameLoading);
36
+
37
+ this.frameFailed = this.frameFailed.bind(this);
38
+ this.frameTarget.addEventListener("turbo:fetch-request-error", this.frameFailed);
35
39
  }
36
40
 
37
41
  disconnect() {
@@ -42,16 +46,19 @@ export default class extends Controller {
42
46
  this.frameTarget.removeEventListener("turbo:frame-load", this.frameLoaded);
43
47
  this.frameTarget.removeEventListener("turbo:click", this.frameLoading);
44
48
  this.frameTarget.removeEventListener("turbo:submit-start", this.frameLoading);
49
+ this.frameTarget.removeEventListener("turbo:fetch-request-error", this.frameFailed);
45
50
  }
46
51
 
47
52
  frameLoading(event) {
48
- if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.add("motion-safe:animate-spin")
49
- this.frameTarget.classList.add("motion-safe:animate-pulse")
53
+ this.#loadingStarted()
54
+ }
55
+
56
+ frameFailed(event) {
57
+ this.#loadingStopped()
50
58
  }
51
59
 
52
60
  frameLoaded(event) {
53
- if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.remove("motion-safe:animate-spin")
54
- this.frameTarget.classList.remove("motion-safe:animate-pulse")
61
+ this.#loadingStopped()
55
62
 
56
63
  let src = event.target.src
57
64
  if (src == this.currentSrc) {
@@ -63,7 +70,7 @@ export default class extends Controller {
63
70
  else
64
71
  this.srcHistory.push(src)
65
72
 
66
- this.updateNavigationButtonsDisplay()
73
+ this.#updateNavigationButtonsDisplay()
67
74
  }
68
75
 
69
76
  refreshButtonClicked(event) {
@@ -87,13 +94,23 @@ export default class extends Controller {
87
94
 
88
95
  get currentSrc() { return this.srcHistory[this.srcHistory.length - 1] }
89
96
 
90
- updateNavigationButtonsDisplay() {
97
+ #loadingStarted() {
98
+ if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.add("motion-safe:animate-spin")
99
+ this.frameTarget.classList.add("motion-safe:animate-pulse")
100
+ }
101
+
102
+ #loadingStopped() {
103
+ if (this.hasRefreshButtonTarget) this.refreshButtonTarget.classList.remove("motion-safe:animate-spin")
104
+ this.frameTarget.classList.remove("motion-safe:animate-pulse")
105
+ }
106
+
107
+ #updateNavigationButtonsDisplay() {
91
108
  if (this.hasHomeButtonTarget) {
92
- this.homeButtonTarget.style.display = this.srcHistory.length > 1 ? '' : 'none'
109
+ this.homeButtonTarget.style.display = this.srcHistory.length > 2 ? '' : 'none'
93
110
  }
94
111
 
95
112
  if (this.hasBackButtonTarget) {
96
- this.backButtonTarget.style.display = this.srcHistory.length > 2 ? '' : 'none'
113
+ this.backButtonTarget.style.display = this.srcHistory.length > 1 ? '' : 'none'
97
114
  }
98
115
  }
99
116
  }
@@ -33,11 +33,11 @@ export default class extends Controller {
33
33
  e.preventDefault()
34
34
 
35
35
  const wrapper = e.target.closest(this.wrapperSelectorValue)
36
-
37
- if (wrapper.dataset.newRecord === "true") {
36
+ if (wrapper.dataset.newRecord !== undefined) {
38
37
  wrapper.remove()
39
38
  } else {
40
39
  wrapper.style.display = "none"
40
+ wrapper.classList.remove(...wrapper.classList)
41
41
 
42
42
  const input = wrapper.querySelector("input[name*='_destroy']")
43
43
  input.value = "1"