plutonium 0.66.0 → 0.67.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-resource/SKILL.md +14 -8
- data/.claude/skills/plutonium-ui/SKILL.md +11 -3
- data/CHANGELOG.md +22 -0
- data/SECURITY.md +1 -1
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +56 -3
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +32 -32
- data/app/assets/plutonium.min.js.map +4 -4
- data/config/brakeman.ignore +1 -1
- data/config/locales/en/js.yml +4 -0
- data/config/locales/en/ui.yml +16 -0
- data/docs/reference/resource/definition.md +12 -7
- data/docs/reference/resource/query.md +11 -1
- data/docs/reference/ui/displays.md +19 -2
- data/docs/reference/ui/forms.md +54 -4
- data/gemfiles/rails_7.gemfile +1 -0
- data/gemfiles/rails_7.gemfile.lock +13 -3
- data/gemfiles/rails_8.0.gemfile +1 -0
- data/gemfiles/rails_8.0.gemfile.lock +10 -1
- data/gemfiles/rails_8.1.gemfile +1 -0
- data/gemfiles/rails_8.1.gemfile.lock +10 -1
- data/lib/plutonium/auth/rodauth.rb +7 -0
- data/lib/plutonium/core/controller.rb +0 -2
- data/lib/plutonium/helpers/content_helper.rb +0 -3
- data/lib/plutonium/query/filter.rb +2 -1
- data/lib/plutonium/query/filters/tags.rb +36 -0
- data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
- data/lib/plutonium/ui/display/base.rb +50 -2
- data/lib/plutonium/ui/display/components/badge.rb +1 -1
- data/lib/plutonium/ui/display/components/binary.rb +20 -0
- data/lib/plutonium/ui/display/components/boolean.rb +1 -1
- data/lib/plutonium/ui/display/components/code.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +14 -6
- data/lib/plutonium/ui/display/components/duration.rb +31 -0
- data/lib/plutonium/ui/display/components/file_size.rb +22 -0
- data/lib/plutonium/ui/display/components/key_value.rb +37 -0
- data/lib/plutonium/ui/display/components/list.rb +33 -0
- data/lib/plutonium/ui/display/components/number.rb +32 -0
- data/lib/plutonium/ui/display/components/progress.rb +36 -0
- data/lib/plutonium/ui/display/components/rating.rb +36 -0
- data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
- data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
- data/lib/plutonium/ui/display/components/tags.rb +18 -0
- data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
- data/lib/plutonium/ui/display/theme.rb +24 -2
- data/lib/plutonium/ui/form/base.rb +30 -1
- data/lib/plutonium/ui/form/components/binary.rb +40 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
- data/lib/plutonium/ui/form/components/list.rb +46 -0
- data/lib/plutonium/ui/form/components/rating.rb +51 -0
- data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
- data/lib/plutonium/ui/form/components/tags.rb +45 -0
- data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
- data/lib/plutonium/ui/form/components/uppy.rb +1 -1
- data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
- data/lib/plutonium/ui/form/theme.rb +20 -4
- data/lib/plutonium/ui/grid/card.rb +13 -1
- data/lib/plutonium/ui/layout/base.rb +10 -1
- data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
- data/lib/plutonium/ui/options/secret_field.rb +32 -0
- data/lib/plutonium/ui/options/tags_field.rb +23 -0
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
- data/lib/plutonium/ui/table/display_theme.rb +30 -2
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/src/css/components.css +43 -0
- data/src/css/flatpickr.css +91 -0
- data/src/js/controllers/easymde_controller.js +1 -0
- data/src/js/controllers/register_controllers.js +2 -0
- data/src/js/controllers/slim_select_controller.js +32 -3
- data/src/js/controllers/timeago_controller.js +40 -0
- metadata +25 -2
data/config/brakeman.ignore
CHANGED
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
{
|
|
121
121
|
"warning_type": "Dangerous Eval",
|
|
122
122
|
"warning_code": 13,
|
|
123
|
-
"fingerprint": "
|
|
123
|
+
"fingerprint": "616e2daa0e04bcadd42f0af563dedff5450d69e0c9a0f4147c528ffbf98e3d8a",
|
|
124
124
|
"check_name": "Evaluation",
|
|
125
125
|
"message": "Dynamic string evaluated as code",
|
|
126
126
|
"file": "lib/plutonium/auth/rodauth.rb",
|
data/config/locales/en/js.yml
CHANGED
|
@@ -13,6 +13,9 @@ en:
|
|
|
13
13
|
copied: "Copied!"
|
|
14
14
|
slim_select:
|
|
15
15
|
search_failed: "Search failed"
|
|
16
|
+
# Chip inputs that accept new values (`as: :list`, tags)
|
|
17
|
+
add_placeholder: "Type to add…"
|
|
18
|
+
add_search_placeholder: "Search or add new"
|
|
16
19
|
dirty_form_guard:
|
|
17
20
|
unsaved_changes: "You have unsaved changes that will be lost. Continue?"
|
|
18
21
|
discard_changes: "Discard your changes?"
|
|
@@ -35,6 +38,7 @@ en:
|
|
|
35
38
|
searchText: "No Results"
|
|
36
39
|
searchPlaceholder: "Search"
|
|
37
40
|
searchingText: "Searching..."
|
|
41
|
+
addableText: 'Press "Enter" to add {value}'
|
|
38
42
|
# flatpickr: `locale: "de"` (the host must load the matching l10n bundle)
|
|
39
43
|
# uppy: `strings: {...}` (Uppy locale strings, merged over the English defaults)
|
|
40
44
|
# intl_tel_input: `{...}` (intl-tel-input `i18n` option)
|
data/config/locales/en/ui.yml
CHANGED
|
@@ -86,8 +86,24 @@ en:
|
|
|
86
86
|
key_placeholder: "Key"
|
|
87
87
|
value_placeholder: "Value"
|
|
88
88
|
add_pair: "+ Add Pair"
|
|
89
|
+
remove_pair: "Remove pair"
|
|
89
90
|
password:
|
|
90
91
|
keep_current_hint: "Leave blank to keep the current value."
|
|
91
92
|
reenter_hint: "Re-enter the new value to save it."
|
|
92
93
|
attachment:
|
|
93
94
|
delete: "Delete"
|
|
95
|
+
rating:
|
|
96
|
+
clear: "Clear"
|
|
97
|
+
binary:
|
|
98
|
+
replace_hint: "Stored: %{size}. Choose a file to replace it."
|
|
99
|
+
display:
|
|
100
|
+
binary: "Binary data (%{size})"
|
|
101
|
+
code:
|
|
102
|
+
copy: "Copy"
|
|
103
|
+
duration:
|
|
104
|
+
days: "%{count}d"
|
|
105
|
+
hours: "%{count}h"
|
|
106
|
+
minutes: "%{count}m"
|
|
107
|
+
seconds: "%{count}s"
|
|
108
|
+
rating:
|
|
109
|
+
label: "%{value} out of %{max}"
|
|
@@ -72,6 +72,7 @@ Plutonium detects, from the model:
|
|
|
72
72
|
| `boolean` | `:boolean` |
|
|
73
73
|
| `date`, `datetime`, `time` | `:date` / `:datetime` / `:time` |
|
|
74
74
|
| `json`, `jsonb` | `:json` |
|
|
75
|
+
| `binary` | `:binary` |
|
|
75
76
|
|
|
76
77
|
Validations on the model inform the UI too: `validates :title, presence: true` → required field; `validates :role, inclusion: { in: [...] }` → select choices.
|
|
77
78
|
|
|
@@ -99,19 +100,20 @@ end
|
|
|
99
100
|
|
|
100
101
|
| Category | Types |
|
|
101
102
|
|---|---|
|
|
102
|
-
| Text | `:string`, `:text`, `:email`, `:url`, `:
|
|
103
|
-
| Rich text | `:
|
|
104
|
-
| Numeric | `:number`, `:integer`, `:decimal`, `:range` |
|
|
103
|
+
| Text | `:string`, `:text`, `:email`, `:url`, `:phone`, `:password` (alias `:secret`) |
|
|
104
|
+
| Rich text | `:rich_text` (Action Text editor: Trix, or Lexxy once installed; **default** for `has_rich_text`), `:markdown` (EasyMDE) |
|
|
105
|
+
| Numeric | `:number`, `:integer`, `:decimal`, `:range`, `:rating` (star picker, `max:`) |
|
|
105
106
|
| Boolean | `:toggle` / `:switch` (switch: **default** for boolean columns), `:boolean` (plain checkbox) |
|
|
106
107
|
| Date/Time | `:date`, `:time`, `:datetime` |
|
|
107
|
-
| Selection | `:select`, `:slim_select`, `:
|
|
108
|
-
|
|
|
108
|
+
| Selection | `:select`, `:slim_select`, `:collection_radio_buttons`, `:collection_checkboxes` |
|
|
109
|
+
| Lists | `:list` (chip input for array values; `suggestions:`, `addable:`, `limit:`), `:tags` (no_fly_list `has_tags` contexts, **auto-detected**) |
|
|
110
|
+
| Files | `:file`, `:uppy`, `:attachment` (Active Storage), `:binary` (bytes in a `binary` column) |
|
|
109
111
|
| Associations | `:association`, `:secure_association`, `:belongs_to`, `:has_many`, `:has_one` |
|
|
110
|
-
| Special | `:hidden`, `:color`, `:
|
|
112
|
+
| Special | `:hidden`, `:color`, `:json`, `:key_value` |
|
|
111
113
|
|
|
112
114
|
### Display types (show / index)
|
|
113
115
|
|
|
114
|
-
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`
|
|
116
|
+
`:string`, `:text`, `:email`, `:url`, `:phone`, `:markdown`, `:number`, `:integer`, `:decimal`, `:boolean`, `:badge`, `:currency`, `:date`, `:time`, `:datetime`, `:association`, `:attachment`, `:color`, `:key_value`, `:binary`, `:list`, `:tags`, `:rich_text`, `:relative_time`, `:code`, `:duration`, `:file_size`, `:progress`, `:rating`
|
|
115
117
|
|
|
116
118
|
#### Auto-inferred display formatting
|
|
117
119
|
|
|
@@ -121,6 +123,9 @@ These render automatically; declare an `as:` only to override or pass options:
|
|
|
121
123
|
|---|---|---|
|
|
122
124
|
| `boolean` | Yes/No pill (`:boolean`) | green "Yes" / neutral "No"; override with `true_label:` / `false_label:` |
|
|
123
125
|
| `enum` | status badge (`:badge`) | known statuses auto-colored; unknown values get a stable decorative color; override per-value with `colors:` |
|
|
126
|
+
| `binary` | size (`:binary`) | `Binary data (12 KB)`, never the bytes |
|
|
127
|
+
| `has_rich_text` | HTML (`:rich_text`) | rendered and sanitized by Action Text |
|
|
128
|
+
| no_fly_list `has_tags` | chips (`:tags`) | declare by context name (`:labels`), not `labels_list` |
|
|
124
129
|
| `has_cents` decimal | currency (`:currency`) | delimited, 2 decimals; symbol from `unit:` on `has_cents` (model-wide) or per-display, else `config.default_currency_unit` / the i18n default (see below) |
|
|
125
130
|
|
|
126
131
|
```ruby
|
|
@@ -82,7 +82,7 @@ For large tables, write an explicit `search` block backed by a trigram or full-t
|
|
|
82
82
|
|
|
83
83
|
## Filters
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
Seven built-in filter types. Use the shorthand symbol or the full class name.
|
|
86
86
|
|
|
87
87
|
`search`, `scope`, `sort_fields`, and `sort_directions` are reserved filter names; they're built-in controls in the `q[<name>]` namespace, so `filter :scope` raises `ArgumentError`.
|
|
88
88
|
|
|
@@ -94,6 +94,7 @@ Six built-in filter types. Use the shorthand symbol or the full class name.
|
|
|
94
94
|
| Date range | `:date_range` | `from`, `to` | `from_label:`, `to_label:` |
|
|
95
95
|
| Select | `:select` | `value` | `choices:`, `multiple:` |
|
|
96
96
|
| Association | `:association` | `value` | `class_name:`, `multiple:` |
|
|
97
|
+
| Tags | `:tags` | `value` | `match:` (`:any` default, or `:all`) |
|
|
97
98
|
|
|
98
99
|
### Text predicates
|
|
99
100
|
|
|
@@ -149,6 +150,15 @@ filter :author, with: :association, class_name: User
|
|
|
149
150
|
filter :tags, with: :association, class_name: Tag, multiple: true
|
|
150
151
|
```
|
|
151
152
|
|
|
153
|
+
### Tags
|
|
154
|
+
|
|
155
|
+
For a [no_fly_list](https://github.com/radioactive-labs/no_fly_list) `has_tags` context. The options are the tags used by records the user can see (the policy scope), so a tenant never sees another tenant's tags.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
filter :labels, with: :tags # records with any chosen tag
|
|
159
|
+
filter :labels, with: :tags, match: :all # records with every chosen tag
|
|
160
|
+
```
|
|
161
|
+
|
|
152
162
|
### Custom filter (lambda)
|
|
153
163
|
|
|
154
164
|
For simple one-offs:
|
|
@@ -78,6 +78,18 @@ Some types render with richer components automatically; you only declare an `as:
|
|
|
78
78
|
| `:badge` | colored status pill | `enum` columns | `colors:` (per-value override) |
|
|
79
79
|
| `:currency` | delimited, 2-decimal money | `has_cents` decimal accessors | `unit:`, `options:` |
|
|
80
80
|
| `:color` | swatch + value | - | - |
|
|
81
|
+
| `:key_value` | two-column list of keys and values; nested hashes/arrays as compact JSON | `hstore` columns | - |
|
|
82
|
+
| `:binary` | `Binary data (12 KB)`, never the bytes | `binary` columns | - |
|
|
83
|
+
| `:number` | delimited, at the column's scale (`1,999.50`) | `float` / `decimal` columns | - |
|
|
84
|
+
| `:rich_text` | Action Text HTML, sanitized | `has_rich_text` fields | - |
|
|
85
|
+
| `:tags` | chips | no_fly_list `has_tags` contexts | - |
|
|
86
|
+
| `:list` | chips | - | - |
|
|
87
|
+
| `:relative_time` | "3 minutes ago", kept live; absolute time on hover | - | - |
|
|
88
|
+
| `:code` | monospace value with a Copy button | - | - |
|
|
89
|
+
| `:duration` | seconds as `2h 15m` | - | - |
|
|
90
|
+
| `:file_size` | bytes as `12 KB` | - | - |
|
|
91
|
+
| `:progress` | bar + percentage | - | `max:` (default 100) |
|
|
92
|
+
| `:rating` | stars | - | `max:` (default 5) |
|
|
81
93
|
|
|
82
94
|
```ruby
|
|
83
95
|
class OrderDefinition < ResourceDefinition
|
|
@@ -85,12 +97,17 @@ class OrderDefinition < ResourceDefinition
|
|
|
85
97
|
display :total, as: :currency, unit: "£"
|
|
86
98
|
display :total, as: :currency, unit: :currency_symbol # Symbol → read off each record
|
|
87
99
|
display :shipped, as: :boolean, true_label: "Sent", false_label: "Pending"
|
|
100
|
+
field :settings, as: :key_value # key/value editor on the form, key/value list on show
|
|
101
|
+
display :last_seen_at, as: :relative_time
|
|
102
|
+
display :api_reference, as: :code
|
|
103
|
+
display :completion, as: :progress, max: 1 # stored as 0..1
|
|
104
|
+
field :score, as: :rating # star picker on the form, stars on show
|
|
88
105
|
end
|
|
89
106
|
```
|
|
90
107
|
|
|
91
108
|
**Badge colors.** Known statuses (`active`, `pending`, `failed`, …) are auto-colored by meaning. Unknown values get a stable decorative color (same value → same color). Override per-value with `colors:`; valid variants: `:neutral`, `:primary`, `:secondary`, `:success`, `:danger`, `:warning`, `:info`, `:accent`.
|
|
92
109
|
|
|
93
|
-
**Currency.**
|
|
110
|
+
**Currency.** The symbol comes from a per-display `unit:` (a literal `"£"`, or a Symbol read off the record for per-row currencies), else the model's `has_cents ... unit:`, else `Plutonium.configuration.default_currency_unit` / the i18n `number.currency.format.unit` (`$` in `en`). Pass `unit: false` for no symbol. `has_cents` decimal accessors infer `:currency` automatically.
|
|
94
111
|
|
|
95
112
|
## Theming
|
|
96
113
|
|
|
@@ -115,7 +132,7 @@ end
|
|
|
115
132
|
|
|
116
133
|
### Theme keys
|
|
117
134
|
|
|
118
|
-
`fields_wrapper`, `fields_inner`, `sections_wrapper`, `section_grid`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `boolean`, `badge`, `currency`, `color`, plus the shared section-chrome keys (`section_wrapper`, `section_header`, `section_summary`, `section_accent`, `section_heading`, `section_description`, `section_caret`, `section_body`).
|
|
135
|
+
`fields_wrapper`, `fields_inner`, `sections_wrapper`, `section_grid`, `label`, `description`, `string`, `text`, `link`, `email`, `phone`, `markdown`, `json`, `key_value` (+ `key_value_key`, `key_value_value`, `key_value_nested`), `list` / `list_item`, `rich_text`, `relative_time`, `code` (+ `code_value`, `code_copy`), `duration`, `file_size`, `progress` (+ `progress_track`, `progress_bar`, `progress_label`), `rating` (+ `rating_filled`, `rating_empty`), `binary`, `boolean`, `badge`, `currency`, `color`, plus the shared section-chrome keys (`section_wrapper`, `section_header`, `section_summary`, `section_accent`, `section_heading`, `section_description`, `section_caret`, `section_body`).
|
|
119
136
|
|
|
120
137
|
::: warning `fields_wrapper` is the card, `fields_inner` is the grid
|
|
121
138
|
`fields_wrapper` is merged into a `Plutonium::UI::Block`, which supplies `pu-card` itself, grid classes put there style the card, not the fields. Override **`fields_inner`** for the unsectioned grid, and **`section_grid`** for the grid inside a [`display_layout`](/reference/resource/definition#display-layout) section.
|
data/docs/reference/ui/forms.md
CHANGED
|
@@ -150,8 +150,8 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
150
150
|
| Tag | Input |
|
|
151
151
|
|---|---|
|
|
152
152
|
| `input_tag` | text (auto-detected type) |
|
|
153
|
-
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `
|
|
154
|
-
| `checkbox_tag`, `select_tag`, `radio_button_tag` | standard |
|
|
153
|
+
| `string_tag`, `text_tag`, `number_tag`, `email_tag`, `password_tag`, `url_tag`, `hidden_tag` | standard HTML inputs |
|
|
154
|
+
| `checkbox_tag`, `select_tag`, `radio_button_tag`, `collection_radio_buttons_tag`, `collection_checkboxes_tag` | standard |
|
|
155
155
|
| `toggle_tag` / `switch_tag` | switch-styled boolean (`as: :toggle` / `:switch`), the **default** for boolean columns; same behavior as a checkbox. Use `checkbox_tag` (`as: :boolean`) for a plain checkbox. |
|
|
156
156
|
|
|
157
157
|
### Plutonium-enhanced tags
|
|
@@ -164,12 +164,17 @@ render field(:title).wrapped(class: "col-span-full") { |f| f.input_tag }
|
|
|
164
164
|
| `phone_tag` / `int_tel_input_tag` | intl-tel-input phone field |
|
|
165
165
|
| `currency_tag` | Money input (number field + optional unit prefix) |
|
|
166
166
|
| `uppy_tag` / `file_tag` | Uppy file upload |
|
|
167
|
+
| `binary_tag` | Plain file input whose bytes go into a `binary` column |
|
|
168
|
+
| `rich_text_tag` | Action Text editor (Trix, or Lexxy once installed) |
|
|
169
|
+
| `list_tag` | Chip input for array values |
|
|
170
|
+
| `tags_tag` | Chip input for a no_fly_list `has_tags` context |
|
|
171
|
+
| `rating_tag` | Star picker (radio buttons, no JS); an optional rating gets a Clear option |
|
|
167
172
|
| `secure_association_tag` | Association with policy-checked options (inline `+` add, typeahead) |
|
|
168
173
|
| `belongs_to_tag` / `has_many_tag` / `has_one_tag` | Association selects |
|
|
169
174
|
| `key_value_store_tag` | Key/value pairs editor |
|
|
170
175
|
|
|
171
176
|
```ruby
|
|
172
|
-
render field(:published_at).wrapped { |f| f.flatpickr_tag
|
|
177
|
+
render field(:published_at).wrapped { |f| f.flatpickr_tag }
|
|
173
178
|
|
|
174
179
|
render field(:avatar).wrapped do |f|
|
|
175
180
|
f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes)
|
|
@@ -237,21 +242,66 @@ The sentinel is guarded client-side by the `password-sentinel` Stimulus controll
|
|
|
237
242
|
- ends with `_password`, `_digest`, `_hash`, `_token`, `_key`, or `_salt`;
|
|
238
243
|
- contains `secret`.
|
|
239
244
|
|
|
245
|
+
The same names are masked everywhere the value would otherwise print: the show page, table cells, the wizard summary, and grid and kanban card slots all render `••••••••` instead of the value. A `field`/`display` `as:` overrides the heuristic on those surfaces too.
|
|
246
|
+
|
|
240
247
|
This is a naming convenience, **not** a security guarantee, tune it per field:
|
|
241
248
|
|
|
242
249
|
```ruby
|
|
243
250
|
# Opt OUT: render the value as a normal, readable text input
|
|
244
|
-
field :api_token, as: :string # a token the admin needs to copy
|
|
251
|
+
field :api_token, as: :string # a token the admin needs to copy (shown on the show page and cards too)
|
|
245
252
|
field :content_hash, as: :string # a checksum, not a secret
|
|
246
253
|
field :public_key, as: :string # *_key matches, but a public key is not secret
|
|
247
254
|
|
|
248
255
|
# Opt IN: mask a secret the heuristic still misses (e.g. no telltale name)
|
|
249
256
|
field :recovery_phrase, as: :password
|
|
257
|
+
field :pin, as: :secret # alias of :password
|
|
250
258
|
```
|
|
251
259
|
|
|
252
260
|
> [!WARNING]
|
|
253
261
|
> The heuristic is name-based and best-effort. A secret column with an unconventional name (e.g. `recovery_phrase`, `pin`) still renders its value into the page unless you set `as: :password`. Audit secret-bearing columns explicitly.
|
|
254
262
|
|
|
263
|
+
### Rich text (Action Text and Lexxy) {#rich-text-fields}
|
|
264
|
+
|
|
265
|
+
A `has_rich_text` field renders Action Text's own editor tag, so it gets whichever editor the app has set up: Trix by default, or [Lexxy](https://lexxy.dev) once the `lexxy` gem is in the bundle (it takes over Action Text's helpers on Rails 8.0/8.1 and registers itself as the editor on 8.2). Lexxy needs Rails 8.0.2 or later. The editor's JavaScript and CSS come from the app's bundle, as Action Text's or Lexxy's install instructions describe. The show page renders the HTML through Action Text, which sanitizes it.
|
|
266
|
+
|
|
267
|
+
### Lists and tags {#list-fields}
|
|
268
|
+
|
|
269
|
+
`as: :list` edits an array value (a JSON or array column) as chips. Options go on `input`:
|
|
270
|
+
|
|
271
|
+
```ruby
|
|
272
|
+
field :nicknames, as: :list
|
|
273
|
+
input :nicknames, suggestions: %w[Basin Tub], limit: 5 # addable: false allows only suggestions
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Removing every chip saves an empty array.
|
|
277
|
+
|
|
278
|
+
[no_fly_list](https://github.com/radioactive-labs/no_fly_list) `has_tags` contexts are detected automatically. Permit them by context name (`labels`, not `labels_list`) and they render as chips on the form, the show page and the table:
|
|
279
|
+
|
|
280
|
+
```ruby
|
|
281
|
+
class Article < ResourceRecord
|
|
282
|
+
include NoFlyList::TaggableRecord
|
|
283
|
+
has_tags :labels, scope: :organization # per-tenant vocabulary
|
|
284
|
+
has_tags :topics, restrict_to_existing: true, limit: 3
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# policy
|
|
288
|
+
def permitted_attributes_for_create = %i[title labels topics]
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
- The input suggests tags that already exist in the record's tag scope.
|
|
292
|
+
- `restrict_to_existing` only allows picking existing tags. Plutonium doesn't decide where that vocabulary is managed: create the tags however suits the app (register the tag model as a resource, seed them, or add them in an interaction).
|
|
293
|
+
- `limit` caps how many tags can be chosen.
|
|
294
|
+
- Add `filter :labels, with: :tags` for a table filter.
|
|
295
|
+
- The table reads tags through the context's association, so preload it (`includes(:labels)`) to avoid a query per row.
|
|
296
|
+
|
|
297
|
+
### Binary columns {#binary-fields}
|
|
298
|
+
|
|
299
|
+
A `binary` column renders `binary_tag`: a plain file input. The uploaded file's bytes are written to the column, and the show page and table print its size (`Binary data (12 KB)`), never the bytes.
|
|
300
|
+
|
|
301
|
+
- Saving without choosing a file keeps the stored bytes. There is no control to clear the column.
|
|
302
|
+
- Browsers can't refill a file input, so after a failed validation the user has to choose the file again.
|
|
303
|
+
- The whole file is read into memory and stored in the row. Use Active Storage (`has_one_attached` + `:uppy`) for anything that isn't small.
|
|
304
|
+
|
|
255
305
|
### Wrapped vs unwrapped
|
|
256
306
|
|
|
257
307
|
- `wrapped`: includes label, hint, and error rendering. Use for normal form fields.
|
data/gemfiles/rails_7.gemfile
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: https://github.com/radioactive-labs/no_fly_list.git
|
|
3
|
+
revision: e1e88d9c7d898ef269e3cabfa12da2fb327459cc
|
|
4
|
+
branch: feat/scoped-tags
|
|
5
|
+
specs:
|
|
6
|
+
no_fly_list (0.7.4)
|
|
7
|
+
activerecord (>= 7.2)
|
|
8
|
+
|
|
1
9
|
PATH
|
|
2
10
|
remote: ..
|
|
3
11
|
specs:
|
|
4
|
-
plutonium (0.
|
|
12
|
+
plutonium (0.66.0)
|
|
5
13
|
action_policy (~> 0.7.0)
|
|
6
14
|
csv
|
|
7
15
|
listen (~> 3.8)
|
|
@@ -490,6 +498,7 @@ DEPENDENCIES
|
|
|
490
498
|
importmap-rails
|
|
491
499
|
minitest
|
|
492
500
|
minitest-reporters
|
|
501
|
+
no_fly_list!
|
|
493
502
|
plutonium!
|
|
494
503
|
propshaft
|
|
495
504
|
puma (>= 5.0)
|
|
@@ -521,7 +530,7 @@ CHECKSUMS
|
|
|
521
530
|
activerecord (7.2.3.2) sha256=e62d24ba7e5f820d24ed122eb9225491b0aaa8398778783b400067c3467285c7
|
|
522
531
|
activestorage (7.2.3.2) sha256=863cb4b21b83505555be2addb939fefb856ab05f6c6d24bab00d6f1a7e4edbfc
|
|
523
532
|
activesupport (7.2.3.2) sha256=ddc90d11dd88086d78ace03407fe3c2a3f5c8853c3c3046313db5ed823ef3312
|
|
524
|
-
acts_as_list (1.2.6)
|
|
533
|
+
acts_as_list (1.2.6) sha256=8345380900b7bee620c07ad00991ccee59af3d8c9e8574f426e321da2865fdc8
|
|
525
534
|
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
|
|
526
535
|
ansi (1.6.0) sha256=ac9ea0c0ea8d32fb4e271348e609963ac78882f34b73836c2a02b3622e666658
|
|
527
536
|
appraisal (2.5.0) sha256=36989221be127913b0dba8d114da2001e6b2dceea7bd4951200eaba764eed3ce
|
|
@@ -580,6 +589,7 @@ CHECKSUMS
|
|
|
580
589
|
net-protocol (0.2.2) sha256=aa73e0cba6a125369de9837b8d8ef82a61849360eba0521900e2c3713aa162a8
|
|
581
590
|
net-smtp (0.5.1) sha256=ed96a0af63c524fceb4b29b0d352195c30d82dd916a42f03c62a3a70e5b70736
|
|
582
591
|
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
|
|
592
|
+
no_fly_list (0.7.4)
|
|
583
593
|
nokogiri (1.19.4-aarch64-linux-gnu) sha256=1269fb644a6de405057a53dd5c762b1209b43ca7424f839454d3dbc677c31a8f
|
|
584
594
|
nokogiri (1.19.4-aarch64-linux-musl) sha256=35c65b9ce72b3bb03207bdbe7067915019dc18c1b9b59139684bd6690fdd01af
|
|
585
595
|
nokogiri (1.19.4-arm-linux-gnu) sha256=a301313e38bb065d68239e79734bcd6f56fb6efaacebde29e9abf2a4735340ca
|
|
@@ -601,7 +611,7 @@ CHECKSUMS
|
|
|
601
611
|
phlexi-form (0.14.3) sha256=47d69c4a49ba22279ba541b9b110e81d62acc4602513f21bb5c3d29fa9c8d5ca
|
|
602
612
|
phlexi-menu (0.4.1) sha256=f6eeb1eb9597caadeee33a53f1520b3613a6ac616cf1e395e9d734a412895fb2
|
|
603
613
|
phlexi-table (0.2.0) sha256=73bee135adbe73fcdb6fdd2672444373556dd9e6bba61e7b5b20ca8b7a1f822c
|
|
604
|
-
plutonium (0.
|
|
614
|
+
plutonium (0.66.0)
|
|
605
615
|
pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
|
|
606
616
|
prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
|
|
607
617
|
prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
|
data/gemfiles/rails_8.0.gemfile
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: https://github.com/radioactive-labs/no_fly_list.git
|
|
3
|
+
revision: e1e88d9c7d898ef269e3cabfa12da2fb327459cc
|
|
4
|
+
branch: feat/scoped-tags
|
|
5
|
+
specs:
|
|
6
|
+
no_fly_list (0.7.4)
|
|
7
|
+
activerecord (>= 7.2)
|
|
8
|
+
|
|
1
9
|
PATH
|
|
2
10
|
remote: ..
|
|
3
11
|
specs:
|
|
4
|
-
plutonium (0.
|
|
12
|
+
plutonium (0.66.0)
|
|
5
13
|
action_policy (~> 0.7.0)
|
|
6
14
|
csv
|
|
7
15
|
listen (~> 3.8)
|
|
@@ -457,6 +465,7 @@ DEPENDENCIES
|
|
|
457
465
|
importmap-rails
|
|
458
466
|
minitest
|
|
459
467
|
minitest-reporters
|
|
468
|
+
no_fly_list!
|
|
460
469
|
plutonium!
|
|
461
470
|
propshaft
|
|
462
471
|
puma (>= 5.0)
|
data/gemfiles/rails_8.1.gemfile
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
|
+
GIT
|
|
2
|
+
remote: https://github.com/radioactive-labs/no_fly_list.git
|
|
3
|
+
revision: e1e88d9c7d898ef269e3cabfa12da2fb327459cc
|
|
4
|
+
branch: feat/scoped-tags
|
|
5
|
+
specs:
|
|
6
|
+
no_fly_list (0.7.4)
|
|
7
|
+
activerecord (>= 7.2)
|
|
8
|
+
|
|
1
9
|
PATH
|
|
2
10
|
remote: ..
|
|
3
11
|
specs:
|
|
4
|
-
plutonium (0.
|
|
12
|
+
plutonium (0.66.0)
|
|
5
13
|
action_policy (~> 0.7.0)
|
|
6
14
|
csv
|
|
7
15
|
listen (~> 3.8)
|
|
@@ -459,6 +467,7 @@ DEPENDENCIES
|
|
|
459
467
|
importmap-rails
|
|
460
468
|
minitest
|
|
461
469
|
minitest-reporters
|
|
470
|
+
no_fly_list!
|
|
462
471
|
plutonium!
|
|
463
472
|
propshaft
|
|
464
473
|
puma (>= 5.0)
|
|
@@ -24,6 +24,13 @@ module Plutonium
|
|
|
24
24
|
rodauth.rails_account
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# A request Rodauth authenticated with a valid JWT reads its session
|
|
28
|
+
# from the token, not the cookie, so there is nothing for a forged
|
|
29
|
+
# request to ride on. Everything else gets the CSRF token check.
|
|
30
|
+
def verified_request?
|
|
31
|
+
super || (rodauth.features.include?(:jwt) && rodauth.valid_jwt?)
|
|
32
|
+
end
|
|
33
|
+
|
|
27
34
|
def logout_url
|
|
28
35
|
rodauth.logout_path
|
|
29
36
|
end
|
|
@@ -10,8 +10,6 @@ module Plutonium
|
|
|
10
10
|
included do
|
|
11
11
|
add_flash_types :success, :warning, :error
|
|
12
12
|
|
|
13
|
-
protect_from_forgery with: :null_session, if: -> { request.headers["Authorization"].present? }
|
|
14
|
-
|
|
15
13
|
rescue_from ::ActionPolicy::Unauthorized do |exception|
|
|
16
14
|
respond_to do |format|
|
|
17
15
|
format.any(:html, :turbo_stream) do
|
|
@@ -10,7 +10,8 @@ module Plutonium
|
|
|
10
10
|
def lookup(type)
|
|
11
11
|
return type if type.is_a?(Class) && type < Filter
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
# camelize, not classify: classify singularizes (`:tags` -> `Tag`).
|
|
14
|
+
class_name = "Plutonium::Query::Filters::#{type.to_s.camelize}"
|
|
14
15
|
class_name.constantize
|
|
15
16
|
rescue NameError
|
|
16
17
|
raise ArgumentError, "Unknown filter type: #{type}. Expected #{class_name} to exist."
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# Filter by a no_fly_list `has_tags` context, through the gem's own
|
|
5
|
+
# query scopes.
|
|
6
|
+
#
|
|
7
|
+
# @example Records with any of the chosen tags
|
|
8
|
+
# filter :labels, with: :tags
|
|
9
|
+
#
|
|
10
|
+
# @example Records with every chosen tag
|
|
11
|
+
# filter :labels, with: :tags, match: :all
|
|
12
|
+
#
|
|
13
|
+
class Tags < Filter
|
|
14
|
+
def initialize(match: :any, **)
|
|
15
|
+
super(**)
|
|
16
|
+
@match = match
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def apply(scope, value:)
|
|
20
|
+
tags = Array(value).compact_blank
|
|
21
|
+
return scope if tags.empty?
|
|
22
|
+
|
|
23
|
+
scope.public_send(:"with_#{@match}_#{key}", tags)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def humanize_value(value)
|
|
27
|
+
Array(value).compact_blank.join(", ")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def customize_inputs
|
|
31
|
+
input :value, as: Plutonium::UI::Form::Components::TagsFilter, context: key
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -34,9 +34,22 @@ module Plutonium
|
|
|
34
34
|
tag ||= inferred_field_component
|
|
35
35
|
return create_component(tag, component_theme_key(tag), **attributes, &) if tag.is_a?(Class)
|
|
36
36
|
|
|
37
|
+
# `field :x, as:` reaches every surface, but some aliases exist on
|
|
38
|
+
# only one (`:toggle` is form-only, `:badge` display-only). Those
|
|
39
|
+
# surfaces render what they would have inferred instead of raising.
|
|
40
|
+
# A tag NO surface defines is a typo and still raises below.
|
|
41
|
+
tag = inferred_field_component if !respond_to?(:"#{tag}_tag") && ResolvesTags.surface_tag?(tag)
|
|
42
|
+
|
|
37
43
|
send(:"#{tag}_tag", **attributes, &)
|
|
38
44
|
end
|
|
39
45
|
|
|
46
|
+
# Whether any field surface (form or display; the table renders through
|
|
47
|
+
# a display builder) defines `tag`.
|
|
48
|
+
def self.surface_tag?(tag)
|
|
49
|
+
[Plutonium::UI::Form::Base::Builder, Plutonium::UI::Display::Base::Builder]
|
|
50
|
+
.any? { |builder| builder.method_defined?(:"#{tag}_tag") }
|
|
51
|
+
end
|
|
52
|
+
|
|
40
53
|
private
|
|
41
54
|
|
|
42
55
|
# Theme key for a directly-rendered component class:
|
|
@@ -17,7 +17,10 @@ module Plutonium
|
|
|
17
17
|
def markdown_tag(**, &)
|
|
18
18
|
create_component(Plutonium::UI::Display::Components::Markdown, :markdown, **, &)
|
|
19
19
|
end
|
|
20
|
-
|
|
20
|
+
|
|
21
|
+
def rich_text_tag(**, &)
|
|
22
|
+
create_component(Plutonium::UI::Display::Components::RichText, :rich_text, **, &)
|
|
23
|
+
end
|
|
21
24
|
|
|
22
25
|
def attachment_tag(**, &)
|
|
23
26
|
create_component(Plutonium::UI::Display::Components::Attachment, :attachment, **, &)
|
|
@@ -51,12 +54,57 @@ module Plutonium
|
|
|
51
54
|
create_component(Plutonium::UI::Display::Components::Currency, :currency, **, &)
|
|
52
55
|
end
|
|
53
56
|
|
|
57
|
+
def number_tag(**, &)
|
|
58
|
+
create_component(Plutonium::UI::Display::Components::Number, :number, **, &)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def key_value_tag(**, &)
|
|
62
|
+
create_component(Plutonium::UI::Display::Components::KeyValue, :key_value, **, &)
|
|
63
|
+
end
|
|
64
|
+
alias_method :hstore_tag, :key_value_tag
|
|
65
|
+
|
|
66
|
+
def list_tag(**, &)
|
|
67
|
+
create_component(Plutonium::UI::Display::Components::List, :list, **, &)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def tags_tag(**, &)
|
|
71
|
+
create_component(Plutonium::UI::Display::Components::Tags, :list, **, &)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def relative_time_tag(**, &)
|
|
75
|
+
create_component(Plutonium::UI::Display::Components::RelativeTime, :relative_time, **, &)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def code_tag(**, &)
|
|
79
|
+
create_component(Plutonium::UI::Display::Components::Code, :code, **, &)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def duration_tag(**, &)
|
|
83
|
+
create_component(Plutonium::UI::Display::Components::Duration, :duration, **, &)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def file_size_tag(**, &)
|
|
87
|
+
create_component(Plutonium::UI::Display::Components::FileSize, :file_size, **, &)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def progress_tag(**, &)
|
|
91
|
+
create_component(Plutonium::UI::Display::Components::Progress, :progress, **, &)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def rating_tag(**, &)
|
|
95
|
+
create_component(Plutonium::UI::Display::Components::Rating, :rating, **, &)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def binary_tag(**, &)
|
|
99
|
+
create_component(Plutonium::UI::Display::Components::Binary, :binary, **, &)
|
|
100
|
+
end
|
|
101
|
+
|
|
54
102
|
# Type aliases for common column types
|
|
55
103
|
alias_method :float_tag, :number_tag
|
|
56
104
|
alias_method :decimal_tag, :number_tag
|
|
57
105
|
alias_method :jsonb_tag, :json_tag
|
|
58
|
-
alias_method :key_value_tag, :hstore_tag
|
|
59
106
|
alias_method :phlexi_tag, :phlexi_render_tag
|
|
107
|
+
alias_method :secret_tag, :password_tag
|
|
60
108
|
end
|
|
61
109
|
|
|
62
110
|
private
|
|
@@ -61,7 +61,7 @@ module Plutonium
|
|
|
61
61
|
|
|
62
62
|
def render_value(value)
|
|
63
63
|
variant = self.class.variant_for(value, colors: @colors)
|
|
64
|
-
span(**attributes, class: tokens("pu-badge", "pu-badge-#{variant}")) do
|
|
64
|
+
span(**mix(attributes, {class: tokens("pu-badge", "pu-badge-#{variant}")})) do
|
|
65
65
|
plain value_label(value)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A `binary` column's size, never its bytes.
|
|
8
|
+
class Binary < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
span(**attributes) do
|
|
13
|
+
Plutonium::Translation.t("plutonium.ui.display.binary", size: ActiveSupport::NumberHelper.number_to_human_size(value.bytesize))
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -19,7 +19,7 @@ module Plutonium
|
|
|
19
19
|
private
|
|
20
20
|
|
|
21
21
|
def pill(label:, variant:, icon:)
|
|
22
|
-
span(**attributes, class: tokens("pu-badge", variant), "aria-label": label) do
|
|
22
|
+
span(**mix(attributes, {class: tokens("pu-badge", variant)}), "aria-label": label) do
|
|
23
23
|
render icon.new(class: "w-3.5 h-3.5")
|
|
24
24
|
plain label
|
|
25
25
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A value in monospace with a copy button: IDs, keys, references.
|
|
8
|
+
class Code < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
div(**attributes, data: {controller: "clipboard"}) do
|
|
13
|
+
code(class: themed(:code_value), data: {clipboard_target: "source"}) { value }
|
|
14
|
+
button(type: :button, class: themed(:code_copy), data: {action: "clipboard#copy"}) do
|
|
15
|
+
Plutonium::Translation.t("plutonium.ui.display.code.copy")
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|