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.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/skills/plutonium-resource/SKILL.md +14 -8
  3. data/.claude/skills/plutonium-ui/SKILL.md +11 -3
  4. data/CHANGELOG.md +22 -0
  5. data/SECURITY.md +1 -1
  6. data/app/assets/plutonium.css +1 -1
  7. data/app/assets/plutonium.js +56 -3
  8. data/app/assets/plutonium.js.map +4 -4
  9. data/app/assets/plutonium.min.js +32 -32
  10. data/app/assets/plutonium.min.js.map +4 -4
  11. data/config/brakeman.ignore +1 -1
  12. data/config/locales/en/js.yml +4 -0
  13. data/config/locales/en/ui.yml +16 -0
  14. data/docs/reference/resource/definition.md +12 -7
  15. data/docs/reference/resource/query.md +11 -1
  16. data/docs/reference/ui/displays.md +19 -2
  17. data/docs/reference/ui/forms.md +54 -4
  18. data/gemfiles/rails_7.gemfile +1 -0
  19. data/gemfiles/rails_7.gemfile.lock +13 -3
  20. data/gemfiles/rails_8.0.gemfile +1 -0
  21. data/gemfiles/rails_8.0.gemfile.lock +10 -1
  22. data/gemfiles/rails_8.1.gemfile +1 -0
  23. data/gemfiles/rails_8.1.gemfile.lock +10 -1
  24. data/lib/plutonium/auth/rodauth.rb +7 -0
  25. data/lib/plutonium/core/controller.rb +0 -2
  26. data/lib/plutonium/helpers/content_helper.rb +0 -3
  27. data/lib/plutonium/query/filter.rb +2 -1
  28. data/lib/plutonium/query/filters/tags.rb +36 -0
  29. data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
  30. data/lib/plutonium/ui/display/base.rb +50 -2
  31. data/lib/plutonium/ui/display/components/badge.rb +1 -1
  32. data/lib/plutonium/ui/display/components/binary.rb +20 -0
  33. data/lib/plutonium/ui/display/components/boolean.rb +1 -1
  34. data/lib/plutonium/ui/display/components/code.rb +23 -0
  35. data/lib/plutonium/ui/display/components/color.rb +14 -6
  36. data/lib/plutonium/ui/display/components/duration.rb +31 -0
  37. data/lib/plutonium/ui/display/components/file_size.rb +22 -0
  38. data/lib/plutonium/ui/display/components/key_value.rb +37 -0
  39. data/lib/plutonium/ui/display/components/list.rb +33 -0
  40. data/lib/plutonium/ui/display/components/number.rb +32 -0
  41. data/lib/plutonium/ui/display/components/progress.rb +36 -0
  42. data/lib/plutonium/ui/display/components/rating.rb +36 -0
  43. data/lib/plutonium/ui/display/components/relative_time.rb +25 -0
  44. data/lib/plutonium/ui/display/components/rich_text.rb +23 -0
  45. data/lib/plutonium/ui/display/components/tags.rb +18 -0
  46. data/lib/plutonium/ui/display/options/inferred_types.rb +13 -0
  47. data/lib/plutonium/ui/display/theme.rb +24 -2
  48. data/lib/plutonium/ui/form/base.rb +30 -1
  49. data/lib/plutonium/ui/form/components/binary.rb +40 -0
  50. data/lib/plutonium/ui/form/components/key_value_store.rb +22 -71
  51. data/lib/plutonium/ui/form/components/list.rb +46 -0
  52. data/lib/plutonium/ui/form/components/rating.rb +51 -0
  53. data/lib/plutonium/ui/form/components/rich_text.rb +20 -0
  54. data/lib/plutonium/ui/form/components/tags.rb +45 -0
  55. data/lib/plutonium/ui/form/components/tags_filter.rb +40 -0
  56. data/lib/plutonium/ui/form/components/uppy.rb +1 -1
  57. data/lib/plutonium/ui/form/options/inferred_types.rb +16 -15
  58. data/lib/plutonium/ui/form/theme.rb +20 -4
  59. data/lib/plutonium/ui/grid/card.rb +13 -1
  60. data/lib/plutonium/ui/layout/base.rb +10 -1
  61. data/lib/plutonium/ui/options/rich_text_field.rb +18 -0
  62. data/lib/plutonium/ui/options/secret_field.rb +32 -0
  63. data/lib/plutonium/ui/options/tags_field.rb +23 -0
  64. data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
  65. data/lib/plutonium/ui/table/components/view_switcher.rb +4 -2
  66. data/lib/plutonium/ui/table/display_theme.rb +30 -2
  67. data/lib/plutonium/version.rb +1 -1
  68. data/package.json +1 -1
  69. data/src/css/components.css +43 -0
  70. data/src/css/flatpickr.css +91 -0
  71. data/src/js/controllers/easymde_controller.js +1 -0
  72. data/src/js/controllers/register_controllers.js +2 -0
  73. data/src/js/controllers/slim_select_controller.js +32 -3
  74. data/src/js/controllers/timeago_controller.js +40 -0
  75. metadata +25 -2
