plutonium 0.43.1 → 0.44.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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +35 -0
  4. data/Rakefile +24 -6
  5. data/app/assets/plutonium.css +1 -1
  6. data/config/initializers/pagy.rb +1 -4
  7. data/gemfiles/rails_7.gemfile +1 -1
  8. data/gemfiles/rails_7.gemfile.lock +128 -103
  9. data/gemfiles/rails_8.0.gemfile +1 -1
  10. data/gemfiles/rails_8.0.gemfile.lock +56 -46
  11. data/gemfiles/rails_8.1.gemfile +1 -1
  12. data/gemfiles/rails_8.1.gemfile.lock +108 -98
  13. data/lib/generators/pu/invites/install_generator.rb +69 -11
  14. data/lib/generators/pu/invites/templates/INSTRUCTIONS +4 -1
  15. data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +1 -1
  16. data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +1 -1
  17. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +13 -8
  18. data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +17 -26
  19. data/lib/generators/pu/invites/templates/packages/invites/app/interactions/invites/cancel_invite_interaction.rb.tt +1 -1
  20. data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +2 -0
  21. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +3 -2
  22. data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/welcome/pending_invitation.html.erb.tt +3 -2
  23. data/lib/generators/pu/lib/plutonium_generators/concerns/logger.rb +2 -0
  24. data/lib/generators/pu/lib/plutonium_generators/concerns/package_selector.rb +4 -6
  25. data/lib/generators/pu/lib/plutonium_generators/concerns/rodauth_redirects.rb +41 -0
  26. data/lib/generators/pu/lib/plutonium_generators/generator.rb +5 -1
  27. data/lib/generators/pu/lib/plutonium_generators/model_generator_base.rb +0 -1
  28. data/lib/generators/pu/lib/plutonium_generators/non_interactive_prompt.rb +27 -0
  29. data/lib/generators/pu/rodauth/templates/app/models/account.rb.tt +4 -0
  30. data/lib/generators/pu/saas/entity_generator.rb +16 -0
  31. data/lib/generators/pu/saas/membership_generator.rb +4 -3
  32. data/lib/generators/pu/saas/portal/USAGE +15 -0
  33. data/lib/generators/pu/saas/portal_generator.rb +122 -0
  34. data/lib/generators/pu/saas/setup/USAGE +17 -22
  35. data/lib/generators/pu/saas/setup_generator.rb +62 -9
  36. data/lib/generators/pu/saas/welcome/USAGE +27 -0
  37. data/lib/generators/pu/saas/welcome/templates/app/controllers/authenticated_controller.rb.tt +18 -0
  38. data/lib/generators/pu/saas/welcome/templates/app/controllers/welcome_controller.rb.tt +69 -0
  39. data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +33 -0
  40. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/onboarding.html.erb.tt +51 -0
  41. data/lib/generators/pu/saas/welcome/templates/app/views/welcome/select_entity.html.erb.tt +22 -0
  42. data/lib/generators/pu/saas/welcome_generator.rb +197 -0
  43. data/lib/plutonium/auth/sequel_adapter.rb +1 -2
  44. data/lib/plutonium/core/controller.rb +18 -8
  45. data/lib/plutonium/core/controllers/association_resolver.rb +19 -6
  46. data/lib/plutonium/core/controllers/authorizable.rb +11 -0
  47. data/lib/plutonium/invites/concerns/cancel_invite.rb +3 -1
  48. data/lib/plutonium/invites/concerns/invite_token.rb +0 -8
  49. data/lib/plutonium/invites/concerns/invite_user.rb +1 -1
  50. data/lib/plutonium/resource/controller.rb +2 -3
  51. data/lib/plutonium/resource/controllers/authorizable.rb +4 -5
  52. data/lib/plutonium/resource/controllers/crud_actions/index_action.rb +1 -1
  53. data/lib/plutonium/resource/controllers/presentable.rb +2 -0
  54. data/lib/plutonium/ui/table/components/pagy_info.rb +1 -7
  55. data/lib/plutonium/ui/table/components/pagy_pagination.rb +4 -6
  56. data/lib/plutonium/version.rb +1 -1
  57. data/package.json +1 -1
  58. data/plutonium.gemspec +2 -2
  59. metadata +18 -10
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AuthenticatedController < ApplicationController
4
+ <% if rodauth? -%>
5
+ include Plutonium::Auth::Rodauth(:<%= rodauth_config %>)
6
+
7
+ before_action { rodauth.require_authentication }
8
+ <% else -%>
9
+ before_action :require_authenticated
10
+
11
+ private
12
+
13
+ def require_authenticated
14
+ # TODO: Implement based on your authentication system
15
+ redirect_to "/login" unless current_user
16
+ end
17
+ <% end -%>
18
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ class WelcomeController < AuthenticatedController
4
+ layout "welcome"
5
+
6
+ def index
7
+ entities = current_user.<%= entity_table.pluralize %>.to_a
8
+ if entities.none?
9
+ @<%= entity_table %> = <%= entity_model %>.new
10
+ <% if profile? -%>
11
+ @profile = current_user.profile || current_user.build_profile
12
+ <% end -%>
13
+ render :onboarding
14
+ elsif entities.one?
15
+ redirect_to portal_root_path(entities.first)
16
+ else
17
+ @entities = entities
18
+ render :select_entity
19
+ end
20
+ end
21
+
22
+ def new_entity
23
+ @<%= entity_table %> = <%= entity_model %>.new
24
+ <% if profile? -%>
25
+ @profile = current_user.profile || current_user.build_profile
26
+ <% end -%>
27
+ render :onboarding
28
+ end
29
+
30
+ def onboard
31
+ @<%= entity_table %> = <%= entity_model %>.new(<%= entity_table %>_params)
32
+ <% if profile? -%>
33
+ @profile = current_user.profile || current_user.build_profile
34
+ @profile.assign_attributes(profile_params)
35
+
36
+ if @<%= entity_table %>.valid? && @profile.valid?
37
+ <% else -%>
38
+ if @<%= entity_table %>.valid?
39
+ <% end -%>
40
+ ActiveRecord::Base.transaction do
41
+ @<%= entity_table %>.save!
42
+ <%= membership_model %>.create!(<%= entity_table %>: @<%= entity_table %>, <%= user_table %>: current_user, role: :owner)
43
+ <% if profile? -%>
44
+ @profile.save!
45
+ <% end -%>
46
+ end
47
+ redirect_to portal_root_path(@<%= entity_table %>), notice: "Welcome! Your workspace is ready."
48
+ else
49
+ render :onboarding, status: :unprocessable_entity
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def <%= entity_table %>_params
56
+ params.require(:<%= entity_table %>).permit(:name)
57
+ end
58
+ <% if profile? -%>
59
+
60
+ def profile_params
61
+ params.require(:profile).permit(:name)
62
+ end
63
+ <% end -%>
64
+
65
+ def portal_root_path(<%= entity_table %>)
66
+ <%= portal_engine %>::Engine.routes.url_helpers.<%= entity_table %>_root_path(<%= entity_table %>: <%= entity_table %>)
67
+ end
68
+ helper_method :portal_root_path
69
+ end
@@ -0,0 +1,33 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Welcome</title>
5
+ <meta charset="utf-8">
6
+ <meta name="viewport" content="width=device-width,initial-scale=1">
7
+ <meta name="csrf-param" content="authenticity_token" />
8
+ <meta name="csrf-token" content="<%%= form_authenticity_token %>" />
9
+ <%%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
10
+ <%%= javascript_importmap_tags %>
11
+ </head>
12
+ <body class="antialiased min-h-screen bg-[var(--pu-body)]">
13
+ <main class="p-4 min-h-screen flex flex-col items-center justify-center gap-2 px-6 py-8 mx-auto lg:py-0">
14
+ <%% if flash.any? %>
15
+ <%% flash.each do |type, message| %>
16
+ <div class="fixed z-50 top-16 inset-x-0 mx-auto flex items-center w-full max-w-md p-4 rounded-[var(--pu-radius-lg)]"
17
+ style="box-shadow: var(--pu-shadow-md); background: <%%= type == 'alert' ? 'var(--color-danger-50)' : 'var(--color-success-50)' %>; color: <%%= type == 'alert' ? 'var(--color-danger-600)' : 'var(--color-success-600)' %>"
18
+ role="alert">
19
+ <div class="ms-3 text-sm font-normal"><%%= message %></div>
20
+ </div>
21
+ <%% end %>
22
+ <%% end %>
23
+
24
+ <!-- Main content card -->
25
+ <div class="w-full sm:max-w-md bg-[var(--pu-card-bg)] border border-[var(--pu-card-border)] rounded-[var(--pu-radius-lg)]"
26
+ style="box-shadow: var(--pu-shadow-lg)">
27
+ <div class="p-6 space-y-4 md:space-y-6 sm:p-8">
28
+ <%%= yield %>
29
+ </div>
30
+ </div>
31
+ </main>
32
+ </body>
33
+ </html>
@@ -0,0 +1,51 @@
1
+ <h1 class="text-lg font-bold leading-tight tracking-tight text-[var(--pu-text)] md:text-xl text-center">
2
+ Welcome! Let's get you set up.
3
+ </h1>
4
+
5
+ <%%= form_with(url: welcome_onboard_path, method: :post, class: "space-y-4 md:space-y-6") do |f| %>
6
+ <% if profile? -%>
7
+ <%% if @<%= entity_table %>.errors.any? || @profile.errors.any? %>
8
+ <% else -%>
9
+ <%% if @<%= entity_table %>.errors.any? %>
10
+ <% end -%>
11
+ <div class="p-4 rounded-[var(--pu-radius-lg)] bg-[color:var(--color-danger-50)] text-[color:var(--color-danger-600)]">
12
+ <ul class="list-disc list-inside text-sm">
13
+ <%% @<%= entity_table %>.errors.full_messages.each do |message| %>
14
+ <li><%%= message %></li>
15
+ <%% end %>
16
+ <% if profile? -%>
17
+ <%% @profile.errors.full_messages.each do |message| %>
18
+ <li><%%= message %></li>
19
+ <%% end %>
20
+ <% end -%>
21
+ </ul>
22
+ </div>
23
+ <%% end %>
24
+ <% if profile? -%>
25
+
26
+ <%%= f.fields_for :profile, @profile do |pf| %>
27
+ <div>
28
+ <%%= pf.label :name, "Your Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
29
+ <%%= pf.text_field :name,
30
+ class: "pu-input w-full",
31
+ placeholder: "e.g. Jane Smith",
32
+ autofocus: true %>
33
+ </div>
34
+ <%% end %>
35
+ <% end -%>
36
+
37
+ <%%= f.fields_for :<%= entity_table %>, @<%= entity_table %> do |ef| %>
38
+ <div>
39
+ <%%= ef.label :name, "Workspace Name", class: "block mb-2 text-sm font-medium text-[var(--pu-text)]" %>
40
+ <%%= ef.text_field :name,
41
+ class: "pu-input w-full",
42
+ placeholder: "e.g. Acme Corp",
43
+ <% unless profile? -%>
44
+ autofocus: true,
45
+ <% end -%>
46
+ required: true %>
47
+ </div>
48
+ <%% end %>
49
+
50
+ <%%= f.submit "Get Started", class: "w-full pu-btn pu-btn-md pu-btn-primary" %>
51
+ <%% end %>
@@ -0,0 +1,22 @@
1
+ <h1 class="text-lg font-bold leading-tight tracking-tight text-[var(--pu-text)] md:text-xl text-center">
2
+ Select a Workspace
3
+ </h1>
4
+
5
+ <div class="space-y-2">
6
+ <%% @entities.each do |<%= entity_table %>| %>
7
+ <a href="<%%= portal_root_path(<%= entity_table %>) %>"
8
+ class="flex items-center gap-3 w-full p-4 text-left bg-[var(--pu-surface-alt)] border border-[var(--pu-border)] rounded-[var(--pu-radius-lg)] hover:border-[var(--color-primary-500)] hover:bg-[var(--pu-surface)] transition-colors">
9
+ <div class="flex-1">
10
+ <p class="font-medium text-[var(--pu-text)]"><%%= <%= entity_table %>.name %></p>
11
+ </div>
12
+ <%%= render Phlex::TablerIcons::ChevronRight.new(class: "w-5 h-5 text-[var(--pu-text-muted)]") %>
13
+ </a>
14
+ <%% end %>
15
+ </div>
16
+
17
+ <div class="pt-2">
18
+ <a href="<%%= welcome_onboard_path %>"
19
+ class="w-full block pu-btn pu-btn-md pu-btn-outline text-center">
20
+ Create New Workspace
21
+ </a>
22
+ </div>
@@ -0,0 +1,197 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+ require_relative "../lib/plutonium_generators"
5
+
6
+ module Pu
7
+ module Saas
8
+ class WelcomeGenerator < ::Rails::Generators::Base
9
+ include PlutoniumGenerators::Generator
10
+ include PlutoniumGenerators::Concerns::RodauthRedirects
11
+
12
+ source_root File.expand_path("welcome/templates", __dir__)
13
+
14
+ desc "Generate a post-login welcome flow with onboarding and entity selection"
15
+
16
+ class_option :user_model, type: :string, required: true,
17
+ desc: "The user model name (e.g., User)"
18
+
19
+ class_option :entity_model, type: :string, required: true,
20
+ desc: "The entity model name (e.g., Organization)"
21
+
22
+ class_option :portal, type: :string, required: true,
23
+ desc: "The portal engine name (e.g., CustomerPortal)"
24
+
25
+ class_option :membership_model, type: :string,
26
+ desc: "The membership model name (defaults to <Entity><User>)"
27
+
28
+ class_option :rodauth, type: :string, default: "user",
29
+ desc: "Rodauth configuration name"
30
+
31
+ class_option :profile, type: :boolean, default: false,
32
+ desc: "Include profile setup in onboarding"
33
+
34
+ def start
35
+ validate_requirements
36
+ create_authenticated_controller
37
+ create_welcome_controller
38
+ create_views
39
+ add_routes
40
+ configure_rodauth
41
+ show_instructions
42
+ rescue => e
43
+ exception "#{self.class} failed:", e
44
+ end
45
+
46
+ private
47
+
48
+ def validate_requirements
49
+ errors = []
50
+
51
+ unless File.exist?(user_model_path)
52
+ errors << "User model not found: #{user_model_path.relative_path_from(Rails.root)}"
53
+ end
54
+
55
+ unless File.exist?(entity_model_path)
56
+ errors << "Entity model not found: #{entity_model_path.relative_path_from(Rails.root)}"
57
+ end
58
+
59
+ if File.exist?(user_model_path)
60
+ user_content = File.read(user_model_path)
61
+ unless user_content.include?("has_many :#{entity_table.pluralize}")
62
+ errors << "User model missing 'has_many :#{entity_table.pluralize}' — run the membership generator first"
63
+ end
64
+ end
65
+
66
+ if errors.any?
67
+ errors.each { |e| say_status :error, e, :red }
68
+ raise Thor::Error, "Required files missing:\n - #{errors.join("\n - ")}"
69
+ end
70
+ end
71
+
72
+ def create_authenticated_controller
73
+ template "app/controllers/authenticated_controller.rb",
74
+ "app/controllers/authenticated_controller.rb"
75
+ end
76
+
77
+ def create_welcome_controller
78
+ template "app/controllers/welcome_controller.rb",
79
+ "app/controllers/welcome_controller.rb"
80
+ end
81
+
82
+ def create_views
83
+ template "app/views/layouts/welcome.html.erb",
84
+ "app/views/layouts/welcome.html.erb"
85
+
86
+ template "app/views/welcome/select_entity.html.erb",
87
+ "app/views/welcome/select_entity.html.erb"
88
+
89
+ template "app/views/welcome/onboarding.html.erb",
90
+ "app/views/welcome/onboarding.html.erb"
91
+ end
92
+
93
+ def add_routes
94
+ routes_file = "config/routes.rb"
95
+ routes_content = File.read(Rails.root.join(routes_file))
96
+
97
+ if routes_content.include?("# Welcome & onboarding")
98
+ say_status :skip, "Welcome routes already present", :yellow
99
+ return
100
+ end
101
+
102
+ route_code = <<-RUBY
103
+
104
+ # Welcome & onboarding
105
+ get "welcome", to: "welcome#index"
106
+ get "welcome/onboard", to: "welcome#new_entity", as: :welcome_onboard
107
+ post "welcome/onboard", to: "welcome#onboard"
108
+ RUBY
109
+
110
+ # Remove the standalone invites welcome route if it exists,
111
+ # since the main WelcomeController now handles /welcome
112
+ if routes_content.include?('get "welcome", to: "invites/welcome#index"')
113
+ gsub_file routes_file,
114
+ /\n\s*# Welcome route \(handled by invites.*\n\s*get "welcome", to: "invites\/welcome#index"\n/,
115
+ "\n"
116
+ end
117
+
118
+ inject_into_file routes_file,
119
+ route_code,
120
+ before: /^end\s*\z/
121
+ end
122
+
123
+ def configure_rodauth
124
+ return unless rodauth?
125
+
126
+ update_rodauth_redirects("app/rodauth/#{rodauth_config}_rodauth_plugin.rb")
127
+ end
128
+
129
+ def show_instructions
130
+ say "\n"
131
+ say "=" * 79
132
+ say "\n"
133
+ say "Welcome flow installed successfully!"
134
+ say "\n"
135
+ say "Next steps:"
136
+ say "\n"
137
+ say "1. Run migrations (if you haven't already):"
138
+ say " rails db:migrate"
139
+ say "\n"
140
+ say "2. Customize the onboarding view to match your app:"
141
+ say " app/views/welcome/onboarding.html.erb"
142
+ say "\n"
143
+ if profile?
144
+ say "3. Ensure your User model has a `profile` association:"
145
+ say " has_one :profile"
146
+ say "\n"
147
+ end
148
+ say "=" * 79
149
+ say "\n"
150
+ end
151
+
152
+ def user_model
153
+ options[:user_model].camelize
154
+ end
155
+
156
+ def user_table
157
+ options[:user_model].underscore
158
+ end
159
+
160
+ def entity_model
161
+ options[:entity_model].camelize
162
+ end
163
+
164
+ def entity_table
165
+ options[:entity_model].underscore
166
+ end
167
+
168
+ def portal_engine
169
+ options[:portal].camelize
170
+ end
171
+
172
+ def membership_model
173
+ options[:membership_model] || "#{entity_model}#{user_model}"
174
+ end
175
+
176
+ def rodauth_config
177
+ options[:rodauth]
178
+ end
179
+
180
+ def rodauth?
181
+ rodauth_config.present?
182
+ end
183
+
184
+ def profile?
185
+ options[:profile]
186
+ end
187
+
188
+ def entity_model_path
189
+ Rails.root.join("app/models/#{entity_table}.rb")
190
+ end
191
+
192
+ def user_model_path
193
+ Rails.root.join("app/models/#{user_table}.rb")
194
+ end
195
+ end
196
+ end
197
+ end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "sequel/core"
4
-
5
3
  module Plutonium
