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
|
@@ -7,15 +7,23 @@ module Plutonium
|
|
|
7
7
|
class Color < Phlexi::Display::Components::Base
|
|
8
8
|
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
9
9
|
|
|
10
|
+
# Hex, a named color, or a plain rgb()/hsl() call. The value is
|
|
11
|
+
# interpolated into `style:`, so anything else (a `;` or `url(`)
|
|
12
|
+
# would let a stored string inject arbitrary CSS.
|
|
13
|
+
CSS_COLOR = /\A(?:#\h{3,8}|[a-z]+|(?:rgb|hsl)a?\([\d\s.,%\/]+\))\z/i
|
|
14
|
+
|
|
10
15
|
def render_value(value)
|
|
11
|
-
div(**attributes
|
|
12
|
-
div(
|
|
13
|
-
|
|
14
|
-
style: "background-color: #{value};"
|
|
15
|
-
)
|
|
16
|
-
span(class: "text-sm text-[var(--pu-text-muted)]") { value }
|
|
16
|
+
div(**attributes) do
|
|
17
|
+
div(class: themed(:color_indicator), style: swatch_style(value))
|
|
18
|
+
span(class: themed(:color_label)) { value }
|
|
17
19
|
end
|
|
18
20
|
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def swatch_style(value)
|
|
25
|
+
"background-color: #{value};" if CSS_COLOR.match?(value)
|
|
26
|
+
end
|
|
19
27
|
end
|
|
20
28
|
end
|
|
21
29
|
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A number of seconds (or an ActiveSupport::Duration) as "2h 15m".
|
|
8
|
+
class Duration < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
UNITS = {days: 86_400, hours: 3600, minutes: 60, seconds: 1}.freeze
|
|
12
|
+
|
|
13
|
+
def render_value(value)
|
|
14
|
+
span(**attributes) { value }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def normalize_value(value)
|
|
20
|
+
remaining = value.to_i
|
|
21
|
+
parts = UNITS.filter_map do |unit, size|
|
|
22
|
+
count, remaining = remaining.divmod(size)
|
|
23
|
+
Plutonium::Translation.t("plutonium.ui.display.duration.#{unit}", count:) if count.positive?
|
|
24
|
+
end
|
|
25
|
+
parts.empty? ? Plutonium::Translation.t("plutonium.ui.display.duration.seconds", count: 0) : parts.join(" ")
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A byte count as "12 KB".
|
|
8
|
+
class FileSize < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
span(**attributes) { value }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def normalize_value(value) = ActiveSupport::NumberHelper.number_to_human_size(value)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A Hash as a two-column list of keys and values: hstore columns, or
|
|
8
|
+
# any hash-valued field declared `as: :key_value`. Nested hashes and
|
|
9
|
+
# arrays stay on one line as compact JSON.
|
|
10
|
+
class KeyValue < Phlexi::Display::Components::Base
|
|
11
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
12
|
+
|
|
13
|
+
def render_value(value)
|
|
14
|
+
dl(**attributes) do
|
|
15
|
+
value.each do |key, item|
|
|
16
|
+
dt(class: themed(:key_value_key)) { key.to_s }
|
|
17
|
+
dd(class: themed(:key_value_value)) { format_item(item) }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def normalize_value(value) = value.to_h
|
|
25
|
+
|
|
26
|
+
def format_item(item)
|
|
27
|
+
case item
|
|
28
|
+
when nil then "—"
|
|
29
|
+
when Hash, Array then code(class: themed(:key_value_nested)) { item.to_json }
|
|
30
|
+
else item.to_s
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# An array value as a row of chips.
|
|
8
|
+
class List < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
# DisplaysValue renders a multi-valued field once per item; a list
|
|
12
|
+
# is one component for the whole array.
|
|
13
|
+
def view_template
|
|
14
|
+
items = normalize_value(field.value)
|
|
15
|
+
return render(field.placeholder_tag(**@placeholder_attributes)) if items.empty?
|
|
16
|
+
|
|
17
|
+
wrapped { render_value(items) }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def render_value(value)
|
|
21
|
+
ul(**attributes) do
|
|
22
|
+
value.each { |item| li(class: themed(:list_item)) { item.to_s } }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def normalize_value(value) = Array(value).compact_blank
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# phlexi's Number coerces through Float(), which drops a decimal
|
|
8
|
+
# column's scale (10.50 → "10.5") and digits past ~15 significant
|
|
9
|
+
# figures. Decimals stay BigDecimal and render at the column's scale.
|
|
10
|
+
# Explicit `options:` still go straight to number_to_delimited.
|
|
11
|
+
class Number < Phlexi::Display::Components::Number
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def format_number(value)
|
|
15
|
+
return super unless value.is_a?(BigDecimal) && @options.empty?
|
|
16
|
+
|
|
17
|
+
digits = scale ? ActiveSupport::NumberHelper.number_to_rounded(value, precision: scale) : value.to_s("F")
|
|
18
|
+
ActiveSupport::NumberHelper.number_to_delimited(digits)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def normalize_value(value)
|
|
22
|
+
value.is_a?(BigDecimal) ? value : super
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def scale
|
|
26
|
+
field.object.class.try(:type_for_attribute, field.key)&.scale
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A value out of `max:` (default 100) as a bar and a percentage.
|
|
8
|
+
#
|
|
9
|
+
# display :completion, as: :progress # 0..100
|
|
10
|
+
# display :ratio, as: :progress, max: 1 # 0..1
|
|
11
|
+
class Progress < Phlexi::Display::Components::Base
|
|
12
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
13
|
+
|
|
14
|
+
def render_value(value)
|
|
15
|
+
percent = (value.to_f / @max * 100).clamp(0, 100).round
|
|
16
|
+
div(**attributes, role: :progressbar, aria_valuenow: value, aria_valuemin: 0, aria_valuemax: @max) do
|
|
17
|
+
div(class: themed(:progress_track)) do
|
|
18
|
+
div(class: themed(:progress_bar), style: "width: #{percent}%")
|
|
19
|
+
end
|
|
20
|
+
span(class: themed(:progress_label)) { "#{percent}%" }
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_attributes
|
|
27
|
+
super
|
|
28
|
+
@max = attributes.delete(:max) || 100
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_value(value) = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A score as stars out of `max:` (default 5).
|
|
8
|
+
class Rating < Phlexi::Display::Components::Base
|
|
9
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
10
|
+
|
|
11
|
+
def render_value(value)
|
|
12
|
+
filled = value.to_f.round.clamp(0, @max)
|
|
13
|
+
label = Plutonium::Translation.t("plutonium.ui.display.rating.label", value: filled, max: @max)
|
|
14
|
+
div(**attributes, role: :img, aria_label: label) do
|
|
15
|
+
@max.times do |index|
|
|
16
|
+
state = (index < filled) ? "filled" : "empty"
|
|
17
|
+
span(data: {rating_star: state}) do
|
|
18
|
+
render Phlex::TablerIcons::Star.new(class: themed(:"rating_#{state}"))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def build_attributes
|
|
27
|
+
super
|
|
28
|
+
@max = attributes.delete(:max) || 5
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def normalize_value(value) = value
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A timestamp as "3 minutes ago", kept live by the timeago controller.
|
|
8
|
+
# The absolute time is the fallback text and the tooltip.
|
|
9
|
+
class RelativeTime < Phlexi::Display::Components::Base
|
|
10
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
11
|
+
|
|
12
|
+
def render_value(value)
|
|
13
|
+
absolute = I18n.l(value, format: :long)
|
|
14
|
+
time(**attributes, datetime: value.iso8601, title: absolute,
|
|
15
|
+
data: {controller: "timeago", timeago_datetime_value: value.iso8601}) { absolute }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def normalize_value(value) = value
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
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 `has_rich_text` field. Action Text renders and sanitizes the HTML
|
|
8
|
+
# (including attachments) through its content layout.
|
|
9
|
+
class RichText < Phlexi::Display::Components::Base
|
|
10
|
+
include Phlexi::Display::Components::Concerns::DisplaysValue
|
|
11
|
+
|
|
12
|
+
def render_value(value)
|
|
13
|
+
div(**attributes) { raw(safe(value.to_s)) }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def normalize_value(value) = value
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Display
|
|
6
|
+
module Components
|
|
7
|
+
# A no_fly_list `has_tags` context as chips. Reads the tag records
|
|
8
|
+
# through the context's association, so a table can preload it
|
|
9
|
+
# (`includes(:labels)`) to avoid a query per row.
|
|
10
|
+
class Tags < List
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def normalize_value(value) = value.map(&:name)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -6,16 +6,29 @@ module Plutonium
|
|
|
6
6
|
module Options
|
|
7
7
|
module InferredTypes
|
|
8
8
|
include Plutonium::UI::Options::HasCentsField
|
|
9
|
+
include Plutonium::UI::Options::SecretField
|
|
10
|
+
include Plutonium::UI::Options::TagsField
|
|
11
|
+
include Plutonium::UI::Options::RichTextField
|
|
9
12
|
|
|
10
13
|
private
|
|
11
14
|
|
|
12
15
|
def infer_field_component
|
|
16
|
+
return :tags if tags_field?
|
|
17
|
+
return :rich_text if rich_text_field?
|
|
18
|
+
|
|
19
|
+
# Mask the same secret-bearing names the form does, so the show
|
|
20
|
+
# page and table never print what the form refuses to echo.
|
|
21
|
+
return :password if secret_field_name?
|
|
22
|
+
|
|
13
23
|
# has_cents decimal accessors infer as :float/:decimal; render money.
|
|
14
24
|
return :currency if has_cents_field?
|
|
15
25
|
|
|
16
26
|
case inferred_field_type
|
|
17
27
|
when :attachment
|
|
18
28
|
:attachment
|
|
29
|
+
when :binary
|
|
30
|
+
# phlexi-display falls back to :string, printing the raw bytes.
|
|
31
|
+
:binary
|
|
19
32
|
when :boolean
|
|
20
33
|
# phlexi-display falls back to :string, rendering "true"/"false".
|
|
21
34
|
:boolean
|
|
@@ -41,8 +41,9 @@ module Plutonium
|
|
|
41
41
|
link: "text-lg text-primary-600 dark:text-primary-400 hover:text-primary-500 dark:hover:text-primary-300 whitespace-pre-line transition-colors",
|
|
42
42
|
|
|
43
43
|
# Color display
|
|
44
|
-
color: "flex items-center
|
|
45
|
-
color_indicator: "w-
|
|
44
|
+
color: "flex items-center gap-2",
|
|
45
|
+
color_indicator: "w-6 h-6 rounded border border-[var(--pu-border)]",
|
|
46
|
+
color_label: "text-sm text-[var(--pu-text-muted)]",
|
|
46
47
|
|
|
47
48
|
# Boolean / badge pills — variant class is applied by the component.
|
|
48
49
|
boolean: "",
|
|
@@ -57,6 +58,27 @@ module Plutonium
|
|
|
57
58
|
json: "text-sm text-[var(--pu-text)] whitespace-pre font-mono bg-[var(--pu-surface-alt)] border border-[var(--pu-border-muted)] rounded-[var(--pu-radius-md)] p-4 overflow-x-auto",
|
|
58
59
|
prefixed_icon: "w-6 h-6 mr-2 text-[var(--pu-text-muted)]",
|
|
59
60
|
markdown: "format dark:format-invert format-primary max-w-none",
|
|
61
|
+
rich_text: "format dark:format-invert format-primary max-w-none",
|
|
62
|
+
key_value: "grid grid-cols-[auto_1fr] gap-x-6 gap-y-2 text-[var(--pu-text)]",
|
|
63
|
+
key_value_key: "font-medium text-[var(--pu-text-muted)] break-words",
|
|
64
|
+
key_value_value: "break-words",
|
|
65
|
+
key_value_nested: "font-mono text-sm",
|
|
66
|
+
list: "flex flex-wrap gap-1.5",
|
|
67
|
+
list_item: "pu-badge pu-badge-neutral",
|
|
68
|
+
relative_time: "text-lg text-[var(--pu-text)]",
|
|
69
|
+
code: "flex items-center gap-2",
|
|
70
|
+
code_value: "font-mono text-sm text-[var(--pu-text)] bg-[var(--pu-surface-alt)] border border-[var(--pu-border-muted)] rounded-[var(--pu-radius-sm)] px-2 py-0.5 break-all",
|
|
71
|
+
code_copy: "pu-btn pu-btn-xs pu-btn-ghost",
|
|
72
|
+
duration: "text-lg text-[var(--pu-text)] tabular-nums",
|
|
73
|
+
file_size: "text-lg text-[var(--pu-text)] tabular-nums",
|
|
74
|
+
progress: "flex items-center gap-3",
|
|
75
|
+
progress_track: "h-2 w-full max-w-xs rounded-full bg-[var(--pu-surface-alt)] overflow-hidden",
|
|
76
|
+
progress_bar: "h-full rounded-full bg-primary-500",
|
|
77
|
+
progress_label: "text-sm text-[var(--pu-text-muted)] tabular-nums",
|
|
78
|
+
rating: "flex items-center gap-0.5",
|
|
79
|
+
rating_filled: "w-5 h-5 text-warning-500 fill-current",
|
|
80
|
+
rating_empty: "w-5 h-5 text-[var(--pu-border)]",
|
|
81
|
+
binary: "text-sm text-[var(--pu-text-muted)]",
|
|
60
82
|
|
|
61
83
|
# Attachments
|
|
62
84
|
attachment_value_wrapper: "grid grid-cols-[repeat(auto-fill,minmax(0,200px))] gap-4",
|
|
@@ -43,6 +43,11 @@ module Plutonium
|
|
|
43
43
|
end
|
|
44
44
|
alias_method :markdown_tag, :easymde_tag
|
|
45
45
|
|
|
46
|
+
# Action Text editor (Trix, or Lexxy when installed). See {Components::RichText}.
|
|
47
|
+
def rich_text_tag(**, &)
|
|
48
|
+
create_component(Components::RichText, :rich_text, **, &)
|
|
49
|
+
end
|
|
50
|
+
|
|
46
51
|
def toggle_tag(**, &)
|
|
47
52
|
create_component(Plutonium::UI::Form::Components::Toggle, :toggle, **, &)
|
|
48
53
|
end
|
|
@@ -54,12 +59,30 @@ module Plutonium
|
|
|
54
59
|
def password_tag(**, &)
|
|
55
60
|
create_component(Components::Password, :password, **, &)
|
|
56
61
|
end
|
|
62
|
+
alias_method :secret_tag, :password_tag
|
|
57
63
|
|
|
58
64
|
def slim_select_tag(**attributes, &)
|
|
59
65
|
attributes[:data_controller] = tokens(attributes[:data_controller], "slim-select")
|
|
60
66
|
select_tag(**attributes, required: false, class!: "", &)
|
|
61
67
|
end
|
|
62
68
|
|
|
69
|
+
# Chip input for array values. See {Components::List}.
|
|
70
|
+
def list_tag(**attributes, &)
|
|
71
|
+
attributes[:data_controller] = tokens(attributes[:data_controller], "slim-select")
|
|
72
|
+
create_component(Components::List, :select, required: false, class!: "", **attributes, &)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Chip input for a no_fly_list `has_tags` context. See {Components::Tags}.
|
|
76
|
+
def tags_tag(**attributes, &)
|
|
77
|
+
attributes[:data_controller] = tokens(attributes[:data_controller], "slim-select")
|
|
78
|
+
create_component(Components::Tags, :select, required: false, class!: "", **attributes, &)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Star picker. See {Components::Rating}.
|
|
82
|
+
def rating_tag(**, &)
|
|
83
|
+
create_component(Components::Rating, :rating, **, &)
|
|
84
|
+
end
|
|
85
|
+
|
|
63
86
|
def flatpickr_tag(**, &)
|
|
64
87
|
create_component(Components::Flatpickr, :flatpickr, **, &)
|
|
65
88
|
end
|
|
@@ -86,6 +109,10 @@ module Plutonium
|
|
|
86
109
|
alias_method :"#{name}_tag", :uppy_tag
|
|
87
110
|
end
|
|
88
111
|
|
|
112
|
+
def binary_tag(**, &)
|
|
113
|
+
create_component(Components::Binary, :file, **, &)
|
|
114
|
+
end
|
|
115
|
+
|
|
89
116
|
def key_value_store_tag(**, &)
|
|
90
117
|
create_component(Components::KeyValueStore, :key_value_store, **, &)
|
|
91
118
|
end
|
|
@@ -116,6 +143,9 @@ module Plutonium
|
|
|
116
143
|
alias_method :belongs_to_tag, :secure_association_tag
|
|
117
144
|
alias_method :has_many_tag, :secure_association_tag
|
|
118
145
|
alias_method :has_one_tag, :secure_association_tag
|
|
146
|
+
# phlexi bound this to ITS has_many_tag at definition time, so it
|
|
147
|
+
# has to be re-pointed explicitly or habtm skips the policy scope.
|
|
148
|
+
alias_method :has_and_belongs_to_many_tag, :secure_association_tag
|
|
119
149
|
|
|
120
150
|
def secure_polymorphic_association_tag(**attributes, &)
|
|
121
151
|
attributes[:data_controller] = tokens(attributes[:data_controller], "slim-select") # TODO: put this behind a config
|
|
@@ -134,7 +164,6 @@ module Plutonium
|
|
|
134
164
|
alias_method :datetime_tag, :flatpickr_tag
|
|
135
165
|
alias_method :date_tag, :flatpickr_tag
|
|
136
166
|
alias_method :time_tag, :flatpickr_tag
|
|
137
|
-
alias_method :rich_text_tag, :markdown_tag
|
|
138
167
|
alias_method :json_tag, :json_input_tag
|
|
139
168
|
alias_method :jsonb_tag, :json_input_tag
|
|
140
169
|
alias_method :hstore_tag, :key_value_store_tag
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Form
|
|
6
|
+
module Components
|
|
7
|
+
# File input for a `binary` column: the uploaded file's bytes become
|
|
8
|
+
# the value. Choosing no file extracts nil, which the resource
|
|
9
|
+
# controller compacts away, so an edit that doesn't replace the file
|
|
10
|
+
# keeps the stored bytes. There is no way to clear the column here.
|
|
11
|
+
class Binary < Phlexi::Form::Components::FileInput
|
|
12
|
+
protected
|
|
13
|
+
|
|
14
|
+
def build_input_attributes
|
|
15
|
+
super
|
|
16
|
+
apply_default_hint
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def normalize_input(input_value)
|
|
20
|
+
input_value.read if input_value.is_a?(ActionDispatch::Http::UploadedFile)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
# Says what a blank submit keeps: the bytes in the database, not a
|
|
26
|
+
# rejected upload still assigned to the record on a failed re-render.
|
|
27
|
+
def apply_default_hint
|
|
28
|
+
return if field.has_hint?
|
|
29
|
+
|
|
30
|
+
stored = field.object.attribute_in_database(field.key) if field.object.persisted?
|
|
31
|
+
return if stored.blank?
|
|
32
|
+
|
|
33
|
+
size = ActiveSupport::NumberHelper.number_to_human_size(stored.bytesize)
|
|
34
|
+
field.hint(Plutonium::Translation.t("plutonium.ui.form.binary.replace_hint", size:))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -44,21 +44,6 @@ module Plutonium
|
|
|
44
44
|
attributes[:data][:key_value_store_limit_value] = limit
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
def render_header
|
|
48
|
-
div(class: "key-value-store-header") do
|
|
49
|
-
if attributes[:label]
|
|
50
|
-
h3(class: "text-lg font-semibold text-[var(--pu-text)]") do
|
|
51
|
-
plain attributes[:label]
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
if attributes[:description]
|
|
55
|
-
p(class: "text-sm text-[var(--pu-text-muted)]") do
|
|
56
|
-
plain attributes[:description]
|
|
57
|
-
end
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
|
|
62
47
|
def render_key_value_pairs
|
|
63
48
|
# Hidden sentinel input ensures the field is always present in params when the
|
|
64
49
|
# component is rendered. Without this, removing all pairs would submit nothing,
|
|
@@ -75,42 +60,37 @@ module Plutonium
|
|
|
75
60
|
|
|
76
61
|
def render_key_value_pair(key, value, index)
|
|
77
62
|
div(
|
|
78
|
-
class: "key-value-pair flex items-center gap-2
|
|
63
|
+
class: "key-value-pair flex items-center gap-2",
|
|
79
64
|
data_key_value_store_target: "pair"
|
|
80
65
|
) do
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.key_placeholder"),
|
|
85
|
-
value: key,
|
|
86
|
-
name: "#{field_name}[#{index}][key]",
|
|
87
|
-
id: "#{field.dom.id}_#{index}_key",
|
|
88
|
-
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
89
|
-
data_key_value_store_target: "keyInput"
|
|
90
|
-
)
|
|
91
|
-
|
|
92
|
-
# Value input
|
|
93
|
-
input(
|
|
94
|
-
type: :text,
|
|
95
|
-
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.value_placeholder"),
|
|
96
|
-
value: value,
|
|
97
|
-
name: "#{field_name}[#{index}][value]",
|
|
98
|
-
id: "#{field.dom.id}_#{index}_value",
|
|
99
|
-
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
100
|
-
data_key_value_store_target: "valueInput"
|
|
101
|
-
)
|
|
102
|
-
|
|
103
|
-
# Remove button
|
|
66
|
+
render_pair_input(:key, key, index)
|
|
67
|
+
render_pair_input(:value, value, index)
|
|
68
|
+
|
|
104
69
|
button(
|
|
105
70
|
type: :button,
|
|
106
|
-
class: "
|
|
71
|
+
class: "pu-btn pu-btn-sm pu-btn-ghost text-danger-600 dark:text-danger-400",
|
|
72
|
+
aria_label: Plutonium::Translation.t("plutonium.ui.form.key_value_store.remove_pair"),
|
|
107
73
|
data_action: "key-value-store#removePair"
|
|
108
74
|
) do
|
|
109
|
-
|
|
75
|
+
render Phlex::TablerIcons::X.new(class: "w-4 h-4")
|
|
110
76
|
end
|
|
111
77
|
end
|
|
112
78
|
end
|
|
113
79
|
|
|
80
|
+
# `part` is :key or :value. The template row passes "__INDEX__", which
|
|
81
|
+
# the controller swaps for the real index when it clones the row.
|
|
82
|
+
def render_pair_input(part, value, index)
|
|
83
|
+
input(
|
|
84
|
+
type: :text,
|
|
85
|
+
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.#{part}_placeholder"),
|
|
86
|
+
value:,
|
|
87
|
+
name: "#{field_name}[#{index}][#{part}]",
|
|
88
|
+
id: "#{field.dom.id}_#{index}_#{part}",
|
|
89
|
+
class: "pu-input flex-1 min-w-0",
|
|
90
|
+
data_key_value_store_target: "#{part}Input"
|
|
91
|
+
)
|
|
92
|
+
end
|
|
93
|
+
|
|
114
94
|
def render_add_button
|
|
115
95
|
div(class: "key-value-store-actions mt-2") do
|
|
116
96
|
button(
|
|
@@ -129,36 +109,7 @@ module Plutonium
|
|
|
129
109
|
|
|
130
110
|
def render_template
|
|
131
111
|
template(data_key_value_store_target: "template") do
|
|
132
|
-
|
|
133
|
-
class: "key-value-pair flex items-center gap-2 p-2 border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)]",
|
|
134
|
-
data_key_value_store_target: "pair"
|
|
135
|
-
) do
|
|
136
|
-
input(
|
|
137
|
-
type: :text,
|
|
138
|
-
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.key_placeholder"),
|
|
139
|
-
name: "#{field_name}[__INDEX__][key]",
|
|
140
|
-
id: "#{field.dom.id}___INDEX___key",
|
|
141
|
-
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
142
|
-
data_key_value_store_target: "keyInput"
|
|
143
|
-
)
|
|
144
|
-
|
|
145
|
-
input(
|
|
146
|
-
type: :text,
|
|
147
|
-
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.value_placeholder"),
|
|
148
|
-
name: "#{field_name}[__INDEX__][value]",
|
|
149
|
-
id: "#{field.dom.id}___INDEX___value",
|
|
150
|
-
class: "flex-1 px-3 py-1 text-sm border border-[var(--pu-border)] rounded-[var(--pu-radius-sm)] focus:ring-2 focus:ring-primary-500 focus:border-primary-500 bg-[var(--pu-surface)] text-[var(--pu-text)]",
|
|
151
|
-
data_key_value_store_target: "valueInput"
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
button(
|
|
155
|
-
type: :button,
|
|
156
|
-
class: "px-2 py-1 text-danger-600 hover:text-danger-800 focus:outline-none transition-colors",
|
|
157
|
-
data_action: "key-value-store#removePair"
|
|
158
|
-
) do
|
|
159
|
-
plain "×"
|
|
160
|
-
end
|
|
161
|
-
end
|
|
112
|
+
render_key_value_pair(nil, nil, "__INDEX__")
|
|
162
113
|
end
|
|
163
114
|
end
|
|
164
115
|
|