plutonium 0.25.2 → 0.26.1
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 +2 -2
- data/app/assets/plutonium.js +152 -12
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +3 -3
- data/docs/guide/deep-dive/resources.md +1 -1
- data/docs/modules/definition.md +55 -20
- data/docs/modules/table.md +1 -1
- data/docs/public/plutonium.mdc +27 -14
- data/lib/plutonium/core/controller.rb +2 -2
- data/lib/plutonium/core/controllers/entity_scoping.rb +5 -0
- data/lib/plutonium/engine.rb +3 -2
- data/lib/plutonium/resource/register.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +8 -2
- data/lib/plutonium/ui/page/interactive_action.rb +23 -0
- data/lib/plutonium/ui/table/resource.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/slim_select.css +37 -0
- data/src/js/controllers/remote_modal_controller.js +24 -17
- data/src/js/controllers/slim_select_controller.js +201 -8
- data/src/js/core.js +2 -1
- data/src/js/plutonium.js +0 -2
- data/yarn.lock +3840 -0
- metadata +15 -14
@@ -371,7 +371,7 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
371
371
|
|
372
372
|
# Add filters to the sidebar
|
373
373
|
filter :published, with: ->(scope, value) { value ? scope.where.not(published_at: nil) : scope.where(published_at: nil) }, as: :boolean
|
374
|
-
filter :user, as: :select,
|
374
|
+
filter :user, as: :select, choices: User.pluck(:name, :id)
|
375
375
|
|
376
376
|
# Define named scopes that appear as buttons
|
377
377
|
scope :all
|
data/docs/modules/definition.md
CHANGED
@@ -71,7 +71,7 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
71
71
|
# Only override when you need custom input behavior:
|
72
72
|
input :content, as: :rich_text # Override text -> rich_text
|
73
73
|
input :title, placeholder: "Enter title" # Add placeholder
|
74
|
-
input :category, as: :select,
|
74
|
+
input :category, as: :select, choices: %w[Tech Business] # Add options
|
75
75
|
input :published_at, as: :date # Override datetime -> date only
|
76
76
|
end
|
77
77
|
```
|
@@ -158,8 +158,8 @@ end
|
|
158
158
|
```ruby
|
159
159
|
class PostDefinition < Plutonium::Resource::Definition
|
160
160
|
# Provide options for select inputs
|
161
|
-
input :category, as: :select,
|
162
|
-
input :author, as: :select,
|
161
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
162
|
+
input :author, as: :select, choices: -> { User.active.pluck(:name, :id) }
|
163
163
|
end
|
164
164
|
```
|
165
165
|
|
@@ -177,7 +177,7 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
177
177
|
|
178
178
|
# Use `pre_submit` to create dynamic forms where inputs appear based on other inputs.
|
179
179
|
input :send_notifications, as: :boolean, pre_submit: true
|
180
|
-
input :notification_channel, as: :select,
|
180
|
+
input :notification_channel, as: :select, choices: %w[Email SMS],
|
181
181
|
condition: -> { object.send_notifications? }
|
182
182
|
|
183
183
|
# Show debug fields only in development
|
@@ -280,6 +280,7 @@ The block syntax offers more control over rendering, allowing for custom compone
|
|
280
280
|
- Building complex layouts with multiple components or custom HTML (for `display` only).
|
281
281
|
- You need conditional logic to determine which component to render.
|
282
282
|
- You need to call specific form builder methods with custom logic (for `input`).
|
283
|
+
- **You need dynamic choices for select inputs** (since `choices:` option only accepts static arrays).
|
283
284
|
|
284
285
|
::: code-group
|
285
286
|
```ruby [Custom Display Components]
|
@@ -302,6 +303,29 @@ input :birth_date do |f|
|
|
302
303
|
end
|
303
304
|
end
|
304
305
|
```
|
306
|
+
```ruby [Dynamic Choices for Select Inputs]
|
307
|
+
# Dynamic choices based on object state
|
308
|
+
input :widget_type do |f|
|
309
|
+
choices = case object.question_type
|
310
|
+
when nil, ""
|
311
|
+
[]
|
312
|
+
when "text"
|
313
|
+
["input", "textarea"]
|
314
|
+
when "choice"
|
315
|
+
["radio", "select", "checkbox"]
|
316
|
+
when "scale"
|
317
|
+
["slider", "radio_scale", "select_scale"]
|
318
|
+
when "date"
|
319
|
+
["date_picker", "datetime_picker"]
|
320
|
+
when "boolean"
|
321
|
+
["checkbox", "toggle", "yes_no"]
|
322
|
+
else
|
323
|
+
Question::WIDGET_TYPES
|
324
|
+
end
|
325
|
+
|
326
|
+
f.select_tag choices: choices
|
327
|
+
end
|
328
|
+
```
|
305
329
|
```ruby [Conditional Rendering]
|
306
330
|
# Conditional display based on value
|
307
331
|
display :metrics do |field|
|
@@ -829,32 +853,43 @@ display :content,
|
|
829
853
|
condition: -> { current_user.can_see_content? }
|
830
854
|
```
|
831
855
|
|
832
|
-
###
|
856
|
+
### Choices Options (for selects)
|
833
857
|
```ruby
|
834
|
-
|
835
|
-
input :
|
858
|
+
# Static choices
|
859
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
836
860
|
|
837
|
-
#
|
838
|
-
|
839
|
-
|
840
|
-
|
841
|
-
|
861
|
+
# Dynamic choices require block form
|
862
|
+
input :author do |f|
|
863
|
+
choices = User.active.pluck(:name, :id)
|
864
|
+
f.select_tag choices: choices
|
865
|
+
end
|
842
866
|
|
843
|
-
#
|
844
|
-
input :
|
845
|
-
|
846
|
-
|
867
|
+
# Dynamic choices with access to context
|
868
|
+
input :team_members do |f|
|
869
|
+
choices = current_user.organization.users.active.pluck(:name, :id)
|
870
|
+
f.select_tag choices: choices
|
871
|
+
end
|
872
|
+
|
873
|
+
# Dynamic choices based on object state
|
874
|
+
input :related_posts do |f|
|
875
|
+
choices = if object.persisted?
|
876
|
+
Post.where.not(id: object.id).published.pluck(:title, :id)
|
877
|
+
else
|
878
|
+
[]
|
879
|
+
end
|
880
|
+
f.select_tag choices: choices
|
881
|
+
end
|
847
882
|
```
|
848
883
|
|
849
|
-
::: tip
|
850
|
-
|
884
|
+
::: tip Dynamic Choices Context
|
885
|
+
When using block forms for dynamic choices, you have access to:
|
851
886
|
- `current_user` - The authenticated user
|
852
887
|
- `current_parent` - Parent record for nested resources
|
853
888
|
- `object` - The record being edited (in edit forms)
|
854
889
|
- `request` and `params` - Request information
|
855
890
|
- All helper methods available in the form context
|
856
891
|
|
857
|
-
|
892
|
+
Block forms are required for dynamic choices since the `choices:` option only accepts static arrays.
|
858
893
|
:::
|
859
894
|
|
860
895
|
### File Upload Options
|
@@ -868,7 +903,7 @@ input :documents, as: :file, multiple: true,
|
|
868
903
|
## Dynamic Configuration & Policies
|
869
904
|
|
870
905
|
::: danger IMPORTANT
|
871
|
-
Definitions are instantiated outside the controller context, which means **`current_user` and other controller methods are NOT available** within the definition file itself. However, `condition` and
|
906
|
+
Definitions are instantiated outside the controller context, which means **`current_user` and other controller methods are NOT available** within the definition file itself. However, `condition` procs and input blocks ARE evaluated in the rendering context where `current_user` and the record (`object`) are available.
|
872
907
|
:::
|
873
908
|
|
874
909
|
The `condition` option configures **if an input is rendered**. It does not control if a field's *value* is accessible. For that, you must use policies.
|
data/docs/modules/table.md
CHANGED
data/docs/public/plutonium.mdc
CHANGED
@@ -90,7 +90,7 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
90
90
|
|
91
91
|
# Only declare fields when you want to override defaults:
|
92
92
|
field :content, as: :rich_text # Override text → rich_text
|
93
|
-
field :status, as: :select,
|
93
|
+
field :status, as: :select, choices: %w[draft published archived]
|
94
94
|
|
95
95
|
# Display customization (show/index pages)
|
96
96
|
display :title, as: :string
|
@@ -119,7 +119,7 @@ class PostDefinition < Plutonium::Resource::Definition
|
|
119
119
|
# Input configuration (new/edit forms)
|
120
120
|
input :title, placeholder: "Enter post title" # No need for as: :string (auto-detected)
|
121
121
|
input :content, as: :rich_text
|
122
|
-
input :category, as: :select,
|
122
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
123
123
|
|
124
124
|
# New: Custom component classes
|
125
125
|
input :color_picker, as: ColorPickerComponent
|
@@ -239,19 +239,32 @@ display :content,
|
|
239
239
|
condition: -> { Rails.env.development? } # Cosmetic condition only
|
240
240
|
```
|
241
241
|
|
242
|
-
####
|
242
|
+
#### Choices Options (for selects)
|
243
243
|
```ruby
|
244
|
-
|
245
|
-
input :
|
246
|
-
|
247
|
-
#
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
244
|
+
# Static choices
|
245
|
+
input :category, as: :select, choices: %w[Tech Business Lifestyle]
|
246
|
+
|
247
|
+
# Dynamic choices require block form
|
248
|
+
input :author do |f|
|
249
|
+
choices = User.active.pluck(:name, :id)
|
250
|
+
f.select_tag choices: choices
|
251
|
+
end
|
252
|
+
|
253
|
+
# Dynamic choices with access to context
|
254
|
+
input :team_members do |f|
|
255
|
+
choices = current_user.organization.users.active.pluck(:name, :id)
|
256
|
+
f.select_tag choices: choices
|
257
|
+
end
|
258
|
+
|
259
|
+
# Dynamic choices based on object state
|
260
|
+
input :related_posts do |f|
|
261
|
+
choices = if object.persisted?
|
262
|
+
Post.where.not(id: object.id).published.pluck(:title, :id)
|
263
|
+
else
|
264
|
+
[]
|
265
|
+
end
|
266
|
+
f.select_tag choices: choices
|
267
|
+
end
|
255
268
|
```
|
256
269
|
|
257
270
|
#### File Upload Options
|
@@ -79,13 +79,13 @@ module Plutonium
|
|
79
79
|
url_args[:id] = element.to_param unless resource_route_config[:route_type] == :resource
|
80
80
|
url_args[:action] ||= :show
|
81
81
|
else
|
82
|
-
url_args[element.model_name.
|
82
|
+
url_args[element.model_name.singular.to_sym] = element.to_param
|
83
83
|
end
|
84
84
|
end
|
85
85
|
end
|
86
86
|
url_args[:controller] = "/#{controller_chain.join("::").underscore}"
|
87
87
|
|
88
|
-
url_args[:"#{parent.model_name.
|
88
|
+
url_args[:"#{parent.model_name.singular}_id"] = parent.to_param if parent.present?
|
89
89
|
if scoped_to_entity? && scoped_entity_strategy == :path
|
90
90
|
url_args[scoped_entity_param_key] = current_scoped_entity
|
91
91
|
end
|
@@ -43,6 +43,11 @@ module Plutonium
|
|
43
43
|
current_engine.scoped_entity_param_key
|
44
44
|
end
|
45
45
|
|
46
|
+
def scoped_entity_route_key
|
47
|
+
ensure_legal_entity_scoping_method_access!(__method__)
|
48
|
+
current_engine.scoped_entity_route_key
|
49
|
+
end
|
50
|
+
|
46
51
|
# Returns the class of the scoped entity.
|
47
52
|
#
|
48
53
|
# @return [Class] the scoped entity class
|
data/lib/plutonium/engine.rb
CHANGED
@@ -5,12 +5,13 @@ module Plutonium
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
class_methods do
|
8
|
-
attr_reader :scoped_entity_class, :scoped_entity_strategy, :scoped_entity_param_key
|
8
|
+
attr_reader :scoped_entity_class, :scoped_entity_strategy, :scoped_entity_param_key, :scoped_entity_route_key
|
9
9
|
|
10
|
-
def scope_to_entity(entity_class, strategy: :path, param_key: nil)
|
10
|
+
def scope_to_entity(entity_class, strategy: :path, param_key: nil, route_key: nil)
|
11
11
|
@scoped_entity_class = entity_class
|
12
12
|
@scoped_entity_strategy = strategy
|
13
13
|
@scoped_entity_param_key = param_key || entity_class.model_name.singular_route_key.to_sym
|
14
|
+
@scoped_entity_route_key = route_key || entity_class.model_name.singular.to_sym
|
14
15
|
end
|
15
16
|
|
16
17
|
def scoped_to_entity?
|
@@ -91,12 +91,18 @@ module Plutonium
|
|
91
91
|
# @param [Symbol] name The name of the nested resource field
|
92
92
|
# @raise [ArgumentError] if the nested input definition is missing required configuration
|
93
93
|
def render_nested_resource_field(name)
|
94
|
+
nested_input_definition = resource_definition.defined_nested_inputs[name]
|
95
|
+
condition = nested_input_definition[:options]&.fetch(:condition, nil)
|
96
|
+
if condition && !instance_exec(&condition)
|
97
|
+
return
|
98
|
+
end
|
99
|
+
|
94
100
|
context = NestedFieldContext.new(
|
95
101
|
name: name,
|
96
102
|
definition: build_nested_fields_definition(name),
|
97
103
|
resource_class: resource_class,
|
98
104
|
resource_definition: resource_definition,
|
99
|
-
object_class:
|
105
|
+
object_class: nested_input_definition[:options]&.fetch(:object_class, nil)
|
100
106
|
)
|
101
107
|
|
102
108
|
render_nested_field_container(context) do
|
@@ -171,7 +177,7 @@ module Plutonium
|
|
171
177
|
end
|
172
178
|
|
173
179
|
def render_template_for_nested_fields(context, options, nesting_method:)
|
174
|
-
|
180
|
+
template data_nested_resource_form_fields_target: "template" do
|
175
181
|
send(nesting_method, context.name, as: context.nested_fields_input_param, **options, template: true) do |nested|
|
176
182
|
render_nested_fields_fieldset(nested, context)
|
177
183
|
end
|
@@ -32,6 +32,29 @@ module Plutonium
|
|
32
32
|
transition-opacity duration-300 ease-in-out",
|
33
33
|
data: {controller: "remote-modal"}
|
34
34
|
) do
|
35
|
+
# Close button
|
36
|
+
button(
|
37
|
+
type: "button",
|
38
|
+
class: "absolute top-4 right-4 p-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors duration-200",
|
39
|
+
data: {action: "remote-modal#close"},
|
40
|
+
"aria-label": "Close dialog"
|
41
|
+
) do
|
42
|
+
svg(
|
43
|
+
class: "w-5 h-5",
|
44
|
+
fill: "none",
|
45
|
+
stroke: "currentColor",
|
46
|
+
viewBox: "0 0 24 24",
|
47
|
+
xmlns: "http://www.w3.org/2000/svg"
|
48
|
+
) do |s|
|
49
|
+
s.path(
|
50
|
+
stroke_linecap: "round",
|
51
|
+
stroke_linejoin: "round",
|
52
|
+
stroke_width: "2",
|
53
|
+
d: "M6 18L18 6M6 6l12 12"
|
54
|
+
)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
35
58
|
render_page_header
|
36
59
|
render partial("interactive_action_form")
|
37
60
|
end
|
@@ -60,7 +60,7 @@ module Plutonium
|
|
60
60
|
column_options = column_definition[:options] || {}
|
61
61
|
|
62
62
|
# Check for conditional rendering
|
63
|
-
condition = column_options[:condition]
|
63
|
+
condition = column_options[:condition]
|
64
64
|
conditionally_hidden = condition && !instance_exec(&condition)
|
65
65
|
next if conditionally_hidden
|
66
66
|
|
data/lib/plutonium/version.rb
CHANGED
data/package.json
CHANGED
data/src/css/slim_select.css
CHANGED
@@ -309,3 +309,40 @@
|
|
309
309
|
.ss-main.ss-valid .ss-values .ss-placeholder {
|
310
310
|
@apply text-green-700 dark:text-green-500;
|
311
311
|
}
|
312
|
+
|
313
|
+
/* Modal-specific styles for SlimSelect dropdown */
|
314
|
+
.ss-dropdown-container {
|
315
|
+
position: absolute;
|
316
|
+
z-index: 9999;
|
317
|
+
inset: 40% 0px auto;
|
318
|
+
}
|
319
|
+
|
320
|
+
.ss-dropdown-container .ss-content {
|
321
|
+
position: static !important;
|
322
|
+
transform: none !important;
|
323
|
+
width: 100% !important;
|
324
|
+
border-radius: 0 !important;
|
325
|
+
margin: 0 !important;
|
326
|
+
pointer-events: none !important; /* Disabled by default */
|
327
|
+
}
|
328
|
+
|
329
|
+
/* When active (dropdown is expanded), enable pointer events */
|
330
|
+
.ss-dropdown-container.ss-active .ss-content {
|
331
|
+
pointer-events: auto !important;
|
332
|
+
}
|
333
|
+
|
334
|
+
.ss-dropdown-container .ss-list {
|
335
|
+
max-height: 250px !important;
|
336
|
+
overflow-y: auto !important;
|
337
|
+
}
|
338
|
+
|
339
|
+
/* Ensure the dropdown doesn't block other elements when closed */
|
340
|
+
.ss-dropdown-container:not(:has(.ss-content)),
|
341
|
+
.ss-dropdown-container:not(.ss-active) {
|
342
|
+
pointer-events: none !important;
|
343
|
+
}
|
344
|
+
|
345
|
+
/* Prevent interaction with closed dropdown */
|
346
|
+
.ss-dropdown-container:not(.ss-active) * {
|
347
|
+
pointer-events: none !important;
|
348
|
+
}
|
@@ -2,23 +2,30 @@ import { Controller } from "@hotwired/stimulus";
|
|
2
2
|
|
3
3
|
// Connects to data-controller="remote-modal"
|
4
4
|
export default class extends Controller {
|
5
|
-
|
6
|
-
|
7
|
-
|
5
|
+
connect() {
|
6
|
+
// Store original scroll position
|
7
|
+
this.originalScrollPosition = window.scrollY;
|
8
8
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
9
|
+
// Show the modal
|
10
|
+
this.element.showModal();
|
11
|
+
// Add close event listener
|
12
|
+
this.element.addEventListener("close", this.handleClose.bind(this));
|
13
|
+
}
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
close() {
|
16
|
+
// Close the modal
|
17
|
+
this.element.close();
|
18
|
+
// Restore the original scroll position
|
19
|
+
window.scrollTo(0, this.originalScrollPosition);
|
20
|
+
}
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
22
|
+
disconnect() {
|
23
|
+
// Clean up event listener when controller is disconnected
|
24
|
+
this.element.removeEventListener("close", this.handleClose);
|
25
|
+
}
|
26
|
+
|
27
|
+
handleClose() {
|
28
|
+
// Restore the original scroll position after dialog closes
|
29
|
+
window.scrollTo(0, this.originalScrollPosition);
|
30
|
+
}
|
31
|
+
}
|