6
4
  module Auth
7
5
  # Provides runtime detection of the database adapter for Sequel configuration.
@@ -26,6 +24,7 @@ module Plutonium
26
24
  # @return [Sequel::Database] configured Sequel database connection
27
25
  # @raise [RuntimeError] if the Sequel adapter initialization fails
28
26
  def db
27
+ require "sequel/core"
29
28
  adapter = sequel_adapter
30
29
  begin
31
30
  if RUBY_ENGINE == "jruby"
@@ -145,7 +145,10 @@ module Plutonium
145
145
  end
146
146
 
147
147
  # Build the named helper: e.g., "blogging_post_nested_post_metadata_path"
148
- parent_singular = parent.model_name.singular
148
+ # For plural parent resources (resources :posts), nested routes use the singular member name (post_nested_...)
149
+ # For singular parent resources (resource :entities), nested routes use the route name as-is (entities_nested_...)
150
+ parent_is_singular_route = current_engine.routes.singular_resource_route?(parent.model_name.plural)
151
+ parent_prefix = parent_is_singular_route ? parent.model_name.plural : parent.model_name.singular
149
152
  nested_resource_name = "#{prefix}#{association_name}"
150
153
 
151
154
  # Determine if this is a collection action (no specific record)
@@ -158,12 +161,10 @@ module Plutonium
158
161
  # - :new action uses singular (new_blogging_post_nested_comment)
