plutonium 0.54.0 → 0.55.0
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/.claude/skills/plutonium-behavior/SKILL.md +22 -0
- data/.claude/skills/plutonium-resource/SKILL.md +55 -0
- data/.claude/skills/plutonium-ui/SKILL.md +2 -1
- data/CHANGELOG.md +14 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +18 -0
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +30 -30
- data/app/assets/plutonium.min.js.map +4 -4
- data/docs/public/images/reference/structured-inputs-removed.png +0 -0
- data/docs/public/images/reference/structured-inputs.png +0 -0
- data/docs/reference/resource/definition.md +110 -0
- data/docs/superpowers/plans/2026-06-02-structured-inputs.md +1061 -0
- data/docs/superpowers/plans/2026-06-02-structured-inputs.md.tasks.json +60 -0
- data/docs/superpowers/specs/2026-06-01-structured-inputs-design.md +191 -0
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/plutonium/definition/base.rb +1 -0
- data/lib/plutonium/definition/structured_inputs.rb +67 -0
- data/lib/plutonium/interaction/README.md +24 -78
- data/lib/plutonium/interaction/base.rb +10 -2
- data/lib/plutonium/resource/controller.rb +6 -1
- data/lib/plutonium/resource/controllers/interactive_actions.rb +10 -6
- data/lib/plutonium/structured_inputs/param_cleaner.rb +36 -0
- data/lib/plutonium/structured_inputs/params_concern.rb +36 -0
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +3 -3
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +178 -0
- data/lib/plutonium/ui/form/concerns/repeater_field_styles.rb +24 -0
- data/lib/plutonium/ui/form/resource.rb +4 -1
- data/lib/plutonium/ui/modal/slideover.rb +9 -3
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +10 -5
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/structured_input_row_controller.js +26 -0
- metadata +14 -5
- data/docs/superpowers/specs/2026-06-01-interaction-repeater-inputs-design.md +0 -178
- data/lib/plutonium/interaction/nested_attributes.rb +0 -93
|
Binary file
|
|
Binary file
|
|
@@ -333,6 +333,116 @@ end
|
|
|
333
333
|
- **`update_only: true` hides the Add button** — for `has_one` and "settings"-style associations.
|
|
334
334
|
- **Custom class names** — use `class_name:` in the model AND `using:` in the definition.
|
|
335
335
|
|
|
336
|
+
## Structured inputs
|
|
337
|
+
|
|
338
|
+
Classless inline fieldsets backed by a JSON/jsonb column. No model associations
|
|
339
|
+
required — the whole sub-form is serialised into a single column as a hash
|
|
340
|
+
(single form) or an array of hashes (repeater).
|
|
341
|
+
|
|
342
|
+

|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
# model
|
|
346
|
+
class Listing < ApplicationRecord
|
|
347
|
+
include Plutonium::Resource::Record
|
|
348
|
+
# columns: address (json), contacts (json)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# definition
|
|
352
|
+
class ListingDefinition < ResourceDefinition
|
|
353
|
+
# single → stored as a hash
|
|
354
|
+
structured_input :address do |f|
|
|
355
|
+
f.input :street
|
|
356
|
+
f.input :city
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# repeater → stored as an array of hashes (max 5 rows)
|
|
360
|
+
structured_input :contacts, repeat: 5 do |f|
|
|
361
|
+
f.input :label
|
|
362
|
+
f.input :phone_number
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Options
|
|
368
|
+
|
|
369
|
+
| Option | Description |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `repeat:` | `true` (default cap of 10) or an integer max-rows cap. Omit for a single-hash form. |
|
|
372
|
+
| `using:` | Another Definition class whose `input` declarations are used as the fieldset. |
|
|
373
|
+
| `fields:` | Subset of fields to take from the `using:` definition. |
|
|
374
|
+
|
|
375
|
+
### Removing rows
|
|
376
|
+
|
|
377
|
+
Each repeater row has a **Remove** button. Removing a row collapses it to a
|
|
378
|
+
compact _Removed — Restore_ bar and disables its inputs, so the browser omits
|
|
379
|
+
them from the submission. The server simply rebuilds the JSON column from the
|
|
380
|
+
rows it receives — there is no `_destroy` marker. **Restore** brings the row
|
|
381
|
+
back before saving.
|
|
382
|
+
|
|
383
|
+

|
|
384
|
+
|
|
385
|
+
### Policy
|
|
386
|
+
|
|
387
|
+
Permit the column name as a plain symbol — Plutonium handles the nested hash
|
|
388
|
+
params automatically:
|
|
389
|
+
|
|
390
|
+
```ruby
|
|
391
|
+
def permitted_attributes_for_create
|
|
392
|
+
super + %i[address contacts]
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
### On interactions
|
|
397
|
+
|
|
398
|
+
`structured_input` is also available on `Plutonium::Interaction::Base`. The
|
|
399
|
+
attribute is declared automatically; `execute` receives the value as a `Hash`
|
|
400
|
+
(single) or `Array<Hash>` (repeater). `nested_input` and
|
|
401
|
+
`accepts_nested_attributes_for` are **not** available on interactions.
|
|
402
|
+
|
|
403
|
+
### Validation
|
|
404
|
+
|
|
405
|
+
::: warning Structured inputs are not validated for you
|
|
406
|
+
The fields are classless render declarations, so there is nothing for Plutonium
|
|
407
|
+
to attach validations to (unlike [`nested_input`](#nested-inputs), whose nested
|
|
408
|
+
records run their own model validations). Whatever the form submits is stored
|
|
409
|
+
as-is, after blank rows are dropped — **no per-field server-side validation**.
|
|
410
|
+
:::
|
|
411
|
+
|
|
412
|
+
Specifically:
|
|
413
|
+
|
|
414
|
+
- **HTML constraints are client-side only.** A field's `required:` and a
|
|
415
|
+
select's `choices:` guide the browser but are **not** enforced on the server —
|
|
416
|
+
an API call or a crafted request can submit anything.
|
|
417
|
+
- **Selects silently drop unknown values.** If a stored value is not among a
|
|
418
|
+
`as: :select` field's `choices:`, the `<select>` renders **blank**, and saving
|
|
419
|
+
the form **overwrites the stored value with `nil`** (the option list is the
|
|
420
|
+
only thing constraining it). This is standard `<select>` behaviour, but it
|
|
421
|
+
bites harder here because JSON values aren't constrained by a DB enum and your
|
|
422
|
+
`choices:` can drift. Keep `choices:` a stable superset, or use a free-text
|
|
423
|
+
input, when values can change over time.
|
|
424
|
+
|
|
425
|
+
To enforce anything, add the validation yourself — it runs server-side:
|
|
426
|
+
|
|
427
|
+
```ruby
|
|
428
|
+
# resource: validate the JSON column on the model
|
|
429
|
+
class Listing < ApplicationRecord
|
|
430
|
+
include Plutonium::Resource::Record
|
|
431
|
+
|
|
432
|
+
validate :contacts_have_labels
|
|
433
|
+
def contacts_have_labels
|
|
434
|
+
Array(contacts).each_with_index do |row, i|
|
|
435
|
+
errors.add(:contacts, "row #{i + 1} needs a label") if row["label"].blank?
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|
|
439
|
+
|
|
440
|
+
# interaction: it's an ActiveModel, validated before `execute`
|
|
441
|
+
validate do
|
|
442
|
+
contacts.each { |c| errors.add(:contacts, "label required") if c[:label].blank? }
|
|
443
|
+
end
|
|
444
|
+
```
|
|
445
|
+
|
|
336
446
|
## File uploads
|
|
337
447
|
|
|
338
448
|
```ruby
|