plutonium 0.64.0 → 0.66.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 +44 -41
- data/.claude/skills/plutonium-app/SKILL.md +115 -59
- data/.claude/skills/plutonium-async-interactions/SKILL.md +19 -19
- data/.claude/skills/plutonium-auth/SKILL.md +119 -53
- data/.claude/skills/plutonium-behavior/SKILL.md +110 -78
- data/.claude/skills/plutonium-dashboard/SKILL.md +168 -0
- data/.claude/skills/plutonium-kanban/SKILL.md +81 -49
- data/.claude/skills/plutonium-resource/SKILL.md +236 -145
- data/.claude/skills/plutonium-tenancy/SKILL.md +104 -55
- data/.claude/skills/plutonium-testing/SKILL.md +130 -33
- data/.claude/skills/plutonium-ui/SKILL.md +171 -95
- data/.claude/skills/plutonium-wizard/SKILL.md +93 -82
- data/CHANGELOG.md +80 -0
- data/CLAUDE.md +282 -210
- data/README.md +9 -9
- 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/sync-skills.mjs +6 -3
- 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 +174 -0
- data/docs/blog/introducing-plutonium-i18n.md +90 -0
- data/docs/blog/introducing-plutonium.md +4 -5
- data/docs/blog/whats-new-async-kanban-wizards.md +1 -1
- data/docs/getting-started/installation.md +5 -5
- data/docs/getting-started/tutorial/02-first-resource.md +3 -3
- data/docs/getting-started/tutorial/03-authentication.md +7 -7
- data/docs/getting-started/tutorial/04-authorization.md +21 -4
- data/docs/getting-started/tutorial/05-custom-actions.md +2 -2
- data/docs/getting-started/tutorial/06-nested-resources.md +5 -2
- data/docs/getting-started/tutorial/07-author-portal.md +2 -2
- data/docs/getting-started/tutorial/08-customizing-ui.md +45 -30
- data/docs/getting-started/tutorial/index.md +1 -1
- data/docs/guides/adding-resources.md +10 -7
- data/docs/guides/authentication.md +25 -25
- data/docs/guides/authorization.md +24 -24
- data/docs/guides/creating-packages.md +17 -17
- data/docs/guides/custom-actions.md +32 -32
- data/docs/guides/customizing-ui.md +29 -26
- data/docs/guides/dashboards.md +300 -0
- data/docs/guides/index.md +4 -3
- data/docs/guides/kanban.md +56 -56
- data/docs/guides/multi-tenancy.md +35 -22
- data/docs/guides/nested-resources.md +21 -21
- data/docs/guides/performance.md +3 -3
- data/docs/guides/search-filtering.md +13 -13
- data/docs/guides/testing.md +16 -12
- data/docs/guides/theming.md +32 -17
- data/docs/guides/troubleshooting.md +2 -2
- data/docs/guides/user-invites.md +17 -17
- data/docs/guides/user-profile.md +51 -24
- data/docs/guides/wizards.md +55 -55
- 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 +24 -24
- data/docs/reference/app/index.md +15 -18
- data/docs/reference/app/packages.md +8 -8
- data/docs/reference/app/portals.md +77 -29
- data/docs/reference/auth/accounts.md +15 -15
- data/docs/reference/auth/index.md +12 -12
- data/docs/reference/auth/profile.md +67 -29
- data/docs/reference/behavior/async-interactions.md +24 -24
- data/docs/reference/behavior/controllers.md +28 -28
- data/docs/reference/behavior/index.md +5 -5
- data/docs/reference/behavior/interactions.md +44 -44
- data/docs/reference/behavior/policies.md +48 -28
- data/docs/reference/configuration.md +88 -31
- 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/generators/lite.md +7 -7
- data/docs/reference/i18n.md +262 -0
- data/docs/reference/index.md +11 -2
- data/docs/reference/kanban/authorization.md +9 -9
- data/docs/reference/kanban/dsl.md +33 -33
- data/docs/reference/kanban/index.md +1 -1
- data/docs/reference/kanban/positioning.md +18 -16
- data/docs/reference/resource/actions.md +51 -51
- data/docs/reference/resource/definition.md +166 -88
- data/docs/reference/resource/export.md +11 -7
- data/docs/reference/resource/index.md +16 -14
- data/docs/reference/resource/model.md +24 -24
- data/docs/reference/{positioning.md → resource/positioning.md} +78 -75
- data/docs/reference/resource/query.md +14 -12
- data/docs/reference/tenancy/entity-scoping.md +65 -35
- data/docs/reference/tenancy/index.md +11 -11
- data/docs/reference/tenancy/invites.md +20 -20
- data/docs/reference/tenancy/nested-resources.md +13 -13
- data/docs/reference/testing/index.md +116 -22
- data/docs/reference/ui/assets.md +65 -25
- data/docs/reference/ui/components.md +20 -20
- data/docs/reference/ui/displays.md +19 -22
- data/docs/reference/ui/forms.md +35 -35
- data/docs/reference/ui/index.md +17 -15
- data/docs/reference/ui/layouts.md +21 -21
- data/docs/reference/ui/pages.md +22 -22
- data/docs/reference/ui/tables.md +7 -7
- data/docs/reference/wizard/anchoring-resume.md +33 -32
- data/docs/reference/wizard/dsl.md +45 -45
- data/docs/reference/wizard/index.md +6 -6
- data/docs/reference/wizard/one-time.md +18 -18
- data/docs/reference/wizard/registration-launch.md +32 -32
- data/docs/reference/wizard/storage-config.md +23 -23
- 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/profile/conn_generator.rb +6 -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/resource/record/associated_with.rb +23 -2
- 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/concerns/typeahead_attributes.rb +7 -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 +101 -10
- 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,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
|
|
@@ -336,13 +336,25 @@ module Plutonium
|
|
|
336
336
|
# equivalent of that.
|
|
337
337
|
#
|
|
338
338
|
# No-op for ActiveStorage fields, and for uploaders declaring no rules.
|
|
339
|
+
#
|
|
340
|
+
# Which attributes are files is decided by their INPUT DECLARATION (an
|
|
341
|
+
# `as:` file alias), not by the shape of the value they currently hold.
|
|
342
|
+
# A direct upload arrives already staged, as a token String, so keying
|
|
343
|
+
# off IO-shape (respond_to?(:read)) skipped exactly those — an oversized
|
|
344
|
+
# cached token validated clean and dispatched, the rule surfacing as a
|
|
345
|
+
# run failure the submitter never sees. This is how a wizard decides the
|
|
346
|
+
# same question (Plutonium::Wizard::Attachments.field?).
|
|
339
347
|
def validate_dispatch_attachments
|
|
340
|
-
staged = attributes.except("resource", "resources").select { |_, v| dispatch_attachment?(v) }.keys
|
|
341
348
|
stage_dispatch_attachments!
|
|
342
349
|
|
|
343
|
-
|
|
350
|
+
self.class.defined_inputs.each do |name, config|
|
|
351
|
+
next unless Plutonium::Definition::InputAliases.file_input?(config.dig(:options, :as))
|
|
352
|
+
|
|
353
|
+
value = public_send(name)
|
|
354
|
+
next if value.blank?
|
|
355
|
+
|
|
344
356
|
messages = Plutonium::Attachments.validation_errors(
|
|
345
|
-
|
|
357
|
+
value, **dispatch_attachment_options(name)
|
|
346
358
|
)
|
|
347
359
|
messages.each { |message| errors.add(name, message) }
|
|
348
360
|
end
|
|
@@ -17,7 +17,7 @@ module Plutonium
|
|
|
17
17
|
extend ActiveSupport::Concern
|
|
18
18
|
|
|
19
19
|
included do
|
|
20
|
-
presents label: "
|
|
20
|
+
presents label: t("plutonium.invites.cancel_invite.label"), icon: Phlex::TablerIcons::CircleX
|
|
21
21
|
|
|
22
22
|
attribute :resource
|
|
23
23
|
end
|
|
@@ -34,11 +34,11 @@ module Plutonium
|
|
|
34
34
|
private
|
|
35
35
|
|
|
36
36
|
def success_message
|
|
37
|
-
"
|
|
37
|
+
I18n.t("plutonium.invites.cancel_invite.cancelled")
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
def not_pending_message
|
|
41
|
-
"
|
|
41
|
+
I18n.t("plutonium.invites.cancel_invite.not_pending")
|
|
42
42
|
end
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -97,7 +97,7 @@ module Plutonium
|
|
|
97
97
|
# @raise [ActiveRecord::RecordInvalid] if constraints are violated
|
|
98
98
|
def validate_email_constraints!(user_email)
|
|
99
99
|
if enforce_email? && user_email.downcase != email.downcase
|
|
100
|
-
errors.add(:base,
|
|
100
|
+
errors.add(:base, :invite_email_mismatch, email: email)
|
|
101
101
|
raise ActiveRecord::RecordInvalid.new(self)
|
|
102
102
|
end
|
|
103
103
|
|
|
@@ -105,7 +105,7 @@ module Plutonium
|
|
|
105
105
|
user_domain = extract_domain(user_email)
|
|
106
106
|
|
|
107
107
|
if user_domain != required_domain
|
|
108
|
-
errors.add(:base,
|
|
108
|
+
errors.add(:base, :invite_domain_mismatch, domain: required_domain)
|
|
109
109
|
raise ActiveRecord::RecordInvalid.new(self)
|
|
110
110
|
end
|
|
111
111
|
end
|
|
@@ -115,16 +115,34 @@ module Plutonium
|
|
|
115
115
|
#
|
|
116
116
|
# This method:
|
|
117
117
|
# 1. Validates email constraints
|
|
118
|
-
# 2.
|
|
119
|
-
#
|
|
120
|
-
#
|
|
118
|
+
# 2. Acquires a row lock and re-checks the pending state to serialize
|
|
119
|
+
# concurrent acceptances (prevents a TOCTOU race from creating
|
|
120
|
+
# memberships for multiple users from a single invite when
|
|
121
|
+
# `enforce_email?` is false)
|
|
122
|
+
# 3. Marks the invite as accepted
|
|
123
|
+
# 4. Creates the entity membership
|
|
124
|
+
# 5. Notifies the invitable (if present)
|
|
121
125
|
#
|
|
122
126
|
# @param user [Object] the user accepting the invitation
|
|
123
|
-
# @raise [ActiveRecord::RecordInvalid] if acceptance fails
|
|
127
|
+
# @raise [ActiveRecord::RecordInvalid] if acceptance fails (e.g. the
|
|
128
|
+
# invite was already accepted by a concurrent request)
|
|
124
129
|
def accept_for_user!(user)
|
|
125
130
|
validate_email_constraints!(user.email)
|
|
126
131
|
|
|
127
|
-
|
|
132
|
+
with_lock do
|
|
133
|
+
unless pending?
|
|
134
|
+
error =
|
|
135
|
+
if accepted?
|
|
136
|
+
:invite_already_accepted
|
|
137
|
+
elsif cancelled?
|
|
138
|
+
:invite_cancelled
|
|
139
|
+
else
|
|
140
|
+
:invite_expired
|
|
141
|
+
end
|
|
142
|
+
errors.add(:base, error)
|
|
143
|
+
raise ActiveRecord::RecordInvalid.new(self)
|
|
144
|
+
end
|
|
145
|
+
|
|
128
146
|
update!(
|
|
129
147
|
:state => :accepted,
|
|
130
148
|
:accepted_at => Time.current,
|