159
162
  # - member actions (show/edit/update/destroy) use singular (blogging_post_nested_comment)
160
163
  is_collection_action = action == :index || action == :create || (no_record && action != :new)
161
- helper_base = if is_singular
162
- "#{parent_singular}_#{nested_resource_name}"
163
- elsif is_collection_action
164
- "#{parent_singular}_#{nested_resource_name}"
164
+ helper_base = if is_singular || is_collection_action
165
+ "#{parent_prefix}_#{nested_resource_name}"
165
166
  else
166
- "#{parent_singular}_#{nested_resource_name.to_s.singularize}"
167
+ "#{parent_prefix}_#{nested_resource_name.to_s.singularize}"
167
168
  end
168
169
 
169
170
  # Only add helper prefix for actions that have named route helpers (new, edit)
@@ -173,10 +174,19 @@ module Plutonium
173
174
  else "#{action}_"
174
175
  end
175
176
 
176
- helper_name = :"#{helper_suffix}#{helper_base}_path"
177
+ # Add entity scope prefix for path-based entity scoping
178
+ entity_prefix = if scoped_to_entity? && scoped_entity_strategy == :path
179
+ "#{scoped_entity_param_key}_"
180
+ end
181
+
182
+ helper_name = :"#{helper_suffix}#{entity_prefix}#{helper_base}_path"
177
183
 
