bullet_train 1.2.27 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/account/controllers/base.rb +2 -0
  3. data/app/controllers/concerns/account/memberships/controller_base.rb +2 -1
  4. data/app/controllers/concerns/account/teams/controller_base.rb +1 -1
  5. data/app/controllers/concerns/sessions/controller_base.rb +35 -0
  6. data/app/controllers/sessions_controller.rb +0 -32
  7. data/app/helpers/account/dates_helper.rb +11 -31
  8. data/app/helpers/account/markdown_helper.rb +8 -1
  9. data/app/helpers/account/users_helper.rb +43 -24
  10. data/app/helpers/attributes_helper.rb +7 -22
  11. data/app/helpers/invitation_only_helper.rb +9 -1
  12. data/app/javascript/controllers/bulk_actions_controller.js +1 -0
  13. data/app/models/billing/mock_limiter.rb +1 -1
  14. data/app/models/concerns/memberships/base.rb +17 -0
  15. data/app/models/concerns/records/base.rb +3 -1
  16. data/app/models/concerns/teams/base.rb +4 -8
  17. data/app/models/concerns/users/base.rb +15 -1
  18. data/app/views/account/memberships/_index.html.erb +1 -1
  19. data/app/views/account/onboarding/user_details/edit.html.erb +1 -0
  20. data/app/views/account/users/_form.html.erb +6 -4
  21. data/app/views/devise/registrations/new.html.erb +1 -1
  22. data/app/views/layouts/docs.html.erb +15 -3
  23. data/app/views/layouts/public.html.erb +31 -0
  24. data/config/locales/en/base.yml +9 -0
  25. data/config/locales/en/memberships.en.yml +3 -0
  26. data/config/locales/en/users.en.yml +5 -0
  27. data/docs/application-hash.md +25 -0
  28. data/docs/billing/stripe.md +3 -3
  29. data/docs/billing/usage.md +7 -7
  30. data/docs/field-partials/buttons.md +4 -4
  31. data/docs/field-partials/date-related-fields.md +13 -0
  32. data/docs/field-partials/file-field.md +1 -2
  33. data/docs/field-partials/super-select.md +23 -4
  34. data/docs/field-partials.md +24 -11
  35. data/docs/font-awesome-pro.md +1 -1
  36. data/docs/index.md +9 -8
  37. data/docs/indirection.md +5 -1
  38. data/docs/overriding.md +1 -1
  39. data/docs/seeds.md +3 -3
  40. data/docs/super-scaffolding/delegated-types.md +27 -24
  41. data/docs/super-scaffolding/options.md +24 -0
  42. data/docs/super-scaffolding/sortable.md +1 -1
  43. data/docs/super-scaffolding.md +7 -6
  44. data/docs/testing.md +1 -1
  45. data/docs/themes.md +4 -4
  46. data/docs/tunneling.md +2 -2
  47. data/docs/upgrades.md +7 -7
  48. data/docs/zapier.md +1 -1
  49. data/lib/bullet_train/configuration.rb +9 -3
  50. data/lib/bullet_train/resolver.rb +11 -6
  51. data/lib/bullet_train/version.rb +1 -1
  52. data/lib/bullet_train.rb +13 -8
  53. data/lib/tasks/bullet_train_tasks.rake +30 -0
  54. metadata +24 -7
  55. data/README.md +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e8a27d423ade0e7db5dd6e8d1b472331768eb749b86c25564ab320283a1dec56
4
- data.tar.gz: a301e8f5d0cd2e12e58955e099b7e28bd73d74bf46d7eb99b80d5992be19b5d9
3
+ metadata.gz: 67f18258ccc08e8f9c7ab4febd061776dbd014c14a851ff53a24b08b8f52740a
4
+ data.tar.gz: d4a1eaccc46bdc38aac7ea2996f9908b899ddd7310e041a566015a59ff36e50e
5
5
  SHA512:
