bullet_train 1.2.26 → 1.3.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 (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 +23 -6
  55. data/README.md +0 -29
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92369c4eed5118a67aee8033b4c235d727018adbe08fc044b4276a2144fc21d5
4
- data.tar.gz: 7a6c7e388b43fd9aaf8d6f8678c8c012df38383fda99785597753bf3936a5db1
3
+ metadata.gz: 67f18258ccc08e8f9c7ab4febd061776dbd014c14a851ff53a24b08b8f52740a
4
+ data.tar.gz: d4a1eaccc46bdc38aac7ea2996f9908b899ddd7310e041a566015a59ff36e50e
5
5
  SHA512:
6
- metadata.gz: b244c1b0a5b121ac8bea3a54919b00128579d5712043be2083e870f30268984d57738e0cc67062fdaa344461a1233e0cb7f89ef051f853f14b60eb405c022322
7
- data.tar.gz: 3ac1517be499c42123573fcd376797e55ff9e86c23fdcbe7ca78080ffc43caedb10f874350c5489bad2423a6e26c0fb6ff13bcf3b8884c2c6cbbdeb6660559a1
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>