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,227 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
# Translation lookups for everything Plutonium renders.
|
|
5
|
+
#
|
|
6
|
+
# All of Plutonium's own strings live under the `plutonium.*` namespace in
|
|
7
|
+
# `config/locales`, loaded at the lowest precedence so a package, portal or
|
|
8
|
+
# the host application can override any key. Beyond the fixed strings, this
|
|
9
|
+
# module resolves the *derived* text a definition never declares (field
|
|
10
|
+
# placeholders and hints, action / scope / filter labels, kanban columns,
|
|
11
|
+
# wizard steps) by convention, with a portal-specific layer on top:
|
|
12
|
+
#
|
|
13
|
+
# plutonium.portals.<portal>.fields.<model>.<attr>.placeholder
|
|
14
|
+
# plutonium.fields.<model>.<attr>.placeholder
|
|
15
|
+
#
|
|
16
|
+
# `<portal>` is the package namespace (`admin_portal`), `<model>` the
|
|
17
|
+
# `model_name.i18n_key` (`blogging/article`), walked up `lookup_ancestors`
|
|
18
|
+
# the way `human_attribute_name` does. Every lookup is a plain hash probe on
|
|
19
|
+
# the I18n backend and returns nil when nothing is defined, so a missing key
|
|
20
|
+
# costs a few microseconds and renders exactly what it did before.
|
|
21
|
+
#
|
|
22
|
+
# Not named `Plutonium::I18n` on purpose: that constant would shadow `::I18n`
|
|
23
|
+
# inside every `module Plutonium` body.
|
|
24
|
+
module Translation
|
|
25
|
+
extend self
|
|
26
|
+
|
|
27
|
+
# Request-local portal, set by the controller so lookups deep in the query
|
|
28
|
+
# layer (filter labels, kanban rejections) see the same portal the view does.
|
|
29
|
+
class Current < ActiveSupport::CurrentAttributes
|
|
30
|
+
attribute :portal, :value_labels
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Kinds of derived labels resolved by convention. The value is the key
|
|
34
|
+
# segment under `plutonium.` (and under `plutonium.portals.<portal>.`).
|
|
35
|
+
LABEL_KINDS = {
|
|
36
|
+
action: "actions",
|
|
37
|
+
scope: "scopes",
|
|
38
|
+
filter: "filters",
|
|
39
|
+
kanban_column: "kanban_columns",
|
|
40
|
+
wizard_step: "wizard_steps"
|
|
41
|
+
}.freeze
|
|
42
|
+
|
|
43
|
+
# Field slots resolved by convention when the definition leaves them blank.
|
|
44
|
+
FIELD_SLOTS = %i[placeholder hint description].freeze
|
|
45
|
+
|
|
46
|
+
# Plain translation of a fixed Plutonium string. Full keys only; there is
|
|
47
|
+
# no lazy-lookup prefix, so every call site is greppable.
|
|
48
|
+
def t(key, **options)
|
|
49
|
+
::I18n.t(key, **options)
|
|
50
|
+
end
|
|
51
|
+
alias_method :translate, :t
|
|
52
|
+
|
|
53
|
+
# Translate a Pagy dictionary key with the current I18n locale. Pagy keeps
|
|
54
|
+
# its own (faster) dictionaries and a thread-local locale; sync the locale
|
|
55
|
+
# per call so a host that switches `I18n.locale` per request is honoured.
|
|
56
|
+
def pagy(key, **options)
|
|
57
|
+
::Pagy::I18n.locale = ::I18n.locale.to_s
|
|
58
|
+
::Pagy::I18n.translate(key, **options)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Resolve an option value that may be a lazy translation (a proc built by
|
|
62
|
+
# {Lazy#t}) or any other zero-arity callable.
|
|
63
|
+
def resolve(value)
|
|
64
|
+
value.is_a?(Proc) ? value.call : value
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# The i18n key segment for a package / portal module, or nil for the main app.
|
|
68
|
+
def portal_key(package)
|
|
69
|
+
return if package.nil?
|
|
70
|
+
|
|
71
|
+
(package.respond_to?(:name) ? package.name : package.to_s).underscore
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Text for a field slot (:placeholder, :hint, :description) resolved by
|
|
75
|
+
# convention. Returns nil when no key is defined.
|
|
76
|
+
#
|
|
77
|
+
# field_text(Blogging::Article, :title, :placeholder)
|
|
78
|
+
# # => plutonium.portals.<portal>.fields.blogging/article.title.placeholder
|
|
79
|
+
# # plutonium.fields.blogging/article.title.placeholder
|
|
80
|
+
# # helpers.placeholder.blogging/article.title (placeholder only)
|
|
81
|
+
def field_text(klass, attribute, slot, portal: Current.portal)
|
|
82
|
+
raise ArgumentError, "unknown field slot #{slot.inspect}" unless FIELD_SLOTS.include?(slot)
|
|
83
|
+
|
|
84
|
+
model_keys_for(klass).each do |model_key|
|
|
85
|
+
[portal_scope(portal, "fields"), "plutonium.fields"].compact.each do |scope|
|
|
86
|
+
value = probe("#{scope}.#{model_key}.#{attribute}.#{slot}")
|
|
87
|
+
return value if value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
next unless slot == :placeholder
|
|
91
|
+
|
|
92
|
+
value = probe("helpers.placeholder.#{model_key}.#{attribute}")
|
|
93
|
+
return value if value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Whether the locale defines one/other forms for the model's name
|
|
100
|
+
# (`activerecord.models.<model>: {one:, other:}`), on it or an ancestor.
|
|
101
|
+
def locale_plural?(klass)
|
|
102
|
+
return false unless klass.respond_to?(:i18n_scope)
|
|
103
|
+
|
|
104
|
+
model_keys_for(klass).any? { |key| ::I18n.t("#{klass.i18n_scope}.models.#{key}", default: nil).is_a?(Hash) }
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Fill the convention text into `options` for each slot the definition
|
|
108
|
+
# left blank, e.g. `fill_field_text(opts, Blogging::Post, :title, :placeholder, :hint)`.
|
|
109
|
+
def fill_field_text(options, klass, attribute, *slots)
|
|
110
|
+
slots.each do |slot|
|
|
111
|
+
next if options.key?(slot)
|
|
112
|
+
|
|
113
|
+
text = field_text(klass, attribute, slot)
|
|
114
|
+
options = options.merge(slot => text) if text
|
|
115
|
+
end
|
|
116
|
+
options
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Label for a derived key (an action, scope, filter, kanban column or
|
|
120
|
+
# wizard step) resolved by convention. Returns nil when no key is defined,
|
|
121
|
+
# so callers keep their `humanize` fallback.
|
|
122
|
+
#
|
|
123
|
+
# label_for(:action, Blogging::Article, :publish)
|
|
124
|
+
# # => plutonium.portals.<portal>.actions.blogging/article.publish
|
|
125
|
+
# # plutonium.actions.blogging/article.publish
|
|
126
|
+
def label_for(kind, klass, key, portal: Current.portal)
|
|
127
|
+
segment = LABEL_KINDS.fetch(kind) { raise ArgumentError, "unknown label kind #{kind.inspect}" }
|
|
128
|
+
|
|
129
|
+
model_keys_for(klass).each do |model_key|
|
|
130
|
+
[portal_scope(portal, segment), "plutonium.#{segment}"].compact.each do |scope|
|
|
131
|
+
value = probe("#{scope}.#{model_key}.#{key}")
|
|
132
|
+
return value if value
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Text for a dashboard or one of its cards, resolved by convention with the
|
|
140
|
+
# same portal layering as {#label_for}. Returns nil when nothing is defined.
|
|
141
|
+
#
|
|
142
|
+
# dashboard_text(SalesDashboard, :label)
|
|
143
|
+
# # => plutonium.portals.<portal>.dashboards.sales.label
|
|
144
|
+
# # plutonium.dashboards.sales.label
|
|
145
|
+
# dashboard_text(SalesDashboard, :cards, :orders, :label)
|
|
146
|
+
# # => plutonium.portals.<portal>.dashboards.sales.cards.orders.label
|
|
147
|
+
# # plutonium.dashboards.sales.cards.orders.label
|
|
148
|
+
def dashboard_text(dashboard_class, *path, portal: Current.portal)
|
|
149
|
+
return if dashboard_class.nil?
|
|
150
|
+
|
|
151
|
+
suffix = [dashboard_class.i18n_key, *path].join(".")
|
|
152
|
+
[portal_scope(portal, "dashboards"), "plutonium.dashboards"].compact.each do |scope|
|
|
153
|
+
value = probe("#{scope}.#{suffix}")
|
|
154
|
+
return value if value
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Label for an enum-like value rendered by a badge or filter, resolved the
|
|
161
|
+
# way Rails resolves enum attribute values:
|
|
162
|
+
#
|
|
163
|
+
# activerecord.attributes.<model>.<attribute>/<value> (Rails convention)
|
|
164
|
+
# plutonium.values.<model>.<attribute>.<value>
|
|
165
|
+
#
|
|
166
|
+
# Badges render once per cell, so the answer is memoised for the request.
|
|
167
|
+
def value_label(klass, attribute, value, portal: Current.portal)
|
|
168
|
+
return if value.nil? || klass.nil?
|
|
169
|
+
|
|
170
|
+
cache = (Current.value_labels ||= {})
|
|
171
|
+
cache.fetch([::I18n.locale, portal_key(portal), klass, attribute, value.to_s]) do |cache_key|
|
|
172
|
+
cache[cache_key] = uncached_value_label(klass, attribute, value, portal)
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def uncached_value_label(klass, attribute, value, portal)
|
|
177
|
+
model_keys_for(klass).each do |model_key|
|
|
178
|
+
[portal_scope(portal, "values"), "plutonium.values"].compact.each do |scope|
|
|
179
|
+
found = probe("#{scope}.#{model_key}.#{attribute}.#{value}")
|
|
180
|
+
return found if found
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
if klass.respond_to?(:human_attribute_name)
|
|
184
|
+
found = probe("#{klass.i18n_scope}.attributes.#{model_key}.#{attribute}/#{value}")
|
|
185
|
+
return found if found
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
nil
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Class-level `t` for definitions, interactions and wizards. Returns a lazy
|
|
193
|
+
# proc so the lookup runs on every render in the request's locale rather
|
|
194
|
+
# than once, in whatever locale was active when the class loaded.
|
|
195
|
+
#
|
|
196
|
+
# input :email, placeholder: t("forms.shared.email_placeholder")
|
|
197
|
+
# action :publish, label: t("plutonium.actions.blogging/article.publish")
|
|
198
|
+
module Lazy
|
|
199
|
+
def t(key, **options)
|
|
200
|
+
-> { ::I18n.t(key, **options) }
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private :uncached_value_label
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def probe(key)
|
|
209
|
+
value = ::I18n.t(key, default: nil)
|
|
210
|
+
value.is_a?(String) ? value : nil
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def portal_scope(portal, segment)
|
|
214
|
+
key = portal_key(portal)
|
|
215
|
+
key && "plutonium.portals.#{key}.#{segment}"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# `blogging/article`, then each STI / abstract ancestor that has a model
|
|
219
|
+
# name, mirroring ActiveModel::Translation#human_attribute_name.
|
|
220
|
+
def model_keys_for(klass)
|
|
221
|
+
return [] unless klass.respond_to?(:model_name)
|
|
222
|
+
|
|
223
|
+
ancestors = klass.respond_to?(:lookup_ancestors) ? klass.lookup_ancestors : [klass]
|
|
224
|
+
ancestors.filter_map { |k| k.model_name.i18n_key if k.respond_to?(:model_name) }
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
end
|
|
@@ -26,8 +26,8 @@ module Plutonium
|
|
|
26
26
|
aria: {expanded: "false", haspopup: "true"},
|
|
27
27
|
data: {resource_drop_down_target: "trigger"}
|
|
28
28
|
) do
|
|
29
|
-
span(class: "sr-only") { "
|
|
30
|
-
plain "
|
|
29
|
+
span(class: "sr-only") { t("plutonium.ui.actions_dropdown.open_menu") }
|
|
30
|
+
plain t("plutonium.ui.actions_dropdown.actions")
|
|
31
31
|
render Phlex::TablerIcons::ChevronDown.new(class: "w-4 h-4 ml-1")
|
|
32
32
|
end
|
|
33
33
|
end
|
|
@@ -23,7 +23,7 @@ module Plutonium
|
|
|
23
23
|
# instant before the controller's first reflow.
|
|
24
24
|
nav(
|
|
25
25
|
class: "flex min-w-0 py-3 mb-2 overflow-hidden",
|
|
26
|
-
aria_label: "
|
|
26
|
+
aria_label: t("plutonium.ui.breadcrumbs.label"),
|
|
27
27
|
data: {controller: "breadcrumbs"}
|
|
28
28
|
) do
|
|
29
29
|
ol(
|
|
@@ -87,7 +87,7 @@ module Plutonium
|
|
|
87
87
|
"m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z"
|
|
88
88
|
)
|
|
89
89
|
end
|
|
90
|
-
span(class: "sr-only sm:not-sr-only") { plain "
|
|
90
|
+
span(class: "sr-only sm:not-sr-only") { plain t("plutonium.ui.breadcrumbs.home") }
|
|
91
91
|
end
|
|
92
92
|
end
|
|
93
93
|
end
|
|
@@ -193,7 +193,7 @@ module Plutonium
|
|
|
193
193
|
def render_overflow_trigger
|
|
194
194
|
button(
|
|
195
195
|
type: "button",
|
|
196
|
-
aria: {label: "
|
|
196
|
+
aria: {label: t("plutonium.ui.breadcrumbs.show_collapsed"), haspopup: "true", expanded: "false"},
|
|
197
197
|
data: {resource_drop_down_target: "trigger"},
|
|
198
198
|
class: "flex items-center justify-center ms-1 w-6 h-6 rounded-[var(--pu-radius-md)] " \
|
|
199
199
|
"text-[var(--pu-text-muted)] hover:text-[var(--pu-text)] hover:bg-[var(--pu-surface-alt)] transition-colors"
|
|
@@ -16,7 +16,7 @@ module Plutonium
|
|
|
16
16
|
class: BUTTON_CLASSES,
|
|
17
17
|
data_controller: "color-mode",
|
|
18
18
|
data_action: "click->color-mode#toggleMode",
|
|
19
|
-
title: "
|
|
19
|
+
title: t("plutonium.ui.color_mode.toggle")
|
|
20
20
|
) do
|
|
21
21
|
render Phlex::TablerIcons::DeviceDesktop.new(size: ICON_SIZE, stroke: ICON_STROKE, class: "color-mode-icon-auto")
|
|
22
22
|
render Phlex::TablerIcons::Sun.new(size: ICON_SIZE, stroke: ICON_STROKE, class: "color-mode-icon-light hidden")
|
|
@@ -10,6 +10,20 @@ module Plutonium
|
|
|
10
10
|
include Kit
|
|
11
11
|
include Tokens
|
|
12
12
|
|
|
13
|
+
# Phlexi field-builder options that configure the field itself, not the
|
|
14
|
+
# rendered tag. Each surface routes its OWN keys to `field()` (displays
|
|
15
|
+
# say :description, forms say :hint) and strips the UNION from the tag
|
|
16
|
+
# attributes, so a key declared on the wrong surface is dropped rather
|
|
17
|
+
# than leaked as an HTML attribute.
|
|
18
|
+
DISPLAY_FIELD_LEVEL_KEYS = %i[label description placeholder].freeze
|
|
19
|
+
FORM_FIELD_LEVEL_KEYS = %i[hint label placeholder].freeze
|
|
20
|
+
FIELD_LEVEL_KEYS = (DISPLAY_FIELD_LEVEL_KEYS | FORM_FIELD_LEVEL_KEYS).freeze
|
|
21
|
+
|
|
22
|
+
# Table-column options that configure the header, not the cell. They may
|
|
23
|
+
# be declared on `field`, `display` or `column` and flow to the column
|
|
24
|
+
# without making the column "render alone" (see Table::Resource).
|
|
25
|
+
COLUMN_FIELD_LEVEL_KEYS = %i[label align].freeze
|
|
26
|
+
|
|
13
27
|
if Rails.env.development?
|
|
14
28
|
def around_template(&)
|
|
15
29
|
comment { "open:#{self.class.name}" }
|
|
@@ -20,6 +34,38 @@ module Plutonium
|
|
|
20
34
|
|
|
21
35
|
protected
|
|
22
36
|
|
|
37
|
+
# Translate one of Plutonium's own strings. Full keys only
|
|
38
|
+
# (`t("plutonium.ui.table.filters")`), so every call site is greppable.
|
|
39
|
+
def t(key, **options)
|
|
40
|
+
Plutonium::Translation.t(key, **options)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Resolve proc-valued help / header options (label, hint, description,
|
|
44
|
+
# placeholder, align) for this render, so `label: t("...")` and other
|
|
45
|
+
# lazy values work on displays, tables and forms alike. Other option
|
|
46
|
+
# keys are left alone: a `formatter:` or `condition:` proc is a
|
|
47
|
+
# callback, not a value. Same arity rule as Form::Resource: a zero-arity
|
|
48
|
+
# proc keeps its own binding, a one-arity proc receives this component.
|
|
49
|
+
def resolve_field_level_procs(options)
|
|
50
|
+
resolvable_keys = FIELD_LEVEL_KEYS | COLUMN_FIELD_LEVEL_KEYS
|
|
51
|
+
resolve_option_procs(options) { |key| resolvable_keys.include?(key) }
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Resolve every proc-valued option the block admits (all but
|
|
55
|
+
# `condition:` by default), by the one arity rule: a zero-arity proc is
|
|
56
|
+
# called plainly and keeps its own binding, a one-arity proc receives
|
|
57
|
+
# this component for `object` / `params` / helpers.
|
|
58
|
+
def resolve_option_procs(options)
|
|
59
|
+
return options if options.blank?
|
|
60
|
+
|
|
61
|
+
options.to_h do |key, value|
|
|
62
|
+
resolvable = value.is_a?(Proc) && (block_given? ? yield(key) : key != :condition)
|
|
63
|
+
[key, resolvable ? call_option_proc(value) : value]
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def call_option_proc(value) = value.arity.zero? ? value.call : value.call(self)
|
|
68
|
+
|
|
23
69
|
def phlexi_render(arg, &)
|
|
24
70
|
return unless arg
|
|
25
71
|
raise ArgumentError, "phlexi_render requires a default render block" unless block_given?
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# The card grid. Every lazy card is a `<turbo-frame src loading="lazy">`
|
|
7
|
+
# holding a skeleton of its own shape, so the page paints at once and
|
|
8
|
+
# each card's queries run in a separate request as it scrolls into
|
|
9
|
+
# view. A card declared `lazy: false` renders inline.
|
|
10
|
+
class Board < Plutonium::UI::Component::Base
|
|
11
|
+
include Phlex::Rails::Helpers::TurboFrameTag
|
|
12
|
+
|
|
13
|
+
# The grid is 12 columns on large screens, 2 on tablets and 1 on
|
|
14
|
+
# phones. Written out in full so Tailwind's scanner sees every class it
|
|
15
|
+
# must emit; a span is picked from this table, never interpolated.
|
|
16
|
+
GRID_CLASSES = "pu-dashboard grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-12"
|
|
17
|
+
|
|
18
|
+
SPAN_CLASSES = {
|
|
19
|
+
1 => "lg:col-span-1",
|
|
20
|
+
2 => "lg:col-span-2",
|
|
21
|
+
3 => "lg:col-span-3",
|
|
22
|
+
4 => "lg:col-span-4",
|
|
23
|
+
5 => "lg:col-span-5",
|
|
24
|
+
6 => "md:col-span-2 lg:col-span-6",
|
|
25
|
+
7 => "md:col-span-2 lg:col-span-7",
|
|
26
|
+
8 => "md:col-span-2 lg:col-span-8",
|
|
27
|
+
9 => "md:col-span-2 lg:col-span-9",
|
|
28
|
+
10 => "md:col-span-2 lg:col-span-10",
|
|
29
|
+
11 => "md:col-span-2 lg:col-span-11",
|
|
30
|
+
12 => "md:col-span-2 lg:col-span-12"
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
def initialize(dashboard:)
|
|
34
|
+
@dashboard = dashboard
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def view_template
|
|
38
|
+
cards = dashboard.visible_cards
|
|
39
|
+
return EmptyCard(t("plutonium.dashboard.no_cards")) if cards.empty?
|
|
40
|
+
|
|
41
|
+
div(class: GRID_CLASSES, data: {dashboard: dashboard.class.i18n_key}) do
|
|
42
|
+
cards.each { |card| render_slot(card) }
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :dashboard
|
|
49
|
+
|
|
50
|
+
def render_slot(card)
|
|
51
|
+
if card.lazy?
|
|
52
|
+
turbo_frame_tag(
|
|
53
|
+
card.frame_id,
|
|
54
|
+
src: dashboard_card_path(card),
|
|
55
|
+
loading: "lazy",
|
|
56
|
+
refresh: "morph",
|
|
57
|
+
class: tokens("block", span_classes(card)),
|
|
58
|
+
**refresh_attributes(card)
|
|
59
|
+
) { render Skeleton.new(card:) }
|
|
60
|
+
else
|
|
61
|
+
div(class: span_classes(card)) { render Card.for(dashboard:, card:) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# On a tablet's two columns a card half the desktop row or wider takes
|
|
66
|
+
# the full row; anything narrower takes one column.
|
|
67
|
+
def span_classes(card) = SPAN_CLASSES.fetch(card.span)
|
|
68
|
+
|
|
69
|
+
# `refresh` wires the `frame-refresh` controller, which reloads the
|
|
70
|
+
# frame every N seconds while the tab is visible.
|
|
71
|
+
def refresh_attributes(card)
|
|
72
|
+
seconds = dashboard.refresh_for(card)
|
|
73
|
+
return {} unless seconds
|
|
74
|
+
|
|
75
|
+
{data: {controller: "frame-refresh", frame_refresh_interval_value: seconds}}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# The card chrome every kind shares: a `pu-card` with an optional icon,
|
|
7
|
+
# the title (linked when the card has an `href:`), an optional
|
|
8
|
+
# description, and a body the subclass fills in.
|
|
9
|
+
#
|
|
10
|
+
# The body is rendered under a guard: outside local-request mode a
|
|
11
|
+
# card whose block raises logs and reports the error and renders a short
|
|
12
|
+
# notice in place, so one broken query never takes the whole dashboard down.
|
|
13
|
+
class Card < Plutonium::UI::Component::Base
|
|
14
|
+
include Phlex::Rails::Helpers::LinkTo
|
|
15
|
+
|
|
16
|
+
# The component class for a declared card.
|
|
17
|
+
def self.for(dashboard:, card:)
|
|
18
|
+
component = case card.kind
|
|
19
|
+
when :metric then Metric
|
|
20
|
+
when :chart then Chart
|
|
21
|
+
else Custom
|
|
22
|
+
end
|
|
23
|
+
component.new(dashboard:, card:)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def initialize(dashboard:, card:)
|
|
27
|
+
@dashboard = dashboard
|
|
28
|
+
@card = card
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def view_template
|
|
32
|
+
Block(class: tokens("pu-dashboard-card flex flex-col h-full", card_classes)) do
|
|
33
|
+
render_header
|
|
34
|
+
div(class: "pu-dashboard-card-body") { guarded { render_body } }
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
attr_reader :dashboard, :card
|
|
41
|
+
|
|
42
|
+
def render_body
|
|
43
|
+
raise NotImplementedError, "#{self.class}#render_body"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Extra classes a kind adds to the card surface.
|
|
47
|
+
def card_classes = nil
|
|
48
|
+
|
|
49
|
+
def render_header
|
|
50
|
+
div(class: "pu-dashboard-card-header") do
|
|
51
|
+
render_icon if card.icon
|
|
52
|
+
div(class: "min-w-0 flex-1") do
|
|
53
|
+
h3(class: "pu-dashboard-card-title") { render_title }
|
|
54
|
+
if (description = card.description).present?
|
|
55
|
+
p(class: "pu-dashboard-card-description") { description }
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
render_link if href
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def render_icon
|
|
63
|
+
div(class: "pu-dashboard-card-icon") { render card.icon.new(class: "size-5") }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def render_title
|
|
67
|
+
if href
|
|
68
|
+
a(href:, class: "hover:underline") { card.label }
|
|
69
|
+
else
|
|
70
|
+
card.label
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_link
|
|
75
|
+
a(href:, class: "pu-dashboard-card-link", aria: {label: t("plutonium.dashboard.open")}) do
|
|
76
|
+
render Phlex::TablerIcons::ArrowUpRight.new(class: "size-4")
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def href
|
|
81
|
+
return @href if defined?(@href)
|
|
82
|
+
|
|
83
|
+
@href = card.href_for(dashboard)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Render the body into a string first, so a failure leaves no
|
|
87
|
+
# half-written markup behind before the notice replaces it.
|
|
88
|
+
def guarded(&)
|
|
89
|
+
html = capture(&)
|
|
90
|
+
raw(safe(html))
|
|
91
|
+
rescue => e
|
|
92
|
+
raise if raise_card_errors?
|
|
93
|
+
|
|
94
|
+
report_error(e)
|
|
95
|
+
render_error
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Logged as well as reported: `Rails.error.report` writes nothing to
|
|
99
|
+
# the log, so with no error subscriber (Sentry, Honeybadger, ...) a
|
|
100
|
+
# swallowed card failure would otherwise leave no trace at all.
|
|
101
|
+
def report_error(error)
|
|
102
|
+
Rails.logger.error do
|
|
103
|
+
"[plutonium.dashboard] #{dashboard.class.name}##{card.key} failed to load: " \
|
|
104
|
+
"#{error.class}: #{error.message}\n#{error.backtrace&.first(10)&.join("\n")}"
|
|
105
|
+
end
|
|
106
|
+
Rails.error.report(
|
|
107
|
+
error,
|
|
108
|
+
handled: true,
|
|
109
|
+
source: "plutonium.dashboard",
|
|
110
|
+
context: {dashboard: dashboard.class.name, card: card.key.to_s}
|
|
111
|
+
)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def raise_card_errors?
|
|
115
|
+
Rails.application.config.consider_all_requests_local
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def render_error
|
|
119
|
+
div(class: "pu-dashboard-card-error", role: "alert") do
|
|
120
|
+
render Phlex::TablerIcons::AlertCircle.new(class: "size-4 shrink-0")
|
|
121
|
+
span { t("plutonium.dashboard.card_error") }
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# A Chartkick chart. The card's block returns Chartkick data (a
|
|
7
|
+
# `{label => value}` hash, an array of pairs, or an array of
|
|
8
|
+
# `{name:, data:}` series); this component serialises it onto a
|
|
9
|
+
# `data-controller="chart"` element and the Stimulus controller draws it
|
|
10
|
+
# once the on-demand charts bundle has loaded.
|
|
11
|
+
class Chart < Card
|
|
12
|
+
def initialize(dashboard:, card:, script_url: nil)
|
|
13
|
+
super(dashboard:, card:)
|
|
14
|
+
@script_url = script_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def render_body
|
|
20
|
+
data = card.evaluate(dashboard)
|
|
21
|
+
|
|
22
|
+
div(
|
|
23
|
+
class: "pu-chart",
|
|
24
|
+
style: "height: #{card.options[:height]};",
|
|
25
|
+
data: {
|
|
26
|
+
controller: "chart",
|
|
27
|
+
chart_type_value: card.chart_class,
|
|
28
|
+
chart_data_value: data.to_json,
|
|
29
|
+
chart_options_value: card.chart_options.to_json,
|
|
30
|
+
chart_script_value: script_url
|
|
31
|
+
}
|
|
32
|
+
) do
|
|
33
|
+
span(class: "pu-chart-loading") { t("plutonium.ui.loading") }
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def script_url
|
|
38
|
+
@script_url || view_context.resource_charts_script_url
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# A free-form card. The block is `instance_exec`ed here, in Phlex, so it
|
|
7
|
+
# emits markup directly (`ul { ... }`, `render SomeComponent.new`); any
|
|
8
|
+
# method it calls that this component lacks is forwarded to the
|
|
9
|
+
# dashboard instance, so a dashboard's own helpers and `current_user`
|
|
10
|
+
# are in scope.
|
|
11
|
+
class Custom < Card
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def render_body
|
|
15
|
+
instance_exec(&card.block)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(name, ...)
|
|
19
|
+
if dashboard.respond_to?(name, true)
|
|
20
|
+
dashboard.send(name, ...)
|
|
21
|
+
else
|
|
22
|
+
super
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def respond_to_missing?(name, include_private = false)
|
|
27
|
+
dashboard.respond_to?(name, true) || super
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module UI
|
|
5
|
+
module Dashboard
|
|
6
|
+
# What the card endpoint renders: the card inside the turbo frame the
|
|
7
|
+
# board is waiting on. Wrapped unconditionally, so a direct visit to the
|
|
8
|
+
# card URL shows the same card inside the full layout.
|
|
9
|
+
class Frame < Plutonium::UI::Component::Base
|
|
10
|
+
include Phlex::Rails::Helpers::TurboFrameTag
|
|
11
|
+
|
|
12
|
+
def initialize(dashboard:, card:)
|
|
13
|
+
@dashboard = dashboard
|
|
14
|
+
@card = card
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def view_template
|
|
18
|
+
turbo_frame_tag(@card.frame_id, class: "block") do
|
|
19
|
+
render Card.for(dashboard: @dashboard, card: @card)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|