6
- metadata.gz: 7e8baf4363e672b4993569be1d7c7dc6245d15ba98c437b9b329edaba98d16551f5325e8952264838d920748153c34f5dc63efbfc0d1f856120f202a937218e2
7
- data.tar.gz: e4b8cf978ad8efba8f90f75c099f9490573aee3a1f535a0b1de937b339d22d22462308099bf64713d5ef742a69d74681f7bf680a599124df572109d5a9511cf7
6
+ metadata.gz: 255cfc9f4e0abcf1c25ff7d1e562f0a7c9bce5d11eefa1847feaae5247b04fa0a48679733d382216c0081cda8add87963a8a5c616cf438264cec1e7e0a316247
7
+ data.tar.gz: b777a4c8d8f978ed1037c433b976351793dfadfdb59dfde8f23d4fdec72031d1dea52c9e36e664f2f560c9617fe8382017ff3ec5f9760b1afb17e77cea332297
@@ -123,4 +123,6 @@ module Account::Controllers::Base
123
123
  def set_last_seen_at
124
124
  current_user.update_attribute(:last_seen_at, Time.current)
125
125
  end
126
+
127
+ ActiveSupport.run_load_hooks :bullet_train_account_controllers_base, self
126
128
  end
@@ -110,7 +110,8 @@ module Account::Memberships::ControllerBase
110
110
  strong_params = params.require(:membership).permit(
111
111
  :user_first_name,
112
112
  :user_last_name,
113
- :user_profile_photo_id,
113
+ :user_profile_photo_id, # For Cloudinary
114
+ :user_profile_photo, # For ActiveStorage
114
115
  *permitted_fields,
115
116
  *permitted_arrays,
116
117
  )
@@ -9,7 +9,7 @@ module Account::Teams::ControllerBase
9
9
 
10
10
  prepend_before_action do
11
11
  if params["action"] == "new"
12
- current_user.current_team = nil
12
+ current_user&.current_team = nil
13
13
  end
14
14
  end
15
15
 
@@ -1,6 +1,41 @@
1
1
  module Sessions::ControllerBase
2
2
  extend ActiveSupport::Concern
3
3
 
4
+ # If user_return_to points to an oauth path we disable Turbo on the sign in form.
5
+ # This makes it work when we need to redirect to external sites and/or custom protocols.
6
+ # With Turbo enabled the browser will block those redirects with a CORS error.
7
+ # https://github.com/bullet-train-co/bullet_train/issues/384
8
+ def user_return_to_is_oauth
9
+ session["user_return_to"]&.match(/^\/oauth/)
10
+ end
11
+
12
+ included do
13
+ helper_method :user_return_to_is_oauth
14
+ end
15
+
16
+ def new
17
+ # We allow people to pass in a URL to redirect to after sign in is complete. We have to do this because Safari
18
+ # doesn't allow them to set this in a session before a redirect if there isn't already a session. However, for
19
+ # security reasons we have to make sure we control the URL where we will redirect to, otherwise people could
20
+ # trick folks into redirecting to a fake destination in a phishing scheme.
21
+ if params[:return_url]&.start_with?(ENV["BASE_URL"])
22
+ store_location_for(resource_name, params[:return_url])
23
+ end
24
+
25
+ super
26
+ end
27
+
28
+ def destroy
29
+ if params.include?(:onboard_logout)
30
+ signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
31
+ set_flash_message! :notice, :signed_out if signed_out
32
+ yield if block_given?
33
+ redirect_to root_path
34
+ else
35
+ super
36
+ end
37
+ end
38
+
4
39
  def pre_otp
5
40
  if (@email = params["user"]["email"].downcase.strip.presence)
6
41
  @user = User.find_by(email: @email)
@@ -1,35 +1,3 @@
1
1
  class SessionsController < Devise::SessionsController
2
2
  include Sessions::ControllerBase
