plutonium 0.15.6 → 0.15.7

Sign up to get free protection for your applications and to get access to all the features.
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"