bullet_train 1.2.27 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/app/controllers/concerns/account/controllers/base.rb +2 -0
- data/app/controllers/concerns/account/memberships/controller_base.rb +2 -1
- data/app/controllers/concerns/account/teams/controller_base.rb +1 -1
- data/app/controllers/concerns/sessions/controller_base.rb +35 -0
- data/app/controllers/sessions_controller.rb +0 -32
- data/app/helpers/account/dates_helper.rb +41 -24
- data/app/helpers/account/markdown_helper.rb +8 -1
- data/app/helpers/account/users_helper.rb +43 -24
- data/app/helpers/attributes_helper.rb +7 -22
- data/app/helpers/invitation_only_helper.rb +9 -1
- data/app/javascript/controllers/bulk_actions_controller.js +1 -0
- data/app/models/billing/mock_limiter.rb +1 -1
- data/app/models/concerns/memberships/base.rb +17 -0
- data/app/models/concerns/records/base.rb +3 -1
- data/app/models/concerns/teams/base.rb +4 -8
- data/app/models/concerns/users/base.rb +15 -1
- data/app/views/account/memberships/_index.html.erb +1 -1
- data/app/views/account/onboarding/user_details/edit.html.erb +1 -0
- data/app/views/account/users/_form.html.erb +6 -4
- data/app/views/devise/registrations/new.html.erb +1 -1
- data/app/views/layouts/docs.html.erb +15 -3
- data/app/views/layouts/public.html.erb +31 -0
- data/config/locales/en/base.yml +9 -0
- data/config/locales/en/memberships.en.yml +3 -0
- data/config/locales/en/users.en.yml +5 -0
- data/docs/application-hash.md +25 -0
- data/docs/billing/stripe.md +3 -3
- data/docs/billing/usage.md +7 -7
- data/docs/field-partials/buttons.md +4 -4
- data/docs/field-partials/date-related-fields.md +13 -0
- data/docs/field-partials/file-field.md +1 -2
- data/docs/field-partials/super-select.md +23 -4
- data/docs/field-partials.md +24 -11
- data/docs/font-awesome-pro.md +1 -1
- data/docs/index.md +9 -8
- data/docs/indirection.md +5 -1
- data/docs/overriding.md +1 -1
- data/docs/seeds.md +3 -3
- data/docs/super-scaffolding/delegated-types.md +27 -24
- data/docs/super-scaffolding/options.md +24 -0
- data/docs/super-scaffolding/sortable.md +1 -1
- data/docs/super-scaffolding.md +7 -6
- data/docs/testing.md +1 -1
- data/docs/themes.md +4 -4
- data/docs/tunneling.md +2 -2
- data/docs/upgrades.md +7 -7
- data/docs/zapier.md +1 -1
- data/lib/bullet_train/configuration.rb +9 -3
- data/lib/bullet_train/resolver.rb +11 -6
- data/lib/bullet_train/version.rb +1 -1
- data/lib/bullet_train.rb +13 -8
- data/lib/tasks/bullet_train_tasks.rake +30 -0
- metadata +25 -8
- data/README.md +0 -29
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3e8839a83798a6b0f6cb390b45cf7b32b2d332b23474472d296d4d5d56182074
|
4
|
+
data.tar.gz: 5a37fae85e7079df274dc18d398dcdbedc4a8e1c316dfd701b2fec8e4f7ec844
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b2f84890b0ec4c6cb993f0dc08e0487ed48b787761a9e5130a6f9d8203e4e6e86459a4f06f2aa19bcf28813b5ec7efbe3f7366decda3334eccd6e09b25092532
|
7
|
+
data.tar.gz: c67cd314e9e323b09523b2babb42fdd0068de0544e6ac84fde651d89e8db7a678de33f58f88d0e23467972c1d768fad5bb9a7435b967f46dc3b8961d19ab12a4
|
@@ -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
|
)
|
@@ -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,57 @@
|
|
1
1
|
module Account::DatesHelper
|
2
|
-
|
3
|
-
def display_date(timestamp, custom_date_format = nil)
|
2
|
+
def display_date(timestamp, custom_date_format = nil, format: :default, date_format: nil)
|
4
3
|
return nil unless timestamp
|
5
|
-
if
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
format = date_format if date_format
|
5
|
+
|
6
|
+
if format && format == :default
|
7
|
+
# e.g. October 11, 2018
|
8
|
+
if custom_date_format
|
9
|
+
local_time(timestamp).strftime(custom_date_format)
|
10
|
+
elsif local_time(timestamp).year == local_time(Time.now).year
|
11
|
+
local_time(timestamp).strftime("%B %-d")
|
12
|
+
else
|
13
|
+
local_time(timestamp).strftime("%B %-d, %Y")
|
14
|
+
end
|
9
15
|
else
|
10
|
-
local_time(timestamp).
|
16
|
+
localize(local_time(timestamp).to_date, format: format)
|
11
17
|
end
|
12
18
|
end
|
13
19
|
|
14
|
-
|
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)
|
20
|
+
def display_time(timestamp, custom_time_format = nil, format: :default, time_format: nil)
|
18
21
|
return nil unless timestamp
|
22
|
+
format = time_format if time_format
|
19
23
|
|
20
|
-
|
21
|
-
|
22
|
-
|
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)}"
|
24
|
+
if format && format == :default
|
25
|
+
# e.g. 4:22 PM
|
26
|
+
local_time(timestamp).strftime(custom_time_format || "%l:%M %p")
|
26
27
|
else
|
27
|
-
|
28
|
+
localize(local_time(timestamp).to_time, format: format)
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
|
-
|
32
|
-
|
33
|
-
|
32
|
+
def display_date_and_time(timestamp, custom_date_format = nil, custom_time_format = nil, format: :default, date_format: nil, time_format: nil)
|
33
|
+
return nil unless timestamp
|
34
|
+
format = "#{date_format} #{time_format}" if date_format && time_format
|
35
|
+
|
36
|
+
if format && format == :default
|
37
|
+
# e.g. Today at 4:22 PM
|
38
|
+
# e.g. Yesterday at 2:12 PM
|
39
|
+
# e.g. April 24 at 7:39 AM
|
40
|
+
# today?
|
41
|
+
if local_time(timestamp).to_date == local_time(Time.now).to_date
|
42
|
+
"Today at #{display_time(timestamp, custom_time_format)}"
|
43
|
+
# yesterday?
|
44
|
+
elsif (local_time(timestamp).to_date) == (local_time(Time.now).to_date - 1.day)
|
45
|
+
"Yesterday at #{display_time(timestamp, custom_time_format)}"
|
46
|
+
else
|
47
|
+
"#{display_date(timestamp, custom_date_format)} at #{display_time(timestamp, custom_time_format)}"
|
48
|
+
end
|
49
|
+
else
|
50
|
+
localize(local_time(timestamp).to_datetime, format: format)
|
51
|
+
end
|
34
52
|
end
|
35
53
|
|
36
|
-
def local_time(
|
37
|
-
|
38
|
-
time.in_time_zone(current_user.time_zone)
|
54
|
+
def local_time(timestamp)
|
55
|
+
timestamp&.in_time_zone(current_user.time_zone)
|
39
56
|
end
|
40
57
|
end
|
@@ -1,5 +1,12 @@
|
|
1
1
|
module Account::MarkdownHelper
|
2
2
|
def markdown(string)
|
3
|
-
|
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
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
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
|
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
|
-
@
|
3
|
+
@_current_attribute_settings&.dig(:object)
|
4
4
|
end
|
5
5
|
|
6
6
|
def current_attributes_strategy
|
7
|
-
@
|
7
|
+
@_current_attributes_settings&.dig(:strategy)
|
8
8
|
end
|
9
9
|
|
10
|
-
def with_attribute_settings(
|
11
|
-
|
12
|
-
@
|
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
|
-
|
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?
|
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?
|
@@ -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
|
-
|
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,
|
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
|
-
|
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.
|
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
|
-
<%=
|
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
|
-
|
17
|
-
|
16
|
+
<div class="sm:col-span-2">
|
17
|
+
<% if cloudinary_enabled? %>
|
18
18
|
<%= render 'shared/fields/cloudinary_image', method: :profile_photo_id %>
|
19
|
-
|
20
|
-
|
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,
|
@@ -109,7 +109,7 @@
|
|
109
109
|
<% end %>
|
110
110
|
<% end %>
|
111
111
|
|
112
|
-
<%= render 'account/shared/menu/item', url: '/docs/i18n', label: '
|
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"
|