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