plutonium 0.63.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/.standard.yml +1 -1
- data/CHANGELOG.md +65 -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 +3 -3
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +2 -2
- 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 +15 -30
- 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 +71 -31
- 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
|
@@ -188,14 +188,19 @@ module Plutonium
|
|
|
188
188
|
end
|
|
189
189
|
end
|
|
190
190
|
|
|
191
|
-
# Attachment/file input names (as Strings)
|
|
192
|
-
#
|
|
193
|
-
#
|
|
194
|
-
#
|
|
191
|
+
# Attachment/file input names (as Strings) for the current resource —
|
|
192
|
+
# inputs declared with a file `as:` (`:file`/`:uppy`/`:attachment`)
|
|
193
|
+
# unioned with the attachments reflected on the model, so auto-detected
|
|
194
|
+
# attachments (no `input` declaration) are covered too. Excluded from the
|
|
195
|
+
# extraction-record pre-assignment so a single-read Rack upload isn't
|
|
196
|
+
# consumed before create/update reads it.
|
|
195
197
|
def attachment_input_keys
|
|
196
|
-
current_definition.defined_inputs.filter_map { |name, config|
|
|
198
|
+
declared = current_definition.defined_inputs.filter_map { |name, config|
|
|
197
199
|
name.to_s if Plutonium::Definition::InputAliases.file_input?(config.dig(:options, :as))
|
|
198
200
|
}
|
|
201
|
+
reflected = (resource_class.has_one_attached_field_names + resource_class.has_many_attached_field_names)
|
|
202
|
+
.map(&:to_s)
|
|
203
|
+
declared | reflected
|
|
199
204
|
end
|
|
200
205
|
|
|
201
206
|
# Returns the resource parameters, including scoped and parent parameters
|
|
@@ -36,7 +36,7 @@ module Plutonium
|
|
|
36
36
|
# GET /resources/new
|
|
37
37
|
def new
|
|
38
38
|
authorize_current! resource_class
|
|
39
|
-
set_page_title "
|
|
39
|
+
set_page_title t("plutonium.resource.titles.new", resource: resource_class.model_name.human)
|
|
40
40
|
|
|
41
41
|
@resource_record = build_resource_record
|
|
42
42
|
maybe_apply_submitted_resource_params!
|
|
@@ -47,7 +47,7 @@ module Plutonium
|
|
|
47
47
|
# POST /resources(.{format})
|
|
48
48
|
def create
|
|
49
49
|
authorize_current! resource_class
|
|
50
|
-
set_page_title "
|
|
50
|
+
set_page_title t("plutonium.resource.titles.new", resource: resource_class.model_name.human)
|
|
51
51
|
|
|
52
52
|
@resource_record = build_resource_record resource_params
|
|
53
53
|
|
|
@@ -57,13 +57,13 @@ module Plutonium
|
|
|
57
57
|
format.html { render :new, status: :unprocessable_content }
|
|
58
58
|
elsif resource_record!.save
|
|
59
59
|
after_create_persisted
|
|
60
|
+
notice = t("plutonium.resource.created", resource: resource_class.model_name.human)
|
|
60
61
|
format.turbo_stream do
|
|
61
|
-
flash.notice =
|
|
62
|
+
flash.notice = notice
|
|
62
63
|
render turbo_stream: stacked_modal_create_streams
|
|
63
64
|
end
|
|
64
65
|
format.html do
|
|
65
|
-
redirect_to redirect_url_after_submit,
|
|
66
|
-
notice: "#{resource_class.model_name.human} was successfully created."
|
|
66
|
+
redirect_to redirect_url_after_submit, notice:
|
|
67
67
|
end
|
|
68
68
|
format.any do
|
|
69
69
|
@current_policy = nil # Reset cached policy so it uses the instance instead of class
|
|
@@ -85,7 +85,7 @@ module Plutonium
|
|
|
85
85
|
# GET /resources/1/edit
|
|
86
86
|
def edit
|
|
87
87
|
authorize_current! resource_record!
|
|
88
|
-
set_page_title "
|
|
88
|
+
set_page_title t("plutonium.resource.titles.edit", record: resource_record!.to_label.titleize)
|
|
89
89
|
|
|
90
90
|
maybe_apply_submitted_resource_params!
|
|
91
91
|
|
|
@@ -95,7 +95,7 @@ module Plutonium
|
|
|
95
95
|
# PATCH/PUT /resources/1(.{format})
|
|
96
96
|
def update
|
|
97
97
|
authorize_current! resource_record!
|
|
98
|
-
set_page_title "
|
|
98
|
+
set_page_title t("plutonium.resource.titles.edit", record: resource_record!.to_label.titleize)
|
|
99
99
|
|
|
100
100
|
resource_record!.attributes = resource_params
|
|
101
101
|
|
|
@@ -104,14 +104,13 @@ module Plutonium
|
|
|
104
104
|
format.turbo_stream { render turbo_stream: turbo_stream.replace(helpers.turbo_scoped_dom_id("resource-form"), view_context.render(build_form(action: :edit))) }
|
|
105
105
|
format.html { render :edit, status: :unprocessable_content }
|
|
106
106
|
elsif resource_record!.save
|
|
107
|
+
notice = t("plutonium.resource.updated", resource: resource_class.model_name.human)
|
|
107
108
|
format.turbo_stream do
|
|
108
|
-
flash.notice =
|
|
109
|
+
flash.notice = notice
|
|
109
110
|
render turbo_stream: helpers.turbo_stream_redirect(redirect_url_after_submit)
|
|
110
111
|
end
|
|
111
112
|
format.html do
|
|
112
|
-
redirect_to redirect_url_after_submit,
|
|
113
|
-
notice: "#{resource_class.model_name.human} was successfully updated.",
|
|
114
|
-
status: :see_other
|
|
113
|
+
redirect_to redirect_url_after_submit, notice:, status: :see_other
|
|
115
114
|
end
|
|
116
115
|
format.any do
|
|
117
116
|
render :show, status: :ok, location: redirect_url_after_submit
|
|
@@ -134,29 +133,27 @@ module Plutonium
|
|
|
134
133
|
respond_to do |format|
|
|
135
134
|
resource_record!.destroy
|
|
136
135
|
|
|
136
|
+
notice = t("plutonium.resource.deleted", resource: resource_class.model_name.human)
|
|
137
137
|
format.turbo_stream do
|
|
138
|
-
flash.notice =
|
|
138
|
+
flash.notice = notice
|
|
139
139
|
render turbo_stream: helpers.turbo_stream_redirect(redirect_url_after_destroy)
|
|
140
140
|
end
|
|
141
141
|
format.html do
|
|
142
|
-
redirect_to redirect_url_after_destroy,
|
|
143
|
-
notice: "#{resource_class.model_name.human} was successfully deleted."
|
|
142
|
+
redirect_to redirect_url_after_destroy, notice:
|
|
144
143
|
end
|
|
145
144
|
format.json { head :no_content }
|
|
146
145
|
rescue ActiveRecord::InvalidForeignKey, ActiveRecord::DeleteRestrictionError
|
|
146
|
+
alert = t("plutonium.resource.referenced", resource: resource_class.model_name.human)
|
|
147
147
|
format.turbo_stream do
|
|
148
|
-
flash.alert =
|
|
148
|
+
flash.alert = alert
|
|
149
149
|
render turbo_stream: helpers.turbo_stream_redirect(resource_url_for(resource_record!))
|
|
150
150
|
end
|
|
151
151
|
format.html do
|
|
152
|
-
redirect_to resource_url_for(resource_record!),
|
|
153
|
-
alert: "#{resource_class.model_name.human} is referenced by other records."
|
|
152
|
+
redirect_to resource_url_for(resource_record!), alert:
|
|
154
153
|
end
|
|
155
154
|
format.any do
|
|
156
155
|
@errors = ActiveModel::Errors.new resource_record!
|
|
157
|
-
@errors.add :base,
|
|
158
|
-
:existing_references,
|
|
159
|
-
message: "is referenced by other records"
|
|
156
|
+
@errors.add :base, :existing_references
|
|
160
157
|
|
|
161
158
|
render "errors", status: :unprocessable_content
|
|
162
159
|
end
|
|
@@ -37,12 +37,6 @@ module Plutonium
|
|
|
37
37
|
# Collections rendered here preload what they are about to show.
|
|
38
38
|
include Plutonium::Resource::Controllers::EagerLoading
|
|
39
39
|
|
|
40
|
-
# Placeholder written when a column is neither an `export` block nor a
|
|
41
|
-
# real attribute on the record, so the export degrades to a usable file
|
|
42
|
-
# instead of a mid-stream NoMethodError (which would truncate the
|
|
43
|
-
# already-committed download).
|
|
44
|
-
INVALID_COLUMN = "<<invalid column>>"
|
|
45
|
-
|
|
46
40
|
included do
|
|
47
41
|
before_action :authorize_export_csv!, only: :export_csv
|
|
48
42
|
# Row-level authorization is the scope itself
|
|
@@ -121,9 +115,17 @@ module Plutonium
|
|
|
121
115
|
# A cell beginning with = + - @ (or a leading tab/CR) is executed as a
|
|
122
116
|
# formula by Excel/Sheets. Prefix it with a single quote so the value
|
|
123
117
|
# imports as literal text (CSV/formula injection).
|
|
118
|
+
#
|
|
119
|
+
# The leading-character check runs against the NFKC-normalized string so
|
|
120
|
+
# that full-width (double-byte) variants — = + - @ (U+FF1D/FF0B/FF0D/FF20) —
|
|
121
|
+
# which East Asian locales (e.g. Japanese Excel) treat as formula
|
|
122
|
+
# initiators, are also neutralized. The *original* string is prefixed
|
|
123
|
+
# (not the normalized one), so legitimate full-width content is preserved
|
|
124
|
+
# verbatim and only gains a defensive leading quote.
|
|
124
125
|
def neutralize_csv_formula(value)
|
|
125
126
|
string = value.to_s
|
|
126
|
-
|
|
127
|
+
normalized = string.unicode_normalize(:nfkc)
|
|
128
|
+
/\A[=+\-@\t\r]/.match?(normalized) ? "'#{string}" : string
|
|
127
129
|
end
|
|
128
130
|
|
|
129
131
|
# The primary key is always the first column, followed by the
|
|
@@ -143,7 +145,7 @@ module Plutonium
|
|
|
143
145
|
# Associations render as their display label — the same as the index —
|
|
144
146
|
# instead of "#<User:0x…>"; scalars pass through untouched. A name that
|
|
145
147
|
# is neither an `export` block nor a real attribute renders the
|
|
146
|
-
#
|
|
148
|
+
# invalid-column placeholder rather than aborting the stream.
|
|
147
149
|
def export_csv_value(record, name)
|
|
148
150
|
definition = current_definition.defined_exports[name]
|
|
149
151
|
return definition[:block].call(record) if definition && definition[:block]
|
|
@@ -151,7 +153,7 @@ module Plutonium
|
|
|
151
153
|
begin
|
|
152
154
|
value = record.public_send(name)
|
|
153
155
|
rescue NoMethodError
|
|
154
|
-
return
|
|
156
|
+
return invalid_column_placeholder
|
|
155
157
|
end
|
|
156
158
|
|
|
157
159
|
case value
|
|
@@ -165,6 +167,14 @@ module Plutonium
|
|
|
165
167
|
definition = current_definition.defined_exports[name]
|
|
166
168
|
definition&.dig(:options, :label) || name.to_s.humanize
|
|
167
169
|
end
|
|
170
|
+
|
|
171
|
+
# Placeholder written when a column is neither an `export` block nor a
|
|
172
|
+
# real attribute on the record, so the export degrades to a usable file
|
|
173
|
+
# instead of a mid-stream NoMethodError (which would truncate the
|
|
174
|
+
# already-committed download).
|
|
175
|
+
def invalid_column_placeholder
|
|
176
|
+
@invalid_column_placeholder ||= I18n.t("plutonium.resource.export_csv.invalid_column")
|
|
177
|
+
end
|
|
168
178
|
end
|
|
169
179
|
end
|
|
170
180
|
end
|
|
@@ -114,7 +114,7 @@ module Plutonium
|
|
|
114
114
|
if from && !record_in_kanban_column?(record, from)
|
|
115
115
|
return render_kanban_rejection(
|
|
116
116
|
params[:from_column],
|
|
117
|
-
reason: "
|
|
117
|
+
reason: t("plutonium.kanban.move.not_in_column", column: from.label)
|
|
118
118
|
)
|
|
119
119
|
end
|
|
120
120
|
|
|
@@ -123,11 +123,11 @@ module Plutonium
|
|
|
123
123
|
unless from && to&.accepts?(from.key) && !from.locked?
|
|
124
124
|
reason =
|
|
125
125
|
if from&.locked?
|
|
126
|
-
"
|
|
126
|
+
t("plutonium.kanban.move.locked_source", column: from.label)
|
|
127
127
|
elsif to
|
|
128
|
-
"
|
|
128
|
+
t("plutonium.kanban.move.not_accepted", column: to.label)
|
|
129
129
|
else
|
|
130
|
-
"
|
|
130
|
+
t("plutonium.kanban.move.not_allowed")
|
|
131
131
|
end
|
|
132
132
|
return render_kanban_rejection(params[:from_column], reason:)
|
|
133
133
|
end
|
|
@@ -151,7 +151,7 @@ module Plutonium
|
|
|
151
151
|
if to.wip && from.key != to.key && dest_cards.size + 1 > to.wip
|
|
152
152
|
return render_kanban_rejection(
|
|
153
153
|
params[:from_column],
|
|
154
|
-
reason: "
|
|
154
|
+
reason: t("plutonium.kanban.move.wip_limit_reached", column: to.label, wip: to.wip)
|
|
155
155
|
)
|
|
156
156
|
end
|
|
157
157
|
|
|
@@ -180,7 +180,7 @@ module Plutonium
|
|
|
180
180
|
# board. (Static boards always register — see Definition::IndexViews.)
|
|
181
181
|
if run_enter_interaction && current_definition.defined_actions[to.enter_interaction_key].nil?
|
|
182
182
|
Rails.logger.warn { "[plutonium] kanban enter_interaction on column `#{to.key}` is not registered — enter_interaction is unsupported on dynamic (`columns do…end`) boards; rejecting the drop." }
|
|
183
|
-
return render_kanban_rejection(params[:from_column], reason: "
|
|
183
|
+
return render_kanban_rejection(params[:from_column], reason: t("plutonium.kanban.move.drop_incomplete"))
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
# An input-less drop interaction is `immediate` — the client commits it
|
|
@@ -304,7 +304,7 @@ module Plutonium
|
|
|
304
304
|
# fix the input and resubmit.
|
|
305
305
|
if drop_immediate
|
|
306
306
|
reason = @interaction.errors.full_messages.to_sentence.presence ||
|
|
307
|
-
"
|
|
307
|
+
t("plutonium.kanban.move.enter_interaction_failed", column: to.label)
|
|
308
308
|
return render_kanban_rejection(from.key, reason:)
|
|
309
309
|
end
|
|
310
310
|
return render :kanban_move_form, formats: [:html], **modal_render_options, status: :unprocessable_content
|
|
@@ -373,7 +373,7 @@ module Plutonium
|
|
|
373
373
|
skip_verify_authorize_current!
|
|
374
374
|
render_kanban_rejection(
|
|
375
375
|
params[:from_column],
|
|
376
|
-
reason: "
|
|
376
|
+
reason: t("plutonium.kanban.move.unauthorized"),
|
|
377
377
|
status: :forbidden
|
|
378
378
|
)
|
|
379
379
|
rescue ActiveRecord::RecordNotFound
|
|
@@ -384,13 +384,13 @@ module Plutonium
|
|
|
384
384
|
# `find` raised before authorize_current!, so satisfy that verifier; the
|
|
385
385
|
# scope verifier is already satisfied by kanban_base_relation.
|
|
386
386
|
skip_verify_authorize_current!
|
|
387
|
-
render_kanban_rejection(params[:from_column], reason: "
|
|
387
|
+
render_kanban_rejection(params[:from_column], reason: t("plutonium.kanban.move.record_missing"))
|
|
388
388
|
rescue ActiveRecord::RecordInvalid => e
|
|
389
389
|
# An on_exit/on_enter hook (or the interaction) left the record invalid,
|
|
390
390
|
# so save! raised and the transaction rolled back. Snap back with the
|
|
391
391
|
# validation reason rather than let a 500 HTML page morph into the board.
|
|
392
392
|
reason = e.record.errors.full_messages.to_sentence.presence ||
|
|
393
|
-
"
|
|
393
|
+
t("plutonium.kanban.move.record_invalid")
|
|
394
394
|
render_kanban_rejection(params[:from_column], reason:)
|
|
395
395
|
end
|
|
396
396
|
|
|
@@ -437,15 +437,15 @@ module Plutonium
|
|
|
437
437
|
if from && !record_in_kanban_column?(record, from)
|
|
438
438
|
return render_kanban_rejection(
|
|
439
439
|
params[:from_column],
|
|
440
|
-
reason: "
|
|
440
|
+
reason: t("plutonium.kanban.move.not_in_column", column: from.label)
|
|
441
441
|
)
|
|
442
442
|
end
|
|
443
443
|
unless from && to.accepts?(from.key) && !from.locked?
|
|
444
444
|
reason =
|
|
445
445
|
if from&.locked?
|
|
446
|
-
"
|
|
446
|
+
t("plutonium.kanban.move.locked_source", column: from.label)
|
|
447
447
|
else
|
|
448
|
-
"
|
|
448
|
+
t("plutonium.kanban.move.not_accepted", column: to.label)
|
|
449
449
|
end
|
|
450
450
|
return render_kanban_rejection(params[:from_column], reason:)
|
|
451
451
|
end
|
|
@@ -473,7 +473,7 @@ module Plutonium
|
|
|
473
473
|
skip_verify_authorize_current!
|
|
474
474
|
render_kanban_rejection(
|
|
475
475
|
params[:from_column],
|
|
476
|
-
reason: "
|
|
476
|
+
reason: t("plutonium.kanban.move.unauthorized"),
|
|
477
477
|
status: :forbidden
|
|
478
478
|
)
|
|
479
479
|
rescue ActiveRecord::RecordNotFound
|
|
@@ -527,7 +527,7 @@ module Plutonium
|
|
|
527
527
|
# from the block for safety and dynamic edge cases.
|
|
528
528
|
def current_kanban_board
|
|
529
529
|
@current_kanban_board ||= current_definition.defined_kanban_board ||
|
|
530
|
-
Plutonium::Kanban::DSL.build(¤t_definition.defined_kanban_block)
|
|
530
|
+
Plutonium::Kanban::DSL.build(resource_class: resource_class, ¤t_definition.defined_kanban_block)
|
|
531
531
|
end
|
|
532
532
|
|
|
533
533
|
# Authorized + query-applied UN-paginated relation.
|
|
@@ -799,7 +799,7 @@ module Plutonium
|
|
|
799
799
|
apply_kanban_column_enter!(resource_record!, column)
|
|
800
800
|
rescue => e
|
|
801
801
|
Rails.logger.error { "[plutonium] kanban quick-add on_enter failed for column #{params[:kanban_column].inspect}: #{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}" }
|
|
802
|
-
flash[:alert] = "
|
|
802
|
+
flash[:alert] = t("plutonium.kanban.quick_add.default_column_fallback", column: column&.label || params[:kanban_column])
|
|
803
803
|
end
|
|
804
804
|
|
|
805
805
|
# Applies a column's on_enter to an already-persisted record, then appends
|
|
@@ -91,7 +91,7 @@ module Plutonium
|
|
|
91
91
|
# writing a position derived from neighbours that never implied one.
|
|
92
92
|
if config.delegate? && !current_query_object.sorted_ascending_only_by?(config.attribute)
|
|
93
93
|
return render_position_reconciliation(
|
|
94
|
-
reason: "
|
|
94
|
+
reason: t("plutonium.positioning.reposition.foreign_sort", attribute: config.attribute.to_s.humanize.downcase)
|
|
95
95
|
)
|
|
96
96
|
end
|
|
97
97
|
|
|
@@ -168,17 +168,17 @@ module Plutonium
|
|
|
168
168
|
end
|
|
169
169
|
|
|
170
170
|
render_position_reconciliation(
|
|
171
|
-
reason: "
|
|
171
|
+
reason: t("plutonium.positioning.reposition.unauthorized"),
|
|
172
172
|
status: :forbidden
|
|
173
173
|
)
|
|
174
174
|
rescue ActiveRecord::RecordNotFound
|
|
175
175
|
# The row was destroyed between render and drop. `find` raised before
|
|
176
176
|
# authorize_current!, so satisfy that verifier explicitly.
|
|
177
177
|
skip_verify_authorize_current!
|
|
178
|
-
render_position_reconciliation(reason: "
|
|
178
|
+
render_position_reconciliation(reason: t("plutonium.positioning.reposition.record_missing"))
|
|
179
179
|
rescue ActiveRecord::RecordInvalid => e
|
|
180
180
|
reason = e.record.errors.full_messages.to_sentence.presence ||
|
|
181
|
-
"
|
|
181
|
+
t("plutonium.positioning.reposition.record_invalid")
|
|
182
182
|
render_position_reconciliation(reason:)
|
|
183
183
|
end
|
|
184
184
|
|
|
@@ -223,7 +223,7 @@ module Plutonium
|
|
|
223
223
|
# says "the same product" rather than restating the column.
|
|
224
224
|
def position_foreign_group_reason(record)
|
|
225
225
|
scope = record.class.positioning_scope_attr.to_s.delete_suffix("_id").humanize.downcase
|
|
226
|
-
"
|
|
226
|
+
t("plutonium.positioning.reposition.foreign_group", group: scope)
|
|
227
227
|
end
|
|
228
228
|
|
|
229
229
|
# Fills in a neighbour the CLIENT could not see.
|
|
@@ -140,8 +140,14 @@ module Plutonium
|
|
|
140
140
|
# gets useful typeahead without declaring `search`). If neither
|
|
141
141
|
# search block nor fallback column is available, the relation is
|
|
142
142
|
# returned unfiltered (capped).
|
|
143
|
+
#
|
|
144
|
+
# The input's `scope:` (set on the Association filter) narrows the
|
|
145
|
+
# relation BEFORE authorization — mirroring ResourceSelect so the
|
|
146
|
+
# dropdown and typeahead present the same candidate set (a
|
|
147
|
+
# filtered subset rather than all policy-permitted records).
|
|
143
148
|
def filter_association(klass, query, options)
|
|
144
|
-
relation =
|
|
149
|
+
relation = apply_scope(klass.all, options[:scope])
|
|
150
|
+
relation = options[:skip_authorization] ? relation : authorized_resource_scope(klass, relation: relation)
|
|
145
151
|
if query.present?
|
|
146
152
|
if (search_block = associated_definition_search_block(klass))
|
|
147
153
|
relation = search_block.call(relation, query)
|
|
@@ -154,6 +160,23 @@ module Plutonium
|
|
|
154
160
|
relation.limit(Typeahead::TYPEAHEAD_LIMIT + 1).to_a
|
|
155
161
|
end
|
|
156
162
|
|
|
163
|
+
# Applies an optional association scope to a relation. Identical to
|
|
164
|
+
# ResourceSelect#apply_scope so dropdown and typeahead agree.
|
|
165
|
+
# Symbol → relation.public_send(sym) (named scope, e.g. :verified)
|
|
166
|
+
# Proc → arity 0 → relation.instance_exec(&scope) (kanban form: -> { where(...) })
|
|
167
|
+
# → arity ≥1 → scope.call(relation) (documented form: ->(s) { s.verified })
|
|
168
|
+
# nil → relation unchanged
|
|
169
|
+
# The arity dispatch matches call_option_proc (Form::Resource). Unsupported
|
|
170
|
+
# types fail loud rather than silently doing nothing.
|
|
171
|
+
def apply_scope(relation, scope)
|
|
172
|
+
case scope
|
|
173
|
+
when Symbol then relation.public_send(scope)
|
|
174
|
+
when Proc then scope.arity.zero? ? relation.instance_exec(&scope) : scope.call(relation)
|
|
175
|
+
when nil then relation
|
|
176
|
+
else raise ArgumentError, "Unsupported association scope: #{scope.inspect} (expected Symbol, Proc, or nil)"
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
157
180
|
# Resolves the associated resource's `search` block, if declared.
|
|
158
181
|
# Goes through `resource_definition` so portal/package namespacing
|
|
159
182
|
# is honored (same fallback chain as the rest of the controller).
|
|
@@ -13,10 +13,29 @@ module Plutonium
|
|
|
13
13
|
|
|
14
14
|
attr_accessor :default_sort_config, :default_scope_name
|
|
15
15
|
|
|
16
|
+
# Keys the query object reserves under the `q` namespace for built-in
|
|
17
|
+
# controls (e.g. `q[scope]`, `q[sort_fields]`). A filter sharing one of
|
|
18
|
+
# these names would collide with the control in the URL and in the filter
|
|
19
|
+
# form's hidden state, so defining one is rejected — see #define_filter.
|
|
20
|
+
RESERVED_FILTER_NAMES = %i[search scope sort_fields sort_directions].freeze
|
|
21
|
+
|
|
16
22
|
# Initializes a QueryObject with the given resource_class and parameters.
|
|
17
23
|
#
|
|
18
24
|
# @param resource_class [Object] The resource class.
|
|
19
25
|
# @param params [Hash] The parameters for initialization.
|
|
26
|
+
# The model this query object filters, for label conventions keyed by model.
|
|
27
|
+
attr_reader :resource_class
|
|
28
|
+
|
|
29
|
+
# Display label for a filter: its own `label` when it is a Filter (an
|
|
30
|
+
# explicit `label:` or the plutonium.filters.<model>.<name> convention),
|
|
31
|
+
# else the convention, else the humanized name.
|
|
32
|
+
def filter_label(name)
|
|
33
|
+
filter = filter_definitions[name]
|
|
34
|
+
return filter.label if filter.is_a?(Plutonium::Query::Filter)
|
|
35
|
+
|
|
36
|
+
Plutonium::Translation.label_for(:filter, resource_class, name) || name.to_s.humanize
|
|
37
|
+
end
|
|
38
|
+
|
|
20
39
|
def initialize(resource_class, params, request_path, &)
|
|
21
40
|
@resource_class = resource_class
|
|
22
41
|
@params = params
|
|
@@ -32,7 +51,13 @@ module Plutonium
|
|
|
32
51
|
#
|
|
33
52
|
# @param name [Symbol] The name of the filter.
|
|
34
53
|
# @param body [Proc, nil] The body of the filter.
|
|
54
|
+
# @raise [ArgumentError] if the name collides with a reserved query control.
|
|
35
55
|
def define_filter(name, body, &)
|
|
56
|
+
if RESERVED_FILTER_NAMES.include?(name.to_sym)
|
|
57
|
+
raise ArgumentError,
|
|
58
|
+
"filter name :#{name} is reserved — the query string uses q[#{name}] as a " \
|
|
59
|
+
"built-in control (one of: #{RESERVED_FILTER_NAMES.join(", ")}). Rename the filter."
|
|
60
|
+
end
|
|
36
61
|
filter_definitions[name] = build_query(body, &)
|
|
37
62
|
end
|
|
38
63
|
|
|
@@ -170,7 +195,9 @@ module Plutonium
|
|
|
170
195
|
next if humanized.blank?
|
|
171
196
|
humanized
|
|
172
197
|
else
|
|
173
|
-
entries.map { |k, v|
|
|
198
|
+
entries.map { |k, v|
|
|
199
|
+
Plutonium::Translation.t("plutonium.query.active_filter.input_value", input: k.to_s.humanize.downcase, value: v)
|
|
200
|
+
}.join(", ")
|
|
174
201
|
end
|
|
175
202
|
when Array
|
|
176
203
|
entries = filter_params.reject(&:blank?)
|
|
@@ -187,7 +214,7 @@ module Plutonium
|
|
|
187
214
|
|
|
188
215
|
{
|
|
189
216
|
name: name,
|
|
190
|
-
label: name
|
|
217
|
+
label: filter_label(name),
|
|
191
218
|
value_label: value_label,
|
|
192
219
|
clear_url: build_url(name => nil)
|
|
193
220
|
}
|
|
@@ -242,7 +269,7 @@ module Plutonium
|
|
|
242
269
|
|
|
243
270
|
private
|
|
244
271
|
|
|
245
|
-
attr_reader :
|
|
272
|
+
attr_reader :params, :selected_sort_fields, :selected_sort_directions, :selected_scope_filter
|
|
246
273
|
|
|
247
274
|
# Defines standard queries for search and scope.
|
|
248
275
|
def define_standard_queries
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Plutonium
|
|
4
|
+
module Routing
|
|
5
|
+
# Adds `register_dashboard` to the routing mapper, mirroring
|
|
6
|
+
# `register_wizard`. A dashboard is portal-hosted: its routes are drawn
|
|
7
|
+
# inside the engine's `routes.draw` block, so they inherit the portal's
|
|
8
|
+
# scope, auth and layout, and dispatch to a synthesized portal-namespaced
|
|
9
|
+
# `DashboardsController` that includes {Plutonium::Dashboard::Controller}.
|
|
10
|
+
#
|
|
11
|
+
# @example inside a portal engine's routes
|
|
12
|
+
# AdminPortal::Engine.routes.draw do
|
|
13
|
+
# register_dashboard HomeDashboard, at: "/" # the portal root
|
|
14
|
+
# register_dashboard SalesDashboard, at: "sales" # /dashboards/sales
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# Draws (portal-relative):
|
|
18
|
+
# GET /dashboards/sales → DashboardsController#show (sales_dashboard_path)
|
|
19
|
+
# GET /dashboards/sales/cards/:card → DashboardsController#card (sales_dashboard_card_path)
|
|
20
|
+
#
|
|
21
|
+
# Every path sits under the `dashboards/` segment, so a dashboard can never
|
|
22
|
+
# shadow (or be shadowed by) a `register_resource` route of the same name:
|
|
23
|
+
# `at: "sales"` and a `Sale` resource would otherwise both claim `/sales`.
|
|
24
|
+
# `prefix: nil` turns the segment off for a mount (`/sales`), and a string
|
|
25
|
+
# swaps it (`prefix: "reports"` → `/reports/sales`).
|
|
26
|
+
#
|
|
27
|
+
# A root mount draws `root` for the page and `/dashboards/<name>/cards/:card`
|
|
28
|
+
# for the cards, where `<name>` is `as:` or the class slug
|
|
29
|
+
# (`HomeDashboard` → `home`).
|
|
30
|
+
module DashboardRegistration
|
|
31
|
+
DASHBOARD_CONTROLLER_NAME = "dashboards"
|
|
32
|
+
DASHBOARD_PATH_SEGMENT = "dashboards"
|
|
33
|
+
|
|
34
|
+
# @param dashboard_class [Class] a Plutonium::Dashboard::Base subclass
|
|
35
|
+
# @param at [String] the path under the prefix; "/" (or "") mounts at the root
|
|
36
|
+
# @param as [String, Symbol, nil] override the route helper prefix
|
|
37
|
+
# @param prefix [String, nil] the leading path segment, "dashboards" by
|
|
38
|
+
# default; nil (or "") draws the mount at the bare `at:` path
|
|
39
|
+
def register_dashboard(dashboard_class, at:, as: nil, prefix: DASHBOARD_PATH_SEGMENT)
|
|
40
|
+
unless dashboard_class.is_a?(Class) && dashboard_class < Plutonium::Dashboard::Base
|
|
41
|
+
raise ArgumentError, "register_dashboard: #{dashboard_class.inspect} must subclass Plutonium::Dashboard::Base"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
engine = dashboard_route_engine
|
|
45
|
+
raise ArgumentError, "register_dashboard: routes must be drawn on a Plutonium engine or the application" if engine.nil?
|
|
46
|
+
|
|
47
|
+
mount_path = strip_slashes(at)
|
|
48
|
+
prefix = strip_slashes(prefix)
|
|
49
|
+
root = mount_path.empty?
|
|
50
|
+
helper_name = (as || mount_path.presence || dashboard_class.route_name).to_s.tr("/", "_")
|
|
51
|
+
defaults = {dashboard_class: dashboard_class.name}
|
|
52
|
+
|
|
53
|
+
ensure_dashboard_controller!(engine)
|
|
54
|
+
engine.dashboard_register.register(dashboard_class)
|
|
55
|
+
|
|
56
|
+
controller = DASHBOARD_CONTROLLER_NAME
|
|
57
|
+
if root
|
|
58
|
+
get "/", to: "#{controller}#show", as: :root, defaults: defaults
|
|
59
|
+
else
|
|
60
|
+
get File.join(prefix, mount_path), to: "#{controller}#show", as: :"#{helper_name}_dashboard", defaults: defaults
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
cards_path = File.join(prefix, root ? helper_name : mount_path, "cards/:card")
|
|
64
|
+
get cards_path, to: "#{controller}#card", as: :"#{helper_name}_dashboard_card", defaults: defaults
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
# Possessive quantifiers (`/++`) so stripping the slashes can't backtrack:
|
|
70
|
+
# a plain `/+\z` is O(n²) on a string of many slashes (rb/polynomial-redos).
|
|
71
|
+
def strip_slashes(path)
|
|
72
|
+
path.to_s.sub(%r{\A/++}, "").sub(%r{/++\z}, "")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Resolve (creating if needed) the controller the routes dispatch to:
|
|
76
|
+
# `<Portal>::DashboardsController` on a portal, `::DashboardsController`
|
|
77
|
+
# on the main app. An app-defined class of that name wins.
|
|
78
|
+
def ensure_dashboard_controller!(engine)
|
|
79
|
+
portal_module = dashboard_portal_module(engine)
|
|
80
|
+
if portal_module.nil?
|
|
81
|
+
define_dashboard_controller(Object, "DashboardsController", "ApplicationController", nil)
|
|
82
|
+
else
|
|
83
|
+
define_dashboard_controller(
|
|
84
|
+
portal_module,
|
|
85
|
+
"DashboardsController",
|
|
86
|
+
"#{portal_module.name}::PlutoniumController",
|
|
87
|
+
"#{portal_module.name}::Concerns::Controller"
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Synthesize the controller unless the app defines one. A controller this
|
|
93
|
+
# module synthesized earlier is rebuilt when its parent has been reloaded
|
|
94
|
+
# (development), so it never keeps serving a stale `PlutoniumController`.
|
|
95
|
+
def define_dashboard_controller(namespace, const_name, parent_name, concern_name)
|
|
96
|
+
parent = parent_name.safe_constantize || ActionController::Base
|
|
97
|
+
|
|
98
|
+
if namespace.const_defined?(const_name, false)
|
|
99
|
+
existing = namespace.const_get(const_name, false)
|
|
100
|
+
return existing unless existing.instance_variable_get(:@plutonium_synthesized) && existing.superclass != parent
|
|
101
|
+
|
|
102
|
+
namespace.send(:remove_const, const_name)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
klass = Class.new(parent) do
|
|
106
|
+
include Plutonium::Dashboard::Controller
|
|
107
|
+
end
|
|
108
|
+
klass.instance_variable_set(:@plutonium_synthesized, true)
|
|
109
|
+
namespace.const_set(const_name, klass)
|
|
110
|
+
|
|
111
|
+
# `Class.new(parent)` fires Rails' `inherited` hook while the class is
|
|
112
|
+
# still anonymous, so `module_parents` finds no engine and the hook
|
|
113
|
+
# includes the *application's* URL helpers over the portal's. Include
|
|
114
|
+
# the portal's again, last, so bare helpers (`root_path` in the
|
|
115
|
+
# sidebar) resolve against the engine like a source-defined controller.
|
|
116
|
+
klass.include(namespace.railtie_routes_url_helpers) if namespace != Object
|
|
117
|
+
|
|
118
|
+
if concern_name && (concern = concern_name.safe_constantize)
|
|
119
|
+
klass.include concern
|
|
120
|
+
end
|
|
121
|
+
klass
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# The Plutonium engine owning this route set (mirrors RouteSetExtensions#engine).
|
|
125
|
+
def dashboard_route_engine
|
|
126
|
+
rs = respond_to?(:route_set) ? route_set : @set
|
|
127
|
+
rs.respond_to?(:engine) ? rs.engine : nil
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def dashboard_portal_module(engine)
|
|
131
|
+
return nil if engine == Rails.application.class
|
|
132
|
+
|
|
133
|
+
engine.module_parent
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -10,6 +10,11 @@ module Plutonium
|
|
|
10
10
|
def resource_register
|
|
11
11
|
@resource_register ||= Plutonium::Resource::Register.new
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
# Dashboards mounted on this engine with `register_dashboard`.
|
|
15
|
+
def dashboard_register
|
|
16
|
+
@dashboard_register ||= Plutonium::Dashboard::Register.new
|
|
17
|
+
end
|
|
13
18
|
end
|
|
14
19
|
end
|
|
15
20
|
end
|
|
@@ -25,6 +25,7 @@ module Plutonium
|
|
|
25
25
|
if engine
|
|
26
26
|
resource_route_config_lookup.clear
|
|
27
27
|
engine.resource_register.clear
|
|
28
|
+
engine.dashboard_register.clear
|
|
28
29
|
end
|
|
29
30
|
|
|
30
31
|
super
|
|
@@ -36,7 +37,7 @@ module Plutonium
|
|
|
36
37
|
# @return [void]
|
|
37
38
|
# @yield Executes the given block in the context of route drawing.
|
|
38
39
|
def draw(&block)
|
|
39
|
-
if self.class.supported_engine?(engine)
|
|
40
|
+
if engine && self.class.supported_engine?(engine)
|
|
40
41
|
scope_params = entity_scope_params_for_path_strategy
|
|
41
42
|
ActiveSupport::Notifications.instrument("plutonium.resource_routes.draw", app: engine.to_s) do
|
|
42
43
|
super do
|