178
184
  # Build the arguments for the helper
179
- helper_args = [parent.to_param]
185
+ helper_args = []
186
+ # Add entity scope param for path-based entity scoping
187
+ helper_args << current_scoped_entity.to_param if entity_prefix
188
+ # Singular parent resources (resource :entity) have no :id param in the route
189
+ helper_args << parent.to_param unless parent_is_singular_route
180
190
  # Include element ID for plural routes (has_many) when we have a record instance
181
191
  # Skip ID for collection actions (:index, :create) which don't need a member ID
182
192
  unless is_singular || no_record || is_collection_action
@@ -61,21 +61,34 @@ module Plutonium
61
61
  #
62
62
  # For Blogging::Comment, returns [:blogging_comments, :comments]
63
63
  # For Comment, returns [:comments]
64
+ # For Blogging::PostDetail, returns [:blogging_post_details, :post_details, :blogging_post_detail, :post_detail]
65
+ #
66
+ # Tries both plural (has_many) and singular (has_one) forms.
64
67
  #
65
68
  # @param klass [Class] The target class
66
69
  # @return [Array<Symbol>] Candidate association names in priority order
67
70
  def association_candidates_for(klass)
68
71
  candidates = []
69
72
 
70
- # Full namespaced name: Blogging::Comment => :blogging_comments
71
- full_name = klass.model_name.plural.to_sym
72
- candidates << full_name
73
+ # Full namespaced name (plural): Blogging::Comment => :blogging_comments
74
+ full_plural = klass.model_name.plural.to_sym
75
+ candidates << full_plural
73
76
 