@@ -120,7 +120,7 @@
120
120
  {
121
121
  "warning_type": "Dangerous Eval",
122
122
  "warning_code": 13,
123
- "fingerprint": "e5f68f8342a094ffd2124c1650428135e0549da39950158725414bfca0bb8f1e",
123
+ "fingerprint": "616e2daa0e04bcadd42f0af563dedff5450d69e0c9a0f4147c528ffbf98e3d8a",
124
124
  "check_name": "Evaluation",
125
125
  "message": "Dynamic string evaluated as code",
126
126
  "file": "lib/plutonium/auth/rodauth.rb",
@@ -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)
@@ -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`, `:tel`, `:password` |
103
- | Rich text | `:markdown` (EasyMDE editor) |
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`, `:radio_buttons`, `:check_boxes` |
108
- | Files | `:file`, `:uppy`, `:attachment` |
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`, `:phone` |
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
- Six built-in filter types. Use the shorthand symbol or the full class name.
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.** No symbol is shown unless you pass `unit:`, a literal string (`"£"`) or a Symbol read off the record for per-row currencies. `has_cents` decimal accessors infer `:currency` automatically (still symbol-less until you set `unit:`).
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.
@@ -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`, `tel_tag`, `hidden_tag` | standard HTML inputs |
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(min_date: Date.today, enable_time: true) }
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.
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rotp"
6
+ gem "no_fly_list", github: "radioactive-labs/no_fly_list", branch: "feat/scoped-tags"
6
7
  gem "rails", "~> 7.2"
7
8
  gem "sqlite3"
8
9
  gem "puma", ">= 5.0"
@@ -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.64.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.64.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
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rotp"
6
+ gem "no_fly_list", github: "radioactive-labs/no_fly_list", branch: "feat/scoped-tags"
6
7
  gem "rails", "~> 8.0.0"
7
8
  gem "sqlite3"
8
9
  gem "puma", ">= 5.0"
@@ -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.62.2)
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)
@@ -3,6 +3,7 @@
3
3
  source "https://rubygems.org"
4
4
 
5
5
  gem "rotp"
6
+ gem "no_fly_list", github: "radioactive-labs/no_fly_list", branch: "feat/scoped-tags"
6
7
  gem "rails", "~> 8.1.0"
7
8
  gem "sqlite3"
8
9
  gem "puma", ">= 5.0"
@@ -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.65.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,9 +10,6 @@ module Plutonium
10
10
  title: content,
11
11
  data: {
12
12
  controller: "timeago",
13
- timeago_refresh_interval_value: 1000,
14
- timeago_include_seconds_value: true,
15
- timeago_add_suffix_value: true,
16
13
  timeago_datetime_value: date.iso8601
17
14
  }
18
15
  )
@@ -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
- class_name = "Plutonium::Query::Filters::#{type.to_s.classify}"
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
- alias_method :rich_text_tag, :markdown_tag
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