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,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# The controller concern behind `register_dashboard`. Mixed into a
|
|
6
|
+
# portal-namespaced `DashboardsController` synthesized at route-draw time
|
|
7
|
+
# (see {Plutonium::Routing::DashboardRegistration}), so it inherits the
|
|
8
|
+
# portal's auth, entity scoping and layout exactly like a resource
|
|
9
|
+
# controller. Define `<Portal>::DashboardsController` yourself (including
|
|
10
|
+
# this module) to take over.
|
|
11
|
+
#
|
|
12
|
+
# Two actions: `show` renders the page with one lazy turbo frame per card,
|
|
13
|
+
# and `card` answers a single frame. Both run `authorize?` on the dashboard;
|
|
14
|
+
# `card` additionally rejects unknown and hidden cards with a 404.
|
|
15
|
+
module Controller
|
|
16
|
+
extend ActiveSupport::Concern
|
|
17
|
+
include Plutonium::Core::Controller
|
|
18
|
+
|
|
19
|
+
included do
|
|
20
|
+
helper_method :current_user, :current_dashboard, :dashboard_card_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class_methods do
|
|
24
|
+
# The gem's shared partials (`plutonium/_flash`, ...) resolve through a
|
|
25
|
+
# "plutonium" view prefix that a bare host has no ancestor to supply.
|
|
26
|
+
def _prefixes
|
|
27
|
+
@_dashboard_view_prefixes ||= (super | ["plutonium"])
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# GET the mount — the full page.
|
|
32
|
+
def show
|
|
33
|
+
authorize_dashboard!
|
|
34
|
+
set_page_title(current_dashboard_class.label)
|
|
35
|
+
render Plutonium::UI::Page::Dashboard.new(dashboard: current_dashboard)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# GET <mount>/cards/:card — one card, wrapped in its turbo frame.
|
|
39
|
+
def card
|
|
40
|
+
authorize_dashboard!
|
|
41
|
+
card = current_dashboard.visible_card!(params[:card])
|
|
42
|
+
render Plutonium::UI::Dashboard::Frame.new(dashboard: current_dashboard, card:)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
# Defers to the host's auth concern (a portal controller's Rodauth) and
|
|
48
|
+
# is nil on a bare main-app host.
|
|
49
|
+
def current_user
|
|
50
|
+
defined?(super) ? super : nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# The dashboard class rides the route defaults. Resolve it through the
|
|
54
|
+
# allowlist of loaded dashboards rather than `constantize`-ing the raw
|
|
55
|
+
# value, so a route param can never reach an arbitrary constant.
|
|
56
|
+
def current_dashboard_class
|
|
57
|
+
@current_dashboard_class ||= begin
|
|
58
|
+
name = params.fetch(:dashboard_class).to_s
|
|
59
|
+
Plutonium::Dashboard::Base.descendants.find { |klass| klass.name == name } ||
|
|
60
|
+
raise(Plutonium::Dashboard::UnknownDashboardError, "unknown dashboard #{name.inspect}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_dashboard
|
|
65
|
+
@current_dashboard ||= current_dashboard_class.new(view_context)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# `authorize?` false → 403 through the existing ActionPolicy::Unauthorized
|
|
69
|
+
# rescue, with a fake result object so the exception needs no policy.
|
|
70
|
+
def authorize_dashboard!
|
|
71
|
+
return if current_dashboard.authorize?
|
|
72
|
+
|
|
73
|
+
raise ::ActionPolicy::Unauthorized.new(
|
|
74
|
+
current_dashboard,
|
|
75
|
+
:authorize?,
|
|
76
|
+
Plutonium::Dashboard::AuthorizationDeniedResult.new(I18n.t("plutonium.dashboard.authorization_denied"))
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The card endpoint's path, threading the entity scope segment through so
|
|
81
|
+
# the frame stays inside the tenant. Resolved by the route's
|
|
82
|
+
# `dashboard_class` default, so `at:` / `as:` and a scope prefix are honoured.
|
|
83
|
+
def dashboard_card_path(card)
|
|
84
|
+
options = {card: card.key}
|
|
85
|
+
options[scoped_entity_param_key] = params[scoped_entity_param_key] if scoped_to_entity?
|
|
86
|
+
current_engine.routes.url_helpers.public_send(dashboard_card_route_helper, **options)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def dashboard_card_route_helper
|
|
90
|
+
@dashboard_card_route_helper ||= begin
|
|
91
|
+
name = Plutonium::Dashboard::RouteResolution.route_name(
|
|
92
|
+
current_engine.routes, current_dashboard_class, action: "card"
|
|
93
|
+
)
|
|
94
|
+
raise "no register_dashboard route found for #{current_dashboard_class.name}" unless name
|
|
95
|
+
|
|
96
|
+
:"#{name}_path"
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# The author-facing class macros: `metric`, `chart`, `card`, `refresh`
|
|
6
|
+
# and `width`. Mixed into {Base}.
|
|
7
|
+
module DSL
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
UNSET = Object.new
|
|
11
|
+
private_constant :UNSET
|
|
12
|
+
|
|
13
|
+
class_methods do
|
|
14
|
+
# The declared cards, in declaration order.
|
|
15
|
+
def cards
|
|
16
|
+
@cards ||= []
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# A single number with an optional change indicator. The block returns
|
|
20
|
+
# the value, or a hash: `{value:, previous:}` / `{value:, change:, trend:}`.
|
|
21
|
+
#
|
|
22
|
+
# metric(:orders, icon: Phlex::TablerIcons::ShoppingCart) { Order.count }
|
|
23
|
+
# metric(:revenue, format: :currency, positive: :up) do
|
|
24
|
+
# {value: Order.this_month.sum(:total), previous: Order.last_month.sum(:total)}
|
|
25
|
+
# end
|
|
26
|
+
def metric(key, **options, &block)
|
|
27
|
+
add_card(key, kind: :metric, options:, block:)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# A Chartkick chart. The block returns Chartkick data: a `{label => value}`
|
|
31
|
+
# hash, an array of pairs, or an array of `{name:, data:}` series. Options
|
|
32
|
+
# other than `type:` / `height:` and the common card options are passed
|
|
33
|
+
# straight through to Chartkick (`colors:`, `stacked:`, `suffix:`, ...).
|
|
34
|
+
#
|
|
35
|
+
# chart(:signups, type: :line, span: 8) { User.group_by_day(:created_at, last: 30).count }
|
|
36
|
+
def chart(key, **options, &block)
|
|
37
|
+
add_card(key, kind: :chart, options:, block:)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# A free-form card. The block renders Phlex markup inside the card body
|
|
41
|
+
# and can call the dashboard's own methods.
|
|
42
|
+
#
|
|
43
|
+
# card(:recent, span: :full) do
|
|
44
|
+
# ul { dashboard.recent_orders.each { |o| li { o.number } } }
|
|
45
|
+
# end
|
|
46
|
+
def card(key, **options, &block)
|
|
47
|
+
add_card(key, kind: :custom, options:, block:)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Default refresh interval (seconds) for every lazy card. A card's own
|
|
51
|
+
# `refresh:` overrides it; `refresh: false` on a card opts out.
|
|
52
|
+
def refresh(seconds = UNSET)
|
|
53
|
+
return @refresh if seconds.equal?(UNSET)
|
|
54
|
+
|
|
55
|
+
valid = seconds.nil? || (seconds.respond_to?(:to_i) && seconds.to_i.positive?)
|
|
56
|
+
raise ArgumentError, "refresh must be a positive number of seconds or nil, got #{seconds.inspect}" unless valid
|
|
57
|
+
|
|
58
|
+
@refresh = seconds&.to_i
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Page width, one of {Plutonium::UI::PageWidth::SIZES}. Defaults to
|
|
62
|
+
# `:full`: a card grid wants the room an index page gets, not the
|
|
63
|
+
# reading column detail pages are constrained to.
|
|
64
|
+
def width(value = UNSET)
|
|
65
|
+
return @width || :full if value.equal?(UNSET)
|
|
66
|
+
|
|
67
|
+
@width = Plutonium::UI::PageWidth.validate!(value)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# @return [Card, nil]
|
|
71
|
+
def find_card(key)
|
|
72
|
+
key = key.to_s
|
|
73
|
+
cards.find { |card| card.key.to_s == key }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# @return [Card]
|
|
77
|
+
# @raise [UnknownCardError] when no card has that key
|
|
78
|
+
def find_card!(key)
|
|
79
|
+
find_card(key) || raise(UnknownCardError, "#{name} declares no card #{key.inspect}")
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
|
|
84
|
+
def add_card(key, kind:, options:, block:)
|
|
85
|
+
key = key.to_sym
|
|
86
|
+
if find_card(key)
|
|
87
|
+
raise ArgumentError, "#{name || "dashboard"} already declares a card #{key.inspect}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
cards << Card.new(key, kind:, dashboard_class: self, block:, **options)
|
|
91
|
+
cards.last
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Class-level state must not leak into subclasses by reference.
|
|
95
|
+
def inherited(subclass)
|
|
96
|
+
super
|
|
97
|
+
subclass.instance_variable_set(:@cards, cards.dup)
|
|
98
|
+
subclass.instance_variable_set(:@refresh, @refresh)
|
|
99
|
+
subclass.instance_variable_set(:@width, @width)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Per-engine registry of the dashboards mounted with `register_dashboard`,
|
|
6
|
+
# so the sidebar can list them in registration order.
|
|
7
|
+
#
|
|
8
|
+
# Classes are stored by name and resolved on read, so a code reload never
|
|
9
|
+
# hands out a stale class object.
|
|
10
|
+
class Register
|
|
11
|
+
def initialize
|
|
12
|
+
@names = []
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param dashboard_class [Class] a {Base} subclass
|
|
16
|
+
def register(dashboard_class)
|
|
17
|
+
unless dashboard_class.is_a?(Class) && dashboard_class < Plutonium::Dashboard::Base
|
|
18
|
+
raise ArgumentError, "#{dashboard_class.inspect} must subclass Plutonium::Dashboard::Base"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
@names << dashboard_class.name unless @names.include?(dashboard_class.name)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# @return [Array<Class>] registered dashboards, in registration order
|
|
25
|
+
def dashboards
|
|
26
|
+
@names.map(&:constantize)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def registered?(dashboard_class)
|
|
30
|
+
@names.include?(dashboard_class.name)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def clear
|
|
34
|
+
@names.clear
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def empty? = @names.empty?
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Dashboard
|
|
5
|
+
# Resolves a `register_dashboard` mount's named routes from a route set by
|
|
6
|
+
# the `dashboard_class` route default every dashboard route carries.
|
|
7
|
+
#
|
|
8
|
+
# The helper name is not derivable from the class: it comes from `at:` /
|
|
9
|
+
# `as:`, and an entity-scoped portal prefixes it with the scope segment
|
|
10
|
+
# (`organization_scoped_team_dashboard`). Looking the route up by its
|
|
11
|
+
# default tracks whatever the registration actually drew.
|
|
12
|
+
module RouteResolution
|
|
13
|
+
module_function
|
|
14
|
+
|
|
15
|
+
# @param route_set [ActionDispatch::Routing::RouteSet]
|
|
16
|
+
# @param dashboard_class [Class]
|
|
17
|
+
# @param action [String, Symbol] "show" or "card"
|
|
18
|
+
# @return [Symbol, nil] the route name (e.g. :sales_dashboard_card)
|
|
19
|
+
def route_name(route_set, dashboard_class, action:)
|
|
20
|
+
route = route_set.routes.find do |r|
|
|
21
|
+
d = r.defaults
|
|
22
|
+
r.name.present? &&
|
|
23
|
+
d[:action].to_s == action.to_s &&
|
|
24
|
+
d[:dashboard_class].to_s == dashboard_class.name
|
|
25
|
+
end
|
|
26
|
+
route&.name&.to_sym
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
# Dashboards: a declarative page of metric, chart and free-form cards.
|
|
5
|
+
#
|
|
6
|
+
# A dashboard is a plain class (`app/dashboards/sales_dashboard.rb`) that
|
|
7
|
+
# subclasses {Plutonium::Dashboard::Base} and declares its cards with the
|
|
8
|
+
# class-level DSL. It is mounted in a portal (or the main app) with
|
|
9
|
+
# `register_dashboard` in the routes, exactly like `register_wizard`:
|
|
10
|
+
#
|
|
11
|
+
# class SalesDashboard < Plutonium::Dashboard::Base
|
|
12
|
+
# presents label: "Sales", icon: Phlex::TablerIcons::ChartBar
|
|
13
|
+
#
|
|
14
|
+
# metric(:orders) { Order.count }
|
|
15
|
+
# chart(:revenue, type: :line, span: 8) { Order.group_by_day(:created_at).sum(:total) }
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# AdminPortal::Engine.routes.draw do
|
|
19
|
+
# register_dashboard SalesDashboard, at: "sales"
|
|
20
|
+
# end
|
|
21
|
+
#
|
|
22
|
+
# Every card is loaded in its own lazy turbo frame, so the page renders
|
|
23
|
+
# instantly and each card's queries run in a separate request.
|
|
24
|
+
module Dashboard
|
|
25
|
+
# Raised when a route names a dashboard class that is not loaded (never a
|
|
26
|
+
# user-facing 404 in practice: the class is handed to `register_dashboard`).
|
|
27
|
+
class UnknownDashboardError < StandardError; end
|
|
28
|
+
|
|
29
|
+
# Raised by the card endpoint when the `:card` segment names no declared
|
|
30
|
+
# card, or a card whose `condition:` hides it for this request. Mapped to
|
|
31
|
+
# a 404 by the railtie so a hidden card is indistinguishable from a missing one.
|
|
32
|
+
class UnknownCardError < StandardError; end
|
|
33
|
+
|
|
34
|
+
# DOM id prefix of the per-card turbo frames, shared by the page and the
|
|
35
|
+
# card endpoint.
|
|
36
|
+
FRAME_PREFIX = "pu-dashboard-card"
|
|
37
|
+
|
|
38
|
+
# Stands in for a policy result when `authorize?` denies entry, so the
|
|
39
|
+
# standard ActionPolicy::Unauthorized rescue can render the message.
|
|
40
|
+
AuthorizationDeniedResult = Struct.new(:message)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -26,6 +26,11 @@ module Plutonium
|
|
|
26
26
|
@merged_defined_actions ||= begin
|
|
27
27
|
customize_actions
|
|
28
28
|
merged = self.class.defined_actions.merge(instance_defined_actions)
|
|
29
|
+
# The standard actions are declared on Base, so bind each action to
|
|
30
|
+
# THIS definition's model here, where it is known, for the
|
|
31
|
+
# plutonium.actions.<model>.<action> label convention.
|
|
32
|
+
model = self.class.model_class_or_nil
|
|
33
|
+
merged = merged.transform_values { |action| action.for_resource(model) } if model
|
|
29
34
|
merged.sort_by { |k, v| v.position }.to_h
|
|
30
35
|
end
|
|
31
36
|
end
|
|
@@ -49,7 +54,7 @@ module Plutonium
|
|
|
49
54
|
action(:destroy, route_options: {method: :delete},
|
|
50
55
|
record_action: true, collection_record_action: true, category: :danger,
|
|
51
56
|
icon: Phlex::TablerIcons::Trash, position: 100,
|
|
52
|
-
confirmation: "
|
|
57
|
+
confirmation: t("plutonium.actions.confirm_destroy"), turbo_frame: "_top")
|
|
53
58
|
|
|
54
59
|
# Example of dynamic route options using custom url_resolver:
|
|
55
60
|
#
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
module Plutonium
|
|
4
4
|
module Definition
|
|
5
|
+
# Raised when a definition's model cannot be inferred from its name.
|
|
6
|
+
class ModelClassError < NameError; end
|
|
7
|
+
|
|
5
8
|
# Base class for Plutonium definitions
|
|
6
9
|
#
|
|
7
10
|
# @abstract Subclass and override {#customize_fields}, {#customize_inputs},
|
|
@@ -25,6 +28,10 @@ module Plutonium
|
|
|
25
28
|
# @note This class is not thread-safe. Ensure proper synchronization
|
|
26
29
|
# if used in a multi-threaded environment.
|
|
27
30
|
class Base
|
|
31
|
+
# `input :email, placeholder: t("forms.email_placeholder")` — a lazy
|
|
32
|
+
# translation, resolved on every render in the request's locale.
|
|
33
|
+
extend Plutonium::Translation::Lazy
|
|
34
|
+
|
|
28
35
|
include DefineableProps
|
|
29
36
|
include ConfigAttr
|
|
30
37
|
include InheritableConfigAttr
|
|
@@ -69,6 +76,28 @@ module Plutonium
|
|
|
69
76
|
# fields
|
|
70
77
|
defineable_props :field, :input, :display, :column
|
|
71
78
|
|
|
79
|
+
# `as: :phlexi_render, with:` is deprecated in favour of the block form.
|
|
80
|
+
# Warn here, at declaration, rather than in the component: it fires once
|
|
81
|
+
# per declaration with the definition's own file and line, instead of
|
|
82
|
+
# once per rendered cell.
|
|
83
|
+
module PhlexiRenderDeprecation
|
|
84
|
+
PHLEXI_RENDER_TAGS = %i[phlexi_render phlexi].freeze
|
|
85
|
+
|
|
86
|
+
def display(name, **options, &block)
|
|
87
|
+
if PHLEXI_RENDER_TAGS.include?(options[:as])
|
|
88
|
+
Plutonium.deprecator.warn(
|
|
89
|
+
"`display :#{name}, as: :#{options[:as]}, with:` is deprecated; use a block-form display " \
|
|
90
|
+
"instead: `display :#{name} do |f| ... end`. It renders in a Phlex context once and gives " \
|
|
91
|
+
"you `f.object`.",
|
|
92
|
+
caller_locations(1)
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
super
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
singleton_class.prepend PhlexiRenderDeprecation
|
|
99
|
+
prepend PhlexiRenderDeprecation
|
|
100
|
+
|
|
72
101
|
# export
|
|
73
102
|
defineable_prop :export
|
|
74
103
|
|
|
@@ -170,13 +199,22 @@ module Plutonium
|
|
|
170
199
|
@model_class ||= infer_model_class
|
|
171
200
|
end
|
|
172
201
|
|
|
202
|
+
# `model_class`, or nil for a definition that has no model to infer (an
|
|
203
|
+
# anonymous or base definition). Convention lookups keyed by the model
|
|
204
|
+
# (action / scope labels) use this so such definitions keep rendering.
|
|
205
|
+
def self.model_class_or_nil
|
|
206
|
+
model_class
|
|
207
|
+
rescue ModelClassError
|
|
208
|
+
nil
|
|
209
|
+
end
|
|
210
|
+
|
|
173
211
|
def self.infer_model_class
|
|
174
|
-
raise
|
|
212
|
+
raise ModelClassError, "cannot infer a model class for an anonymous definition; define `model_class` on it" if name.nil?
|
|
175
213
|
|
|
176
214
|
segments = name.split("::")
|
|
177
215
|
base = segments.pop.delete_suffix("Definition")
|
|
178
216
|
if base.empty?
|
|
179
|
-
raise
|
|
217
|
+
raise ModelClassError, "#{name} does not follow the `<Model>Definition` naming convention; define `model_class` on it"
|
|
180
218
|
end
|
|
181
219
|
|
|
182
220
|
# Only an ActiveRecord model can be the answer, so a namespace module, a
|
|
@@ -190,7 +228,7 @@ module Plutonium
|
|
|
190
228
|
return klass if klass.is_a?(Class) && klass < ActiveRecord::Base
|
|
191
229
|
end
|
|
192
230
|
|
|
193
|
-
raise
|
|
231
|
+
raise ModelClassError, "could not infer a model class from #{name}; define `model_class` on it"
|
|
194
232
|
end
|
|
195
233
|
private_class_method :infer_model_class
|
|
196
234
|
|
|
@@ -118,7 +118,7 @@ module Plutonium
|
|
|
118
118
|
# interaction that references the definition at its own load time
|
|
119
119
|
# would deadlock the autoloader. In practice interactions depend only
|
|
120
120
|
# on their model, so this constraint is naturally satisfied.
|
|
121
|
-
board = Plutonium::Kanban::DSL.build(&block)
|
|
121
|
+
board = Plutonium::Kanban::DSL.build(resource_class: model_class_or_nil, &block)
|
|
122
122
|
# Cache the compiled board so the controller can reuse it instead of
|
|
123
123
|
# recompiling per request (see KanbanActions#current_kanban_board).
|
|
124
124
|
self.defined_kanban_board = board
|
|
@@ -62,7 +62,7 @@ module Plutonium
|
|
|
62
62
|
# its own `default_sort` means every resource under it renders the grip
|
|
63
63
|
# in the disabled state until the user sorts by position. That is the
|
|
64
64
|
# intended precedence — the app asked for that ordering — and it is
|
|
65
|
-
# documented in docs/reference/positioning.md.
|
|
65
|
+
# documented in docs/reference/resource/positioning.md.
|
|
66
66
|
default_sort config.attribute, :asc if _default_sort.equal?(Sorting::DEFAULT_SORT)
|
|
67
67
|
|
|
68
68
|
# Hidden: it has a POST member route (routing/mapper_extensions.rb) and
|
|
@@ -37,7 +37,7 @@ module Plutonium
|
|
|
37
37
|
#
|
|
38
38
|
# @return [String] The label defined in the presentation metadata or a default generated from the class name.
|
|
39
39
|
def label
|
|
40
|
-
presentation_metadata[:label] || name.demodulize.titleize
|
|
40
|
+
Plutonium::Translation.resolve(presentation_metadata[:label]) || name.demodulize.titleize
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
# Returns the icon for the interaction.
|
|
@@ -51,7 +51,7 @@ module Plutonium
|
|
|
51
51
|
#
|
|
52
52
|
# @return [String, nil] The description defined in the presentation metadata.
|
|
53
53
|
def description
|
|
54
|
-
presentation_metadata[:description]
|
|
54
|
+
Plutonium::Translation.resolve(presentation_metadata[:description])
|
|
55
55
|
end
|
|
56
56
|
end
|
|
57
57
|
|
|
@@ -33,6 +33,19 @@ module Plutonium
|
|
|
33
33
|
Plutonium.configuration.assets.script
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
# URL of the on-demand charts bundle. Chart cards carry it in a data
|
|
37
|
+
# attribute and the `chart` Stimulus controller injects it once, so pages
|
|
38
|
+
# without a chart never pay for Chart.js.
|
|
39
|
+
#
|
|
40
|
+
# @return [String] the charts bundle URL
|
|
41
|
+
def resource_charts_script_url
|
|
42
|
+
if Plutonium.configuration.development?
|
|
43
|
+
resource_development_asset_url(:js, "plutonium-charts.js")
|
|
44
|
+
else
|
|
45
|
+
asset_path(Plutonium.configuration.assets.charts_script)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
36
49
|
private
|
|
37
50
|
|
|
38
51
|
# Generate the appropriate asset URL based on the environment
|
|
@@ -52,9 +65,9 @@ module Plutonium
|
|
|
52
65
|
#
|
|
53
66
|
# @param type [Symbol] asset type (:css or :js)
|
|
54
67
|
# @return [String] asset URL for development
|
|
55
|
-
def resource_development_asset_url(type)
|
|
68
|
+
def resource_development_asset_url(type, asset_key = nil)
|
|
56
69
|
manifest_file = (type == :css) ? "css.manifest" : "js.manifest"
|
|
57
|
-
asset_key
|
|
70
|
+
asset_key ||= (type == :css) ? "plutonium.css" : "plutonium.js"
|
|
58
71
|
|
|
59
72
|
filename = JSON.parse(File.read(Plutonium.root.join("src", "build", manifest_file)))[asset_key]
|
|
60
73
|
"/build/#{filename}"
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
module Plutonium
|
|
2
2
|
module Helpers
|
|
3
3
|
module DisplayHelper
|
|
4
|
+
# The model's human name, pluralised for `count`. A locale that defines
|
|
5
|
+
# `activerecord.models.<model>.one/other` supplies the plural; when it
|
|
6
|
+
# only defines a single string (or nothing), the English inflector
|
|
7
|
+
# pluralises the singular as before.
|
|
4
8
|
def resource_name(resource_class, count = 1)
|
|
5
|
-
resource_class.model_name
|
|
9
|
+
model_name = resource_class.model_name
|
|
10
|
+
return model_name.human if count == 1
|
|
11
|
+
return model_name.human(count: count) if Plutonium::Translation.locale_plural?(resource_class)
|
|
12
|
+
|
|
13
|
+
model_name.human.pluralize(count)
|
|
6
14
|
end
|
|
7
15
|
|
|
8
16
|
def resource_name_plural(resource_class)
|
|
@@ -44,7 +52,7 @@ module Plutonium
|
|
|
44
52
|
end
|
|
45
53
|
|
|
46
54
|
# Maybe this is a record?
|
|
47
|
-
return "
|
|
55
|
+
return t("plutonium.resource.record_label", resource: resource_name(obj.class), id: obj.id) if obj.respond_to?(:id)
|
|
48
56
|
|
|
49
57
|
# Oh well. Just convert it to a string.
|
|
50
58
|
obj.to_s
|
|
@@ -351,8 +351,8 @@ module Plutonium
|
|
|
351
351
|
# user row is gone. Refusing is the whole point: a run whose initiator
|
|
352
352
|
# no longer exists has no one left to authorize as.
|
|
353
353
|
if run.initiator.nil?
|
|
354
|
-
raise UnresolvableError,
|
|
355
|
-
|
|
354
|
+
raise UnresolvableError, I18n.t("plutonium.async.context.initiator_missing",
|
|
355
|
+
run: run.id, type: run.initiator_type, id: run.initiator_id)
|
|
356
356
|
end
|
|
357
357
|
|
|
358
358
|
# nil scoped_entity is LEGITIMATE — an unscoped portal has no tenant,
|
|
@@ -362,9 +362,8 @@ module Plutonium
|
|
|
362
362
|
# drops the entity filter entirely, returning records from EVERY tenant.
|
|
363
363
|
# That is precisely the fail-open case this class exists to prevent.
|
|
364
364
|
if run.scoped_entity_type.present? && run.scoped_entity.nil?
|
|
365
|
-
raise UnresolvableError,
|
|
366
|
-
|
|
367
|
-
"which no longer exists; refusing to resolve targets without a tenant"
|
|
365
|
+
raise UnresolvableError, I18n.t("plutonium.async.context.tenant_missing",
|
|
366
|
+
run: run.id, type: run.scoped_entity_type, id: run.scoped_entity_id)
|
|
368
367
|
end
|
|
369
368
|
|
|
370
369
|
# Same distinction one more time, and the same direction of failure. A
|
|
@@ -372,9 +371,8 @@ module Plutonium
|
|
|
372
371
|
# dispatch" — and that hands the run entity scoping instead of parent
|
|
373
372
|
# scoping, which is wider.
|
|
374
373
|
if run.parent_type.present? && run.parent.nil?
|
|
375
|
-
raise UnresolvableError,
|
|
376
|
-
|
|
377
|
-
"which no longer exists; refusing to resolve targets without its parent"
|
|
374
|
+
raise UnresolvableError, I18n.t("plutonium.async.context.parent_missing",
|
|
375
|
+
run: run.id, type: run.parent_type, id: run.parent_id)
|
|
378
376
|
end
|
|
379
377
|
end
|
|
380
378
|
|
|
@@ -206,8 +206,7 @@ module Plutonium
|
|
|
206
206
|
elsif (remaining = perform_each(resolved.records))
|
|
207
207
|
# The loop stopped, so the remaining targets were never attempted.
|
|
208
208
|
# A run that did not do its job must not read as completed.
|
|
209
|
-
run.fail!("
|
|
210
|
-
"#{remaining} target(s) were not attempted")
|
|
209
|
+
run.fail!(I18n.t("plutonium.async.executor.halted", policy: run.failure_policy, count: remaining))
|
|
211
210
|
else
|
|
212
211
|
# Partial failure under :continue is COMPLETED, not failed. The author
|
|
213
212
|
# declared partial application acceptable, the executor ran to the
|
|
@@ -300,7 +299,7 @@ module Plutonium
|
|
|
300
299
|
def fail_target(record, error)
|
|
301
300
|
run.reload if run.changed?
|
|
302
301
|
|
|
303
|
-
raise BatchAbortedError, "
|
|
302
|
+
raise BatchAbortedError, I18n.t("plutonium.async.executor.batch_aborted", id: record.id, error: error.message) if transactional?
|
|
304
303
|
|
|
305
304
|
run.record_target_failure!(id: record.id, message: error.message)
|
|
306
305
|
halt? ? :halt : nil
|
|
@@ -393,8 +392,8 @@ module Plutonium
|
|
|
393
392
|
# part of the batch anyway would be the one outcome neither policy allows.
|
|
394
393
|
def refuse_partial_batch(resolved)
|
|
395
394
|
unresolved = resolved.missing_ids.size + resolved.unauthorized_ids.size
|
|
396
|
-
run.fail!("
|
|
397
|
-
|
|
395
|
+
run.fail!(I18n.t("plutonium.async.executor.partial_batch_refused",
|
|
396
|
+
unresolved: unresolved, total: run.target_ids.size, policy: run.failure_policy))
|
|
398
397
|
end
|
|
399
398
|
|
|
400
399
|
# Advances progress and records +id+ as dispositioned, in one write —
|
|
@@ -407,9 +406,9 @@ module Plutonium
|
|
|
407
406
|
run.save!
|
|
408
407
|
end
|
|
409
408
|
|
|
410
|
-
def missing_message(id) = "
|
|
409
|
+
def missing_message(id) = I18n.t("plutonium.async.executor.target_missing", id: id)
|
|
411
410
|
|
|
412
|
-
def unauthorized_message(id) = "
|
|
411
|
+
def unauthorized_message(id) = I18n.t("plutonium.async.executor.target_unauthorized", id: id, action: context.policy_action)
|
|
413
412
|
|
|
414
413
|
def continue? = run.failure_policy == :continue
|
|
415
414
|
|
|
@@ -169,7 +169,7 @@ module Plutonium
|
|
|
169
169
|
def to_label
|
|
170
170
|
parts = self.class.name.split("::")
|
|
171
171
|
name = (parts.last == "Run" && parts.size > 1) ? "#{parts[-2]}Run" : parts.last
|
|
172
|
-
"
|
|
172
|
+
I18n.t("plutonium.async.run.label", name: name.titleize, id: to_param)
|
|
173
173
|
end
|
|
174
174
|
|
|
175
175
|
# nil means INDETERMINATE, not zero: opaque work has no denominator, and
|
|
@@ -385,7 +385,7 @@ module Plutonium
|
|
|
385
385
|
def enqueue_job
|
|
386
386
|
Plutonium::Interaction::Async::Job.perform_later(id)
|
|
387
387
|
rescue
|
|
388
|
-
fail!("
|
|
388
|
+
fail!(I18n.t("plutonium.async.run.enqueue_failed"))
|
|
389
389
|
raise
|
|
390
390
|
end
|
|
391
391
|
end
|
|
@@ -38,7 +38,7 @@ module Plutonium
|
|
|
38
38
|
column :outcome, as: :badge,
|
|
39
39
|
colors: {running: :info, completed_with_errors: :warning}
|
|
40
40
|
|
|
41
|
-
field :target_label, label: "
|
|
41
|
+
field :target_label, label: t("plutonium.async.run.target_type")
|
|
42
42
|
|
|
43
43
|
# The details tab, with the live panel above the fields.
|
|
44
44
|
#
|
|
@@ -38,6 +38,8 @@ module Plutonium
|
|
|
38
38
|
# @note Subclasses must implement the #execute method.
|
|
39
39
|
# @see https://radioactive-labs.github.io/plutonium-core/reference/behavior/interactions
|
|
40
40
|
class Base
|
|
41
|
+
extend Plutonium::Translation::Lazy
|
|
42
|
+
|
|
41
43
|
include ActiveModel::Model
|
|
42
44
|
include ActiveModel::Attributes
|
|
43
45
|
include Plutonium::Definition::DefineableProps
|