74
- # Demodulized name: Blogging::Comment => :comments
77
+ # Demodulized name (plural): Blogging::Comment => :comments
75
78
  demodulized = klass.name.demodulize
76
79
  if demodulized != klass.name
77
- short_name = demodulized.underscore.pluralize.to_sym
78
- candidates << short_name unless candidates.include?(short_name)
80
+ short_plural = demodulized.underscore.pluralize.to_sym
81
+ candidates << short_plural unless candidates.include?(short_plural)
82
+ end
83
+
84
+ # Full namespaced name (singular): Blogging::PostDetail => :blogging_post_detail
85
+ full_singular = klass.model_name.singular.to_sym
86
+ candidates << full_singular unless candidates.include?(full_singular)
87
+
88
+ # Demodulized name (singular): Blogging::PostDetail => :post_detail
89
+ if demodulized != klass.name
90
+ short_singular = demodulized.underscore.to_sym
91
+ candidates << short_singular unless candidates.include?(short_singular)
79
92
  end
80
93
 
81
94
  candidates
@@ -24,11 +24,22 @@ module Plutonium
24
24
  end
25
25
 
26
26
  options[:with] ||= ::ActionPolicy.lookup(resource, namespace: authorization_namespace)
27
+ options[:context] = (options[:context] || {}).deep_merge(current_policy_context)
27
28
  relation ||= resource.all