3
-
4
- # If user_return_to points to an oauth path we disable Turbo on the sign in form.
5
- # This makes it work when we need to redirect to external sites and/or custom protocols.
6
- # With Turbo enabled the browser will block those redirects with a CORS error.
7
- # https://github.com/bullet-train-co/bullet_train/issues/384
8
- def user_return_to_is_oauth
9
- session["user_return_to"]&.match(/^\/oauth/)
10
- end
11
- helper_method :user_return_to_is_oauth
12
-
13
- def new
14
- # We allow people to pass in a URL to redirect to after sign in is complete. We have to do this because Safari
15
- # doesn't allow them to set this in a session before a redirect if there isn't already a session. However, for
16
- # security reasons we have to make sure we control the URL where we will redirect to, otherwise people could
17
- # trick folks into redirecting to a fake destination in a phishing scheme.
18
- if params[:return_url]&.start_with?(ENV["BASE_URL"])
19
- store_location_for(resource_name, params[:return_url])
20
- end
21
-
22
- super
23
- end
24
-
25
- def destroy
26
- if params.include?(:onboard_logout)
27
- signed_out = (Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name))
28
- set_flash_message! :notice, :signed_out if signed_out
29
- yield if block_given?
30
- redirect_to root_path
31
- else
32
- super
33
- end
34
- end
35
3
  end
@@ -1,40 +1,20 @@
1
1
  module Account::DatesHelper
2
- # e.g. October 11, 2018
3
- def display_date(timestamp, custom_date_format = nil)
4
- return nil unless timestamp
5
- if custom_date_format
6
- local_time(timestamp).strftime(custom_date_format)
7
- elsif local_time(timestamp).year == local_time(Time.now).year
8
- local_time(timestamp).strftime("%B %-d")
9
- else
10
- local_time(timestamp).strftime("%B %-d, %Y")
11
- end
2
+ def display_date(timestamp, format: :default, date_format: nil)
3
+ format = date_format if date_format
4
+ localize(local_time(timestamp).to_date, format: format) if timestamp
12
5
  end
13
6
 
14
- # e.g. October 11, 2018 at 4:22 PM
15
- # e.g. Yesterday at 2:12 PM
16
- # e.g. April 24 at 7:39 AM
17
- def display_date_and_time(timestamp, custom_date_format = nil, custom_time_format = nil)
18
- return nil unless timestamp
19
-
20
- # today?
21
- if local_time(timestamp).to_date == local_time(Time.now).to_date
22
- "Today at #{display_time(timestamp, custom_time_format)}"
23
- # yesterday?
24
- elsif (local_time(timestamp).to_date) == (local_time(Time.now).to_date - 1.day)
25
- "Yesterday at #{display_time(timestamp, custom_time_format)}"
26
- else
27
- "#{display_date(timestamp, custom_date_format)} at #{display_time(timestamp, custom_time_format)}"
28
- end
7
+ def display_time(timestamp, format: :default, time_format: nil)
8
+ format = time_format if time_format
9
+ localize(local_time(timestamp).to_time, format: format) if timestamp
29
10
  end
30
11
 
31
- # e.g. 4:22 PM
32
- def display_time(timestamp, custom_time_format = nil)
33
- local_time(timestamp).strftime(custom_time_format || "%l:%M %p")
12
+ def display_date_and_time(timestamp, format: :default, date_format: nil, time_format: nil)
13
+ format = "#{date_format} #{time_format}" if date_format && time_format
14
+ localize(local_time(timestamp).to_datetime, format: format) if timestamp
34
15
  end
35
16
 
36
- def local_time(time)
37
- return time if current_user.time_zone.nil?
38
- time.in_time_zone(current_user.time_zone)
17
+ def local_time(timestamp)
18
+ timestamp&.in_time_zone(current_user.time_zone)
39
19
  end
40
20
  end
@@ -1,5 +1,12 @@
1
1
  module Account::MarkdownHelper
2
2
  def markdown(string)
3
- CommonMarker.render_html(string, :UNSAFE, [:table]).html_safe
3
+ if defined?(Commonmarker.to_html)
4
+ Commonmarker.to_html(string, options: {
5
+ plugins: {syntax_highlighter: {theme: "InspiredGitHub"}},
6
+ render: {width: 120, unsafe: true}
7
+ }).html_safe
8
+ else
9
+ CommonMarker.render_html(string, :UNSAFE, [:table]).html_safe
10
+ end
4
11
  end
