plutonium 0.48.0 → 0.49.1
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-invites/SKILL.md +41 -0
- data/CHANGELOG.md +38 -0
- data/app/assets/plutonium.js +73 -25
- data/app/assets/plutonium.js.map +3 -3
- data/app/assets/plutonium.min.js +29 -29
- data/app/assets/plutonium.min.js.map +3 -3
- data/app/views/plutonium/_flash.html.erb +1 -1
- data/config/initializers/pagy.rb +1 -1
- data/docs/guides/user-invites.md +64 -0
- data/docs/public/templates/plutonium.rb +3 -0
- data/docs/superpowers/plans/2026-05-06-multi-invite-model-support.md +1487 -0
- data/docs/superpowers/plans/2026-05-06-multi-invite-model-support.md.tasks.json +15 -0
- data/gemfiles/rails_7.gemfile.lock +27 -1
- data/gemfiles/rails_8.0.gemfile.lock +27 -1
- data/gemfiles/rails_8.1.gemfile.lock +1 -1
- data/lib/generators/pu/gem/actual_db_schema/actual_db_schema_generator.rb +24 -0
- data/lib/generators/pu/invites/install_generator.rb +136 -35
- data/lib/generators/pu/invites/templates/app/interactions/invite_user_interaction.rb.tt +8 -2
- data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt +7 -1
- data/lib/generators/pu/invites/templates/db/migrate/create_user_invites.rb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/user_invitations_controller.rb.tt +9 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/controllers/invites/welcome_controller.rb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/definitions/invites/user_invite_definition.rb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/mailers/invites/user_invite_mailer.rb.tt +8 -8
- data/lib/generators/pu/invites/templates/packages/invites/app/models/invites/user_invite.rb.tt +13 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/policies/invites/user_invite_policy.rb.tt +3 -3
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/landing.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/show.html.erb.tt +1 -1
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invitations/signup.html.erb.tt +2 -2
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb.tt +4 -4
- data/lib/generators/pu/invites/templates/packages/invites/app/views/layouts/invites/invitation.html.erb.tt +5 -1
- data/lib/generators/pu/lib/plutonium_generators/concerns/configures_sqlite.rb +9 -3
- data/lib/generators/pu/lite/rails_pulse/rails_pulse_generator.rb +6 -3
- data/lib/generators/pu/lite/rails_pulse/templates/config/initializers/rails_pulse.rb.tt +18 -0
- data/lib/generators/pu/saas/welcome/templates/app/views/layouts/welcome.html.erb.tt +5 -1
- data/lib/plutonium/core/controller.rb +10 -3
- data/lib/plutonium/engine.rb +1 -1
- data/lib/plutonium/invites/concerns/invite_token.rb +11 -3
- data/lib/plutonium/invites/concerns/invite_user.rb +13 -4
- data/lib/plutonium/invites/controller.rb +14 -1
- data/lib/plutonium/invites/pending_invite_check.rb +37 -28
- data/lib/plutonium/resource/controllers/interactive_actions.rb +13 -9
- data/lib/plutonium/resource/policy.rb +23 -8
- data/lib/plutonium/rodauth/controller_methods.rb +5 -1
- data/lib/plutonium/ui/color_mode_selector.rb +7 -18
- data/lib/plutonium/ui/layout/rodauth_layout.rb +6 -0
- data/lib/plutonium/ui/layout/sidebar.rb +1 -1
- data/lib/plutonium/ui/table/components/pagy_info.rb +1 -1
- data/lib/plutonium/version.rb +1 -1
- data/package.json +1 -1
- data/plutonium.gemspec +16 -0
- data/src/js/controllers/color_mode_controller.js +41 -34
- data/src/js/controllers/flatpickr_controller.js +23 -0
- data/src/js/controllers/sidebar_controller.js +28 -1
- metadata +19 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"planPath": "docs/superpowers/plans/2026-05-06-multi-invite-model-support.md",
|
|
3
|
+
"tasks": [
|
|
4
|
+
{"id": 1, "subject": "Task 1: PendingInviteCheck — multi-class support", "status": "completed"},
|
|
5
|
+
{"id": 2, "subject": "Task 2: Auto append_view_path in invites concerns + qualify flash partial", "status": "completed"},
|
|
6
|
+
{"id": 3, "subject": "Task 3: Plutonium::Invites::Controller — overridable invitation_path", "status": "completed"},
|
|
7
|
+
{"id": 4, "subject": "Task 4: install_generator — --invite_model option + naming helpers", "status": "completed"},
|
|
8
|
+
{"id": 5, "subject": "Task 5: Parameterize all templates by invite model name", "status": "completed", "blockedBy": [4]},
|
|
9
|
+
{"id": 6, "subject": "Task 6: Scope routes per invite type with idempotent re-run", "status": "completed", "blockedBy": [4]},
|
|
10
|
+
{"id": 7, "subject": "Task 7: Welcome controller — multi-class integration on re-run", "status": "completed", "blockedBy": [1, 4]},
|
|
11
|
+
{"id": 8, "subject": "Task 8: End-to-end dual-invocation generator test", "status": "completed", "blockedBy": [5, 6, 7]},
|
|
12
|
+
{"id": 9, "subject": "Task 9: Update skill + guide docs", "status": "completed", "blockedBy": [8]}
|
|
13
|
+
],
|
|
14
|
+
"lastUpdated": "2026-05-06T14:00:00Z"
|
|
15
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.49.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
listen (~> 3.8)
|
|
7
7
|
pagy (~> 43.0)
|
|
@@ -100,6 +100,8 @@ GEM
|
|
|
100
100
|
minitest (>= 5.1)
|
|
101
101
|
securerandom (>= 0.3)
|
|
102
102
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
103
|
+
addressable (2.9.0)
|
|
104
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
103
105
|
ansi (1.5.0)
|
|
104
106
|
appraisal (2.5.0)
|
|
105
107
|
bundler
|
|
@@ -118,6 +120,15 @@ GEM
|
|
|
118
120
|
bundler-audit (0.9.3)
|
|
119
121
|
bundler (>= 1.2.0)
|
|
120
122
|
thor (~> 1.0)
|
|
123
|
+
capybara (3.40.0)
|
|
124
|
+
addressable
|
|
125
|
+
matrix
|
|
126
|
+
mini_mime (>= 0.1.3)
|
|
127
|
+
nokogiri (~> 1.11)
|
|
128
|
+
rack (>= 1.6.0)
|
|
129
|
+
rack-test (>= 0.6.3)
|
|
130
|
+
regexp_parser (>= 1.5, < 3.0)
|
|
131
|
+
xpath (~> 3.2)
|
|
121
132
|
cgi (0.5.1)
|
|
122
133
|
chunky_png (1.4.0)
|
|
123
134
|
combustion (1.5.0)
|
|
@@ -174,6 +185,7 @@ GEM
|
|
|
174
185
|
net-pop
|
|
175
186
|
net-smtp
|
|
176
187
|
marcel (1.1.0)
|
|
188
|
+
matrix (0.4.3)
|
|
177
189
|
mini_mime (1.1.5)
|
|
178
190
|
minitest (6.0.2)
|
|
179
191
|
drb (~> 2.0)
|
|
@@ -265,6 +277,7 @@ GEM
|
|
|
265
277
|
psych (5.3.1)
|
|
266
278
|
date
|
|
267
279
|
stringio
|
|
280
|
+
public_suffix (7.0.5)
|
|
268
281
|
puma (7.2.0)
|
|
269
282
|
nio4r (~> 2.0)
|
|
270
283
|
rabl (0.17.0)
|
|
@@ -325,6 +338,7 @@ GEM
|
|
|
325
338
|
regexp_parser (2.11.3)
|
|
326
339
|
reline (0.6.3)
|
|
327
340
|
io-console (~> 0.5)
|
|
341
|
+
rexml (3.4.4)
|
|
328
342
|
roda (3.102.0)
|
|
329
343
|
rack
|
|
330
344
|
rodauth (2.42.0)
|
|
@@ -362,7 +376,14 @@ GEM
|
|
|
362
376
|
rubocop-ast (>= 1.47.1, < 2.0)
|
|
363
377
|
ruby-next-core (1.2.0)
|
|
364
378
|
ruby-progressbar (1.13.0)
|
|
379
|
+
rubyzip (3.2.2)
|
|
365
380
|
securerandom (0.4.1)
|
|
381
|
+
selenium-webdriver (4.43.0)
|
|
382
|
+
base64 (~> 0.2)
|
|
383
|
+
logger (~> 1.4)
|
|
384
|
+
rexml (~> 3.2, >= 3.2.5)
|
|
385
|
+
rubyzip (>= 1.2.2, < 4.0)
|
|
386
|
+
websocket (~> 1.0)
|
|
366
387
|
semantic_range (3.1.1)
|
|
367
388
|
sequel (5.102.0)
|
|
368
389
|
bigdecimal
|
|
@@ -419,11 +440,14 @@ GEM
|
|
|
419
440
|
unicode-emoji (4.2.0)
|
|
420
441
|
uri (1.1.1)
|
|
421
442
|
useragent (0.16.11)
|
|
443
|
+
websocket (1.2.11)
|
|
422
444
|
websocket-driver (0.8.0)
|
|
423
445
|
base64
|
|
424
446
|
websocket-extensions (>= 0.1.0)
|
|
425
447
|
websocket-extensions (0.1.5)
|
|
426
448
|
wisper (2.0.1)
|
|
449
|
+
xpath (3.2.0)
|
|
450
|
+
nokogiri (~> 1.8)
|
|
427
451
|
yaml (0.4.0)
|
|
428
452
|
zeitwerk (2.7.5)
|
|
429
453
|
|
|
@@ -442,6 +466,7 @@ DEPENDENCIES
|
|
|
442
466
|
bcrypt
|
|
443
467
|
brakeman
|
|
444
468
|
bundle-audit
|
|
469
|
+
capybara
|
|
445
470
|
combustion
|
|
446
471
|
importmap-rails
|
|
447
472
|
minitest
|
|
@@ -454,6 +479,7 @@ DEPENDENCIES
|
|
|
454
479
|
rodauth-rails
|
|
455
480
|
rotp
|
|
456
481
|
rqrcode
|
|
482
|
+
selenium-webdriver
|
|
457
483
|
sequel-activerecord_connection
|
|
458
484
|
sqlite3
|
|
459
485
|
standard
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: ..
|
|
3
3
|
specs:
|
|
4
|
-
plutonium (0.
|
|
4
|
+
plutonium (0.49.0)
|
|
5
5
|
action_policy (~> 0.7.0)
|
|
6
6
|
listen (~> 3.8)
|
|
7
7
|
pagy (~> 43.0)
|
|
@@ -98,6 +98,8 @@ GEM
|
|
|
98
98
|
securerandom (>= 0.3)
|
|
99
99
|
tzinfo (~> 2.0, >= 2.0.5)
|
|
100
100
|
uri (>= 0.13.1)
|
|
101
|
+
addressable (2.9.0)
|
|
102
|
+
public_suffix (>= 2.0.2, < 8.0)
|
|
101
103
|
ansi (1.5.0)
|
|
102
104
|
appraisal (2.5.0)
|
|
103
105
|
bundler
|
|
@@ -116,6 +118,15 @@ GEM
|
|
|
116
118
|
bundler-audit (0.9.3)
|
|
117
119
|
bundler (>= 1.2.0)
|
|
118
120
|
thor (~> 1.0)
|
|
121
|
+
capybara (3.40.0)
|
|
122
|
+
addressable
|
|
123
|
+
matrix
|
|
124
|
+
mini_mime (>= 0.1.3)
|
|
125
|
+
nokogiri (~> 1.11)
|
|
126
|
+
rack (>= 1.6.0)
|
|
127
|
+
rack-test (>= 0.6.3)
|
|
128
|
+
regexp_parser (>= 1.5, < 3.0)
|
|
129
|
+
xpath (~> 3.2)
|
|
119
130
|
chunky_png (1.4.0)
|
|
120
131
|
combustion (1.5.0)
|
|
121
132
|
activesupport (>= 3.0.0)
|
|
@@ -164,6 +175,7 @@ GEM
|
|
|
164
175
|
net-pop
|
|
165
176
|
net-smtp
|
|
166
177
|
marcel (1.1.0)
|
|
178
|
+
matrix (0.4.3)
|
|
167
179
|
mini_mime (1.1.5)
|
|
168
180
|
minitest (6.0.2)
|
|
169
181
|
drb (~> 2.0)
|
|
@@ -241,6 +253,7 @@ GEM
|
|
|
241
253
|
psych (5.3.1)
|
|
242
254
|
date
|
|
243
255
|
stringio
|
|
256
|
+
public_suffix (7.0.5)
|
|
244
257
|
puma (7.2.0)
|
|
245
258
|
nio4r (~> 2.0)
|
|
246
259
|
rabl (0.17.0)
|
|
@@ -300,6 +313,7 @@ GEM
|
|
|
300
313
|
regexp_parser (2.11.3)
|
|
301
314
|
reline (0.6.3)
|
|
302
315
|
io-console (~> 0.5)
|
|
316
|
+
rexml (3.4.4)
|
|
303
317
|
roda (3.102.0)
|
|
304
318
|
rack
|
|
305
319
|
rodauth (2.42.0)
|
|
@@ -337,7 +351,14 @@ GEM
|
|
|
337
351
|
rubocop-ast (>= 1.47.1, < 2.0)
|
|
338
352
|
ruby-next-core (1.2.0)
|
|
339
353
|
ruby-progressbar (1.13.0)
|
|
354
|
+
rubyzip (3.2.2)
|
|
340
355
|
securerandom (0.4.1)
|
|
356
|
+
selenium-webdriver (4.43.0)
|
|
357
|
+
base64 (~> 0.2)
|
|
358
|
+
logger (~> 1.4)
|
|
359
|
+
rexml (~> 3.2, >= 3.2.5)
|
|
360
|
+
rubyzip (>= 1.2.2, < 4.0)
|
|
361
|
+
websocket (~> 1.0)
|
|
341
362
|
semantic_range (3.1.1)
|
|
342
363
|
sequel (5.102.0)
|
|
343
364
|
bigdecimal
|
|
@@ -387,11 +408,14 @@ GEM
|
|
|
387
408
|
unicode-emoji (4.2.0)
|
|
388
409
|
uri (1.1.1)
|
|
389
410
|
useragent (0.16.11)
|
|
411
|
+
websocket (1.2.11)
|
|
390
412
|
websocket-driver (0.8.0)
|
|
391
413
|
base64
|
|
392
414
|
websocket-extensions (>= 0.1.0)
|
|
393
415
|
websocket-extensions (0.1.5)
|
|
394
416
|
wisper (2.0.1)
|
|
417
|
+
xpath (3.2.0)
|
|
418
|
+
nokogiri (~> 1.8)
|
|
395
419
|
yaml (0.4.0)
|
|
396
420
|
zeitwerk (2.7.5)
|
|
397
421
|
|
|
@@ -403,6 +427,7 @@ DEPENDENCIES
|
|
|
403
427
|
bcrypt
|
|
404
428
|
brakeman
|
|
405
429
|
bundle-audit
|
|
430
|
+
capybara
|
|
406
431
|
combustion
|
|
407
432
|
importmap-rails
|
|
408
433
|
minitest
|
|
@@ -415,6 +440,7 @@ DEPENDENCIES
|
|
|
415
440
|
rodauth-rails
|
|
416
441
|
rotp
|
|
417
442
|
rqrcode
|
|
443
|
+
selenium-webdriver
|
|
418
444
|
sequel-activerecord_connection
|
|
419
445
|
sqlite3
|
|
420
446
|
standard
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../lib/plutonium_generators"
|
|
4
|
+
|
|
5
|
+
module Pu
|
|
6
|
+
module Gem
|
|
7
|
+
# Installs actual_db_schema, which tracks phantom migrations across git
|
|
8
|
+
# branches so switching branches with diverging migration sets doesn't
|
|
9
|
+
# leave db/schema.rb out of sync.
|
|
10
|
+
#
|
|
11
|
+
# https://github.com/share-group/actual_db_schema
|
|
12
|
+
class ActualDbSchemaGenerator < Rails::Generators::Base
|
|
13
|
+
include PlutoniumGenerators::Generator
|
|
14
|
+
|
|
15
|
+
desc "Install the actual_db_schema gem"
|
|
16
|
+
|
|
17
|
+
def start
|
|
18
|
+
bundle "actual_db_schema", group: %i[development test]
|
|
19
|
+
rescue => e
|
|
20
|
+
exception "#{self.class} failed:", e
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -21,6 +21,9 @@ module Pu
|
|
|
21
21
|
class_option :user_model, type: :string, default: "User",
|
|
22
22
|
desc: "The user model name"
|
|
23
23
|
|
|
24
|
+
class_option :invite_model, type: :string, default: nil,
|
|
25
|
+
desc: "The invite model class name. Defaults to <EntityModel><UserModel>Invite (e.g., OrganizationUserInvite). Override for separate flows in multi-entity apps."
|
|
26
|
+
|
|
24
27
|
class_option :membership_model, type: :string,
|
|
25
28
|
desc: "The membership model name (defaults to <Entity><User>)"
|
|
26
29
|
|
|
@@ -72,44 +75,80 @@ module Pu
|
|
|
72
75
|
|
|
73
76
|
def create_user_invites_migration
|
|
74
77
|
migration_template "db/migrate/create_user_invites.rb",
|
|
75
|
-
"db/migrate/
|
|
78
|
+
"db/migrate/create_#{invite_table}.rb"
|
|
76
79
|
end
|
|
77
80
|
|
|
78
81
|
def create_model
|
|
79
82
|
template "packages/invites/app/models/invites/user_invite.rb",
|
|
80
|
-
"packages/invites/app/models/invites
|
|
83
|
+
"packages/invites/app/models/invites/#{invite_underscore}.rb"
|
|
81
84
|
end
|
|
82
85
|
|
|
83
86
|
def create_mailer
|
|
84
87
|
template "packages/invites/app/mailers/invites/user_invite_mailer.rb",
|
|
85
|
-
"packages/invites/app/mailers/invites
|
|
88
|
+
"packages/invites/app/mailers/invites/#{invite_underscore}_mailer.rb"
|
|
86
89
|
|
|
87
90
|
template "packages/invites/app/views/invites/user_invite_mailer/invitation.html.erb",
|
|
88
|
-
"packages/invites/app/views/invites/
|
|
91
|
+
"packages/invites/app/views/invites/#{invite_underscore}_mailer/invitation.html.erb"
|
|
89
92
|
|
|
90
93
|
template "packages/invites/app/views/invites/user_invite_mailer/invitation.text.erb",
|
|
91
|
-
"packages/invites/app/views/invites/
|
|
94
|
+
"packages/invites/app/views/invites/#{invite_underscore}_mailer/invitation.text.erb"
|
|
92
95
|
end
|
|
93
96
|
|
|
94
97
|
def create_controllers
|
|
95
98
|
template "packages/invites/app/controllers/invites/user_invitations_controller.rb",
|
|
96
|
-
"packages/invites/app/controllers/invites
|
|
99
|
+
"packages/invites/app/controllers/invites/#{invitations_path}_controller.rb"
|
|
100
|
+
|
|
101
|
+
# Welcome controller is a one-shot — only generate if it doesn't exist yet.
|
|
102
|
+
welcome_path = "packages/invites/app/controllers/invites/welcome_controller.rb"
|
|
103
|
+
unless File.exist?(Rails.root.join(welcome_path))
|
|
104
|
+
template "packages/invites/app/controllers/invites/welcome_controller.rb",
|
|
105
|
+
welcome_path
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def add_welcome_invite_class
|
|
110
|
+
welcome_path = "packages/invites/app/controllers/invites/welcome_controller.rb"
|
|
111
|
+
return unless File.exist?(Rails.root.join(welcome_path))
|
|
97
112
|
|
|
98
|
-
|
|
99
|
-
|
|
113
|
+
content = File.read(Rails.root.join(welcome_path))
|
|
114
|
+
new_class = "::Invites::#{invite_model}"
|
|
115
|
+
|
|
116
|
+
# Already present? bail. Use a non-word lookahead so we don't match
|
|
117
|
+
# `::Invites::FunderInvite` when looking for `::Invites::Funder`.
|
|
118
|
+
return if content =~ /#{Regexp.escape(new_class)}(?!\w)/
|
|
119
|
+
|
|
120
|
+
# Find `def invite_classes` block; inject before the closing `]`.
|
|
121
|
+
injection = content.sub(/(\bdef invite_classes\b.*?\[)([^\]]*)(\])/m) do
|
|
122
|
+
before, list, after = Regexp.last_match[1], Regexp.last_match[2], Regexp.last_match[3]
|
|
123
|
+
existing = list.strip
|
|
124
|
+
new_list = existing.empty? ? new_class : "#{existing.chomp(",").strip}, #{new_class}"
|
|
125
|
+
"#{before}#{new_list}#{after}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if injection != content
|
|
129
|
+
File.write(Rails.root.join(welcome_path), injection)
|
|
130
|
+
say_status :inject, "Added #{new_class} to welcome controller's invite_classes", :green
|
|
131
|
+
end
|
|
100
132
|
end
|
|
101
133
|
|
|
102
134
|
def create_views
|
|
103
135
|
%w[landing show signup error].each do |view|
|
|
104
136
|
template "packages/invites/app/views/invites/user_invitations/#{view}.html.erb",
|
|
105
|
-
"packages/invites/app/views/invites
|
|
137
|
+
"packages/invites/app/views/invites/#{invitations_path}/#{view}.html.erb"
|
|
106
138
|
end
|
|
107
139
|
|
|
108
|
-
|
|
109
|
-
|
|
140
|
+
# Welcome view is a one-shot too.
|
|
141
|
+
pending_path = "packages/invites/app/views/invites/welcome/pending_invitation.html.erb"
|
|
142
|
+
unless File.exist?(Rails.root.join(pending_path))
|
|
143
|
+
template "packages/invites/app/views/invites/welcome/pending_invitation.html.erb",
|
|
144
|
+
pending_path
|
|
145
|
+
end
|
|
110
146
|
|
|
111
|
-
|
|
112
|
-
|
|
147
|
+
layout_path = "packages/invites/app/views/layouts/invites/invitation.html.erb"
|
|
148
|
+
unless File.exist?(Rails.root.join(layout_path))
|
|
149
|
+
template "packages/invites/app/views/layouts/invites/invitation.html.erb",
|
|
150
|
+
layout_path
|
|
151
|
+
end
|
|
113
152
|
end
|
|
114
153
|
|
|
115
154
|
def create_interactions
|
|
@@ -122,17 +161,17 @@ module Pu
|
|
|
122
161
|
|
|
123
162
|
def create_definition
|
|
124
163
|
template "packages/invites/app/definitions/invites/user_invite_definition.rb",
|
|
125
|
-
"packages/invites/app/definitions/invites
|
|
164
|
+
"packages/invites/app/definitions/invites/#{invite_underscore}_definition.rb"
|
|
126
165
|
end
|
|
127
166
|
|
|
128
167
|
def create_policy
|
|
129
168
|
template "packages/invites/app/policies/invites/user_invite_policy.rb",
|
|
130
|
-
"packages/invites/app/policies/invites
|
|
169
|
+
"packages/invites/app/policies/invites/#{invite_underscore}_policy.rb"
|
|
131
170
|
end
|
|
132
171
|
|
|
133
172
|
def add_entity_association
|
|
134
173
|
inject_into_file entity_model_path,
|
|
135
|
-
" has_many
|
|
174
|
+
" has_many :#{invite_table}, class_name: \"Invites::#{invite_model}\", dependent: :destroy\n",
|
|
136
175
|
before: /^\s*# add has_many associations above\.\n/
|
|
137
176
|
end
|
|
138
177
|
|
|
@@ -203,31 +242,47 @@ module Pu
|
|
|
203
242
|
|
|
204
243
|
def add_routes
|
|
205
244
|
routes_content = File.read(Rails.root.join("config/routes.rb"))
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
245
|
+
flow_marker = "# Invitation routes for #{invite_model}"
|
|
246
|
+
|
|
247
|
+
if routes_content.include?(flow_marker)
|
|
248
|
+
say_status :skip, "Invitation routes for #{invite_model} already present", :yellow
|
|
249
|
+
else
|
|
250
|
+
welcome_present = routes_content.include?("# Invitation welcome routes")
|
|
210
251
|
|
|
211
|
-
|
|
252
|
+
welcome_block = if welcome_present
|
|
253
|
+
""
|
|
254
|
+
else
|
|
255
|
+
<<-RUBY
|
|
212
256
|
|
|
213
|
-
#
|
|
257
|
+
# Invitation welcome routes (shared across all invite flows)
|
|
214
258
|
scope module: :invites do
|
|
215
259
|
get "invitations/welcome", to: "welcome#index", as: :invites_welcome_check
|
|
216
260
|
delete "invitations/welcome", to: "welcome#skip", as: :invites_welcome_skip
|
|
217
|
-
get "invitations/:token", to: "user_invitations#show", as: :invitation
|
|
218
|
-
post "invitations/:token/accept", to: "user_invitations#accept", as: :accept_invitation
|
|
219
|
-
get "invitations/:token/signup", to: "user_invitations#signup", as: :invitation_signup
|
|
220
|
-
post "invitations/:token/signup", to: "user_invitations#signup"
|
|
221
261
|
end
|
|
222
|
-
|
|
262
|
+
RUBY
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
flow_block = <<-RUBY
|
|
266
|
+
|
|
267
|
+
#{flow_marker}
|
|
268
|
+
scope module: :invites do
|
|
269
|
+
get "#{invitations_path}/:token", to: "#{invitations_path}#show", as: :#{invite_route_prefix}_invitation
|
|
270
|
+
post "#{invitations_path}/:token/accept", to: "#{invitations_path}#accept", as: :accept_#{invite_route_prefix}_invitation
|
|
271
|
+
get "#{invitations_path}/:token/signup", to: "#{invitations_path}#signup", as: :#{invite_route_prefix}_invitation_signup
|
|
272
|
+
post "#{invitations_path}/:token/signup", to: "#{invitations_path}#signup"
|
|
273
|
+
end
|
|
274
|
+
RUBY
|
|
223
275
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
276
|
+
inject_into_file "config/routes.rb",
|
|
277
|
+
welcome_block + flow_block,
|
|
278
|
+
before: /^end\s*\z/
|
|
279
|
+
end
|
|
227
280
|
|
|
228
281
|
# If no main WelcomeController exists, add /welcome route pointing to
|
|
229
282
|
# Invites::WelcomeController so Rodauth's login_redirect "/welcome" works.
|
|
230
|
-
|
|
283
|
+
routes_content = File.read(Rails.root.join("config/routes.rb"))
|
|
284
|
+
unless File.exist?(Rails.root.join("app/controllers/welcome_controller.rb")) ||
|
|
285
|
+
routes_content.include?(%(get "welcome", to: "invites/welcome#index"))
|
|
231
286
|
welcome_route = <<-RUBY
|
|
232
287
|
|
|
233
288
|
# Welcome route (handled by invites package — replace with pu:saas:welcome for full onboarding)
|
|
@@ -236,7 +291,7 @@ module Pu
|
|
|
236
291
|
|
|
237
292
|
inject_into_file "config/routes.rb",
|
|
238
293
|
welcome_route,
|
|
239
|
-
before: /^\s*#
|
|
294
|
+
before: /^\s*# Invitation welcome routes/
|
|
240
295
|
end
|
|
241
296
|
end
|
|
242
297
|
|
|
@@ -342,11 +397,23 @@ module Pu
|
|
|
342
397
|
" return redirect_to(invites_welcome_check_path) if pending_invite\n\n",
|
|
343
398
|
after: /def index\n/
|
|
344
399
|
|
|
345
|
-
# Add
|
|
346
|
-
|
|
400
|
+
# Add invite_classes method if neither it nor invite_class is present
|
|
401
|
+
if file_content !~ /def invite_classes\b/ && file_content !~ /def invite_class\b/
|
|
347
402
|
inject_into_file relative_path,
|
|
348
|
-
"\n def
|
|
403
|
+
"\n def invite_classes\n [::Invites::#{invite_model}]\n end\n",
|
|
349
404
|
before: /^end\s*\z/
|
|
405
|
+
else
|
|
406
|
+
# Inject this invite_model into the existing invite_classes array if missing.
|
|
407
|
+
host_content = File.read(Rails.root.join(relative_path))
|
|
408
|
+
if host_content =~ /def invite_classes\b/ && host_content !~ /::Invites::#{invite_model}\b/
|
|
409
|
+
updated = host_content.sub(/(\bdef invite_classes\b.*?\[)([^\]]*)(\])/m) do
|
|
410
|
+
before, list, after = Regexp.last_match[1], Regexp.last_match[2], Regexp.last_match[3]
|
|
411
|
+
existing = list.strip
|
|
412
|
+
new_list = existing.empty? ? "::Invites::#{invite_model}" : "#{existing.chomp(",").strip}, ::Invites::#{invite_model}"
|
|
413
|
+
"#{before}#{new_list}#{after}"
|
|
414
|
+
end
|
|
415
|
+
File.write(Rails.root.join(relative_path), updated)
|
|
416
|
+
end
|
|
350
417
|
end
|
|
351
418
|
|
|
352
419
|
# Update Invites::WelcomeController to redirect to /welcome (the main hub)
|
|
@@ -437,6 +504,40 @@ module Pu
|
|
|
437
504
|
"app/policies/#{user_table}_policy.rb"
|
|
438
505
|
end
|
|
439
506
|
|
|
507
|
+
def invite_model
|
|
508
|
+
return options[:invite_model].camelize if options[:invite_model].present?
|
|
509
|
+
|
|
510
|
+
# Flatten "::" so namespaced entities like Blogging::Post produce a
|
|
511
|
+
# valid (single-segment) class name: BloggingPostUserInvite.
|
|
512
|
+
entity_part = entity_model.delete(":")
|
|
513
|
+
user_part = user_model.delete(":")
|
|
514
|
+
"#{entity_part}#{user_part}Invite"
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def invite_underscore
|
|
518
|
+
invite_model.underscore
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
def invite_table
|
|
522
|
+
invite_model.tableize
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# e.g. UserInvite -> UserInvitationsController, FunderInvite -> FunderInvitationsController.
|
|
526
|
+
# If the input ends in "Invite", swap to "Invitations"; else append "Invitations".
|
|
527
|
+
def invitations_controller_class
|
|
528
|
+
base = invite_model.sub(/Invite\z/, "")
|
|
529
|
+
"#{base}InvitationsController"
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def invitations_path
|
|
533
|
+
invitations_controller_class.sub(/Controller\z/, "").underscore
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# Route helper prefix: "user" for UserInvite, "funder" for FunderInvite.
|
|
537
|
+
def invite_route_prefix
|
|
538
|
+
invite_model.sub(/Invite\z/, "").underscore.presence || "invite"
|
|
539
|
+
end
|
|
540
|
+
|
|
440
541
|
def membership_model
|
|
441
542
|
options[:membership_model] || "#{entity_model}#{user_model}"
|
|
442
543
|
end
|
|
@@ -6,11 +6,17 @@ class <%= entity_model %>::InviteUserInteraction < Plutonium::Resource::Interact
|
|
|
6
6
|
presents label: "Invite <%= user_model.underscore.humanize.titleize %>", icon: Phlex::TablerIcons::Mail
|
|
7
7
|
|
|
8
8
|
attribute :role
|
|
9
|
-
input :role, as: :select, choices: Invites
|
|
10
|
-
<% if membership_model != "EntityUser" || user_model != "User" -%>
|
|
9
|
+
input :role, as: :select, choices: Invites::<%= invite_model %>.roles.keys.excluding("owner")
|
|
10
|
+
<% if membership_model != "EntityUser" || user_model != "User" || entity_association_name != "entity" -%>
|
|
11
11
|
|
|
12
12
|
private
|
|
13
13
|
<% end -%>
|
|
14
|
+
<% if entity_association_name != "entity" -%>
|
|
15
|
+
|
|
16
|
+
def invite_entity_attribute
|
|
17
|
+
:<%= entity_association_name %>
|
|
18
|
+
end
|
|
19
|
+
<% end -%>
|
|
14
20
|
<% if membership_model != "EntityUser" -%>
|
|
15
21
|
|
|
16
22
|
def membership_class
|
data/lib/generators/pu/invites/templates/app/interactions/user_invite_user_interaction.rb.tt
CHANGED
|
@@ -6,13 +6,19 @@ class <%= user_model %>::InviteUserInteraction < Plutonium::Resource::Interactio
|
|
|
6
6
|
presents label: "Invite <%= user_model.underscore.humanize.titleize %>", icon: Phlex::TablerIcons::Mail
|
|
7
7
|
|
|
8
8
|
attribute :role
|
|
9
|
-
input :role, as: :select, choices: Invites
|
|
9
|
+
input :role, as: :select, choices: Invites::<%= invite_model %>.roles.keys.excluding("owner")
|
|
10
10
|
|
|
11
11
|
private
|
|
12
12
|
|
|
13
13
|
def entity
|
|
14
14
|
current_scoped_entity
|
|
15
15
|
end
|
|
16
|
+
<% if entity_association_name != "entity" -%>
|
|
17
|
+
|
|
18
|
+
def invite_entity_attribute
|
|
19
|
+
:<%= entity_association_name %>
|
|
20
|
+
end
|
|
21
|
+
<% end -%>
|
|
16
22
|
<% if membership_model != "EntityUser" -%>
|
|
17
23
|
|
|
18
24
|
def membership_class
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
class
|
|
3
|
+
class Create<%= invite_table.camelize %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
|
4
4
|
def change
|
|
5
|
-
create_table
|
|
5
|
+
create_table :<%= invite_table %> do |t|
|
|
6
6
|
# Entity association
|
|
7
7
|
t.belongs_to :<%= entity_association_name %>, null: false, foreign_key: true
|
|
8
8
|
|
|
@@ -35,13 +35,13 @@ class CreateUserInvites < ActiveRecord::Migration[<%= ActiveRecord::Migration.cu
|
|
|
35
35
|
|
|
36
36
|
# Only one pending invite per email per entity
|
|
37
37
|
t.index [:<%= entity_association_name %>_id, :email], unique: true, where: "state = 0",
|
|
38
|
-
name: "
|
|
38
|
+
name: "index_<%= invite_table %>_on_entity_email_pending"
|
|
39
39
|
|
|
40
40
|
# Only one pending invite per invitable (when invitable is present)
|
|
41
41
|
t.index [:invitable_type, :invitable_id],
|
|
42
42
|
unique: true,
|
|
43
43
|
where: "state = 0 AND invitable_id IS NOT NULL",
|
|
44
|
-
name: "
|
|
44
|
+
name: "index_<%= invite_table %>_on_invitable_pending"
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
end
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Invites
|
|
4
|
-
class
|
|
4
|
+
class <%= invitations_controller_class %> < ApplicationController
|
|
5
5
|
<% if rodauth? -%>
|
|
6
6
|
include Plutonium::Auth::Rodauth(:<%= rodauth_config %>)
|
|
7
7
|
<% end -%>
|
|
8
8
|
include Plutonium::Invites::Controller
|
|
9
9
|
|
|
10
|
-
prepend_view_path Invites::Engine.root.join("app/views")
|
|
11
10
|
layout "invites/invitation"
|
|
12
11
|
helper_method :login_path
|
|
13
12
|
|
|
14
13
|
private
|
|
15
14
|
|
|
16
15
|
def invite_class
|
|
17
|
-
::Invites
|
|
16
|
+
::Invites::<%= invite_model %>
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def invitation_path_for(token)
|
|
20
|
+
<%= invite_route_prefix %>_invitation_path(token: token)
|
|
18
21
|
end
|
|
19
22
|
|
|
20
23
|
def user_class
|
|
@@ -68,7 +71,9 @@ module Invites
|
|
|
68
71
|
|
|
69
72
|
def sign_in_user(user)
|
|
70
73
|
rodauth.account_from_login(user.email)
|
|
71
|
-
|
|
74
|
+
# login_session just persists the session; `login` would redirect to
|
|
75
|
+
# rodauth.login_redirect and short-circuit our post-accept redirect.
|
|
76
|
+
rodauth.login_session("signup")
|
|
72
77
|
end
|
|
73
78
|
<% else -%>
|
|
74
79
|
def create_user_for_signup(email, password)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Invites
|
|
4
|
-
class
|
|
4
|
+
class <%= invite_model %>Definition < Invites::ResourceDefinition
|
|
5
5
|
action :resend, interaction: Invites::ResendInviteInteraction, collection_record_action: false
|
|
6
6
|
action :cancel, interaction: Invites::CancelInviteInteraction, collection_record_action: false
|
|
7
7
|
|