plutonium 0.64.0 → 0.65.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/SKILL.md +4 -1
- data/.claude/skills/plutonium-app/SKILL.md +14 -0
- data/.claude/skills/plutonium-dashboard/SKILL.md +161 -0
- data/.claude/skills/plutonium-resource/SKILL.md +94 -14
- data/.claude/skills/plutonium-ui/SKILL.md +22 -1
- data/CHANGELOG.md +59 -0
- data/CLAUDE.md +282 -210
- data/SECURITY.md +1 -1
- data/app/assets/plutonium-charts.js +20604 -0
- data/app/assets/plutonium-charts.js.map +7 -0
- data/app/assets/plutonium-charts.min.js +41 -0
- data/app/assets/plutonium-charts.min.js.map +7 -0
- data/app/assets/plutonium.css +1 -1
- data/app/assets/plutonium.js +794 -629
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +48 -48
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/plutonium/_flash_alerts.html.erb +2 -2
- data/app/views/plutonium/_resource_header.html.erb +2 -2
- data/app/views/plutonium/_resource_sidebar.html.erb +11 -2
- data/app/views/plutonium/_toast.html.erb +2 -2
- data/app/views/rodauth/_login_form.html.erb +1 -1
- data/app/views/rodauth/_password_visibility.html.erb +1 -1
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +2 -2
- data/app/views/rodauth/create_account.html.erb +4 -4
- data/app/views/rodauth/reset_password_request.html.erb +1 -1
- data/app/views/rodauth/verify_account_resend.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +1 -1
- data/config/brakeman.ignore +4 -4
- data/config/initializers/rabl.rb +0 -40
- data/config/locales/en/api_client.yml +22 -0
- data/config/locales/en/async.yml +34 -0
- data/config/locales/en/dashboard.yml +25 -0
- data/config/locales/en/invites.yml +37 -0
- data/config/locales/en/js.yml +40 -0
- data/config/locales/en/kanban.yml +28 -0
- data/config/locales/en/plutonium.yml +26 -0
- data/config/locales/en/positioning.yml +14 -0
- data/config/locales/en/profile.yml +29 -0
- data/config/locales/en/resource.yml +60 -0
- data/config/locales/en/ui.yml +93 -0
- data/config/locales/en/wizard.yml +46 -0
- data/docs/.vitepress/config.ts +34 -1
- data/docs/.vitepress/theme/components/HomeHero.vue +1 -1
- data/docs/.vitepress/theme/components/HomeInTheBox.vue +14 -0
- data/docs/.vitepress/theme/custom.css +34 -377
- data/docs/blog/introducing-plutonium-dashboards.md +175 -0
- data/docs/blog/introducing-plutonium-i18n.md +91 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +1 -0
- data/docs/guides/kanban.md +2 -2
- data/docs/public/images/blog/dashboards-card-error.png +0 -0
- data/docs/public/images/blog/dashboards-loading.png +0 -0
- data/docs/public/images/blog/dashboards-overview.png +0 -0
- data/docs/public/images/blog/dashboards-sidebar.png +0 -0
- data/docs/public/images/blog/i18n-es-admin.png +0 -0
- data/docs/public/images/guides/dashboard-card-error.png +0 -0
- data/docs/public/images/guides/dashboard-loading.png +0 -0
- data/docs/public/images/guides/dashboard-overview.png +0 -0
- data/docs/public/images/guides/dashboard-sidebar.png +0 -0
- data/docs/reference/app/generators.md +2 -2
- data/docs/reference/app/portals.md +2 -0
- data/docs/reference/configuration.md +84 -27
- data/docs/reference/dashboard/dsl.md +123 -0
- data/docs/reference/dashboard/index.md +35 -0
- data/docs/reference/dashboard/registration.md +76 -0
- data/docs/reference/i18n.md +239 -0
- data/docs/reference/index.md +10 -1
- data/docs/reference/kanban/dsl.md +1 -1
- data/docs/reference/kanban/positioning.md +2 -2
- data/docs/reference/resource/actions.md +1 -1
- data/docs/reference/resource/definition.md +96 -18
- data/docs/reference/resource/export.md +5 -1
- data/docs/reference/resource/index.md +2 -0
- data/docs/reference/{positioning.md → resource/positioning.md} +2 -1
- data/docs/reference/resource/query.md +2 -0
- data/docs/reference/ui/assets.md +8 -0
- data/docs/reference/ui/displays.md +5 -8
- data/docs/reference/ui/index.md +1 -1
- data/docs/reference/wizard/dsl.md +2 -2
- data/esbuild.config.js +5 -2
- data/gemfiles/rails_7.gemfile.lock +2 -2
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/active_model/validations/array_validator.rb +1 -1
- data/lib/active_model/validations/attached_validator.rb +1 -1
- data/lib/active_model/validations/url_validator.rb +2 -2
- data/lib/generators/pu/core/assets/assets_generator.rb +36 -21
- data/lib/generators/pu/core/assets/templates/postcss.config.js +1 -1
- data/lib/generators/pu/core/update/update_generator.rb +3 -2
- data/lib/generators/pu/dashboard/dashboard_generator.rb +96 -0
- data/lib/generators/pu/dashboard/templates/dashboard.rb.tt +25 -0
- data/lib/generators/pu/docker/install/install_generator.rb +58 -0
- data/lib/generators/pu/docker/install/templates/Dockerfile.dev.tt +18 -3
- data/lib/generators/pu/docker/install/templates/Dockerfile.tt +20 -5
- data/lib/generators/pu/docker/install/templates/{docker-compose.yml → docker-compose.yml.tt} +1 -1
- data/lib/generators/pu/gem/dotenv/dotenv_generator.rb +1 -1
- data/lib/generators/pu/invites/invitable_generator.rb +18 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/actions.rb +0 -95
- data/lib/generators/pu/lib/plutonium_generators/concerns/js_package_manager.rb +146 -0
- data/lib/generators/pu/lib/plutonium_generators/generator.rb +1 -17
- data/lib/generators/pu/pkg/package/package_generator.rb +1 -0
- data/lib/generators/pu/pkg/package/templates/config/locales/en.yml.tt +28 -0
- data/lib/generators/pu/pkg/portal/portal_generator.rb +1 -0
- data/lib/generators/pu/pkg/portal/templates/config/locales/en.yml.tt +30 -0
- data/lib/generators/pu/res/scaffold/scaffold_generator.rb +0 -8
- data/lib/generators/pu/rodauth/concerns/feature_selector.rb +0 -18
- data/lib/plutonium/action/base.rb +27 -3
- data/lib/plutonium/action/interactive.rb +17 -4
- data/lib/plutonium/api_client/concerns/create_api_client.rb +12 -11
- data/lib/plutonium/api_client/concerns/disable_api_client.rb +3 -3
- data/lib/plutonium/auth/sequel_adapter.rb +1 -1
- data/lib/plutonium/configuration.rb +5 -0
- data/lib/plutonium/core/controller.rb +25 -1
- data/lib/plutonium/core/controllers/authorizable.rb +1 -1
- data/lib/plutonium/dashboard/base.rb +114 -0
- data/lib/plutonium/dashboard/card.rb +200 -0
- data/lib/plutonium/dashboard/controller.rb +101 -0
- data/lib/plutonium/dashboard/dsl.rb +104 -0
- data/lib/plutonium/dashboard/register.rb +40 -0
- data/lib/plutonium/dashboard/route_resolution.rb +30 -0
- data/lib/plutonium/dashboard.rb +42 -0
- data/lib/plutonium/definition/actions.rb +6 -1
- data/lib/plutonium/definition/base.rb +41 -3
- data/lib/plutonium/definition/index_views.rb +1 -1
- data/lib/plutonium/definition/positioning.rb +1 -1
- data/lib/plutonium/definition/presentable.rb +2 -2
- data/lib/plutonium/helpers/assets_helper.rb +15 -2
- data/lib/plutonium/helpers/display_helper.rb +10 -2
- data/lib/plutonium/interaction/async/context.rb +6 -8
- data/lib/plutonium/interaction/async/executor.rb +6 -7
- data/lib/plutonium/interaction/async/run.rb +2 -2
- data/lib/plutonium/interaction/async/run_definition.rb +1 -1
- data/lib/plutonium/interaction/base.rb +2 -0
- data/lib/plutonium/interaction/concerns/dispatchable.rb +15 -3
- data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -3
- data/lib/plutonium/invites/concerns/invite_token.rb +25 -7
- data/lib/plutonium/invites/concerns/invite_user.rb +10 -5
- data/lib/plutonium/invites/concerns/resend_invite.rb +4 -4
- data/lib/plutonium/invites/controller.rb +11 -11
- data/lib/plutonium/kanban/column.rb +13 -3
- data/lib/plutonium/kanban/dsl.rb +7 -4
- data/lib/plutonium/models/has_cents.rb +1 -1
- data/lib/plutonium/profile/security_section.rb +10 -18
- data/lib/plutonium/query/filter.rb +10 -1
- data/lib/plutonium/query/filters/association.rb +27 -2
- data/lib/plutonium/query/filters/boolean.rb +4 -4
- data/lib/plutonium/query/filters/date.rb +4 -17
- data/lib/plutonium/query/filters/date_range.rb +2 -2
- data/lib/plutonium/query/filters/select.rb +10 -1
- data/lib/plutonium/query/filters/text.rb +9 -12
- data/lib/plutonium/railtie.rb +17 -0
- data/lib/plutonium/resource/controller.rb +10 -5
- data/lib/plutonium/resource/controllers/crud_actions.rb +17 -20
- data/lib/plutonium/resource/controllers/export_csv.rb +19 -9
- data/lib/plutonium/resource/controllers/kanban_actions.rb +16 -16
- data/lib/plutonium/resource/controllers/position_actions.rb +5 -5
- data/lib/plutonium/resource/controllers/typeahead.rb +24 -1
- data/lib/plutonium/resource/query_object.rb +30 -3
- data/lib/plutonium/routing/dashboard_registration.rb +137 -0
- data/lib/plutonium/routing/resource_registration.rb +5 -0
- data/lib/plutonium/routing/route_set_extensions.rb +2 -1
- data/lib/plutonium/translation.rb +227 -0
- data/lib/plutonium/ui/actions_dropdown.rb +2 -2
- data/lib/plutonium/ui/breadcrumbs.rb +3 -3
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/behaviour.rb +46 -0
- data/lib/plutonium/ui/component/methods.rb +4 -0
- data/lib/plutonium/ui/dashboard/board.rb +80 -0
- data/lib/plutonium/ui/dashboard/card.rb +127 -0
- data/lib/plutonium/ui/dashboard/chart.rb +43 -0
- data/lib/plutonium/ui/dashboard/custom.rb +32 -0
- data/lib/plutonium/ui/dashboard/frame.rb +25 -0
- data/lib/plutonium/ui/dashboard/metric.rb +137 -0
- data/lib/plutonium/ui/dashboard/skeleton.rb +40 -0
- data/lib/plutonium/ui/display/components/attachment.rb +1 -1
- data/lib/plutonium/ui/display/components/badge.rb +7 -1
- data/lib/plutonium/ui/display/components/boolean.rb +2 -2
- data/lib/plutonium/ui/display/resource.rb +23 -16
- data/lib/plutonium/ui/export_button.rb +3 -3
- data/lib/plutonium/ui/form/components/key_value_store.rb +5 -5
- data/lib/plutonium/ui/form/components/password.rb +2 -2
- data/lib/plutonium/ui/form/components/resource_select.rb +27 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +2 -0
- data/lib/plutonium/ui/form/components/uppy.rb +11 -3
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +1 -1
- data/lib/plutonium/ui/form/concerns/renders_repeater_row_controls.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_structured_inputs.rb +1 -1
- data/lib/plutonium/ui/form/query.rb +8 -8
- data/lib/plutonium/ui/form/resource.rb +54 -26
- data/lib/plutonium/ui/frame_navigator_panel.rb +4 -4
- data/lib/plutonium/ui/grid/card.rb +3 -3
- data/lib/plutonium/ui/grid/resource.rb +3 -3
- data/lib/plutonium/ui/interaction/async/run_progress.rb +6 -6
- data/lib/plutonium/ui/interaction/async/running_banner.rb +2 -2
- data/lib/plutonium/ui/kanban/column.rb +6 -6
- data/lib/plutonium/ui/layout/base.rb +19 -1
- data/lib/plutonium/ui/layout/header.rb +1 -1
- data/lib/plutonium/ui/layout/icon_rail.rb +4 -4
- data/lib/plutonium/ui/layout/rodauth_layout.rb +1 -1
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/layout/topbar.rb +2 -2
- data/lib/plutonium/ui/modal/base.rb +4 -4
- data/lib/plutonium/ui/nav_grid_menu.rb +1 -1
- data/lib/plutonium/ui/nav_user.rb +2 -2
- data/lib/plutonium/ui/page/base.rb +10 -2
- data/lib/plutonium/ui/page/dashboard.rb +54 -0
- data/lib/plutonium/ui/page/edit.rb +3 -3
- data/lib/plutonium/ui/page/interactive_action.rb +2 -2
- data/lib/plutonium/ui/page/new.rb +3 -3
- data/lib/plutonium/ui/page/show.rb +2 -2
- data/lib/plutonium/ui/page/wizard.rb +22 -16
- data/lib/plutonium/ui/page/wizard_chooser.rb +10 -10
- data/lib/plutonium/ui/page/wizard_completed.rb +2 -2
- data/lib/plutonium/ui/skeleton_table.rb +1 -1
- data/lib/plutonium/ui/table/base.rb +2 -2
- data/lib/plutonium/ui/table/components/attachment.rb +1 -1
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +19 -3
- data/lib/plutonium/ui/table/components/drag_handle.rb +3 -3
- data/lib/plutonium/ui/table/components/filter_form.rb +11 -10
- data/lib/plutonium/ui/table/components/filter_pills.rb +4 -4
- data/lib/plutonium/ui/table/components/pagy_info.rb +61 -17
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +9 -5
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +1 -1
- data/lib/plutonium/ui/table/components/scopes_bar.rb +6 -2
- data/lib/plutonium/ui/table/components/scopes_pills.rb +3 -3
- data/lib/plutonium/ui/table/components/selection_column.rb +1 -1
- data/lib/plutonium/ui/table/components/toolbar.rb +2 -2
- data/lib/plutonium/ui/table/components/view_switcher.rb +12 -8
- data/lib/plutonium/ui/table/resource.rb +53 -17
- data/lib/plutonium/ui/wizard/review.rb +7 -7
- data/lib/plutonium/ui/wizard/stepper.rb +1 -1
- data/lib/plutonium/ui/wizard/summary_display.rb +6 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium/wizard/base.rb +2 -0
- data/lib/plutonium/wizard/driving.rb +8 -2
- data/lib/plutonium/wizard/dsl.rb +4 -2
- data/lib/plutonium/wizard/field_importer.rb +1 -8
- data/lib/plutonium/wizard/review_step.rb +8 -1
- data/lib/plutonium/wizard/step.rb +14 -3
- data/package.json +4 -2
- data/plutonium.gemspec +4 -1
- data/postcss-gem-import.cjs +34 -0
- data/postcss-gem-import.js +3 -28
- data/src/css/components.css +91 -0
- data/src/css/slim_select.css +0 -38
- data/src/css/tokens.css +24 -0
- data/src/js/controllers/attachment_input_controller.js +15 -6
- data/src/js/controllers/chart_controller.js +162 -0
- data/src/js/controllers/clipboard_controller.js +3 -2
- data/src/js/controllers/dirty_form_guard_controller.js +4 -3
- data/src/js/controllers/easymde_controller.js +3 -0
- data/src/js/controllers/filter_panel_controller.js +15 -6
- data/src/js/controllers/flatpickr_controller.js +8 -0
- data/src/js/controllers/frame_refresh_controller.js +59 -0
- data/src/js/controllers/intl_tel_input_controller.js +5 -0
- data/src/js/controllers/register_controllers.js +4 -0
- data/src/js/controllers/slim_select_controller.js +79 -176
- data/src/js/core.js +9 -1
- data/src/js/i18n.js +69 -0
- data/src/js/plutonium-charts.js +15 -0
- data/src/js/turbo/turbo_confirm.js +5 -3
- metadata +64 -16
- data/.yarnrc.yml +0 -8
- data/config/initializers/hotwire_turbo_monkey_patches.rb +0 -12
- data/lib/generators/pu/field/input/input_generator.rb +0 -32
- data/lib/generators/pu/field/input/templates/input.rb.tt +0 -15
- data/lib/generators/pu/lib/plutonium_generators/concerns/config.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/concerns/serializer.rb +0 -39
- data/lib/generators/pu/lib/plutonium_generators/installer.rb +0 -205
- data/lib/generators/pu/res/scaffold/templates/presenter.rb.tt +0 -13
- data/lib/generators/pu/res/scaffold/templates/query_object.rb.tt +0 -17
- data/public/plutonium-assets/application.js +0 -31419
- data/public/plutonium-assets/plutonium.ico +0 -0
- data/yarn.lock +0 -6858
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# A single headline number with an optional change indicator.
|
|
7
|
+
#
|
|
8
|
+
# The card's block returns either the value itself or a hash:
|
|
9
|
+
#
|
|
10
|
+
# {value: 1_240} # just the number
|
|
11
|
+
# {value: 1_240, previous: 1_100} # change computed: +12.7%
|
|
12
|
+
# {value: 1_240, change: 12.7} # change in percentage points
|
|
13
|
+
# {value: 1_240, change: "+140", trend: :up} # change shown verbatim
|
|
14
|
+
# {value: 98.2, change_label: "vs. last week"} # caption after the change
|
|
15
|
+
#
|
|
16
|
+
# `format:` picks the number formatter (`:number`, `:currency`,
|
|
17
|
+
# `:percentage`, `:human`, or a proc run on the dashboard); `positive:`
|
|
18
|
+
# says which direction is good (`:up` by default, `:down` for churn or
|
|
19
|
+
# error rates) and drives the change colour.
|
|
20
|
+
class Metric < Card
|
|
21
|
+
include ActiveSupport::NumberHelper
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def render_body
|
|
26
|
+
data = normalize(card.evaluate(dashboard))
|
|
27
|
+
|
|
28
|
+
div(class: "pu-metric") do
|
|
29
|
+
div(class: "pu-metric-value") { format_value(data[:value]) }
|
|
30
|
+
render_change(data) if data[:change] || data[:change_label]
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def normalize(result)
|
|
35
|
+
data = result.is_a?(Hash) ? result.symbolize_keys : {value: result}
|
|
36
|
+
data[:change_label] ||= card.options[:change_label]
|
|
37
|
+
|
|
38
|
+
if !data.key?(:change) && data[:previous].is_a?(Numeric) && data[:value].is_a?(Numeric)
|
|
39
|
+
data[:change] = percent_change(data[:value], data[:previous])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
data[:trend] ||= infer_trend(data[:change]) if data.key?(:change)
|
|
43
|
+
data
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def percent_change(value, previous)
|
|
47
|
+
return nil if previous.zero?
|
|
48
|
+
|
|
49
|
+
((value - previous) / previous.to_f.abs) * 100
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def infer_trend(change)
|
|
53
|
+
case change
|
|
54
|
+
when Numeric
|
|
55
|
+
if change.positive? then :up
|
|
56
|
+
elsif change.negative? then :down
|
|
57
|
+
else :flat
|
|
58
|
+
end
|
|
59
|
+
when String
|
|
60
|
+
if change.start_with?("-") then :down
|
|
61
|
+
elsif change.start_with?("+") then :up
|
|
62
|
+
else :flat
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def render_change(data)
|
|
68
|
+
trend = data[:trend] || :flat
|
|
69
|
+
div(class: tokens("pu-metric-change", change_tone_class(trend))) do
|
|
70
|
+
render_trend_icon(trend)
|
|
71
|
+
span(class: "font-medium") { format_change(data[:change]) } if data[:change]
|
|
72
|
+
span(class: "pu-metric-change-label") { data[:change_label] } if data[:change_label].present?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render_trend_icon(trend)
|
|
77
|
+
icon = case trend
|
|
78
|
+
when :up then Phlex::TablerIcons::TrendingUp
|
|
79
|
+
when :down then Phlex::TablerIcons::TrendingDown
|
|
80
|
+
else Phlex::TablerIcons::Minus
|
|
81
|
+
end
|
|
82
|
+
render icon.new(class: "size-4 shrink-0")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def change_tone_class(trend)
|
|
86
|
+
return "pu-metric-change-neutral" if trend == :flat || trend.nil?
|
|
87
|
+
|
|
88
|
+
good = (trend == :up) == (card.options[:positive] == :up)
|
|
89
|
+
good ? "pu-metric-change-positive" : "pu-metric-change-negative"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def format_change(change)
|
|
93
|
+
case change
|
|
94
|
+
when Numeric
|
|
95
|
+
sign = change.positive? ? "+" : ""
|
|
96
|
+
"#{sign}#{number_to_rounded(change, precision: 1, strip_insignificant_zeros: true)}%"
|
|
97
|
+
else change.to_s
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def format_value(value)
|
|
102
|
+
return t("plutonium.dashboard.metric.empty") if value.nil?
|
|
103
|
+
|
|
104
|
+
formatted = case (format = card.options[:format])
|
|
105
|
+
when Proc then dashboard.instance_exec(value, &format)
|
|
106
|
+
when :currency then number_to_currency(value, **currency_options)
|
|
107
|
+
when :percentage then number_to_percentage(value, precision: precision(1))
|
|
108
|
+
when :human then number_to_human(value, precision: precision(3))
|
|
109
|
+
else format_number(value)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
[card.options[:prefix], formatted, card.options[:suffix]].compact.join
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def format_number(value)
|
|
116
|
+
return value.to_s unless value.is_a?(Numeric)
|
|
117
|
+
|
|
118
|
+
if card.options[:precision] || value.is_a?(Float)
|
|
119
|
+
number_to_rounded(value, precision: precision(value.is_a?(Float) ? 2 : 0), delimiter: ",", strip_insignificant_zeros: true)
|
|
120
|
+
else
|
|
121
|
+
number_to_delimited(value)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def currency_options
|
|
126
|
+
options = {precision: precision(2)}
|
|
127
|
+
options[:unit] = card.options[:unit] if card.options[:unit]
|
|
128
|
+
options
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def precision(default)
|
|
132
|
+
card.options.fetch(:precision, default)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# The placeholder a lazy card's frame shows until its response lands.
|
|
7
|
+
# Shaped like the card it stands in for, so the grid does not jump when
|
|
8
|
+
# the real card arrives.
|
|
9
|
+
class Skeleton < Plutonium::UI::Component::Base
|
|
10
|
+
def initialize(card:)
|
|
11
|
+
@card = card
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def view_template
|
|
15
|
+
div(role: "status", class: "pu-card pu-dashboard-card pu-dashboard-skeleton h-full motion-safe:animate-pulse") do
|
|
16
|
+
div(class: "h-3 w-24 rounded-full bg-[var(--pu-border-strong)]")
|
|
17
|
+
render_body
|
|
18
|
+
span(class: "sr-only") { t("plutonium.dashboard.loading_card", card: @card.label) }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def render_body
|
|
25
|
+
case @card.kind
|
|
26
|
+
when :metric
|
|
27
|
+
div(class: "mt-4 h-8 w-32 rounded-[var(--pu-radius-md)] bg-[var(--pu-border)]")
|
|
28
|
+
div(class: "mt-3 h-2 w-20 rounded-full bg-[var(--pu-border)]")
|
|
29
|
+
when :chart
|
|
30
|
+
div(class: "mt-4 rounded-[var(--pu-radius-md)] bg-[var(--pu-border)]", style: "height: #{@card.options[:height]};")
|
|
31
|
+
else
|
|
32
|
+
div(class: "mt-4 h-2 w-full rounded-full bg-[var(--pu-border)]")
|
|
33
|
+
div(class: "mt-3 h-2 w-5/6 rounded-full bg-[var(--pu-border)]")
|
|
34
|
+
div(class: "mt-3 h-2 w-2/3 rounded-full bg-[var(--pu-border)]")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -81,7 +81,7 @@ module Plutonium
|
|
|
81
81
|
end
|
|
82
82
|
|
|
83
83
|
def attachment_extension(attachment)
|
|
84
|
-
attachment.try(:extension) || File.extname(attachment.filename.to_s)
|
|
84
|
+
attachment.try(:extension).presence || File.extname(attachment.filename.to_s).delete(".")
|
|
85
85
|
end
|
|
86
86
|
|
|
87
87
|
def normalize_value(value)
|
|
@@ -62,10 +62,16 @@ module Plutonium
|
|
|
62
62
|
def render_value(value)
|
|
63
63
|
variant = self.class.variant_for(value, colors: @colors)
|
|
64
64
|
span(**attributes, class: tokens("pu-badge", "pu-badge-#{variant}")) do
|
|
65
|
-
plain
|
|
65
|
+
plain value_label(value)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# plutonium.values.<model>.<attr>.<value> or the Rails enum key
|
|
70
|
+
# activerecord.attributes.<model>.<attr>/<value>, else humanized.
|
|
71
|
+
def value_label(value)
|
|
72
|
+
Plutonium::Translation.value_label(field.object.class, field.key, value) || self.class.humanize(value)
|
|
73
|
+
end
|
|
74
|
+
|
|
69
75
|
protected
|
|
70
76
|
|
|
71
77
|
def build_attributes
|
|
@@ -25,9 +25,9 @@ module Plutonium
|
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
def true_label = @true_label || "
|
|
28
|
+
def true_label = @true_label || Plutonium::Translation.t("plutonium.boolean.true")
|
|
29
29
|
|
|
30
|
-
def false_label = @false_label || "
|
|
30
|
+
def false_label = @false_label || Plutonium::Translation.t("plutonium.boolean.false")
|
|
31
31
|
|
|
32
32
|
def build_attributes
|
|
33
33
|
@true_label = attributes.delete(:true_label)
|
|
@@ -123,6 +123,9 @@ module Plutonium
|
|
|
123
123
|
condition = section.condition
|
|
124
124
|
next if condition && !instance_exec(&condition)
|
|
125
125
|
next if resolved.fields.empty?
|
|
126
|
+
# A section whose fields are all hidden by their own `condition:`
|
|
127
|
+
# would render as an empty card; hidden display fields emit nothing.
|
|
128
|
+
next if resolved.fields.all? { |name| field_condition_hidden?(name) }
|
|
126
129
|
|
|
127
130
|
Plutonium::Definition::FormLayout::ResolvedSection.new(
|
|
128
131
|
section: Plutonium::Definition::FormLayout::Section.new(
|
|
@@ -147,16 +150,7 @@ module Plutonium
|
|
|
147
150
|
# `condition:` is excluded for the same reason it is on the form: it
|
|
148
151
|
# asks "should this render here, now?", which is resolved separately
|
|
149
152
|
# and against this display, not turned into a value up front.
|
|
150
|
-
def resolve_section_option_procs(options)
|
|
151
|
-
return options if options.blank?
|
|
152
|
-
|
|
153
|
-
options.to_h do |key, value|
|
|
154
|
-
resolvable = key != :condition && value.is_a?(Proc)
|
|
155
|
-
next [key, value] unless resolvable
|
|
156
|
-
|
|
157
|
-
[key, value.arity.zero? ? value.call : value.call(self)]
|
|
158
|
-
end
|
|
159
|
-
end
|
|
153
|
+
def resolve_section_option_procs(options) = resolve_option_procs(options)
|
|
160
154
|
|
|
161
155
|
# Pure presentation — the section is already resolved (visible) by
|
|
162
156
|
# resolve_display_layout.
|
|
@@ -206,7 +200,7 @@ module Plutonium
|
|
|
206
200
|
|
|
207
201
|
tablist.with_tab(
|
|
208
202
|
identifier: "details",
|
|
209
|
-
title: -> { plain "
|
|
203
|
+
title: -> { plain t("plutonium.ui.details") }
|
|
210
204
|
) do
|
|
211
205
|
render details_display
|
|
212
206
|
end
|
|
@@ -279,6 +273,14 @@ module Plutonium
|
|
|
279
273
|
raise ArgumentError, "#{object.class}##{name} defined in #permitted_associations, but #{reflection.klass} is not a registered resource"
|
|
280
274
|
end
|
|
281
275
|
|
|
276
|
+
# Whether +name+ is hidden on this render by its own `condition:` — the
|
|
277
|
+
# same check render_resource_field makes before rendering it.
|
|
278
|
+
def field_condition_hidden?(name)
|
|
279
|
+
condition = resource_definition.defined_displays.dig(name, :options, :condition) ||
|
|
280
|
+
resource_definition.defined_fields.dig(name, :options, :condition)
|
|
281
|
+
condition.present? && !instance_exec(&condition)
|
|
282
|
+
end
|
|
283
|
+
|
|
282
284
|
def render_resource_field(name)
|
|
283
285
|
when_permitted(name) do
|
|
284
286
|
# field :name, as: :string
|
|
@@ -294,6 +296,9 @@ module Plutonium
|
|
|
294
296
|
display_definition = resource_definition.defined_displays[name] || {}
|
|
295
297
|
display_options = display_definition[:options] || {}
|
|
296
298
|
|
|
299
|
+
field_options = resolve_field_level_procs(field_options)
|
|
300
|
+
display_options = resolve_field_level_procs(display_options)
|
|
301
|
+
|
|
297
302
|
# Check for conditional rendering
|
|
298
303
|
condition = display_options[:condition] || field_options[:condition]
|
|
299
304
|
conditionally_hidden = condition && !instance_exec(&condition)
|
|
@@ -301,13 +306,15 @@ module Plutonium
|
|
|
301
306
|
|
|
302
307
|
tag = display_options[:as] || field_options[:as]
|
|
303
308
|
|
|
304
|
-
#
|
|
305
|
-
#
|
|
306
|
-
|
|
307
|
-
field_level_options = display_options.slice(*
|
|
309
|
+
# Route the display's own field-level options to field(); strip the
|
|
310
|
+
# full union from the tag attributes so a form-only key (e.g. :hint)
|
|
311
|
+
# declared here is dropped rather than leaked as an HTML attribute.
|
|
312
|
+
field_level_options = display_options.slice(*DISPLAY_FIELD_LEVEL_KEYS)
|
|
308
313
|
field_options = field_options.merge(field_level_options)
|
|
314
|
+
# A blank description falls back to plutonium.fields.<model>.<attr>.description.
|
|
315
|
+
field_options = Plutonium::Translation.fill_field_text(field_options, object.class, name, :description)
|
|
309
316
|
|
|
310
|
-
tag_attributes = display_options.except(:wrapper, :as, :condition, *
|
|
317
|
+
tag_attributes = display_options.except(:wrapper, :as, :condition, *FIELD_LEVEL_KEYS)
|
|
311
318
|
|
|
312
319
|
# A `formatter:` produces the display string itself, so render it
|
|
313
320
|
# through the formatted-value component regardless of the field's
|
|
@@ -45,7 +45,7 @@ module Plutonium
|
|
|
45
45
|
style: PRIMARY_STYLE
|
|
46
46
|
) do
|
|
47
47
|
render Phlex::TablerIcons::Download.new(class: "w-4 h-4 shrink-0")
|
|
48
|
-
span { "
|
|
48
|
+
span { t("plutonium.ui.export.export") }
|
|
49
49
|
end
|
|
50
50
|
end
|
|
51
51
|
|
|
@@ -54,7 +54,7 @@ module Plutonium
|
|
|
54
54
|
type: "button",
|
|
55
55
|
class: "pu-btn pu-btn-outline pu-btn-sm",
|
|
56
56
|
style: CARET_STYLE,
|
|
57
|
-
aria: {expanded: "false", haspopup: "menu", label: "
|
|
57
|
+
aria: {expanded: "false", haspopup: "menu", label: t("plutonium.ui.export.more_options")},
|
|
58
58
|
data: {resource_drop_down_target: "trigger"}
|
|
59
59
|
) do
|
|
60
60
|
render Phlex::TablerIcons::ChevronDown.new(class: "w-4 h-4")
|
|
@@ -76,7 +76,7 @@ module Plutonium
|
|
|
76
76
|
class: "flex items-center gap-2 px-4 py-2 text-sm text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] transition-colors"
|
|
77
77
|
) do
|
|
78
78
|
render Phlex::TablerIcons::Download.new(class: "w-4 h-4")
|
|
79
|
-
span { "
|
|
79
|
+
span { t("plutonium.ui.export.export_all") }
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
end
|
|
@@ -81,7 +81,7 @@ module Plutonium
|
|
|
81
81
|
# Key input
|
|
82
82
|
input(
|
|
83
83
|
type: :text,
|
|
84
|
-
placeholder: "
|
|
84
|
+
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.key_placeholder"),
|
|
85
85
|
value: key,
|
|
86
86
|
name: "#{field_name}[#{index}][key]",
|
|
87
87
|
id: "#{field.dom.id}_#{index}_key",
|
|
@@ -92,7 +92,7 @@ module Plutonium
|
|
|
92
92
|
# Value input
|
|
93
93
|
input(
|
|
94
94
|
type: :text,
|
|
95
|
-
placeholder: "
|
|
95
|
+
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.value_placeholder"),
|
|
96
96
|
value: value,
|
|
97
97
|
name: "#{field_name}[#{index}][value]",
|
|
98
98
|
id: "#{field.dom.id}_#{index}_value",
|
|
@@ -122,7 +122,7 @@ module Plutonium
|
|
|
122
122
|
key_value_store_target: "addButton"
|
|
123
123
|
}
|
|
124
124
|
) do
|
|
125
|
-
plain "
|
|
125
|
+
plain Plutonium::Translation.t("plutonium.ui.form.key_value_store.add_pair")
|
|
126
126
|
end
|
|
127
127
|
end
|
|
128
128
|
end
|
|
@@ -135,7 +135,7 @@ module Plutonium
|
|
|
135
135
|
) do
|
|
136
136
|
input(
|
|
137
137
|
type: :text,
|
|
138
|
-
placeholder: "
|
|
138
|
+
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.key_placeholder"),
|
|
139
139
|
name: "#{field_name}[__INDEX__][key]",
|
|
140
140
|
id: "#{field.dom.id}___INDEX___key",
|
|
141
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)]",
|
|
@@ -144,7 +144,7 @@ module Plutonium
|
|
|
144
144
|
|
|
145
145
|
input(
|
|
146
146
|
type: :text,
|
|
147
|
-
placeholder: "
|
|
147
|
+
placeholder: Plutonium::Translation.t("plutonium.ui.form.key_value_store.value_placeholder"),
|
|
148
148
|
name: "#{field_name}[__INDEX__][value]",
|
|
149
149
|
id: "#{field.dom.id}___INDEX___value",
|
|
150
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)]",
|
|
@@ -74,9 +74,9 @@ module Plutonium
|
|
|
74
74
|
def apply_default_hint(value)
|
|
75
75
|
return if field.has_hint?
|
|
76
76
|
if value == SENTINEL
|
|
77
|
-
field.hint("
|
|
77
|
+
field.hint(Plutonium::Translation.t("plutonium.ui.form.password.keep_current_hint"))
|
|
78
78
|
elsif reentry_required?
|
|
79
|
-
field.hint("
|
|
79
|
+
field.hint(Plutonium::Translation.t("plutonium.ui.form.password.reenter_hint"))
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
|
|
@@ -34,13 +34,37 @@ module Plutonium
|
|
|
34
34
|
# capped at `limit`. Shared by `choices` (with the limit) and
|
|
35
35
|
# `normalize_simple_input` (without — so typeahead picks beyond the
|
|
36
36
|
# rendered subset still validate).
|
|
37
|
+
#
|
|
38
|
+
# The custom `scope:` (set on the Association filter) narrows the
|
|
39
|
+
# relation BEFORE authorization, so policy can only further reduce —
|
|
40
|
+
# never widen — what the scope allows. Mirrors the kanban/positioning
|
|
41
|
+
# scope pattern.
|
|
37
42
|
def authorized_relation(limit: nil)
|
|
38
43
|
relation = @association_class.all
|
|
44
|
+
relation = apply_scope(relation, @scope)
|
|
39
45
|
relation = relation.limit(limit) if limit && relation.respond_to?(:limit)
|
|
40
46
|
return relation if @skip_authorization
|
|
41
47
|
authorized_resource_scope(@association_class, relation: relation)
|
|
42
48
|
end
|
|
43
49
|
|
|
50
|
+
# Applies an optional association scope to a relation.
|
|
51
|
+
# Symbol → relation.public_send(sym) (named scope, e.g. :verified)
|
|
52
|
+
# Proc → arity 0 → relation.instance_exec(&scope) (kanban form: -> { where(...) })
|
|
53
|
+
# → arity ≥1 → scope.call(relation) (documented form: ->(s) { s.verified })
|
|
54
|
+
# nil → relation unchanged
|
|
55
|
+
# The arity dispatch matches call_option_proc (Form::Resource) so the
|
|
56
|
+
# documented `->(s) { s.active }` signature works AND the zero-arg
|
|
57
|
+
# kanban form works. Unsupported types fail loud rather than silently
|
|
58
|
+
# doing nothing (the original bug).
|
|
59
|
+
def apply_scope(relation, scope)
|
|
60
|
+
case scope
|
|
61
|
+
when Symbol then relation.public_send(scope)
|
|
62
|
+
when Proc then scope.arity.zero? ? relation.instance_exec(&scope) : scope.call(relation)
|
|
63
|
+
when nil then relation
|
|
64
|
+
else raise ArgumentError, "Unsupported association scope: #{scope.inspect} (expected Symbol, Proc, or nil)"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
44
68
|
def build_attributes
|
|
45
69
|
# Defaults must land BEFORE super — AcceptsChoices.build_attributes
|
|
46
70
|
# consumes :value_method / :label_method off `attributes` into
|
|
@@ -52,6 +76,7 @@ module Plutonium
|
|
|
52
76
|
|
|
53
77
|
@association_class = attributes.delete(:association_class)
|
|
54
78
|
@skip_authorization = attributes.delete(:skip_authorization)
|
|
79
|
+
@scope = attributes.delete(:scope)
|
|
55
80
|
@choice_limit = attributes.fetch(:choice_limit) { DEFAULT_CHOICE_LIMIT }
|
|
56
81
|
attributes.delete(:choice_limit)
|
|
57
82
|
# Stash the typeahead option; the URL helper needs view_context
|
|
@@ -103,6 +128,8 @@ module Plutonium
|
|
|
103
128
|
end
|
|
104
129
|
|
|
105
130
|
choices.values.find { |opt| same_record?(input_value, opt) } && input_value
|
|
131
|
+
rescue
|
|
132
|
+
nil
|
|
106
133
|
end
|
|
107
134
|
|
|
108
135
|
# Two values point at the same record when both decode to the
|
|
@@ -156,7 +156,7 @@ module Plutonium
|
|
|
156
156
|
data: {action: "click->attachment-preview#remove"}
|
|
157
157
|
) do
|
|
158
158
|
span(class: "bi bi-trash")
|
|
159
|
-
plain "
|
|
159
|
+
plain Plutonium::Translation.t("plutonium.ui.form.attachment.delete")
|
|
160
160
|
end
|
|
161
161
|
end
|
|
162
162
|
|
|
@@ -165,7 +165,7 @@ module Plutonium
|
|
|
165
165
|
end
|
|
166
166
|
|
|
167
167
|
def attachment_extension(attachment)
|
|
168
|
-
attachment.try(:extension) || File.extname(attachment.filename.to_s)
|
|
168
|
+
attachment.try(:extension).presence || File.extname(attachment.filename.to_s).delete(".")
|
|
169
169
|
end
|
|
170
170
|
|
|
171
171
|
def build_direct_upload_options
|
|
@@ -184,13 +184,21 @@ module Plutonium
|
|
|
184
184
|
max_file_size: nil,
|
|
185
185
|
min_file_size: nil,
|
|
186
186
|
max_total_size: nil,
|
|
187
|
-
max_file_num: attributes.fetch(:size
|
|
187
|
+
max_file_num: attributes.fetch(:size) { field.multiple? ? field.limit : 1 },
|
|
188
188
|
min_file_num: nil,
|
|
189
189
|
allowed_file_types: nil,
|
|
190
190
|
required_meta_fields: nil,
|
|
191
191
|
endpoint: attributes[:endpoint] || "/upload"
|
|
192
192
|
}.each do |key, default_value|
|
|
193
193
|
value = attributes.key?(key) ? attributes.delete(key) : default_value
|
|
194
|
+
# The attachment-input controller declares Array/Object value types,
|
|
195
|
+
# which Stimulus reads with JSON.parse. Phlex would otherwise render an
|
|
196
|
+
# array attribute space-joined (its class-list behaviour), so
|
|
197
|
+
# allowed_file_types: [".csv", "text/csv"] reached the controller as
|
|
198
|
+
# ".csv text/csv" and JSON.parse threw on connect — after the native
|
|
199
|
+
# file input was already hidden, leaving no usable file picker. Emit
|
|
200
|
+
# structured values as JSON, matching intl_tel_input's options.
|
|
201
|
+
value = value.to_json if value.is_a?(Array) || value.is_a?(Hash)
|
|
194
202
|
direct_upload_options[:data][:"attachment_input_#{key}_value"] = value
|
|
195
203
|
end
|
|
196
204
|
|
|
@@ -263,7 +263,7 @@ module Plutonium
|
|
|
263
263
|
def render_nested_fields_add_button_content(name)
|
|
264
264
|
span(class: "bg-secondary-700 text-white hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800 flex items-center justify-center px-4 py-1.5 text-sm font-medium rounded-lg focus:outline-none focus:ring-4") do
|
|
265
265
|
render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
|
|
266
|
-
span { "
|
|
266
|
+
span { t("plutonium.ui.form.repeater.add", name: name.to_s.singularize.humanize) }
|
|
267
267
|
end
|
|
268
268
|
end
|
|
269
269
|
|
|
@@ -29,7 +29,7 @@ module Plutonium
|
|
|
29
29
|
div(class: "flex items-center justify-end") do
|
|
30
30
|
button(type: :button, class: REMOVE_BUTTON_CLASS, data_action: action) do
|
|
31
31
|
render Phlex::TablerIcons::Trash.new(class: "w-4 h-4")
|
|
32
|
-
span { "
|
|
32
|
+
span { t("plutonium.ui.form.repeater.remove") }
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
end
|
|
@@ -39,10 +39,11 @@ module Plutonium
|
|
|
39
39
|
# a left danger stripe + struck-through label read as "pending delete".
|
|
40
40
|
#
|
|
41
41
|
# @param restore_action [String] Stimulus action for the Restore button
|
|
42
|
-
# @param label [String] text shown beside the trash icon
|
|
42
|
+
# @param label [String, nil] text shown beside the trash icon; defaults to "Removed"
|
|
43
43
|
# @param bar_data [Hash] extra data attributes (Stimulus target/marker)
|
|
44
44
|
# the controller uses to find and toggle this bar
|
|
45
|
-
def render_repeater_removed_bar(restore_action:, label:
|
|
45
|
+
def render_repeater_removed_bar(restore_action:, label: nil, **bar_data)
|
|
46
|
+
label ||= t("plutonium.ui.form.repeater.removed")
|
|
46
47
|
div(
|
|
47
48
|
hidden: true,
|
|
48
49
|
class: "-m-4 flex items-center justify-between gap-3 px-4 py-2.5 " \
|
|
@@ -56,7 +57,7 @@ module Plutonium
|
|
|
56
57
|
end
|
|
57
58
|
button(type: :button, class: RESTORE_BUTTON_CLASS, data_action: restore_action) do
|
|
58
59
|
render Phlex::TablerIcons::ArrowBackUp.new(class: "w-4 h-4")
|
|
59
|
-
span { "
|
|
60
|
+
span { t("plutonium.ui.form.repeater.restore") }
|
|
60
61
|
end
|
|
61
62
|
end
|
|
62
63
|
end
|
|
@@ -137,7 +137,7 @@ module Plutonium
|
|
|
137
137
|
) do
|
|
138
138
|
span(class: "bg-secondary-700 text-white flex items-center justify-center px-4 py-1.5 text-sm font-medium rounded-lg") do
|
|
139
139
|
render Phlex::TablerIcons::Plus.new(class: "w-4 h-4 mr-1")
|
|
140
|
-
span { "
|
|
140
|
+
span { t("plutonium.ui.form.repeater.add", name: name.to_s.singularize.humanize) }
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
end
|
|
@@ -82,7 +82,7 @@ module Plutonium
|
|
|
82
82
|
render Phlex::TablerIcons::Search.new(class: "w-5 h-5 text-[var(--pu-text-muted)]")
|
|
83
83
|
end
|
|
84
84
|
render field(:search, value: search_query)
|
|
85
|
-
.placeholder("
|
|
85
|
+
.placeholder(t("plutonium.ui.table.search_placeholder"))
|
|
86
86
|
.input_tag(
|
|
87
87
|
value: search_query,
|
|
88
88
|
class: "pu-input pu-input-icon-left w-full",
|
|
@@ -106,7 +106,7 @@ module Plutonium
|
|
|
106
106
|
data: {resource_drop_down_target: "trigger"}
|
|
107
107
|
) do
|
|
108
108
|
render Phlex::TablerIcons::Filter.new(class: "w-4 h-4 inline-block align-text-bottom")
|
|
109
|
-
plain "
|
|
109
|
+
plain " #{t("plutonium.ui.table.filters")}"
|
|
110
110
|
if active_count > 0
|
|
111
111
|
plain " "
|
|
112
112
|
span(class: "inline-flex items-center justify-center w-5 h-5 text-xs font-semibold rounded-full text-gray-800 bg-white") do
|
|
@@ -142,13 +142,13 @@ module Plutonium
|
|
|
142
142
|
) do
|
|
143
143
|
render Phlex::TablerIcons::X.new(class: "w-5 h-5")
|
|
144
144
|
end
|
|
145
|
-
span(class: "text-sm font-semibold text-[var(--pu-text)]") { "
|
|
145
|
+
span(class: "text-sm font-semibold text-[var(--pu-text)]") { t("plutonium.ui.table.filters") }
|
|
146
146
|
end
|
|
147
147
|
button(
|
|
148
148
|
type: "button",
|
|
149
149
|
class: "text-sm text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] transition-colors",
|
|
150
150
|
data: {action: "filter-panel#clear"}
|
|
151
|
-
) { "
|
|
151
|
+
) { t("plutonium.ui.table.clear_all") }
|
|
152
152
|
end
|
|
153
153
|
|
|
154
154
|
# Scrollable filter fields
|
|
@@ -160,9 +160,9 @@ module Plutonium
|
|
|
160
160
|
inputs.each do |input_name, _|
|
|
161
161
|
# For multi-input filters (like date range), include the input name in the label
|
|
162
162
|
label = if has_multiple_inputs
|
|
163
|
-
"
|
|
163
|
+
t("plutonium.query.filter_input_label", label: query_object.filter_label(filter_name), input: input_name.to_s.humanize.downcase)
|
|
164
164
|
else
|
|
165
|
-
filter_name
|
|
165
|
+
query_object.filter_label(filter_name)
|
|
166
166
|
end
|
|
167
167
|
render_filter_field nested, definition, input_name, filter_label: label
|
|
168
168
|
end
|
|
@@ -175,7 +175,7 @@ module Plutonium
|
|
|
175
175
|
render field(:submit).submit_button_tag(
|
|
176
176
|
name: nil,
|
|
177
177
|
class!: "pu-btn pu-btn-md pu-btn-primary w-full"
|
|
178
|
-
) { "
|
|
178
|
+
) { t("plutonium.ui.table.apply_filters") }
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
@@ -196,7 +196,7 @@ module Plutonium
|
|
|
196
196
|
label(class: "text-sm font-medium text-[var(--pu-text)]") { filter_label }
|
|
197
197
|
nested.field(name, **field_options) do |f|
|
|
198
198
|
# Set placeholder for blank option text in selects
|
|
199
|
-
f.placeholder(input_options[:include_blank] || "
|
|
199
|
+
f.placeholder(input_options[:include_blank] || t("plutonium.ui.table.all")) if input_options[:include_blank]
|
|
200
200
|
render instance_exec(f, &tag_block)
|
|
201
201
|
end
|
|
202
202
|
end
|