5
12
  end
@@ -1,23 +1,20 @@
1
1
  module Account::UsersHelper
2
- def profile_photo_for(url: nil, email: nil, first_name: nil, last_name: nil)
2
+ def profile_photo_for(url: nil, email: nil, first_name: nil, last_name: nil, profile_header: false)
3
+ size_details = profile_header ? {width: 700, height: 200} : {width: 100, height: 100}
4
+ size_details[:crop] = :fill
5
+
3
6
  if cloudinary_enabled? && !url.blank?
4
- cl_image_path(url, width: 100, height: 100, crop: :fill)
7
+ cl_image_path(url, size_details[:width], size_details[:height], size_details[:crop])
8
+ elsif !url.blank?
9
+ url + "?" + size_details.to_param
5
10
  else
6
- background_color = Colorizer.colorize_similarly(email.to_s, 0.5, 0.6).delete("#")
7
- "https://ui-avatars.com/api/?" + {
8
- color: "ffffff",
9
- background: background_color,
10
- bold: true,
11
- # email.to_s should not be necessary once we fix the edge case of cancelling an unclaimed membership
12
- name: [first_name, last_name].join(" ").strip.presence || email,
13
- size: 200,
14
- }.to_param
11
+ ui_avatar_params(email, first_name, last_name)
15
12
  end
16
13
  end
17
14
 
18
15
  def user_profile_photo_url(user)
