plutonium 0.15.6 → 0.15.8
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.
- checksums.yaml +4 -4
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +25 -11
- data/app/assets/plutonium.js.map +2 -2
- data/app/assets/plutonium.min.js +4 -4
- data/app/assets/plutonium.min.js.map +3 -3
- data/app/views/layouts/rodauth.html.erb +2 -2
- data/docs/guide/getting-started/installation.md +2 -1
- data/docs/guide/getting-started/resources.md +8 -12
- data/docs/public/templates/plutonium.rb +8 -0
- data/lib/generators/pu/core/assets/assets_generator.rb +1 -1
- data/lib/generators/pu/core/assets/templates/tailwind.config.js +11 -1
- data/lib/generators/pu/eject/layout/layout_generator.rb +3 -3
- data/lib/generators/pu/eject/shell/shell_generator.rb +3 -3
- data/lib/generators/pu/extra/colorized_logger/colorized_logger_generator.rb +21 -0
- data/lib/generators/pu/extra/colorized_logger/templates/config/initializers/colorized_logger.rb +22 -0
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/gem/letter_opener/letter_opener_generator.rb +21 -0
- data/lib/generators/pu/gem/redis/redis_generator.rb +0 -2
- data/lib/generators/pu/gem/standard/standard_generator.rb +19 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -1
- data/lib/generators/pu/res/conn/conn_generator.rb +1 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/definition/actions.rb +6 -2
- data/lib/plutonium/definition/base.rb +1 -0
- data/lib/plutonium/definition/nested_inputs.rb +19 -0
- data/lib/plutonium/resource/controller.rb +13 -9
- data/lib/plutonium/resource/controllers/authorizable.rb +2 -2
- data/lib/plutonium/resource/controllers/interactive_actions.rb +1 -1
- data/lib/plutonium/resource/controllers/presentable.rb +15 -8
- data/lib/plutonium/ui/block.rb +13 -0
- data/lib/plutonium/ui/component/kit.rb +10 -0
- data/lib/plutonium/ui/display/resource.rb +29 -11
- data/lib/plutonium/ui/display/theme.rb +1 -1
- data/lib/plutonium/ui/dyna_frame/content.rb +2 -2
- data/lib/plutonium/ui/dyna_frame/host.rb +20 -0
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +285 -0
- data/lib/plutonium/ui/form/resource.rb +39 -29
- data/lib/plutonium/ui/form/theme.rb +1 -1
- data/lib/plutonium/ui/frame_navigator_panel.rb +53 -0
- data/lib/plutonium/ui/panel.rb +63 -0
- data/lib/plutonium/ui/skeleton_table.rb +29 -0
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package-lock.json +2 -2
- data/package.json +1 -1
- data/src/js/controllers/frame_navigator_controller.js +25 -8
- data/src/js/controllers/nested_resource_form_fields_controller.js +2 -2
- data/tailwind.config.js +11 -1
- data/tailwind.options.js +7 -1
- metadata +13 -3
- data/lib/generators/pu/gem/redis/templates/.keep +0 -0
@@ -0,0 +1,285 @@
|
|
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
|
+
# TODO: further decompose this into components
|
9
|
+
# @api private
|
10
|
+
module RendersNestedResourceFields
|
11
|
+
extend ActiveSupport::Concern
|
12
|
+
|
13
|
+
DEFAULT_NESTED_LIMIT = 10
|
14
|
+
NESTED_OPTION_KEYS = [:allow_destroy, :update_only, :macro, :class].freeze
|
15
|
+
SINGULAR_MACROS = %i[belongs_to has_one].freeze
|
16
|
+
|
17
|
+
class NestedInputsDefinition
|
18
|
+
include Plutonium::Definition::DefineableProps
|
19
|
+
|
20
|
+
defineable_props :field, :input
|
21
|
+
end
|
22
|
+
|
23
|
+
class NestedFieldContext
|
24
|
+
attr_reader :name, :definition, :options, :permitted_fields
|
25
|
+
|
26
|
+
def initialize(name:, definition:, resource_class:, resource_definition:, object_class:)
|
27
|
+
@name = name
|
28
|
+
@definition = definition
|
29
|
+
@resource_definition = resource_definition
|
30
|
+
@resource_class = resource_class
|
31
|
+
@options = build_options
|
32
|
+
@permitted_fields = build_permitted_fields
|
33
|
+
@object_class = object_class
|
34
|
+
end
|
35
|
+
|
36
|
+
def nested_attribute_options
|
37
|
+
@nested_attribute_options ||= @resource_class.all_nested_attributes_options[@name] || {}
|
38
|
+
end
|
39
|
+
|
40
|
+
def nested_fields_input_param
|
41
|
+
@options[:as] || :"#{@name}_attributes"
|
42
|
+
end
|
43
|
+
|
44
|
+
def nested_fields_multiple?
|
45
|
+
@options[:multiple]
|
46
|
+
end
|
47
|
+
|
48
|
+
def blank_object
|
49
|
+
(@object_class || nested_attribute_options[:class])&.new
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def build_options
|
55
|
+
options = @resource_definition.defined_nested_inputs[@name][:options].dup || {}
|
56
|
+
merge_nested_fields_options(options)
|
57
|
+
set_nested_fields_limits(options)
|
58
|
+
options
|
59
|
+
end
|
60
|
+
|
61
|
+
def merge_nested_fields_options(options)
|
62
|
+
NESTED_OPTION_KEYS.each do |key|
|
63
|
+
options.fetch(key) { options[key] = nested_attribute_options[key] }
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def set_nested_fields_limits(options)
|
68
|
+
options.fetch(:limit) do
|
69
|
+
options[:limit] = if SINGULAR_MACROS.include?(nested_attribute_options[:macro])
|
70
|
+
1
|
71
|
+
else
|
72
|
+
nested_attribute_options[:limit] || DEFAULT_NESTED_LIMIT
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
options.fetch(:multiple) do
|
77
|
+
options[:multiple] = !SINGULAR_MACROS.include?(nested_attribute_options[:macro])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def build_permitted_fields
|
82
|
+
@options[:fields] || @definition.defined_inputs.keys
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Template object for new nested records
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
# Renders a nested resource field with associated inputs
|
91
|
+
# @param [Symbol] name The name of the nested resource field
|
92
|
+
# @raise [ArgumentError] if the nested input definition is missing required configuration
|
93
|
+
def render_nested_resource_field(name)
|
94
|
+
# debugger if $extracting_input
|
95
|
+
context = NestedFieldContext.new(
|
96
|
+
name: name,
|
97
|
+
definition: build_nested_fields_definition(name),
|
98
|
+
resource_class: resource_class,
|
99
|
+
resource_definition: resource_definition,
|
100
|
+
object_class: resource_definition.defined_nested_inputs[name][:options]&.fetch(:object_class, nil)
|
101
|
+
)
|
102
|
+
|
103
|
+
render_nested_field_container(context) do
|
104
|
+
render_nested_field_header(context)
|
105
|
+
render_nested_field_content(context)
|
106
|
+
render_nested_fields_add_button(context)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def build_nested_fields_definition(name)
|
111
|
+
nested_input_definition = resource_definition.defined_nested_inputs[name]
|
112
|
+
|
113
|
+
if nested_input_definition[:options]&.fetch(:using, nil)
|
114
|
+
nested_input_definition[:options][:using]
|
115
|
+
elsif nested_input_definition[:block]
|
116
|
+
build_nested_fields_definition_from_block(nested_input_definition[:block])
|
117
|
+
else
|
118
|
+
raise_missing_nested_definition_error(name)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
def build_nested_fields_definition_from_block(block)
|
123
|
+
definition = NestedInputsDefinition.new
|
124
|
+
block.call(definition)
|
125
|
+
definition
|
126
|
+
end
|
127
|
+
|
128
|
+
def render_nested_field_container(context, &)
|
129
|
+
div(
|
130
|
+
class: "col-span-full space-y-2 my-4",
|
131
|
+
data: {
|
132
|
+
controller: "nested-resource-form-fields",
|
133
|
+
nested_resource_form_fields_limit_value: context.options[:limit]
|
134
|
+
},
|
135
|
+
&
|
136
|
+
)
|
137
|
+
end
|
138
|
+
|
139
|
+
def render_nested_field_header(context)
|
140
|
+
div do
|
141
|
+
h2(class: "text-lg font-semibold text-gray-900 dark:text-white") { context.name.to_s.humanize }
|
142
|
+
render_nested_fields_header_description(context.options[:description]) if context.options[:description]
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def render_nested_fields_header_description(description)
|
147
|
+
p(class: "text-md font-normal text-gray-500 dark:text-gray-400") { description }
|
148
|
+
end
|
149
|
+
|
150
|
+
def render_nested_field_content(context)
|
151
|
+
if context.nested_fields_multiple?
|
152
|
+
render_multiple_nested_fields(context)
|
153
|
+
else
|
154
|
+
render_single_nested_field(context)
|
155
|
+
end
|
156
|
+
|
157
|
+
div(data_nested_resource_form_fields_target: :target, hidden: true)
|
158
|
+
end
|
159
|
+
|
160
|
+
def render_multiple_nested_fields(context)
|
161
|
+
nesting_method = :nest_many
|
162
|
+
options = {default: {NEW_RECORD: context.blank_object}}
|
163
|
+
render_template_for_nested_fields(context, options, nesting_method:)
|
164
|
+
render_existing_nested_fields(context, options, nesting_method:)
|
165
|
+
end
|
166
|
+
|
167
|
+
def render_single_nested_field(context)
|
168
|
+
nesting_method = :nest_one
|
169
|
+
options = {default: context.blank_object}
|
170
|
+
render_template_for_nested_fields(context, options, nesting_method:)
|
171
|
+
render_existing_nested_fields(context, options, nesting_method:)
|
172
|
+
end
|
173
|
+
|
174
|
+
def render_template_for_nested_fields(context, options, nesting_method:)
|
175
|
+
template_tag data_nested_resource_form_fields_target: "template" do
|
176
|
+
send(nesting_method, context.name, as: context.nested_fields_input_param, **options, template: true) do |nested|
|
177
|
+
render_nested_fields_fieldset(nested, context)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def render_existing_nested_fields(context, options, nesting_method:)
|
183
|
+
send(nesting_method, context.name, as: context.nested_fields_input_param, **options) do |nested|
|
184
|
+
render_nested_fields_fieldset(nested, context)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def render_nested_fields_fieldset(nested, context)
|
189
|
+
fieldset(
|
190
|
+
data_new_record: !nested.object&.persisted?,
|
191
|
+
class: "nested-resource-form-fields border border-gray-200 dark:border-gray-700 rounded-lg p-4 space-y-4 relative"
|
192
|
+
) do
|
193
|
+
render_nested_fields_fieldset_content(nested, context)
|
194
|
+
render_nested_fields_delete_button(nested, context.options)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def render_nested_fields_fieldset_content(nested, context)
|
199
|
+
div(class: "grid grid-cols-1 md:grid-cols-2 2xl:grid-cols-4 gap-4 grid-flow-row-dense") do
|
200
|
+
render_nested_fields_hidden_fields(nested, context)
|
201
|
+
render_nested_fields_visible_fields(nested, context)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def render_nested_fields_hidden_fields(nested, context)
|
206
|
+
if !context.options[:update_only] && context.options[:class]&.respond_to?(:primary_key)
|
207
|
+
render nested.field(context.options[:class].primary_key).hidden_tag
|
208
|
+
end
|
209
|
+
render nested.field(:_destroy).hidden_tag if context.options[:allow_destroy]
|
210
|
+
end
|
211
|
+
|
212
|
+
def render_nested_fields_visible_fields(nested, context)
|
213
|
+
context.permitted_fields.each do |input|
|
214
|
+
render_simple_resource_field(input, context.definition, nested)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
|
218
|
+
def render_nested_fields_delete_button(nested, options)
|
219
|
+
return unless !nested.object&.persisted? || options[:allow_destroy]
|
220
|
+
|
221
|
+
render_nested_fields_delete_button_content
|
222
|
+
end
|
223
|
+
|
224
|
+
def render_nested_fields_delete_button_content
|
225
|
+
div(class: "flex items-center justify-end") do
|
226
|
+
label(class: "inline-flex items-center text-md font-medium text-red-900 cursor-pointer") do
|
227
|
+
plain "Delete"
|
228
|
+
render_nested_fields_delete_checkbox
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
def render_nested_fields_delete_checkbox
|
234
|
+
input(
|
235
|
+
type: :checkbox,
|
236
|
+
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",
|
237
|
+
data_action: "nested-resource-form-fields#remove"
|
238
|
+
)
|
239
|
+
end
|
240
|
+
|
241
|
+
def render_nested_fields_add_button(context)
|
242
|
+
div do
|
243
|
+
button(
|
244
|
+
type: :button,
|
245
|
+
class: "inline-block",
|
246
|
+
data: {
|
247
|
+
action: "nested-resource-form-fields#add",
|
248
|
+
nested_resource_form_fields_target: "addButton"
|
249
|
+
}
|
250
|
+
) do
|
251
|
+
render_nested_fields_add_button_content(context.name)
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def render_nested_fields_add_button_content(name)
|
257
|
+
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
|
258
|
+
render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
|
259
|
+
span { "Add #{name.to_s.singularize.humanize}" }
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
def raise_missing_nested_definition_error(name)
|
264
|
+
raise ArgumentError, %(
|
265
|
+
`nested_input :#{name}` is missing a definition
|
266
|
+
|
267
|
+
you can either pass in a block:
|
268
|
+
```ruby
|
269
|
+
nested_input :#{name} do |definition|
|
270
|
+
input :city
|
271
|
+
input :country
|
272
|
+
end
|
273
|
+
```
|
274
|
+
|
275
|
+
or pass in options:
|
276
|
+
```ruby
|
277
|
+
nested_input :#{name}, using: #{name.to_s.classify}Definition, fields: %i[city country]
|
278
|
+
```
|
279
|
+
)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
285
|
+
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
-
|
67
|
-
|
68
|
-
|
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-
|
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 dyna:static 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
|
}
|
data/lib/plutonium/version.rb
CHANGED
data/package-lock.json
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
{
|
2
2
|
"name": "@radioactive-labs/plutonium",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.6",
|
4
4
|
"lockfileVersion": 3,
|
5
5
|
"requires": true,
|
6
6
|
"packages": {
|
7
7
|
"": {
|
8
8
|
"name": "@radioactive-labs/plutonium",
|
9
|
-
"version": "0.1.
|
9
|
+
"version": "0.1.6",
|
10
10
|
"license": "MIT",
|
11
11
|
"dependencies": {
|
12
12
|
"@hotwired/stimulus": "^3.2.2",
|
data/package.json
CHANGED
@@ -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
|
-
|
49
|
-
|
53
|
+
this.#loadingStarted()
|
54
|
+
}
|
55
|
+
|
56
|
+
frameFailed(event) {
|
57
|
+
this.#loadingStopped()
|
50
58
|
}
|
51
59
|
|
52
60
|
frameLoaded(event) {
|
53
|
-
|
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
|
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
|
-
|
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 >
|
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 >
|
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"
|
data/tailwind.config.js
CHANGED
@@ -1,9 +1,19 @@
|
|
1
1
|
/** @type {import('tailwindcss').Config} */
|
2
2
|
|
3
|
+
const tailwindPlugin = require('tailwindcss/plugin')
|
3
4
|
const options = require("./tailwind.options.js")
|
4
5
|
|
5
6
|
export const content = options.content
|
6
7
|
export const darkMode = options.darkMode
|
7
|
-
export const plugins = options.plugins.map((plugin)
|
8
|
+
export const plugins = options.plugins.map(function (plugin) {
|
9
|
+
switch (typeof plugin) {
|
10
|
+
case "function":
|
11
|
+
return tailwindPlugin(plugin)
|
12
|
+
case "string":
|
13
|
+
return require(plugin)
|
14
|
+
default:
|
15
|
+
throw Error(`unsupported plugin: ${plugin}: ${(typeof plugin)}`)
|
16
|
+
}
|
17
|
+
})
|
8
18
|
export const theme = options.theme
|
9
19
|
export const safelist = options.safelist
|