studio-engine 0.47.2 → 0.48.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.
data/lib/studio.rb CHANGED
@@ -7,6 +7,9 @@ require "studio/environment_banner"
7
7
  require "studio/theme_resolver"
8
8
  require "studio/ui_primitives"
9
9
  require "studio/sidebar_sections"
10
+ require "studio/profile_sections"
11
+ require "studio/profile_image"
12
+ require "studio/oauth_identity"
10
13
  require "studio/username_generator"
11
14
  require "studio/s3"
12
15
  require "studio/image_cache"
@@ -83,6 +86,49 @@ module Studio
83
86
  # end
84
87
  mattr_accessor :sidebar_sections, default: []
85
88
 
89
+ # ---- The shared profile page (/profile) ----
90
+ # The rows that make up /profile. `nil` — the default — means "the engine's
91
+ # standard page", NOT "no rows": a brand-new app with an empty initializer gets
92
+ # a working profile page, which is the entire point of standardizing it.
93
+ #
94
+ # Declare an Array (or a callable receiving the view) to customize. Compose
95
+ # against Studio.default_profile_sections rather than a literal, so a later
96
+ # engine release that adds a standard row delivers it to you:
97
+ #
98
+ # Studio.configure do |config|
99
+ # config.profile_sections = ->(view) {
100
+ # Studio.default_profile_sections +
101
+ # [ { key: :wallet, title: "Identities", partial: "profiles/wallet" } ]
102
+ # }
103
+ # end
104
+ #
105
+ # Drop a standard row by key instead of restating the list:
106
+ #
107
+ # config.profile_sections = Studio.default_profile_sections.reject { |s| s[:key] == :avatar }
108
+ #
109
+ # Shape and resolution rules: lib/studio/profile_sections.rb. A row may declare
110
+ # `requires:` — an attribute the current user must respond to — and is dropped
111
+ # when this host's model does not have it, so the page tolerates the fact that
112
+ # the consuming apps' users tables genuinely disagree.
113
+ mattr_accessor :profile_sections, default: nil
114
+
115
+ # Whether Studio.routes draws /profile (Studio::ProfilesController).
116
+ #
117
+ # ON BY DEFAULT, and it is worth saying why this one can be when
118
+ # draw_admin_emails_routes and draw_onboarding_routes could not: those two
119
+ # claimed helper names a consumer ALREADY OWNED, so drawing them raised
120
+ # `Invalid route name, already in use` while that app's routes.rb loaded and
121
+ # took down its entire route set. `profile` is claimed by none of the five
122
+ # consumers — mcritchie-studio, mcritchie-industries, turf-monster, moms-app
123
+ # and acquisition-studio were each checked (2026-08-14) — and turf-monster's
124
+ # nearest names are complete_profile_account_path / save_profile_account_path,
125
+ # which do not collide.
126
+ #
127
+ # That is exactly why the shared page is /profile and not /account: turf owns
128
+ # account_path, and a shared /account could never be default-on. An app that
129
+ # wants the page gone still sets this false.
130
+ mattr_accessor :draw_profile_routes, default: true
131
+
86
132
  # How long a freshly minted magic link stays live.
87
133
  mattr_accessor :magic_link_ttl, default: 15.minutes
88
134
 
@@ -179,6 +225,17 @@ module Studio
179
225
  # later session may ask again. That is the whole reason this is not a column.
180
226
  FIRST_NAME_SKIP_SESSION_KEY = :onboarding_skipped_first_name
181
227
 
228
+ # How long a first name may be. ONE constant because users.first_name is
229
+ # written from TWO surfaces — the onboarding step (seconds after signup) and
230
+ # /profile (any time after) — and rendered by a third, the profile form's
231
+ # maxlength. Two independently-correct caps that disagreed would let onboarding
232
+ # accept a name /profile then refused to save, a bug with no obvious owner.
233
+ #
234
+ # Keeping it here rather than on either controller also keeps the VIEW off a
235
+ # controller constant: the form needs the number, and a view reaching into
236
+ # Studio::ProfilesController to get it would couple the two for no reason.
237
+ FIRST_NAME_MAX_LENGTH = 40
238
+
182
239
  # The shared rule for "does this account still owe us a first name?" — the one
183
240
  # piece of onboarding logic every app agrees on. Hosts compose it into their own
184
241
  # flow rather than re-deriving it (turf's OnboardingFlow calls straight through).
@@ -545,6 +602,21 @@ module Studio
545
602
  SidebarSections.resolve(sidebar_sections, view)