28
29
 
29
30
  authorized_scope(relation, **options)
30
31
  end
31
32
 
33
+ # Returns the base policy context available to all controllers.
34
+ # Resource controllers extend this with parent/association context.
35
+ #
36
+ # @return [Hash] context containing entity_scope
37
+ def current_policy_context
38
+ {
39
+ entity_scope: entity_scope_for_authorize
40
+ }
41
+ end
42
+
32
43
  def entity_scope_for_authorize
33
44
  # Use the instance variable directly to avoid circular dependency.
34
45
  # When authorizing the scoped entity itself (in fetch_current_scoped_entity),
@@ -17,7 +17,9 @@ module Plutonium
17
17
  extend ActiveSupport::Concern
18
18
 
19
19
  included do
20
- presents label: "Cancel Invitation", icon: "outline/x-circle"
20
+ presents label: "Cancel Invitation", icon: Phlex::TablerIcons::CircleX
21
+
22
+ attribute :resource
21
23
  end
22
24
 
23
25
  def execute
@@ -151,14 +151,6 @@ module Plutonium
151
151
  raise NotImplementedError, "#{self.class}#create_membership_for must be implemented to create the membership record"
152
152
  end
153
153
 
154
- # Alias method for the entity association.
155
- # Override if your entity association has a different name.
156
- #
157
- # @return [Object] the entity record
158
- def entity
159
- raise NotImplementedError, "#{self.class}#entity must be implemented or an entity association must exist"
160
- end
161
-
162
154
  private