19
16
  profile_photo_for(
20
- url: user.profile_photo_id,
17
+ url: get_photo_url_from(user),
21
18
  email: user.email,
22
19
  first_name: user.first_name,
23
20
  last_name: user.last_name
@@ -29,7 +26,7 @@ module Account::UsersHelper
29
26
  user_profile_photo_url(membership.user)
30
27
  else
31
28
  profile_photo_for(
32
- url: membership.user_profile_photo_id,
29
+ url: get_photo_url_from(membership),
33
30
  email: membership.invitation&.email || membership.user_email,
34
31
  first_name: membership.user_first_name,
35
32
  last_name: membership.user_last_name
@@ -37,25 +34,21 @@ module Account::UsersHelper
37
34
  end
38
35
  end
39
36
 
37
+ # TODO: We can do away with these three `profile_header` methods, I'm just
38
+ # leaving them in case we have other developers depending on these methods.
40
39
  def profile_header_photo_for(url: nil, email: nil, first_name: nil, last_name: nil)
41
40
  if cloudinary_enabled? && !url.blank?
42
41
  cl_image_path(url, width: 700, height: 200, crop: :fill)
42
+ elsif !url.blank?
43
+ url + "?" + {size: 200}.to_param
43
44
  else
44
- background_color = Colorizer.colorize_similarly(email.to_s, 0.5, 0.6).delete("#")
45
- "https://ui-avatars.com/api/?" + {
46
- color: "ffffff",
47
- background: background_color,
48
- bold: true,
49
- # email.to_s should not be necessary once we fix the edge case of cancelling an unclaimed membership
50
- name: "#{first_name&.first || email.to_s[0]} #{last_name&.first || email.to_s[1]}",
51
- size: 200,
52
- }.to_param
45
+ ui_avatar_params(email, first_name, last_name)
53
46
  end
54
47
  end
55
48
 
56
49
  def user_profile_header_photo_url(user)
57
50
  profile_header_photo_for(
58
- url: user.profile_photo_id,
51
+ url: get_photo_url_from(user),
59
52
  email: user.email,
60
53
  first_name: user.first_name,
61
54
  last_name: user.last_name
@@ -67,7 +60,7 @@ module Account::UsersHelper
67
60
  user_profile_header_photo_url(membership.user)
68
61
  else
69
62
  profile_header_photo_for(
70
- url: membership.user_profile_photo_id,
63
+ url: get_photo_url_from(membership),
71
64
  email: membership.invitation&.email || membership.user_email,
72
65
  first_name: membership.user_first_name,
73
66
  last_name: membership.user&.last_name || membership.user_last_name
@@ -75,6 +68,32 @@ module Account::UsersHelper
75
68
  end
76
69
  end
77
70
 
71
+ def get_photo_url_from(resource)
72
+ photo_method = if resource.is_a?(User)
73
+ :profile_photo
74
+ elsif resource.is_a?(Membership)
75
+ :user_profile_photo
76
+ end
77
+
78
+ if cloudinary_enabled?
79
+ resource.send("#{photo_method}_id".to_sym)
80
+ elsif resource.send(photo_method).attached?
81
+ url_for(resource.send(photo_method))
82
+ end
83
+ end
84
+
85
+ def ui_avatar_params(email, first_name, last_name)
86
+ background_color = Colorizer.colorize_similarly(email.to_s, 0.5, 0.6).delete("#")
87
+ "https://ui-avatars.com/api/?" + {
88
+ color: "ffffff",
89
+ background: background_color,
90
+ bold: true,
91
+ # email.to_s should not be necessary once we fix the edge case of cancelling an unclaimed membership
92
+ name: "#{first_name&.first || email.to_s[0]} #{last_name&.first || email.to_s[1]}",
93
+ size: 200,
94
+ }.to_param
95
+ end
96
+
78
97
  def current_membership
79
98
  current_user.memberships.where(team: current_team).first
80
99
  end
@@ -1,32 +1,17 @@
1
1
  module AttributesHelper
2
2
  def current_attributes_object
3
- @_attributes_helper_objects&.last
3
+ @_current_attribute_settings&.dig(:object)
4
4
  end
5
5
 
6
6
  def current_attributes_strategy
7
- @_attributes_helper_strategies&.last
7
+ @_current_attributes_settings&.dig(:strategy)
8
8
  end
9
9
 
10
- def with_attribute_settings(options)
11
- @_attributes_helper_objects ||= []
12
- @_attributes_helper_strategies ||= []
13
-
14
- if options[:object]
15
- @_attributes_helper_objects << options[:object]
16
- end
17
-
18
- if options[:strategy]
19
- @_attributes_helper_strategies << options[:strategy]
20
- end
21
-
10
+ def with_attribute_settings(object: current_attributes_object, strategy: current_attributes_strategy)
11
+ old_attribute_settings = @_current_attribute_settings
12
+ @_current_attribute_settings = {object: object, strategy: strategy}
22
13
  yield
23
-
24
- if options[:strategy]
25
- @_attributes_helper_strategies.pop
26
- end
27
-
28
- if options[:object]
29
- @_attributes_helper_objects.pop
30
- end
14
+ ensure
15
+ @_current_attribute_settings = old_attribute_settings
31
16
  end
32
17
  end
@@ -1,6 +1,14 @@
1
+ require "active_support/security_utils"
2
+
1
3
  module InvitationOnlyHelper
2
4
  def invited?
3
- session[:invitation_key].present? && invitation_keys.include?(session[:invitation_key])
5
+ return false unless session[:invitation_key].present?
6
+
7
+ result = invitation_keys.find do |key|
8
+ ActiveSupport::SecurityUtils.secure_compare(key, session[:invitation_key])
9
+ end
10
+
11
+ result.present?
4
12
  end
5
13
 
6
14
  def show_sign_up_options?
@@ -76,6 +76,7 @@ export default class extends Controller {
76
76
  }
77
77
 
78
78
  updateToggleLabel() {
79
+ if (!this.hasSelectableToggleTarget) { return }
79
80
  this.selectableToggleTarget.dispatchEvent(new CustomEvent('toggle', { detail: { useAlternate: this.selectableValue }} ))
80
81
  }
81
82
 
@@ -10,7 +10,7 @@ class Billing::MockLimiter
10
10
  true
11
11
  end
12
12
 
13
- def exhausted?(model)
13
+ def exhausted?(model, enforcement = "hard")
14
14
  false
15
15
  end
16
16
  end
@@ -2,6 +2,8 @@ module Memberships::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
+ attr_accessor :user_profile_photo_removal
6
+
5
7
  # See `docs/permissions.md` for details.
6
8
  include Roles::Support
7
9
 
@@ -16,6 +18,9 @@ module Memberships::Base
16
18
 
17
19
  has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator", dependent: :destroy
18
20
 
21
+ # Image uploading
22
+ has_one_attached :user_profile_photo
23
+
19
24
  after_destroy do
20
25
  # if we're destroying a user's membership to the team they have set as
21
26
  # current, then we need to remove that so they don't get an error.
@@ -25,6 +30,8 @@ module Memberships::Base
25
30
  end
26
31
  end
27
32
 
33
+ after_validation :remove_user_profile_photo, if: :user_profile_photo_removal?
34
+
28
35
  scope :excluding_platform_agents, -> { where(platform_agent_of: nil) }
29
36
  scope :platform_agents, -> { where.not(platform_agent_of: nil) }
30
37
  scope :current_and_invited, -> { includes(:invitation).where("user_id IS NOT NULL OR invitations.id IS NOT NULL").references(:invitation) }
@@ -140,4 +147,14 @@ module Memberships::Base
140
147
  def should_receive_notifications?
141
148
  invitation.present? || user.present?
142
149
  end
150
+
151
+ def user_profile_photo_removal?
152
+ user_profile_photo_removal.present?
153
+ end
154
+
155
+ def remove_user_profile_photo
156
+ user_profile_photo.purge
157
+ end
158
+
159
+ ActiveSupport.run_load_hooks :bullet_train_memberships_base, self
143
160
  end
@@ -21,7 +21,7 @@ module Records::Base
21
21
  end
22
22
 
23
23
  include CableReady::Updatable
24
- enable_updates
24
+ enable_cable_ready_updates
25
25
 
26
26
  extend ActiveHash::Associations::ActiveRecordExtensions
27
27
 
@@ -91,4 +91,6 @@ module Records::Base
91
91
  end.attributes!
92
92
  end
93
93
  end
94
+
95
+ ActiveSupport.run_load_hooks :bullet_train_records_base, self
94
96
  end
@@ -4,7 +4,7 @@ module Teams::Base
4
4
  included do
5
5
  # super scaffolding
6
6
  unless scaffolding_things_disabled?
7
- has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_updates: true
7
+ has_many :scaffolding_absolutely_abstract_creative_concepts, class_name: "Scaffolding::AbsolutelyAbstract::CreativeConcept", dependent: :destroy, enable_cable_ready_updates: true
8
8
  end
9
9
 
10
10
  # memberships and invitations
@@ -27,10 +27,6 @@ module Teams::Base
27
27
  if defined?(Billing::Stripe::Subscription)
28
28
  has_many :billing_stripe_subscriptions, class_name: "Billing::Stripe::Subscription", dependent: :destroy, foreign_key: :team_id
29
29
  end
30
-
31
- if defined?(Billing::Usage::TeamSupport)
32
- include Billing::Usage::TeamSupport
33
- end
34
30
  end
35
31
 
36
32
  # validations
@@ -39,9 +35,7 @@ module Teams::Base
39
35
  end
40
36
 
41
37
  def platform_agent_access_tokens
42
- # TODO This could be written better.
43
- platform_agent_user_ids = memberships.platform_agents.map(&:user_id).compact
44
- Platform::AccessToken.joins(:application).where(resource_owner_id: platform_agent_user_ids, application: {team: nil})
38
+ Platform::AccessToken.joins(:application).where(resource_owner_id: users.where.not(platform_agent_of_id: nil), application: {team: nil})
45
39
  end
46
40
 
47
41
  def admins
@@ -83,4 +77,6 @@ module Teams::Base
83
77
  billing_subscriptions.active.empty?
84
78
  end
85
79
  end
80
+
81
+ ActiveSupport.run_load_hooks :bullet_train_teams_base, self
86
82
  end
@@ -2,6 +2,8 @@ module Users::Base
2
2
  extend ActiveSupport::Concern
3
3
 
4
4
  included do
5
+ attr_accessor :profile_photo_removal
6
+
5
7
  if two_factor_authentication_enabled?
6
8
  devise :two_factor_authenticatable, :two_factor_backupable
7
9
  else
@@ -9,7 +11,7 @@ module Users::Base
9
11
  end
10
12
 
11
13
  devise :omniauthable
12
- devise :pwned_password if BulletTrain::Configuration.default.strong_passwords
14
+ devise :pwned_password if BulletTrain::Configuration.strong_passwords
13
15
  devise :registerable
14
16
  devise :recoverable
15
17
  devise :rememberable
@@ -27,6 +29,9 @@ module Users::Base
27
29
  # oauth providers
28
30
  has_many :oauth_stripe_accounts, class_name: "Oauth::StripeAccount" if stripe_enabled?
29
31
 
32
+ # Image uploading
33
+ has_one_attached :profile_photo
34
+
30
35
  # platform functionality.
31
36
  belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
32
37
 
@@ -35,6 +40,7 @@ module Users::Base
35
40
  validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
36
41
 
37
42
  # callbacks
43
+ after_validation :remove_profile_photo, if: :profile_photo_removal?
38
44
  after_update :set_teams_time_zone
39
45
  end
40
46
 
@@ -166,4 +172,12 @@ module Users::Base
166
172
  team.update(time_zone: time_zone) if team.users.count == 1
167
173
  end
168
174
  end
175
+
176
+ def profile_photo_removal?
177
+ profile_photo_removal.present?
178
+ end
179
+
180
+ def remove_profile_photo
181
+ profile_photo.purge
182
+ end
169
183
  end
@@ -2,7 +2,7 @@
2
2
  <% hide_actions ||= false %>
3
3
  <% hide_back ||= false %>
4
4
 
5
- <%= updates_for context, :memberships do %>
5
+ <%= cable_ready_updates_for context, :memberships do %>
6
6
  <%= render 'account/shared/box' do |box| %>
7
7
  <% box.title t(".contexts.#{context.class.name.underscore}.header") %>
8
8
  <% box.description do %>
@@ -6,6 +6,7 @@
6
6
  <% within_fields_namespace(:self) do %>
7
7
  <%= form_for @user, url: account_onboarding_user_detail_path(@user), method: :put, html: {class: 'form'} do |f| %>
8
8
  <%= render 'account/shared/forms/errors', form: f %>
9
+ <%= render 'account/shared/notices', form: f %>
9
10
 
10
11
  <div class="grid grid-cols-1 gap-y gap-x sm:grid-cols-2">
11
12
  <div class="sm:col-span-1">
@@ -13,11 +13,13 @@
13
13
  <%= render 'shared/fields/text_field', method: :last_name %>
14
14
  </div>
15
15
 
16
- <% if cloudinary_enabled? %>
17
- <div class="sm:col-span-2">
16
+ <div class="sm:col-span-2">
17
+ <% if cloudinary_enabled? %>
18
18
  <%= render 'shared/fields/cloudinary_image', method: :profile_photo_id %>
19
- </div>
20
- <% end %>
19
+ <% else %>
20
+ <%= render 'shared/fields/file_field', method: :profile_photo %>
21
+ <% end %>
22
+ </div>
21
23
 
22
24
  <div class="sm:col-span-2">
23
25
  <%= render 'shared/fields/super_select', method: :time_zone,
@@ -12,7 +12,7 @@
12
12
  <% end %>
13
13
  <% end %>
14
14
 
15
- <div class="grid grid-cols-2 gap-5">
15
+ <div class="grid md:grid-cols-2 gap-5">
16
16
  <div>
17
17
  <%= render 'shared/fields/password_field', form: f, method: :password, options: {show_strength_indicator: true} %>
18
18
  </div>
@@ -109,7 +109,7 @@
109
109
  <% end %>
110
110
  <% end %>
111
111
 
112
- <%= render 'account/shared/menu/item', url: '/docs/i18n', label: 'Internationalzation' do |p| %>
112
+ <%= render 'account/shared/menu/item', url: '/docs/i18n', label: 'Internationalization' do |p| %>
113
113
  <% p.icon do %>
114
114
  <i class="fa-brands fa-js ti ti-world"></i>
115
115
  <% end %>
@@ -152,6 +152,12 @@
152
152
  <i class="fal fa-gear ti ti-settings"></i>
153
153
  <% end %>
154
154
  <% end %>
155
+
156
+ <%= render 'account/shared/menu/item', url: '/docs/application-hash.md', label: 'Application Hash' do |p| %>
157
+ <% p.content_for :icon do %>
158
+ <i class="fal fa-brackets-curly ti ti-view-list-alt"></i>
159
+ <% end %>
160
+ <% end %>
155
161
  <% end %>
156
162
 
157
163
  <%= render 'account/shared/menu/section', title: 'Accounts & Teams' do %>
@@ -204,6 +210,12 @@
204
210
  <i class="fal fa-swatchbook ti ti-widget"></i>
205
211
  <% end %>
206
212
  <% end %>
213
+
214
+ <%= render 'account/shared/menu/item', url: 'https://github.com/bullet-train-co/showcase', label: 'Showcase' do |p| %>
215
+ <% p.icon do %>
216
+ <i class="fal fa-swatchbook ti ti-panel"></i>
217
+ <% end %>
218
+ <% end %>
207
219
  <% end %>
208
220
 
209
221
  <%= render 'account/shared/menu/section', title: 'Billing' do %>
@@ -292,7 +304,7 @@
292
304
  <button
293
305
  data-mobile-menu-target="revealable"
294
306
  data-action="mobile-menu#close"
295
-
307
+
296
308
  data-transition-enter="transition-opacity ease-linear duration-200"
297
309
  data-transition-enter-start="opacity-0"
298
310
  data-transition-enter-end="opacity-100"
@@ -306,7 +318,7 @@
306
318
  </button>
307
319
  <div
308
320
  data-mobile-menu-target="revealable"
309
-
321
+
310
322
  data-transition-enter="transition ease-in-out duration-200 transform"
311
323
  data-transition-enter-start="-translate-x-full"
312
324
  data-transition-enter-end="translate-x-0"
@@ -0,0 +1,31 @@
1
+ <!DOCTYPE html>
2
+ <html class="theme-<%= BulletTrain::Themes::Light.color %> <%= "theme-secondary-#{BulletTrain::Themes::Light.secondary_color}" if BulletTrain::Themes::Light.secondary_color %>">
3
+ <head>
4
+ <%= render 'shared/layouts/head' %>
5
+ </head>
6
+
7
+ <body class="min-h-screen <%= BulletTrain::Themes::Light.background || "bg-gradient-to-br from-secondary-200 to-primary-400 dark:from-primary-900 dark:to-primary-600" %> text-slate-700 text-sm font-normal dark:text-slate-300">
8
+ <div class="md:p-5 main-container-padding">
9
+ <div class="h-screen md:h-auto md:rounded-lg flex shadow main-container">
10
+
11
+ <% if BulletTrain::Themes::Light.navigation == :left %>
12
+ <div class="hidden lg:flex lg:flex-shrink-0 bg-gradient-to-b from-primary-700 to-primary-800 dark:from-slate-800 dark:to-slate-800 md:rounded-l-lg">
13
+ <div class="w-64">
14
+ <%= render "account/shared/menu/sidebar" %>
15
+ </div>
16
+ </div>
17
+ <% end %>
18
+
19
+ <div class="flex flex-col w-0 flex-1 bg-slate-100 dark:bg-slate-800 dark:border-slate-500 md:rounded-lg <%= BulletTrain::Themes::Light.navigation == :left ? "lg:border-l lg:rounded-l-none" : "" %>">
20
+ <main class="flex-1 relative z-0 focus:outline-none" tabindex="0">
21
+ <div class="py-2 px-1">
22
+ <div class="mx-auto px-4 sm:px-6 py-4">
23
+ <%= yield %>
24
+ </div>
25
+ </div>
26
+ </main>
27
+ </div>
28
+ </div>
29
+ </div>
30
+ </body>
31
+ </html>