plutonium 0.34.1 → 0.35.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 +53 -0
- data/.claude/skills/{assets → plutonium-assets}/SKILL.md +13 -8
- data/.claude/skills/{connect-resource → plutonium-connect-resource}/SKILL.md +1 -1
- data/.claude/skills/{controller → plutonium-controller}/SKILL.md +27 -13
- data/.claude/skills/{create-resource → plutonium-create-resource}/SKILL.md +1 -1
- data/.claude/skills/{definition → plutonium-definition}/SKILL.md +10 -10
- data/.claude/skills/{definition-actions → plutonium-definition-actions}/SKILL.md +34 -9
- data/.claude/skills/{definition-fields → plutonium-definition-fields}/SKILL.md +38 -10
- data/.claude/skills/plutonium-definition-query/SKILL.md +356 -0
- data/.claude/skills/{forms → plutonium-forms}/SKILL.md +6 -6
- data/.claude/skills/{installation → plutonium-installation}/SKILL.md +9 -9
- data/.claude/skills/{interaction → plutonium-interaction}/SKILL.md +20 -19
- data/.claude/skills/{model → plutonium-model}/SKILL.md +3 -3
- data/.claude/skills/{model-features → plutonium-model-features}/SKILL.md +3 -3
- data/.claude/skills/{nested-resources → plutonium-nested-resources}/SKILL.md +5 -5
- data/.claude/skills/{package → plutonium-package}/SKILL.md +7 -8
- data/.claude/skills/{policy → plutonium-policy}/SKILL.md +26 -4
- data/.claude/skills/{portal → plutonium-portal}/SKILL.md +33 -31
- data/.claude/skills/{resource → plutonium-resource}/SKILL.md +27 -27
- data/.claude/skills/{rodauth → plutonium-rodauth}/SKILL.md +5 -5
- data/.claude/skills/plutonium-theming/SKILL.md +424 -0
- data/.claude/skills/{views → plutonium-views}/SKILL.md +7 -7
- data/CHANGELOG.md +52 -0
- data/CLAUDE.md +215 -0
- data/CONTRIBUTING.md +72 -18
- data/README.md +100 -19
- data/app/assets/plutonium.css +1 -11
- data/app/assets/plutonium.js +1685 -1146
- data/app/assets/plutonium.js.map +4 -4
- data/app/assets/plutonium.min.js +70 -70
- data/app/assets/plutonium.min.js.map +4 -4
- data/app/views/resource/interactive_bulk_action.html.erb +1 -5
- data/app/views/rodauth/_email_auth_request_form.html.erb +1 -1
- data/app/views/rodauth/_login_form.html.erb +15 -55
- data/app/views/rodauth/_login_form_footer.html.erb +2 -2
- data/app/views/rodauth/_password_visibility.html.erb +2 -8
- data/app/views/rodauth/add_recovery_codes.html.erb +2 -2
- data/app/views/rodauth/change_login.html.erb +36 -19
- data/app/views/rodauth/change_password.html.erb +34 -10
- data/app/views/rodauth/close_account.html.erb +12 -4
- data/app/views/rodauth/confirm_password.html.erb +19 -17
- data/app/views/rodauth/create_account.html.erb +30 -109
- data/app/views/rodauth/email_auth.html.erb +1 -1
- data/app/views/rodauth/logout.html.erb +4 -4
- data/app/views/rodauth/otp_auth.html.erb +13 -4
- data/app/views/rodauth/otp_disable.html.erb +12 -4
- data/app/views/rodauth/otp_setup.html.erb +29 -12
- data/app/views/rodauth/otp_unlock.html.erb +19 -10
- data/app/views/rodauth/otp_unlock_not_available.html.erb +7 -7
- data/app/views/rodauth/recovery_auth.html.erb +12 -4
- data/app/views/rodauth/recovery_codes.html.erb +12 -4
- data/app/views/rodauth/remember.html.erb +7 -7
- data/app/views/rodauth/reset_password.html.erb +23 -7
- data/app/views/rodauth/reset_password_request.html.erb +14 -10
- data/app/views/rodauth/sms_auth.html.erb +13 -4
- data/app/views/rodauth/sms_confirm.html.erb +13 -4
- data/app/views/rodauth/sms_disable.html.erb +12 -4
- data/app/views/rodauth/sms_request.html.erb +1 -1
- data/app/views/rodauth/sms_setup.html.erb +23 -7
- data/app/views/rodauth/two_factor_auth.html.erb +2 -2
- data/app/views/rodauth/two_factor_disable.html.erb +12 -4
- data/app/views/rodauth/two_factor_manage.html.erb +7 -7
- data/app/views/rodauth/unlock_account.html.erb +13 -5
- data/app/views/rodauth/unlock_account_request.html.erb +2 -2
- data/app/views/rodauth/verify_account.html.erb +25 -7
- data/app/views/rodauth/verify_account_resend.html.erb +14 -10
- data/app/views/rodauth/verify_login_change.html.erb +1 -1
- data/app/views/rodauth/webauthn_auth.html.erb +1 -1
- data/app/views/rodauth/webauthn_remove.html.erb +18 -8
- data/app/views/rodauth/webauthn_setup.html.erb +12 -4
- data/docs/.vitepress/config.ts +15 -26
- data/docs/.vitepress/theme/custom.css +388 -29
- data/docs/getting-started/index.md +1 -1
- data/docs/getting-started/tutorial/02-first-resource.md +9 -0
- data/docs/getting-started/tutorial/06-nested-resources.md +2 -2
- data/docs/getting-started/tutorial/07-author-portal.md +191 -0
- data/docs/getting-started/tutorial/{07-customizing-ui.md → 08-customizing-ui.md} +7 -7
- data/docs/getting-started/tutorial/index.md +5 -2
- data/docs/guides/authorization.md +33 -0
- data/docs/guides/creating-packages.md +12 -16
- data/docs/guides/custom-actions.md +36 -0
- data/docs/guides/search-filtering.md +121 -42
- data/docs/guides/theming.md +232 -36
- data/docs/index.md +203 -57
- data/docs/public/og-image.png +0 -0
- data/docs/reference/controller/index.md +14 -16
- data/docs/reference/definition/actions.md +38 -3
- data/docs/reference/definition/fields.md +3 -3
- data/docs/reference/definition/index.md +2 -2
- data/docs/reference/generators/index.md +0 -1
- data/docs/reference/interaction/index.md +14 -10
- data/docs/reference/model/index.md +0 -1
- data/docs/reference/portal/index.md +13 -27
- data/gemfiles/rails_7.gemfile.lock +1 -1
- data/gemfiles/rails_8.0.gemfile.lock +1 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/pkg/portal/portal_generator.rb +0 -2
- data/lib/generators/pu/pkg/portal/templates/app/views/package/dashboard/index.html.erb +28 -72
- data/lib/plutonium/action/interactive.rb +2 -2
- data/lib/plutonium/core/controller.rb +2 -1
- data/lib/plutonium/definition/actions.rb +2 -2
- data/lib/plutonium/lib/deep_freezer.rb +3 -7
- data/lib/plutonium/query/filter.rb +14 -0
- data/lib/plutonium/query/filters/association.rb +49 -0
- data/lib/plutonium/query/filters/boolean.rb +35 -0
- data/lib/plutonium/query/filters/date.rb +97 -0
- data/lib/plutonium/query/filters/date_range.rb +58 -0
- data/lib/plutonium/query/filters/select.rb +55 -0
- data/lib/plutonium/resource/controllers/crud_actions.rb +24 -6
- data/lib/plutonium/resource/controllers/interactive_actions.rb +76 -58
- data/lib/plutonium/resource/controllers/queryable.rb +4 -2
- data/lib/plutonium/resource/query_object.rb +1 -1
- data/lib/plutonium/ui/action_button.rb +23 -65
- data/lib/plutonium/ui/actions_dropdown.rb +103 -0
- data/lib/plutonium/ui/block.rb +1 -1
- data/lib/plutonium/ui/breadcrumbs.rb +12 -19
- data/lib/plutonium/ui/color_mode_selector.rb +1 -1
- data/lib/plutonium/ui/component/kit.rb +6 -0
- data/lib/plutonium/ui/component_classes.rb +102 -0
- data/lib/plutonium/ui/display/base.rb +15 -0
- data/lib/plutonium/ui/display/components/attachment.rb +6 -5
- data/lib/plutonium/ui/display/components/boolean.rb +23 -0
- data/lib/plutonium/ui/display/components/color.rb +23 -0
- data/lib/plutonium/ui/display/resource.rb +1 -1
- data/lib/plutonium/ui/display/theme.rb +29 -15
- data/lib/plutonium/ui/empty_card.rb +3 -3
- data/lib/plutonium/ui/form/base.rb +20 -0
- data/lib/plutonium/ui/form/components/key_value_store.rb +11 -11
- data/lib/plutonium/ui/form/components/resource_select.rb +31 -0
- data/lib/plutonium/ui/form/components/secure_association.rb +1 -2
- data/lib/plutonium/ui/form/components/uppy.rb +5 -4
- data/lib/plutonium/ui/form/concerns/renders_nested_resource_fields.rb +4 -4
- data/lib/plutonium/ui/form/interaction.rb +17 -1
- data/lib/plutonium/ui/form/query.rb +133 -80
- data/lib/plutonium/ui/form/theme.rb +50 -35
- data/lib/plutonium/ui/frame_navigator_panel.rb +2 -2
- data/lib/plutonium/ui/layout/base.rb +1 -1
- data/lib/plutonium/ui/layout/header.rb +4 -7
- data/lib/plutonium/ui/layout/rodauth_layout.rb +7 -7
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/nav_grid_menu.rb +7 -6
- data/lib/plutonium/ui/nav_user.rb +9 -8
- data/lib/plutonium/ui/page/interactive_action.rb +5 -5
- data/lib/plutonium/ui/page_header.rb +29 -10
- data/lib/plutonium/ui/panel.rb +4 -4
- data/lib/plutonium/ui/sidebar_menu.rb +8 -8
- data/lib/plutonium/ui/skeleton_table.rb +7 -8
- data/lib/plutonium/ui/tab_list.rb +5 -5
- data/lib/plutonium/ui/table/base.rb +3 -0
- data/lib/plutonium/ui/table/components/attachment.rb +4 -3
- data/lib/plutonium/ui/table/components/bulk_actions_toolbar.rb +82 -0
- data/lib/plutonium/ui/table/components/pagy_info.rb +2 -2
- data/lib/plutonium/ui/table/components/pagy_pagination.rb +13 -8
- data/lib/plutonium/ui/table/components/row_actions_dropdown.rb +101 -0
- data/lib/plutonium/ui/table/components/scopes_bar.rb +2 -2
- data/lib/plutonium/ui/table/components/selection_column.rb +100 -0
- data/lib/plutonium/ui/table/display_theme.rb +6 -6
- data/lib/plutonium/ui/table/resource.rb +93 -52
- data/lib/plutonium/ui/table/theme.rb +28 -15
- data/lib/plutonium/version.rb +1 -1
- data/package.json +2 -2
- data/plutonium.gemspec +5 -4
- data/src/css/components.css +471 -0
- data/src/css/intl_tel_input.css +2 -2
- data/src/css/plutonium.css +2 -0
- data/src/css/tokens.css +149 -0
- data/src/js/controllers/bulk_actions_controller.js +109 -0
- data/src/js/controllers/filter_panel_controller.js +35 -0
- data/src/js/controllers/register_controllers.js +5 -1
- data/src/js/controllers/resource_drop_down_controller.js +25 -1
- data/src/js/controllers/slim_select_controller.js +6 -2
- data/src/js/turbo/turbo_actions.js +1 -1
- metadata +52 -39
- data/.claude/skills/definition-query/SKILL.md +0 -334
- data/docs/concepts/architecture.md +0 -226
- data/docs/concepts/auto-detection.md +0 -254
- data/docs/concepts/index.md +0 -61
- data/docs/concepts/packages-portals.md +0 -304
- data/docs/concepts/resources.md +0 -224
- data/docs/cookbook/blog.md +0 -411
- data/docs/cookbook/index.md +0 -289
- data/docs/cookbook/saas.md +0 -481
- data/docs/public/CLAUDE.md +0 -578
- data/lib/generators/pu/pkg/portal/templates/app/controllers/resource_controller.rb.tt +0 -5
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# Date filter for date/datetime columns
|
|
5
|
+
#
|
|
6
|
+
# @example Filter by exact date
|
|
7
|
+
# filter :created_at, with: :date, predicate: :eq
|
|
8
|
+
#
|
|
9
|
+
# @example Filter by date before
|
|
10
|
+
# filter :due_date, with: :date, predicate: :lt
|
|
11
|
+
#
|
|
12
|
+
# @example Filter by date on or after
|
|
13
|
+
# filter :start_date, with: :date, predicate: :gteq
|
|
14
|
+
#
|
|
15
|
+
class Date < Filter
|
|
16
|
+
VALID_PREDICATES = [
|
|
17
|
+
:eq, # Equal (on this date)
|
|
18
|
+
:not_eq, # Not equal
|
|
19
|
+
:lt, # Less than (before)
|
|
20
|
+
:lteq, # Less than or equal (on or before)
|
|
21
|
+
:gt, # Greater than (after)
|
|
22
|
+
:gteq # Greater than or equal (on or after)
|
|
23
|
+
].freeze
|
|
24
|
+
|
|
25
|
+
def initialize(predicate: :eq, **)
|
|
26
|
+
super(**)
|
|
27
|
+
unless VALID_PREDICATES.include?(predicate)
|
|
28
|
+
raise ArgumentError, "unsupported predicate #{predicate}. Valid predicates are: #{VALID_PREDICATES.join(", ")}"
|
|
29
|
+
end
|
|
30
|
+
@predicate = predicate
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def apply(scope, value:)
|
|
34
|
+
return scope if value.blank?
|
|
35
|
+
|
|
36
|
+
date_value = parse_date(value)
|
|
37
|
+
return scope unless date_value
|
|
38
|
+
|
|
39
|
+
case @predicate
|
|
40
|
+
when :eq
|
|
41
|
+
scope.where(key => date_value.all_day)
|
|
42
|
+
when :not_eq
|
|
43
|
+
scope.where.not(key => date_value.all_day)
|
|
44
|
+
when :lt
|
|
45
|
+
scope.where(key => ...date_value.beginning_of_day)
|
|
46
|
+
when :lteq
|
|
47
|
+
scope.where(key => ..date_value.end_of_day)
|
|
48
|
+
when :gt
|
|
49
|
+
scope.where(key => (date_value.end_of_day + 1.second)..)
|
|
50
|
+
when :gteq
|
|
51
|
+
scope.where(key => date_value.beginning_of_day..)
|
|
52
|
+
else
|
|
53
|
+
raise NotImplementedError, "date filter predicate #{@predicate}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def customize_inputs
|
|
58
|
+
input :value, as: :date
|
|
59
|
+
field :value, placeholder: generate_placeholder
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
private
|
|
63
|
+
|
|
64
|
+
def parse_date(value)
|
|
65
|
+
case value
|
|
66
|
+
when ::Date, ::DateTime, ::Time, ActiveSupport::TimeWithZone
|
|
67
|
+
value.to_date
|
|
68
|
+
when String
|
|
69
|
+
::Date.parse(value)
|
|
70
|
+
end
|
|
71
|
+
rescue ArgumentError
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def generate_placeholder
|
|
76
|
+
base = key.to_s.humanize
|
|
77
|
+
case @predicate
|
|
78
|
+
when :eq
|
|
79
|
+
base
|
|
80
|
+
when :not_eq
|
|
81
|
+
"#{base} not on..."
|
|
82
|
+
when :lt
|
|
83
|
+
"#{base} before..."
|
|
84
|
+
when :lteq
|
|
85
|
+
"#{base} on or before..."
|
|
86
|
+
when :gt
|
|
87
|
+
"#{base} after..."
|
|
88
|
+
when :gteq
|
|
89
|
+
"#{base} on or after..."
|
|
90
|
+
else
|
|
91
|
+
base
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# DateRange filter for filtering between two dates
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage
|
|
7
|
+
# filter :created_at, with: :date_range
|
|
8
|
+
#
|
|
9
|
+
# @example With custom labels
|
|
10
|
+
# filter :published_at, with: :date_range, from_label: "Published from", to_label: "Published to"
|
|
11
|
+
#
|
|
12
|
+
class DateRange < Filter
|
|
13
|
+
def initialize(from_label: nil, to_label: nil, **)
|
|
14
|
+
super(**)
|
|
15
|
+
@from_label = from_label
|
|
16
|
+
@to_label = to_label
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def apply(scope, from: nil, to: nil)
|
|
20
|
+
from_date = parse_date(from)
|
|
21
|
+
to_date = parse_date(to)
|
|
22
|
+
|
|
23
|
+
if from_date && to_date
|
|
24
|
+
scope.where(key => from_date.beginning_of_day..to_date.end_of_day)
|
|
25
|
+
elsif from_date
|
|
26
|
+
scope.where(key => from_date.beginning_of_day..)
|
|
27
|
+
elsif to_date
|
|
28
|
+
scope.where(key => ..to_date.end_of_day)
|
|
29
|
+
else
|
|
30
|
+
scope
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def customize_inputs
|
|
35
|
+
input :from, as: :date
|
|
36
|
+
input :to, as: :date
|
|
37
|
+
field :from, placeholder: @from_label || "#{key.to_s.humanize} from..."
|
|
38
|
+
field :to, placeholder: @to_label || "#{key.to_s.humanize} to..."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def parse_date(value)
|
|
44
|
+
return nil if value.blank?
|
|
45
|
+
|
|
46
|
+
case value
|
|
47
|
+
when ::Date, ::DateTime, ::Time, ActiveSupport::TimeWithZone
|
|
48
|
+
value.to_date
|
|
49
|
+
when String
|
|
50
|
+
::Date.parse(value)
|
|
51
|
+
end
|
|
52
|
+
rescue ArgumentError
|
|
53
|
+
nil
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Plutonium
|
|
2
|
+
module Query
|
|
3
|
+
module Filters
|
|
4
|
+
# Select filter for choosing from a predefined collection of options
|
|
5
|
+
#
|
|
6
|
+
# @example Basic usage with array
|
|
7
|
+
# filter :status, with: :select, choices: %w[draft published archived]
|
|
8
|
+
#
|
|
9
|
+
# @example With proc for dynamic choices
|
|
10
|
+
# filter :category, with: :select, choices: -> { Category.pluck(:name, :id) }
|
|
11
|
+
#
|
|
12
|
+
# @example With multiple selection
|
|
13
|
+
# filter :tags, with: :select, choices: %w[ruby rails js], multiple: true
|
|
14
|
+
#
|
|
15
|
+
class Select < Filter
|
|
16
|
+
def initialize(choices: nil, multiple: false, **)
|
|
17
|
+
super(**)
|
|
18
|
+
@choices = choices
|
|
19
|
+
@multiple = multiple
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def apply(scope, value:)
|
|
23
|
+
return scope if value.blank?
|
|
24
|
+
|
|
25
|
+
if @multiple && value.is_a?(Array)
|
|
26
|
+
scope.where(key => value.reject(&:blank?))
|
|
27
|
+
else
|
|
28
|
+
scope.where(key => value)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def customize_inputs
|
|
33
|
+
input :value,
|
|
34
|
+
as: :select,
|
|
35
|
+
choices: resolved_choices,
|
|
36
|
+
multiple: @multiple,
|
|
37
|
+
include_blank: @multiple ? false : "All"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
private
|
|
41
|
+
|
|
42
|
+
def resolved_choices
|
|
43
|
+
case @choices
|
|
44
|
+
when Proc
|
|
45
|
+
@choices
|
|
46
|
+
when nil
|
|
47
|
+
[]
|
|
48
|
+
else
|
|
49
|
+
@choices
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -53,9 +53,14 @@ module Plutonium
|
|
|
53
53
|
|
|
54
54
|
respond_to do |format|
|
|
55
55
|
if params[:pre_submit]
|
|
56
|
-
format.
|
|
56
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form)) }
|
|
57
|
+
format.html { render :new, status: :unprocessable_content }
|
|
57
58
|
elsif resource_record!.save
|
|
58
|
-
format.
|
|
59
|
+
format.turbo_stream do
|
|
60
|
+
flash.notice = "#{resource_class.model_name.human} was successfully created."
|
|
61
|
+
render turbo_stream: helpers.turbo_stream_redirect(redirect_url_after_submit)
|
|
62
|
+
end
|
|
63
|
+
format.html do
|
|
59
64
|
redirect_to redirect_url_after_submit,
|
|
60
65
|
notice: "#{resource_class.model_name.human} was successfully created."
|
|
61
66
|
end
|
|
@@ -94,9 +99,14 @@ module Plutonium
|
|
|
94
99
|
|
|
95
100
|
respond_to do |format|
|
|
96
101
|
if params[:pre_submit]
|
|
97
|
-
format.
|
|
102
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("resource-form", view_context.render(build_form)) }
|
|
103
|
+
format.html { render :edit, status: :unprocessable_content }
|
|
98
104
|
elsif resource_record!.save
|
|
99
|
-
format.
|
|
105
|
+
format.turbo_stream do
|
|
106
|
+
flash.notice = "#{resource_class.model_name.human} was successfully updated."
|
|
107
|
+
render turbo_stream: helpers.turbo_stream_redirect(redirect_url_after_submit)
|
|
108
|
+
end
|
|
109
|
+
format.html do
|
|
100
110
|
redirect_to redirect_url_after_submit,
|
|
101
111
|
notice: "#{resource_class.model_name.human} was successfully updated.",
|
|
102
112
|
status: :see_other
|
|
@@ -121,13 +131,21 @@ module Plutonium
|
|
|
121
131
|
respond_to do |format|
|
|
122
132
|
resource_record!.destroy
|
|
123
133
|
|
|
124
|
-
format.
|
|
134
|
+
format.turbo_stream do
|
|
135
|
+
flash.notice = "#{resource_class.model_name.human} was successfully deleted."
|
|
136
|
+
render turbo_stream: helpers.turbo_stream_redirect(redirect_url_after_destroy)
|
|
137
|
+
end
|
|
138
|
+
format.html do
|
|
125
139
|
redirect_to redirect_url_after_destroy,
|
|
126
140
|
notice: "#{resource_class.model_name.human} was successfully deleted."
|
|
127
141
|
end
|
|
128
142
|
format.json { head :no_content }
|
|
129
143
|
rescue ActiveRecord::InvalidForeignKey
|
|
130
|
-
format.
|
|
144
|
+
format.turbo_stream do
|
|
145
|
+
flash.alert = "#{resource_class.model_name.human} is referenced by other records."
|
|
146
|
+
render turbo_stream: helpers.turbo_stream_redirect(resource_url_for(resource_record!))
|
|
147
|
+
end
|
|
148
|
+
format.html do
|
|
131
149
|
redirect_to resource_url_for(resource_record!),
|
|
132
150
|
alert: "#{resource_class.model_name.human} is referenced by other records."
|
|
133
151
|
end
|
|
@@ -18,9 +18,12 @@ module Plutonium
|
|
|
18
18
|
]
|
|
19
19
|
|
|
20
20
|
before_action :authorize_interactive_resource_action!, only: %i[
|
|
21
|
-
interactive_bulk_action commit_interactive_bulk_action
|
|
22
21
|
interactive_resource_action commit_interactive_resource_action
|
|
23
22
|
]
|
|
23
|
+
|
|
24
|
+
before_action :authorize_interactive_bulk_action!, only: %i[
|
|
25
|
+
interactive_bulk_action commit_interactive_bulk_action
|
|
26
|
+
]
|
|
24
27
|
end
|
|
25
28
|
|
|
26
29
|
# GET /resources/1/record_actions/:interactive_action
|
|
@@ -39,7 +42,10 @@ module Plutonium
|
|
|
39
42
|
build_interactive_record_action_interaction
|
|
40
43
|
|
|
41
44
|
if params[:pre_submit]
|
|
42
|
-
|
|
45
|
+
respond_to do |format|
|
|
46
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
|
|
47
|
+
format.html { render :interactive_record_action, formats: [:html], status: :unprocessable_content }
|
|
48
|
+
end
|
|
43
49
|
return
|
|
44
50
|
end
|
|
45
51
|
|
|
@@ -50,15 +56,12 @@ module Plutonium
|
|
|
50
56
|
if outcome.success?
|
|
51
57
|
return_url = redirect_url_after_action_on(resource_record!)
|
|
52
58
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
end
|
|
59
|
+
format.turbo_stream do
|
|
60
|
+
render turbo_stream: helpers.turbo_stream_redirect(return_url)
|
|
61
|
+
end
|
|
62
|
+
format.html do
|
|
63
|
+
redirect_to return_url, status: :see_other
|
|
59
64
|
end
|
|
60
|
-
|
|
61
|
-
format.any { redirect_to return_url, status: :see_other }
|
|
62
65
|
else
|
|
63
66
|
format.any(:html, :turbo_stream) do
|
|
64
67
|
render :interactive_record_action, formats: [:html], status: :unprocessable_content
|
|
@@ -95,9 +98,8 @@ module Plutonium
|
|
|
95
98
|
|
|
96
99
|
if params[:pre_submit]
|
|
97
100
|
respond_to do |format|
|
|
98
|
-
format.
|
|
99
|
-
|
|
100
|
-
end
|
|
101
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
|
|
102
|
+
format.html { render :interactive_resource_action, status: :unprocessable_content }
|
|
101
103
|
end
|
|
102
104
|
return
|
|
103
105
|
end
|
|
@@ -109,15 +111,12 @@ module Plutonium
|
|
|
109
111
|
if outcome.success?
|
|
110
112
|
return_url = redirect_url_after_action_on(resource_class)
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
end
|
|
114
|
+
format.turbo_stream do
|
|
115
|
+
render turbo_stream: helpers.turbo_stream_redirect(return_url)
|
|
116
|
+
end
|
|
117
|
+
format.html do
|
|
118
|
+
redirect_to return_url, status: :see_other
|
|
118
119
|
end
|
|
119
|
-
|
|
120
|
-
format.any { redirect_to return_url, status: :see_other }
|
|
121
120
|
else
|
|
122
121
|
format.any(:html, :turbo_stream) do
|
|
123
122
|
render :interactive_resource_action, formats: [:html], status: :unprocessable_content
|
|
@@ -133,48 +132,51 @@ module Plutonium
|
|
|
133
132
|
|
|
134
133
|
# GET /resources/bulk_actions/:interactive_action?ids[]=1&ids[]=2
|
|
135
134
|
def interactive_bulk_action
|
|
136
|
-
|
|
137
|
-
# # TODO: ensure that the selected list matches the returned value
|
|
138
|
-
# interactive_bulk
|
|
139
|
-
# @interaction = current_interactive_action.interaction.new((params[:interaction] || {}).except(:resources))
|
|
135
|
+
build_interactive_bulk_action_interaction
|
|
140
136
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
137
|
+
if helpers.current_turbo_frame == "remote_modal"
|
|
138
|
+
render layout: false, formats: [:html]
|
|
139
|
+
else
|
|
140
|
+
render :interactive_bulk_action, formats: [:html]
|
|
141
|
+
end
|
|
146
142
|
end
|
|
147
143
|
|
|
148
144
|
# POST /resources/bulk_actions/:interactive_action?ids[]=1&ids[]=2
|
|
149
145
|
def commit_interactive_bulk_action
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
146
|
+
build_interactive_bulk_action_interaction
|
|
147
|
+
|
|
148
|
+
if params[:pre_submit]
|
|
149
|
+
respond_to do |format|
|
|
150
|
+
format.turbo_stream { render turbo_stream: turbo_stream.replace("interaction-form", view_context.render(@interaction.build_form)) }
|
|
151
|
+
format.html { render :interactive_bulk_action, formats: [:html], status: :unprocessable_content }
|
|
152
|
+
end
|
|
153
|
+
return
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
outcome = @interaction.call
|
|
157
|
+
|
|
158
|
+
outcome.to_response.process(self) do |value|
|
|
159
|
+
respond_to do |format|
|
|
160
|
+
if outcome.success?
|
|
161
|
+
return_url = redirect_url_after_action_on(resource_class)
|
|
162
|
+
|
|
163
|
+
format.turbo_stream do
|
|
164
|
+
render turbo_stream: helpers.turbo_stream_redirect(return_url)
|
|
165
|
+
end
|
|
166
|
+
format.html do
|
|
167
|
+
redirect_to return_url, status: :see_other
|
|
168
|
+
end
|
|
169
|
+
else
|
|
170
|
+
format.any(:html, :turbo_stream) do
|
|
171
|
+
render :interactive_bulk_action, formats: [:html], status: :unprocessable_content
|
|
172
|
+
end
|
|
173
|
+
format.any do
|
|
174
|
+
@errors = @interaction.errors
|
|
175
|
+
render "errors", status: :unprocessable_content
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
178
180
|
end
|
|
179
181
|
|
|
180
182
|
private
|
|
@@ -206,6 +208,16 @@ module Plutonium
|
|
|
206
208
|
authorize_current! resource_class, to: :"#{interactive_resource_action}?"
|
|
207
209
|
end
|
|
208
210
|
|
|
211
|
+
def authorize_interactive_bulk_action!
|
|
212
|
+
action_name = params[:interactive_action]&.to_sym
|
|
213
|
+
policy_method = :"#{action_name}?"
|
|
214
|
+
|
|
215
|
+
# Authorize each record individually - fail if any record is not authorized
|
|
216
|
+
interactive_bulk.each do |record|
|
|
217
|
+
authorize_current! record, to: policy_method
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
209
221
|
def interactive_bulk
|
|
210
222
|
@interactive_bulk ||= current_authorized_scope.from_path_param(params.require(:ids))
|
|
211
223
|
end
|
|
@@ -222,6 +234,12 @@ module Plutonium
|
|
|
222
234
|
@interaction
|
|
223
235
|
end
|
|
224
236
|
|
|
237
|
+
def build_interactive_bulk_action_interaction
|
|
238
|
+
@interaction = current_interactive_action.interaction.new(view_context:)
|
|
239
|
+
@interaction.attributes = interaction_params.merge(resources: interactive_bulk)
|
|
240
|
+
@interaction
|
|
241
|
+
end
|
|
242
|
+
|
|
225
243
|
# Returns the submitted resource parameters
|
|
226
244
|
# @return [Hash] The submitted resource parameters
|
|
227
245
|
def submitted_interaction_params
|
|
@@ -31,10 +31,12 @@ module Plutonium
|
|
|
31
31
|
|
|
32
32
|
current_definition.defined_filters.each do |key, value|
|
|
33
33
|
with = value[:options][:with]
|
|
34
|
-
if with
|
|
34
|
+
if with
|
|
35
|
+
# Resolve symbol types (e.g., :text, :select) to filter classes
|
|
36
|
+
filter_class = Plutonium::Query::Filter.lookup(with)
|
|
35
37
|
options = value[:options].except(:with)
|
|
36
38
|
options[:key] ||= key
|
|
37
|
-
with =
|
|
39
|
+
with = filter_class.new(**options)
|
|
38
40
|
end
|
|
39
41
|
query_object.define_filter key, with, &value[:block]
|
|
40
42
|
end
|
|
@@ -198,7 +198,7 @@ module Plutonium
|
|
|
198
198
|
# @return [Symbol] The sort field.
|
|
199
199
|
# @raise [RuntimeError] If unable to determine sort logic.
|
|
200
200
|
def determine_sort_field(name)
|
|
201
|
-
if resource_class.
|
|
201
|
+
if resource_class.column_names.include?(name.to_s)
|
|
202
202
|
name
|
|
203
203
|
elsif resource_class.belongs_to_association_field_names.include?(name)
|
|
204
204
|
resource_class.reflect_on_association(name).foreign_key.to_sym
|
|
@@ -6,6 +6,17 @@ module Plutonium
|
|
|
6
6
|
include Phlex::Rails::Helpers::LinkTo
|
|
7
7
|
include Phlex::Rails::Helpers::ButtonTo
|
|
8
8
|
|
|
9
|
+
# Color to CSS class mapping for standard and soft button variants
|
|
10
|
+
COLOR_CLASSES = {
|
|
11
|
+
primary: {default: "pu-btn-primary", soft: "pu-btn-soft-primary"},
|
|
12
|
+
success: {default: "pu-btn-success", soft: "pu-btn-soft-success"},
|
|
13
|
+
info: {default: "pu-btn-info", soft: "pu-btn-soft-info"},
|
|
14
|
+
warning: {default: "pu-btn-warning", soft: "pu-btn-soft-warning"},
|
|
15
|
+
danger: {default: "pu-btn-danger", soft: "pu-btn-soft-danger"},
|
|
16
|
+
accent: {default: "pu-btn-accent", soft: "pu-btn-soft-accent"},
|
|
17
|
+
secondary: {default: "pu-btn-secondary", soft: "pu-btn-soft-secondary"}
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
9
20
|
def initialize(action, url:, variant: :default)
|
|
10
21
|
@action = action
|
|
11
22
|
@url = url
|
|
@@ -67,80 +78,27 @@ module Plutonium
|
|
|
67
78
|
|
|
68
79
|
def button_classes
|
|
69
80
|
tokens(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
-> { @action.icon
|
|
81
|
+
"pu-btn",
|
|
82
|
+
size_class,
|
|
83
|
+
color_class,
|
|
84
|
+
-> { @action.icon } => "gap-1.5"
|
|
74
85
|
)
|
|
75
86
|
end
|
|
76
87
|
|
|
77
|
-
def
|
|
78
|
-
|
|
79
|
-
"inline-flex items-center justify-center py-1 px-2 rounded-lg focus:outline-none focus:ring-2"
|
|
80
|
-
else
|
|
81
|
-
"flex items-center justify-center px-4 py-2 text-sm font-medium rounded-lg focus:outline-none focus:ring-4"
|
|
82
|
-
end
|
|
88
|
+
def size_class
|
|
89
|
+
(@variant == :table) ? "pu-btn-xs" : "pu-btn-md"
|
|
83
90
|
end
|
|
84
91
|
|
|
85
92
|
def icon_classes
|
|
86
|
-
|
|
87
|
-
"h-4 w-4 mr-1"
|
|
88
|
-
else
|
|
89
|
-
"h-3.5 w-3.5 -ml-1"
|
|
90
|
-
end
|
|
91
|
-
end
|
|
92
|
-
|
|
93
|
-
def size_classes
|
|
94
|
-
(@variant == :table) ? "text-xs" : "text-sm"
|
|
93
|
+
(@variant == :table) ? "h-4 w-4" : "h-3.5 w-3.5"
|
|
95
94
|
end
|
|
96
95
|
|
|
97
|
-
def
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
variant_class(
|
|
101
|
-
"bg-primary-700 text-white hover:bg-primary-800 focus:ring-primary-300 dark:bg-primary-600 dark:hover:bg-primary-700 dark:focus:ring-primary-800",
|
|
102
|
-
table: "bg-primary-100 text-primary-700 hover:bg-primary-200 focus:ring-primary-300 dark:bg-primary-700 dark:text-primary-100 dark:hover:bg-primary-600 dark:focus:ring-primary-600"
|
|
103
|
-
)
|
|
104
|
-
when :success
|
|
105
|
-
variant_class(
|
|
106
|
-
"bg-success-700 text-white hover:bg-success-800 focus:ring-success-300 dark:bg-success-600 dark:hover:bg-success-700 dark:focus:ring-success-800",
|
|
107
|
-
table: "bg-success-100 text-success-700 hover:bg-success-200 focus:ring-success-300 dark:bg-success-700 dark:text-success-100 dark:hover:bg-success-600 dark:focus:ring-success-600"
|
|
108
|
-
)
|
|
109
|
-
when :info
|
|
110
|
-
variant_class(
|
|
111
|
-
"bg-info-700 text-white hover:bg-info-800 focus:ring-info-300 dark:bg-info-600 dark:hover:bg-info-700 dark:focus:ring-info-800",
|
|
112
|
-
table: "bg-info-100 text-info-700 hover:bg-info-200 focus:ring-info-300 dark:bg-info-700 dark:text-info-100 dark:hover:bg-info-600 dark:focus:ring-info-600"
|
|
113
|
-
)
|
|
114
|
-
when :warning
|
|
115
|
-
variant_class(
|
|
116
|
-
"bg-warning-700 text-white hover:bg-warning-800 focus:ring-warning-300 dark:bg-warning-600 dark:hover:bg-warning-700 dark:focus:ring-warning-800",
|
|
117
|
-
table: "bg-warning-100 text-warning-700 hover:bg-warning-200 focus:ring-warning-300 dark:bg-warning-700 dark:text-warning-100 dark:hover:bg-warning-600 dark:focus:ring-warning-600"
|
|
118
|
-
)
|
|
119
|
-
when :danger
|
|
120
|
-
variant_class(
|
|
121
|
-
"bg-danger-700 text-white hover:bg-danger-800 focus:ring-danger-300 dark:bg-danger-600 dark:hover:bg-danger-700 dark:focus:ring-danger-800",
|
|
122
|
-
table: "bg-danger-100 text-danger-700 hover:bg-danger-200 focus:ring-danger-300 dark:bg-danger-700 dark:text-danger-100 dark:hover:bg-danger-600 dark:focus:ring-danger-600"
|
|
123
|
-
)
|
|
124
|
-
when :accent
|
|
125
|
-
variant_class(
|
|
126
|
-
"bg-accent-700 text-white hover:bg-accent-800 focus:ring-accent-300 dark:bg-accent-600 dark:hover:bg-accent-700 dark:focus:ring-accent-800",
|
|
127
|
-
table: "bg-accent-100 text-accent-700 hover:bg-accent-200 focus:ring-accent-300 dark:bg-accent-700 dark:text-accent-100 dark:hover:bg-accent-600 dark:focus:ring-accent-600"
|
|
128
|
-
)
|
|
129
|
-
else
|
|
130
|
-
variant_class(
|
|
131
|
-
"bg-secondary-700 text-white hover:bg-secondary-800 focus:ring-secondary-300 dark:bg-secondary-600 dark:hover:bg-secondary-700 dark:focus:ring-secondary-800",
|
|
132
|
-
table: "bg-secondary-100 text-secondary-700 hover:bg-secondary-200 focus:ring-secondary-300 dark:bg-secondary-700 dark:text-secondary-100 dark:hover:bg-secondary-600 dark:focus:ring-secondary-600"
|
|
133
|
-
)
|
|
134
|
-
end
|
|
135
|
-
end
|
|
96
|
+
def color_class
|
|
97
|
+
color_key = (@action.color || @action.category)&.to_sym || :secondary
|
|
98
|
+
color_mapping = COLOR_CLASSES[color_key] || COLOR_CLASSES[:secondary]
|
|
136
99
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
when :table
|
|
140
|
-
table
|
|
141
|
-
else
|
|
142
|
-
default
|
|
143
|
-
end
|
|
100
|
+
# Table variant uses soft (tinted) buttons, default uses solid buttons
|
|
101
|
+
(@variant == :table) ? color_mapping[:soft] : color_mapping[:default]
|
|
144
102
|
end
|
|
145
103
|
end
|
|
146
104
|
end
|