163
155
 
164
156
  def extract_domain(email)
@@ -57,7 +57,7 @@ module Plutonium
57
57
  # Override to specify the invite model class
58
58
  # @return [Class]
59
59
  def invite_class
60
- Invites::UserInvite
60
+ ::Invites::UserInvite
61
61
  end
62
62
 
63
63
  # Override to specify the membership model class
@@ -7,7 +7,7 @@ module Plutonium
7
7
  # Controller module to handle resource actions and concerns
8
8
  module Controller
9
9
  extend ActiveSupport::Concern
10
- include Pagy::Backend
10
+ include Pagy::Method
11
11
  include Plutonium::Core::Controller
12
12
  include Plutonium::Resource::Controllers::Defineable
13
13
  include Plutonium::Resource::Controllers::Authorizable
@@ -17,8 +17,7 @@ module Plutonium
17
17
  include Plutonium::Resource::Controllers::InteractiveActions
18
18
 
19
19
  included do
20
- # https://github.com/ddnexus/pagy/blob/master/docs/extras/headers.md#headers
21
- after_action { pagy_headers_merge(@pagy) if @pagy }
20
+ after_action { response.headers.merge!(@pagy.headers_hash) if @pagy }
22
21
 
23
22
  helper_method :current_parent, :current_nested_association, :resource_record!, :resource_record?, :resource_param_key, :resource_class
24
23
 
@@ -109,15 +109,14 @@ module Plutonium
109
109
  end
110
110
 
111
111
  # Returns the policy context for the current resource
112
- # Separates parent scoping (nested routes) from entity scoping (multi-tenancy)
112
+ # Extends the base context with parent scoping from nested routes
113
113
  #
114
114
  # @return [Hash] context containing parent, parent_association, and entity_scope
115
115
  def current_policy_context
116
- {
116
+ super.merge(
117
117
  parent: current_parent,
118
- parent_association: current_nested_association,
119
- entity_scope: entity_scope_for_authorize
120
- }
118
+ parent_association: current_nested_association
119
+ )
121
120
  end
122
121
 
123
122
  # Authorizes the current action for the given record of the current resource
@@ -8,7 +8,7 @@ module Plutonium
8
8
  private
9
9
 
10
10
  def setup_index_action!
11
- @pagy, @resource_records = pagy filtered_resource_collection
11
+ @pagy, @resource_records = pagy(:offset, filtered_resource_collection)
12
12
  end
13
13
 
14
14
  def filtered_resource_collection
@@ -59,6 +59,8 @@ module Plutonium
59
59
  return @scoped_entity_association if defined?(@scoped_entity_association)
60
60
 
61
61
  matching_assocs = resource_class.reflect_on_all_associations(:belongs_to).select do |assoc|
62
+ next false if assoc.polymorphic?
63
+
62
64
  assoc.klass.name == scoped_entity_class.name
63
65
  rescue NameError
64
66
  false
@@ -5,8 +5,6 @@ module Plutonium
5
5
  module Table
6
6
  module Components
7
7
  class PagyInfo < Plutonium::UI::Component::Base
8
- include Pagy::Frontend
9
-
10
8
  def initialize(pagy, per_page_options: [5, 10, 20, 50, 100])
11
9
  @pagy = pagy
12
10
  @per_page_options = (per_page_options + [@pagy.limit]).uniq.sort
@@ -57,11 +55,7 @@ module Plutonium
57
55
  end
58
56
 
59
57
  def page_url(limit)
60
- original_limit = @pagy.vars[:limit]
61
- @pagy.vars[:limit] = limit
62
- pagy_url_for(@pagy, @pagy.page)
63
- ensure
64
- @pagy.vars[:limit] = original_limit
58
+ @pagy.page_url(@pagy.page, limit: limit, client_max_limit: limit)
65
59
  end
66
60
  end
67
61
  end