546
603
  end
547
604
 
605
+ # The engine's standard /profile rows. Hosts compose against this rather than
606
+ # restating a literal list, so a later release that adds a standard row
607
+ # delivers it to every app that used the seam as intended.
608
+ def self.default_profile_sections
609
+ ProfileSections.defaults
610
+ end
611
+
612
+ # Profile rows resolved for a view context: a callable config is called with
613
+ # the view, keys symbolize, admin-only rows drop for non-admin viewers, and
614
+ # rows this host's user model cannot serve drop entirely. `nil` config resolves
615
+ # to the standard page.
616
+ def self.profile_sections_for(view)
617
+ ProfileSections.resolve(profile_sections, view)
618
+ end
619
+
548
620
  def self.env_truthy?(value)
549
621
  %w[1 true yes on].include?(value.to_s.strip.downcase)
550
622
  end
@@ -602,6 +674,23 @@ module Studio
602
674
  post "auth/solana/verify", to: "solana_sessions#verify", as: :solana_verify
603
675
  end
604
676
 
677
+ # The shared profile page. ON by default — unlike /admin/emails and the
678
+ # onboarding pair, `profile` is claimed by no consumer, so drawing it
679
+ # cannot take an app's route set down. See Studio.draw_profile_routes.
680
+ #
681
+ # Avatar is its own PATCH rather than a field on #update: an attachment
682
+ # param submitted empty PURGES the attachment, so a single form carrying
683
+ # both would delete someone's photo every time they edited their name.
684
+ if Studio.draw_profile_routes
685
+ get "profile", to: "studio/profiles#show", as: :profile
686
+ patch "profile", to: "studio/profiles#update"
687
+ patch "profile/avatar", to: "studio/profiles#avatar", as: :profile_avatar
688
+ # DELETE, because unlinking removes an identity. Linking is not drawn
689
+ # here: it is OmniAuth's own /auth/:provider, which the middleware owns.
690
+ delete "profile/google", to: "studio/profiles#unlink_google",
691
+ as: :profile_unlink_google
692
+ end
693
+
605
694
  resources :error_logs, only: [:index, :show]
606
695
 
607
696
  # Admin
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: studio-engine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.47.2
4
+ version: 0.48.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex McRitchie
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-08-14 00:00:00.000000000 Z
11
+ date: 2026-08-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -204,6 +204,7 @@ files:
204
204
  - app/controllers/studio/local_reviews_controller.rb
205
205
  - app/controllers/studio/models_controller.rb
206
206
  - app/controllers/studio/onboarding_controller.rb
207
+ - app/controllers/studio/profiles_controller.rb
207
208
  - app/controllers/style_controller.rb
208
209
  - app/controllers/theme_settings_controller.rb
209
210
  - app/helpers/studio/admin_models_table_helper.rb
@@ -219,6 +220,7 @@ files:
219
220
  - app/models/concerns/sluggable.rb
220
221
  - app/models/concerns/studio/board/rankable.rb
221
222
  - app/models/concerns/studio/broadcastable.rb
223
+ - app/models/concerns/studio/user_profile.rb
222
224
  - app/models/current.rb
223
225
  - app/models/error_log.rb
224
226
  - app/models/image_cache.rb
@@ -333,6 +335,10 @@ files:
333
335
  - app/views/studio/models/show.html.erb
334
336
  - app/views/studio/newsletter_mailer/subscribed.html.erb
335
337
  - app/views/studio/newsletter_mailer/subscribed.text.erb
338
+ - app/views/studio/profiles/_avatar_section.html.erb
339
+ - app/views/studio/profiles/_first_name_section.html.erb
340
+ - app/views/studio/profiles/_google_section.html.erb
341
+ - app/views/studio/profiles/show.html.erb
336
342
  - app/views/style/_modal_specimen.html.erb
337
343
  - app/views/style/_modals.html.erb
338
344
  - app/views/style/_specimen.html.erb
@@ -373,6 +379,9 @@ files:
373
379
  - lib/studio/link_token.rb
374
380
  - lib/studio/log_rotation.rb
375
381
  - lib/studio/mail_transport.rb
382
+ - lib/studio/oauth_identity.rb
383
+ - lib/studio/profile_image.rb
384
+ - lib/studio/profile_sections.rb
376
385
  - lib/studio/redis.rb
377
386
  - lib/studio/s3.rb
378
387
  - lib/studio/sidebar_sections.rb