plutonium 0.66.0 → 0.68.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-dashboard/SKILL.md +17 -2
- data/.claude/skills/plutonium-resource/SKILL.md +17 -10
- data/.claude/skills/plutonium-ui/SKILL.md +13 -3
- data/CHANGELOG.md +35 -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/dashboard.yml +1 -0
- data/config/locales/en/js.yml +4 -0
- data/config/locales/en/ui.yml +19 -0
- data/docs/reference/dashboard/dsl.md +54 -3
- data/docs/reference/resource/definition.md +14 -7
- data/docs/reference/resource/query.md +11 -1
- data/docs/reference/ui/displays.md +19 -2
- data/docs/reference/ui/forms.md +77 -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/dashboard/base.rb +80 -1
- data/lib/plutonium/dashboard/card.rb +12 -13
- data/lib/plutonium/dashboard/controller.rb +28 -10
- data/lib/plutonium/dashboard/dsl.rb +59 -2
- data/lib/plutonium/dashboard/evaluation.rb +30 -0
- data/lib/plutonium/dashboard/filter.rb +114 -0
- data/lib/plutonium/dashboard/link.rb +35 -0
- data/lib/plutonium/dashboard/route_resolution.rb +6 -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/resource/query_object.rb +6 -0
- data/lib/plutonium/routing/dashboard_registration.rb +5 -0
- data/lib/plutonium/ui/avatar.rb +2 -2
- data/lib/plutonium/ui/component/resolves_tags.rb +13 -0
- data/lib/plutonium/ui/dashboard/board.rb +4 -1
- data/lib/plutonium/ui/dashboard/frame.rb +1 -1
- data/lib/plutonium/ui/dashboard/metric.rb +18 -5
- data/lib/plutonium/ui/dashboard/toolbar.rb +76 -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 +50 -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/secure_association.rb +31 -8
- data/lib/plutonium/ui/form/components/secure_association_checkboxes.rb +30 -0
- data/lib/plutonium/ui/form/components/secure_association_choices.rb +123 -0
- data/lib/plutonium/ui/form/components/secure_association_radio_buttons.rb +45 -0
- data/lib/plutonium/ui/form/components/secure_association_toggles.rb +18 -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 +34 -4
- data/lib/plutonium/ui/grid/card.rb +25 -5
- 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/page/dashboard.rb +1 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_pills.rb +1 -1
- 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/plutonium.gemspec +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 +35 -4
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,27 @@ 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
|
+
association_choices:
|
|
98
|
+
none: "None"
|
|
99
|
+
empty: "No options available"
|
|
100
|
+
binary:
|
|
101
|
+
replace_hint: "Stored: %{size}. Choose a file to replace it."
|
|
102
|
+
display:
|
|
103
|
+
binary: "Binary data (%{size})"
|
|
104
|
+
code:
|
|
105
|
+
copy: "Copy"
|
|
106
|
+
duration:
|
|
107
|
+
days: "%{count}d"
|
|
108
|
+
hours: "%{count}h"
|
|
109
|
+
minutes: "%{count}m"
|
|
110
|
+
seconds: "%{count}s"
|
|
111
|
+
rating:
|
|
112
|
+
label: "%{value} out of %{max}"
|
|
@@ -30,6 +30,49 @@ Default reload interval for every lazy card. `nil` (the default) disables it.
|
|
|
30
30
|
|
|
31
31
|
Page width, one of `:sm`, `:md`, `:lg`, `:xl`, `:full`. Default `:full`.
|
|
32
32
|
|
|
33
|
+
### filter(key, choices: nil, scopes: nil, default: nil, label: nil)
|
|
34
|
+
|
|
35
|
+
A page-level control every card reads, such as a period or a region.
|
|
36
|
+
|
|
37
|
+
```ruby
|
|
38
|
+
filter :period, choices: {"7" => "Last 7 days", "30" => "Last 30 days", "90" => "Last 90 days"}, default: "30"
|
|
39
|
+
|
|
40
|
+
metric(:orders) { orders.where(created_at: filter_value(:period).to_i.days.ago..).count }
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `choices:` maps each value to its label (a string or a lazy `t` proc), in display order. `default:` must be one of the values and is the first when omitted.
|
|
44
|
+
- The key cannot be one the card route or `url_for` uses (`card`, `format`, `anchor`, `host`, `use_route`, ...), which raises when declared, or a path segment of the dashboard's route (the entity param on a path-scoped portal, a `(:locale)` scope), which raises on the request.
|
|
45
|
+
- The value comes from the query string under the filter's key. A value that is not one of the choices reads as the default.
|
|
46
|
+
- Every lazy card's frame URL carries every filter's value, so a card's own request reads what the page read.
|
|
47
|
+
- The page renders each filter as a segmented row of links above the cards, so a choice is a URL that survives a reload and can be shared. Other query parameters on the page are kept.
|
|
48
|
+
- A card's `href:` is not rewritten with the filter values. Use a scope filter (below) to link a card to the matching list.
|
|
49
|
+
|
|
50
|
+
#### Scope filters
|
|
51
|
+
|
|
52
|
+
`scopes:` takes a resource instead of `choices:`. The choices are "All" plus the scopes the resource's definition declares, labelled and gated (`condition:`) the way the index's scope pills are. The default is `default:`, else the definition's `default_scope`, else "All". A default whose `condition:` hides it from the current user reads as "All" for that user.
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
filter :status, scopes: Post
|
|
56
|
+
|
|
57
|
+
metric(:posts, href: -> { filtered_url(Post) }) { filtered(Post).count }
|
|
58
|
+
chart(:by_author) { filtered(Post).group(:author_name).count }
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- `filtered(Post)` is `authorized_resource_scope(Post)` with the chosen scope applied through the definition's own scope, the same code the index runs, so the card's number matches the list.
|
|
62
|
+
- `filtered_url(Post)` is the Post index on that scope (`q[scope]=...`). With "All" chosen and a `default_scope` on the definition, it sends an explicit blank scope so the index shows everything.
|
|
63
|
+
- Without a scope filter for the resource, `filtered` is the authorized scope as is and `filtered_url` the plain index.
|
|
64
|
+
- A scope's block runs against the dashboard's controller. `current_user`, `current_scoped_entity` and the portal's helpers work there; resource-controller helpers such as `current_parent` do not.
|
|
65
|
+
- One scope filter per resource; a second raises when declared. A `default:` that is not one of the definition's scopes, or a definition scope named `:all` (the "All" choice's value), raises on the request.
|
|
66
|
+
|
|
67
|
+
### link(key, href:, label: nil, icon: nil, condition: nil)
|
|
68
|
+
|
|
69
|
+
A button in the toolbar above the cards, such as the dashboard's settings or a full report. `href:` and `condition:` take the same forms as a card's and are evaluated on the dashboard instance. A link whose `href:` resolves to nil is left out.
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
link :settings, icon: Phlex::TablerIcons::Settings, href: -> { resource_url_for(Setting) },
|
|
73
|
+
condition: -> { allowed_to?(:update?, Setting) }
|
|
74
|
+
```
|
|
75
|
+
|
|
33
76
|
## Cards
|
|
34
77
|
|
|
35
78
|
```ruby
|
|
@@ -70,8 +113,8 @@ The block returns a value or a hash.
|
|
|
70
113
|
| Hash key | Meaning |
|
|
71
114
|
|---|---|
|
|
72
115
|
| `value` | The number or string. `nil` renders `plutonium.dashboard.metric.empty` |
|
|
73
|
-
| `previous` | Computes `change`
|
|
74
|
-
| `change` |
|
|
116
|
+
| `previous` | Computes `change` from this value: a percentage of it, or the difference with `change_unit: :points`. A zero previous yields no percentage (a neutral dash when there is a caption); with `:points` the difference still shows. The change is rounded to one decimal before its trend is read |
|
|
117
|
+
| `change` | A number (`2.5` → `+2.5%`, or `+2.5 pts` with `change_unit: :points`) or a string shown as-is |
|
|
75
118
|
| `trend` | `:up`, `:down`, `:flat`; inferred from the sign of `change` |
|
|
76
119
|
| `change_label` | Caption after the change |
|
|
77
120
|
|
|
@@ -83,6 +126,7 @@ The block returns a value or a hash.
|
|
|
83
126
|
| `prefix:` / `suffix:` | String | Wrapped around the formatted value |
|
|
84
127
|
| `positive:` | `:up` (default), `:down` | Which direction is good; drives the change colour |
|
|
85
128
|
| `change_label:` | String | Same as the hash key |
|
|
129
|
+
| `change_unit:` | `:percent` (default), `:points` | How the change reads. Use `:points` for a value that is itself a percentage: 50% to 55.6% reads `+5.6 pts`, not `+11.2%` |
|
|
86
130
|
|
|
87
131
|
### chart
|
|
88
132
|
|
|
@@ -106,6 +150,11 @@ No kind-specific options. The block is `instance_exec`ed in `Plutonium::UI::Dash
|
|
|
106
150
|
|---|---|
|
|
107
151
|
| `authorize?` | Override to gate the page and every card. Default `true` |
|
|
108
152
|
| `visible_cards` | Cards whose `condition:` passes |
|
|
153
|
+
| `filter_value(key)` | The filter's value for this request, or its default |
|
|
154
|
+
| `filter_values` | `{key => value}` for every filter; what each card's frame URL carries |
|
|
155
|
+
| `filtered(resource_class)` | The authorized scope with the resource's scope filter applied |
|
|
156
|
+
| `filtered_url(resource_class)` | The resource's index on the scope that filter has chosen |
|
|
157
|
+
| `visible_links` | Toolbar links whose `condition:` passes |
|
|
109
158
|
| `visible_card!(key)` | The card, or `Plutonium::Dashboard::UnknownCardError` (404) |
|
|
110
159
|
| `refresh_for(card)` | The card's interval, else the dashboard's; `nil` for a card declared `refresh: false` |
|
|
111
160
|
| `view_context` / `helpers` | The Rails view context |
|
|
@@ -117,7 +166,9 @@ No kind-specific options. The block is `instance_exec`ed in `Plutonium::UI::Dash
|
|
|
117
166
|
|---|---|
|
|
118
167
|
| `cards` | All declared cards, in order |
|
|
119
168
|
| `find_card(key)` / `find_card!(key)` | Lookup by key |
|
|
169
|
+
| `filters` / `find_filter(key)` | Declared filters |
|
|
170
|
+
| `links` | Declared toolbar links |
|
|
120
171
|
| `label`, `description`, `icon` | Resolved presentation |
|
|
121
172
|
| `i18n_key`, `route_name` | `admin_portal/sales`, `sales` |
|
|
122
173
|
|
|
123
|
-
Subclasses inherit the parent's cards and options by copy, so a portal-specific subclass can add cards without touching the parent.
|
|
174
|
+
Subclasses inherit the parent's cards, filters, links and options by copy, so a portal-specific subclass can add cards without touching the parent.
|
|
@@ -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
|
|
@@ -1006,6 +1011,8 @@ end
|
|
|
1006
1011
|
|
|
1007
1012
|
Grid slots, `:image`, `:header`, `:subheader`, `:body`, `:meta`, `:footer`, are all optional. `:meta` accepts an array; the rest are single fields. Slots pointing at policy-blocked fields collapse silently.
|
|
1008
1013
|
|
|
1014
|
+
In the `:media` layout a record whose image does not resolve keeps the cover's space, with a neutral icon, so cards in a row stay one height. An image may be an attachment, an `http` or root-relative URL, or a `data:image/` URI.
|
|
1015
|
+
|
|
1009
1016
|
Only declare `index_views` explicitly to **disable** one (e.g. `index_views :grid` to drop the table view).
|
|
1010
1017
|
|
|
1011
1018
|
## Custom page classes
|
|
@@ -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,19 @@ 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 |
|
|
174
|
+
| `association_checkboxes_tag` / `association_toggles_tag` | has_many / habtm as a list of checkboxes or switches (see [Association inputs](#association-inputs)) |
|
|
175
|
+
| `association_radio_buttons_tag` | belongs_to / has_one as a radio group |
|
|
169
176
|
| `key_value_store_tag` | Key/value pairs editor |
|
|
170
177
|
|
|
171
178
|
```ruby
|
|
172
|
-
render field(:published_at).wrapped { |f| f.flatpickr_tag
|
|
179
|
+
render field(:published_at).wrapped { |f| f.flatpickr_tag }
|
|
173
180
|
|
|
174
181
|
render field(:avatar).wrapped do |f|
|
|
175
182
|
f.uppy_tag(allowed_file_types: %w[.jpg .png], max_file_size: 5.megabytes)
|
|
@@ -237,21 +244,66 @@ The sentinel is guarded client-side by the `password-sentinel` Stimulus controll
|
|
|
237
244
|
- ends with `_password`, `_digest`, `_hash`, `_token`, `_key`, or `_salt`;
|
|
238
245
|
- contains `secret`.
|
|
239
246
|
|
|
247
|
+
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.
|
|
248
|
+
|
|
240
249
|
This is a naming convenience, **not** a security guarantee, tune it per field:
|
|
241
250
|
|
|
242
251
|
```ruby
|
|
243
252
|
# Opt OUT: render the value as a normal, readable text input
|
|
244
|
-
field :api_token, as: :string # a token the admin needs to copy
|
|
253
|
+
field :api_token, as: :string # a token the admin needs to copy (shown on the show page and cards too)
|
|
245
254
|
field :content_hash, as: :string # a checksum, not a secret
|
|
246
255
|
field :public_key, as: :string # *_key matches, but a public key is not secret
|
|
247
256
|
|
|
248
257
|
# Opt IN: mask a secret the heuristic still misses (e.g. no telltale name)
|
|
249
258
|
field :recovery_phrase, as: :password
|
|
259
|
+
field :pin, as: :secret # alias of :password
|
|
250
260
|
```
|
|
251
261
|
|
|
252
262
|
> [!WARNING]
|
|
253
263
|
> 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
264
|
|
|
265
|
+
### Rich text (Action Text and Lexxy) {#rich-text-fields}
|
|
266
|
+
|
|
267
|
+
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.
|
|
268
|
+
|
|
269
|
+
### Lists and tags {#list-fields}
|
|
270
|
+
|
|
271
|
+
`as: :list` edits an array value (a JSON or array column) as chips. Options go on `input`:
|
|
272
|
+
|
|
273
|
+
```ruby
|
|
274
|
+
field :nicknames, as: :list
|
|
275
|
+
input :nicknames, suggestions: %w[Basin Tub], limit: 5 # addable: false allows only suggestions
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
Removing every chip saves an empty array.
|
|
279
|
+
|
|
280
|
+
[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:
|
|
281
|
+
|
|
282
|
+
```ruby
|
|
283
|
+
class Article < ResourceRecord
|
|
284
|
+
include NoFlyList::TaggableRecord
|
|
285
|
+
has_tags :labels, scope: :organization # per-tenant vocabulary
|
|
286
|
+
has_tags :topics, restrict_to_existing: true, limit: 3
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# policy
|
|
290
|
+
def permitted_attributes_for_create = %i[title labels topics]
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
- The input suggests tags that already exist in the record's tag scope.
|
|
294
|
+
- `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).
|
|
295
|
+
- `limit` caps how many tags can be chosen.
|
|
296
|
+
- Add `filter :labels, with: :tags` for a table filter.
|
|
297
|
+
- The table reads tags through the context's association, so preload it (`includes(:labels)`) to avoid a query per row.
|
|
298
|
+
|
|
299
|
+
### Binary columns {#binary-fields}
|
|
300
|
+
|
|
301
|
+
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.
|
|
302
|
+
|
|
303
|
+
- Saving without choosing a file keeps the stored bytes. There is no control to clear the column.
|
|
304
|
+
- Browsers can't refill a file input, so after a failed validation the user has to choose the file again.
|
|
305
|
+
- 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.
|
|
306
|
+
|
|
255
307
|
### Wrapped vs unwrapped
|
|
256
308
|
|
|
257
309
|
- `wrapped`: includes label, hint, and error rendering. Use for normal form fields.
|
|
@@ -283,6 +335,27 @@ input :author, label_method: :email
|
|
|
283
335
|
For large target tables, write an explicit `search` block on the target resource definition; the fallback's leading-wildcard `LIKE` can't use a b-tree index.
|
|
284
336
|
:::
|
|
285
337
|
|
|
338
|
+
### List variants: checkboxes, toggles, radio buttons
|
|
339
|
+
|
|
340
|
+
For a short list of options, render one control per record instead of a dropdown:
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
input :tags, as: :association_checkboxes # has_many / habtm
|
|
344
|
+
input :tags, as: :association_toggles # same, styled as switches
|
|
345
|
+
input :category, as: :association_radio_buttons # belongs_to / has_one
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The variants share the select's security: options come from the target's policy scope (or `choices:`), values are signed global ids, and submitted ids are checked against the authorized relation. Like the select, they always render the records the field holds now, even past `choice_limit:` or outside the viewer's scope, and accept them back on submit, so saving a form never drops a value the viewer did not touch. The inline `+` add button renders next to the list, and `add_action:`, `skip_authorization:`, `choices:` and `label_method:` work the same way.
|
|
349
|
+
|
|
350
|
+
How they differ from the select:
|
|
351
|
+
|
|
352
|
+
- **The list is capped like the select.** `choice_limit:` defaults to the select's cap. There is no typeahead, so a record past the cap can't be picked: use them for short lists, or raise `choice_limit:`.
|
|
353
|
+
- **Unchecking everything clears the association.** The checkbox list submits a blank value, so an empty selection saves as no records.
|
|
354
|
+
- **Optional radio groups get a "None" choice.** A radio group can't be cleared otherwise. Picking it clears the association. On a has_one, Active Record detaches the old record (or destroys it, with `dependent: :destroy`) as soon as the value is assigned, before the form validates, the same as picking a different record. It is left out when the field is required (a checked "None" would satisfy the browser's `required`), and `include_blank: false` drops it on an optional one. Its label is the field's `placeholder:` if set.
|
|
355
|
+
- **The macro has to fit.** Checkboxes and toggles take a has_many or habtm; radio buttons take a belongs_to or has_one. A mismatch raises `ArgumentError` when the input is built, on render and on submit, as do a polymorphic association and `group_method:`.
|
|
356
|
+
|
|
357
|
+
The list and its controls are themed by `association_checkboxes`, `association_toggles`, `association_radio_buttons` (the list), `association_checkbox`, `association_toggle`, `association_radio_button` (each control) and `association_choice_label`.
|
|
358
|
+
|
|
286
359
|
## Submit buttons
|
|
287
360
|
|
|
288
361
|
Default `render_actions` produces the primary submit, plus an optional "Save and add another" / "Update and continue editing" secondary button.
|
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
|