bullet_train 1.0.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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +28 -0
- data/Rakefile +8 -0
- data/app/assets/config/bullet_train_manifest.js +0 -0
- data/app/controllers/account/invitations_controller.rb +146 -0
- data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
- data/app/controllers/account/memberships_controller.rb +132 -0
- data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
- data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
- data/app/controllers/account/teams_controller.rb +122 -0
- data/app/controllers/account/users_controller.rb +59 -0
- data/app/helpers/account/invitations_helper.rb +2 -0
- data/app/helpers/account/memberships_helper.rb +9 -0
- data/app/helpers/account/teams_helper.rb +80 -0
- data/app/helpers/account/users_helper.rb +81 -0
- data/app/helpers/invitation_only_helper.rb +10 -0
- data/app/helpers/invitations_helper.rb +17 -0
- data/app/models/invitation.rb +73 -0
- data/app/models/membership.rb +164 -0
- data/app/models/memberships/reassignments/assignment.rb +12 -0
- data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
- data/app/models/memberships/reassignments.rb +5 -0
- data/app/models/team.rb +81 -0
- data/app/models/user.rb +191 -0
- data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
- data/app/views/account/invitations/_form.html.erb +49 -0
- data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
- data/app/views/account/invitations/index.json.jbuilder +1 -0
- data/app/views/account/invitations/new.html.erb +12 -0
- data/app/views/account/invitations/show.html.erb +30 -0
- data/app/views/account/invitations/show.json.jbuilder +1 -0
- data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
- data/app/views/account/memberships/_form.html.erb +45 -0
- data/app/views/account/memberships/_index.html.erb +66 -0
- data/app/views/account/memberships/_menu_item.html.erb +8 -0
- data/app/views/account/memberships/_tombstones.html.erb +59 -0
- data/app/views/account/memberships/edit.html.erb +22 -0
- data/app/views/account/memberships/index.html.erb +7 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
- data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
- data/app/views/account/memberships/show.html.erb +60 -0
- data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
- data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
- data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
- data/app/views/account/teams/_form.html.erb +22 -0
- data/app/views/account/teams/_index.html.erb +33 -0
- data/app/views/account/teams/_menu_item.html.erb +8 -0
- data/app/views/account/teams/_team.json.jbuilder +9 -0
- data/app/views/account/teams/edit.html.erb +12 -0
- data/app/views/account/teams/index.html.erb +6 -0
- data/app/views/account/teams/index.json.jbuilder +1 -0
- data/app/views/account/teams/new.html.erb +88 -0
- data/app/views/account/teams/show.html.erb +25 -0
- data/app/views/account/teams/show.json.jbuilder +1 -0
- data/app/views/account/users/_breadcrumbs.html.erb +4 -0
- data/app/views/account/users/_form.html.erb +39 -0
- data/app/views/account/users/edit.html.erb +50 -0
- data/app/views/account/users/show.html.erb +17 -0
- data/config/locales/en/invitations.en.yml +69 -0
- data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
- data/config/locales/en/memberships.en.yml +99 -0
- data/config/locales/en/onboarding/user_details.en.yml +11 -0
- data/config/locales/en/onboarding/user_email.en.yml +13 -0
- data/config/locales/en/teams.en.yml +85 -0
- data/config/locales/en/users.en.yml +110 -0
- data/config/routes.rb +2 -0
- data/db/migrate/20161115160419_devise_create_users.rb +41 -0
- data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
- data/db/migrate/20161117154605_create_teams.rb +10 -0
- data/db/migrate/20161117154709_create_memberships.rb +11 -0
- data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
- data/db/migrate/20161204234150_create_invitations.rb +11 -0
- data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
- data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
- data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
- data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
- data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
- data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
- data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
- data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
- data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
- data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
- data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
- data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
- data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
- data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
- data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
- data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
- data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
- data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
- data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
- data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
- data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
- data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
- data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
- data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
- data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
- data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
- data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
- data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
- data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
- data/lib/bullet_train/engine.rb +4 -0
- data/lib/bullet_train/version.rb +3 -0
- data/lib/bullet_train.rb +6 -0
- data/lib/tasks/bullet_train_tasks.rake +4 -0
- metadata +168 -0
data/app/models/user.rb
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
class User < ApplicationRecord
|
|
2
|
+
# 🚫 DEFAULT BULLET TRAIN USER FUNCTIONALITY
|
|
3
|
+
# Typically you should avoid adding your own functionality in this section to avoid merge conflicts in the future.
|
|
4
|
+
# (If you specifically want to change Bullet Train's default behavior, that's OK and you can do that here.)
|
|
5
|
+
|
|
6
|
+
if two_factor_authentication_enabled?
|
|
7
|
+
devise :two_factor_authenticatable, :two_factor_backupable, :omniauthable,
|
|
8
|
+
:registerable, :recoverable, :rememberable, :trackable, :validatable,
|
|
9
|
+
otp_secret_encryption_key: ENV["TWO_FACTOR_ENCRYPTION_KEY"]
|
|
10
|
+
else
|
|
11
|
+
devise :omniauthable, :database_authenticatable, :registerable,
|
|
12
|
+
:recoverable, :rememberable, :trackable, :validatable
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# teams
|
|
16
|
+
has_many :memberships, dependent: :destroy
|
|
17
|
+
has_many :scaffolding_absolutely_abstract_creative_concepts_collaborators, through: :memberships
|
|
18
|
+
has_many :teams, through: :memberships
|
|
19
|
+
belongs_to :current_team, class_name: "Team", optional: true
|
|
20
|
+
accepts_nested_attributes_for :current_team
|
|
21
|
+
|
|
22
|
+
# oauth providers
|
|
23
|
+
has_many :oauth_stripe_accounts, class_name: "Oauth::StripeAccount" if stripe_enabled?
|
|
24
|
+
|
|
25
|
+
# platform functionality.
|
|
26
|
+
belongs_to :platform_agent_of, class_name: "Platform::Application", optional: true
|
|
27
|
+
|
|
28
|
+
# validations
|
|
29
|
+
validate :real_emails_only
|
|
30
|
+
validates :time_zone, inclusion: {in: ActiveSupport::TimeZone.all.map(&:name)}, allow_nil: true
|
|
31
|
+
|
|
32
|
+
# callbacks
|
|
33
|
+
after_update :set_teams_time_zone
|
|
34
|
+
|
|
35
|
+
# ✅ YOUR APPLICATION'S USER FUNCTIONALITY
|
|
36
|
+
# This is the place where you should implement your own features on top of Bullet Train's user functionality. There
|
|
37
|
+
# are a bunch of Super Scaffolding hooks here by default to try and help keep generated code logically organized.
|
|
38
|
+
|
|
39
|
+
# 🚅 add concerns above.
|
|
40
|
+
|
|
41
|
+
# 🚅 add belongs_to associations above.
|
|
42
|
+
|
|
43
|
+
# 🚅 add has_many associations above.
|
|
44
|
+
|
|
45
|
+
# 🚅 add oauth providers above.
|
|
46
|
+
|
|
47
|
+
# 🚅 add has_one associations above.
|
|
48
|
+
|
|
49
|
+
# 🚅 add scopes above.
|
|
50
|
+
|
|
51
|
+
# 🚅 add validations above.
|
|
52
|
+
|
|
53
|
+
# 🚅 add callbacks above.
|
|
54
|
+
|
|
55
|
+
# 🚅 add delegations above.
|
|
56
|
+
|
|
57
|
+
# 🚅 add methods above.
|
|
58
|
+
|
|
59
|
+
# 🚫 DEFAULT BULLET TRAIN USER FUNCTIONALITY
|
|
60
|
+
# We put these at the bottom of this file to keep them out of the way. You should define your own methods above here.
|
|
61
|
+
|
|
62
|
+
# TODO we need to update this to some sort of invalid email address or something
|
|
63
|
+
# people know to ignore. it would be a security problem to have this pointing
|
|
64
|
+
# at anybody's real email address.
|
|
65
|
+
def email_is_oauth_placeholder?
|
|
66
|
+
!!email.match(/noreply\+.*@bullettrain.co/)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def label_string
|
|
70
|
+
name
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def name
|
|
74
|
+
full_name.present? ? full_name : email
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def full_name
|
|
78
|
+
[first_name_was, last_name_was].select(&:present?).join(" ")
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def details_provided?
|
|
82
|
+
first_name.present? && last_name.present? && current_team.name.present?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def send_welcome_email
|
|
86
|
+
UserMailer.welcome(self).deliver_later
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def create_default_team
|
|
90
|
+
# This creates a `Membership`, because `User` `has_many :teams, through: :memberships`
|
|
91
|
+
# TODO The team name should take into account the user's current locale.
|
|
92
|
+
default_team = teams.create(name: "Your Team", time_zone: time_zone)
|
|
93
|
+
memberships.find_by(team: default_team).update role_ids: [Role.admin.id]
|
|
94
|
+
update(current_team: default_team)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def real_emails_only
|
|
98
|
+
if ENV["REALEMAIL_API_KEY"] && !Rails.env.test?
|
|
99
|
+
uri = URI("https://realemail.expeditedaddons.com")
|
|
100
|
+
|
|
101
|
+
# Change the input parameters here
|
|
102
|
+
uri.query = URI.encode_www_form({
|
|
103
|
+
api_key: ENV["REAL_EMAIL_KEY"],
|
|
104
|
+
email: email,
|
|
105
|
+
fix_typos: false
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# Results are returned as a JSON object
|
|
109
|
+
result = JSON.parse(Net::HTTP.get_response(uri).body)
|
|
110
|
+
|
|
111
|
+
if result["syntax_error"]
|
|
112
|
+
errors.add(:email, "is not a valid email address")
|
|
113
|
+
elsif result["domain_error"] || (result.key?("mx_records_found") && !result["mx_records_found"])
|
|
114
|
+
errors.add(:email, "can't actually receive emails")
|
|
115
|
+
elsif result["is_disposable"]
|
|
116
|
+
errors.add(:email, "is a disposable email address")
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def multiple_teams?
|
|
122
|
+
teams.count > 1
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def one_team?
|
|
126
|
+
!multiple_teams?
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def formatted_email_address
|
|
130
|
+
if details_provided?
|
|
131
|
+
"\"#{first_name} #{last_name}\" <#{email}>"
|
|
132
|
+
else
|
|
133
|
+
email
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def administrating_team_ids
|
|
138
|
+
parent_ids_for(Role.admin, :memberships, :team)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def parent_ids_for(role, through, parent)
|
|
142
|
+
parent_id_column = "#{parent}_id"
|
|
143
|
+
key = "#{role.key}_#{through}_#{parent_id_column}s"
|
|
144
|
+
return ability_cache[key] if ability_cache && ability_cache[key]
|
|
145
|
+
role = nil if role.default?
|
|
146
|
+
value = send(through).with_role(role).distinct.pluck(parent_id_column)
|
|
147
|
+
current_cache = ability_cache || {}
|
|
148
|
+
current_cache[key] = value
|
|
149
|
+
update_column :ability_cache, current_cache
|
|
150
|
+
value
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def invalidate_ability_cache
|
|
154
|
+
update_column(:ability_cache, {})
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def otp_qr_code
|
|
158
|
+
issuer = I18n.t("application.name")
|
|
159
|
+
label = "#{issuer}:#{email}"
|
|
160
|
+
RQRCode::QRCode.new(otp_provisioning_uri(label, issuer: issuer))
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def scaffolding_absolutely_abstract_creative_concepts_collaborators
|
|
164
|
+
Scaffolding::AbsolutelyAbstract::CreativeConcepts::Collaborator.joins(:membership).where(membership: {user_id: id})
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def admin_scaffolding_absolutely_abstract_creative_concepts_ids
|
|
168
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.admins.pluck(:creative_concept_id)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def editor_scaffolding_absolutely_abstract_creative_concepts_ids
|
|
172
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.editors.pluck(:creative_concept_id)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def viewer_scaffolding_absolutely_abstract_creative_concepts_ids
|
|
176
|
+
scaffolding_absolutely_abstract_creative_concepts_collaborators.viewers.pluck(:creative_concept_id)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def developer?
|
|
180
|
+
return false unless ENV["DEVELOPER_EMAILS"]
|
|
181
|
+
# we use email_was so they can't try setting their email to the email of an admin.
|
|
182
|
+
return false unless email_was
|
|
183
|
+
ENV["DEVELOPER_EMAILS"].split(",").include?(email_was)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def set_teams_time_zone
|
|
187
|
+
teams.where(time_zone: nil).each do |team|
|
|
188
|
+
team.update(time_zone: time_zone) if team.users.count == 1
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<% invitation ||= @invitation %>
|
|
2
|
+
<% team ||= @team || invitation&.team %>
|
|
3
|
+
<%= render 'account/teams/breadcrumbs', team: team %>
|
|
4
|
+
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :memberships] %>
|
|
5
|
+
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :invitations] %>
|
|
6
|
+
<% if invitation&.persisted? %>
|
|
7
|
+
<%= render 'account/shared/breadcrumb', label: invitation.label_string, url: [:account, invitation] %>
|
|
8
|
+
<% end %>
|
|
9
|
+
<%= render 'account/shared/breadcrumbs/actions', only_for: 'invitations' %>
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<%= form_with(model: [:account, (@team unless invitation.persisted?), invitation], class: 'form', local: true) do |form| %>
|
|
2
|
+
<%= render 'account/shared/forms/errors', form: form %>
|
|
3
|
+
|
|
4
|
+
<%= render 'shared/fields/email_field', form: form, method: :email, options: {autofocus: true} %>
|
|
5
|
+
|
|
6
|
+
<%= form.fields_for :membership do |membership_form| %>
|
|
7
|
+
<div class="grid grid-cols-1 gap-y gap-x sm:grid-cols-6">
|
|
8
|
+
<div class="sm:col-span-3">
|
|
9
|
+
<%= render 'shared/fields/text_field', form: membership_form, method: :user_first_name %>
|
|
10
|
+
</div>
|
|
11
|
+
|
|
12
|
+
<div class="sm:col-span-3">
|
|
13
|
+
<%= render 'shared/fields/text_field', form: membership_form, method: :user_last_name %>
|
|
14
|
+
</div>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<% if can? :manage, @team %>
|
|
19
|
+
<%= form.fields_for :membership do |fields| %>
|
|
20
|
+
<%= fields.hidden_field :team_id, value: @team.id %>
|
|
21
|
+
<div class="space-y-3">
|
|
22
|
+
<% Membership.assignable_roles.each do |role| %>
|
|
23
|
+
<% if current_membership.can_manage_role?(role) %>
|
|
24
|
+
<div class="flex items-top">
|
|
25
|
+
<%= fields.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded mt-0.5"}, role.id, nil %>
|
|
26
|
+
<label for="invitation_membership_attributes_role_ids_<%= role.id %>" class="ml-2 block select-none">
|
|
27
|
+
<span><%= t('invitations.form.invite_as', role_key: t("memberships.fields.role_ids.options.#{role.key}.label")) %></span>
|
|
28
|
+
<div class="mt-0.5 text-gray-400 font-light leading-normal">
|
|
29
|
+
<%= t("memberships.fields.role_ids.options.#{role.key}.description") %>
|
|
30
|
+
</div>
|
|
31
|
+
</label>
|
|
32
|
+
</div>
|
|
33
|
+
<% end %>
|
|
34
|
+
<% end %>
|
|
35
|
+
</div>
|
|
36
|
+
<% end %>
|
|
37
|
+
<% end %>
|
|
38
|
+
|
|
39
|
+
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
|
40
|
+
|
|
41
|
+
<div class="buttons">
|
|
42
|
+
<%= form.submit (form.object.persisted? ? t('.buttons.update') : t('.buttons.create')), class: "button" %>
|
|
43
|
+
<% if form.object.persisted? %>
|
|
44
|
+
<%= link_to t('global.buttons.cancel'), account_invitation_path(invitation), class: "button-secondary" %>
|
|
45
|
+
<% else %>
|
|
46
|
+
<%= link_to t('global.buttons.cancel'), @cancel_path || account_team_invitations_path(@team), class: "button-secondary" %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|
|
49
|
+
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json.array! @invitations, partial: "invitations/invitation", as: :invitation
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<%= render 'account/shared/page' do |p| %>
|
|
2
|
+
<% p.content_for :title, t('.section') %>
|
|
3
|
+
<% p.content_for :body do %>
|
|
4
|
+
<%= render 'account/shared/box', divider: true do |p| %>
|
|
5
|
+
<% p.content_for :title, t('.header') %>
|
|
6
|
+
<% p.content_for :description, t('.description') %>
|
|
7
|
+
<% p.content_for :body do %>
|
|
8
|
+
<%= render 'form', invitation: @invitation %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
<%= render 'account/shared/workflow/box' do |p| %>
|
|
2
|
+
<% p.content_for :title, t(".join_team") %>
|
|
3
|
+
<% p.content_for :body do %>
|
|
4
|
+
<div class="space-y">
|
|
5
|
+
<% if !@invitation.is_for?(current_user) %>
|
|
6
|
+
<p><%= raw t('.has_invited', user_name: @invitation.from_membership.name) %></p>
|
|
7
|
+
|
|
8
|
+
<%= render 'account/shared/alert' do %>
|
|
9
|
+
<%= raw t('.alert', invitation_email: @invitation.email, user_email: current_user.email) %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<p><%= t('.you_can') %></p>
|
|
13
|
+
|
|
14
|
+
<div class="space-y-3 pb-1">
|
|
15
|
+
<p><%= raw t('.accept_the_invitation', user_email: current_user.email) %></p>
|
|
16
|
+
<%= link_to t('.buttons.join_team'), accept_account_invitation_path(@invitation.uuid), method: :post, class: 'button full' %>
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
<%= render 'account/shared/decision_line' %>
|
|
20
|
+
|
|
21
|
+
<div class="space-y-3">
|
|
22
|
+
<p><%= t('.sign_out') %></p>
|
|
23
|
+
<%= link_to t('.buttons.logout'), main_app.destroy_user_session_path, method: 'delete', class: 'button-alternative full' %>
|
|
24
|
+
</div>
|
|
25
|
+
<% else %>
|
|
26
|
+
<%= link_to t('.buttons.join_team'), accept_account_invitation_path(@invitation.uuid), method: :post, class: 'button full' %>
|
|
27
|
+
<% end %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
30
|
+
<% end %>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json.partial! "invitations/invitation", invitation: @invitation
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<% membership ||= @membership %>
|
|
2
|
+
<% team ||= @team || membership&.team %>
|
|
3
|
+
<%= render 'account/teams/breadcrumbs', team: team %>
|
|
4
|
+
<%= render 'account/shared/breadcrumb', label: t('.label'), url: [:account, team, :memberships] %>
|
|
5
|
+
<% if membership&.persisted? %>
|
|
6
|
+
<%= render 'account/shared/breadcrumb', label: membership.label_string, url: [:account, membership] %>
|
|
7
|
+
<% end %>
|
|
8
|
+
<%= render 'account/shared/breadcrumbs/actions', only_for: 'memberships' %>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
<%= form_with(model: [:account, (@team unless membership.persisted?), membership], class: 'form', local: true) do |form| %>
|
|
2
|
+
<%= render 'account/shared/forms/errors', form: form %>
|
|
3
|
+
|
|
4
|
+
<% if membership.unclaimed? || membership.tombstone? %>
|
|
5
|
+
<div class="grid grid-cols-1 gap-y gap-x sm:grid-cols-6">
|
|
6
|
+
<div class="sm:col-span-3">
|
|
7
|
+
<%= render 'shared/fields/text_field', form: form, method: :user_first_name %>
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<div class="sm:col-span-3">
|
|
11
|
+
<%= render 'shared/fields/text_field', form: form, method: :user_last_name %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
|
|
16
|
+
<% if membership.tombstone? %>
|
|
17
|
+
<% if cloudinary_enabled? %>
|
|
18
|
+
<%= render 'shared/fields/cloudinary_image', form: form, method: :user_profile_photo_id %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
|
|
22
|
+
<%= hidden_field_tag 'membership[role_ids][]', nil %>
|
|
23
|
+
|
|
24
|
+
<% Membership.assignable_roles.each do |role| %>
|
|
25
|
+
<% if role.manageable_by?(current_membership.roles) %>
|
|
26
|
+
<div class="flex items-top">
|
|
27
|
+
<%= form.check_box :role_ids, {multiple: true, class: "h-4 w-4 text-blue focus:ring-blue-dark border-gray-300 rounded mt-0.5"}, role.id, nil %>
|
|
28
|
+
<label for="membership_role_ids_<%= role.id %>" class="ml-2 block select-none">
|
|
29
|
+
<%= t('.grant_privileges_of', role_key: t(".fields.role_ids.options.#{role.key}.label")) %>
|
|
30
|
+
<div class="mt-0.5 text-gray-400 font-light leading-normal">
|
|
31
|
+
<%= t(".fields.role_ids.options.#{role.key}.description") %>
|
|
32
|
+
</div>
|
|
33
|
+
</label>
|
|
34
|
+
</div>
|
|
35
|
+
<% end %>
|
|
36
|
+
<% end %>
|
|
37
|
+
|
|
38
|
+
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
|
39
|
+
|
|
40
|
+
<div class="buttons">
|
|
41
|
+
<%= form.submit t('.buttons.update'), class: "button" %>
|
|
42
|
+
<%= link_to t('global.buttons.cancel'), [:account, @team, :memberships], class: "button-secondary" %>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<% end %>
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
<% context ||= @team %>
|
|
2
|
+
<% hide_actions ||= false %>
|
|
3
|
+
<% hide_back ||= false %>
|
|
4
|
+
|
|
5
|
+
<%= render 'account/shared/box' do |p| %>
|
|
6
|
+
<% p.content_for :title, t(".contexts.#{context.class.name.underscore}.header") %>
|
|
7
|
+
<% p.content_for :description do %>
|
|
8
|
+
<%= raw t(".contexts.#{context.class.name.underscore}.#{memberships.any? ? 'description' : 'description_empty'}") %>
|
|
9
|
+
<% end %>
|
|
10
|
+
|
|
11
|
+
<% p.content_for :table do %>
|
|
12
|
+
<% if memberships.any? %>
|
|
13
|
+
<table class="table">
|
|
14
|
+
<thead>
|
|
15
|
+
<tr>
|
|
16
|
+
<th><%= t('memberships.singular') %></th>
|
|
17
|
+
<th><%= t('memberships.fields.role_ids.heading') %></th>
|
|
18
|
+
<%# 🚅 super scaffolding will insert new field headers above this line. %>
|
|
19
|
+
<th></th>
|
|
20
|
+
</tr>
|
|
21
|
+
</thead>
|
|
22
|
+
<tbody data-model="Membership" data-scope="current">
|
|
23
|
+
<% memberships.each do |membership| %>
|
|
24
|
+
<tr data-id="<%= membership.id %>">
|
|
25
|
+
|
|
26
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
27
|
+
<%= link_to [:account, membership], class: 'block flex items-center group hover:no-underline no-underline' do %>
|
|
28
|
+
<div class="flex-shrink-0 h-10 w-10">
|
|
29
|
+
<%= image_tag membership_profile_photo_url(membership), title: membership.label_string, class: 'h-10 w-10 rounded-full' %>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="ml-3">
|
|
33
|
+
<span class="group-hover:underline"><%= membership.label_string %></span>
|
|
34
|
+
<% if membership.unclaimed? %>
|
|
35
|
+
<span class="ml-1.5 px-2 inline-flex text-xs text-green-dark bg-green-light border border-green-dark rounded-md">
|
|
36
|
+
Invited
|
|
37
|
+
</span>
|
|
38
|
+
<% end %>
|
|
39
|
+
</div>
|
|
40
|
+
<% end %>
|
|
41
|
+
</td>
|
|
42
|
+
|
|
43
|
+
<td>
|
|
44
|
+
<% if membership.roles_without_defaults.any? %>
|
|
45
|
+
<%= membership.roles_without_defaults.map { |role| t("memberships.fields.role_ids.options.#{role.key}.label") }.to_sentence %>
|
|
46
|
+
<% else %>
|
|
47
|
+
<%= t("memberships.fields.role_ids.options.default.label") %>
|
|
48
|
+
<% end %>
|
|
49
|
+
</td>
|
|
50
|
+
<td class="text-right">
|
|
51
|
+
<%= link_to t('.buttons.show'), [:account, membership], class: 'button-secondary button-smaller' %>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% end %>
|
|
59
|
+
|
|
60
|
+
<% unless hide_actions %>
|
|
61
|
+
<% p.content_for :actions do %>
|
|
62
|
+
<%= link_to t('invitations.buttons.new'), new_account_team_invitation_path(@team, cancel_path: account_team_memberships_path(@team)), class: "#{first_button_primary}" %>
|
|
63
|
+
<%= link_to t('global.buttons.back'), [:account, context], class: "#{first_button_primary} back" unless hide_back %>
|
|
64
|
+
<% end %>
|
|
65
|
+
<% end %>
|
|
66
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<%= render 'account/shared/menu/item', {
|
|
2
|
+
url: main_app.polymorphic_path([:account, current_team, :memberships]),
|
|
3
|
+
label: t('memberships.navigation.label'),
|
|
4
|
+
} do |p| %>
|
|
5
|
+
<% p.content_for :icon do %>
|
|
6
|
+
<i class="<%= t('memberships.navigation.icon') %>"></i>
|
|
7
|
+
<% end %>
|
|
8
|
+
<% end %>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<% context ||= @team %>
|
|
2
|
+
<% hide_actions ||= false %>
|
|
3
|
+
|
|
4
|
+
<%= render 'account/shared/box' do |p| %>
|
|
5
|
+
<% p.content_for :title, t("memberships.tombstones.contexts.#{context.class.name.underscore}.header") %>
|
|
6
|
+
<% p.content_for :description do %>
|
|
7
|
+
<%= raw t("memberships.tombstones.contexts.#{context.class.name.underscore}.#{memberships.any? ? 'description' : 'description_empty'}") %>
|
|
8
|
+
<% end %>
|
|
9
|
+
|
|
10
|
+
<% p.content_for :body do %>
|
|
11
|
+
<% if memberships.any? %>
|
|
12
|
+
<table class="table">
|
|
13
|
+
<thead>
|
|
14
|
+
<tr>
|
|
15
|
+
<th><%= t('memberships.singular') %></th>
|
|
16
|
+
<th><%= t('roles.header') %></th>
|
|
17
|
+
<%# 🚅 super scaffolding will insert new field headers above this line. %>
|
|
18
|
+
<th></th>
|
|
19
|
+
</tr>
|
|
20
|
+
</thead>
|
|
21
|
+
<tbody data-model="Membership" data-scope="tombstones">
|
|
22
|
+
<% memberships.each do |membership| %>
|
|
23
|
+
<tr data-id="<%= membership.id %>">
|
|
24
|
+
|
|
25
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
26
|
+
<%= link_to [:account, membership], class: 'block flex items-center group hover:no-underline no-underline' do %>
|
|
27
|
+
<div class="flex-shrink-0 h-10 w-10">
|
|
28
|
+
<%= image_tag membership_profile_photo_url(membership), title: membership.label_string, class: 'h-10 w-10 rounded-full' %>
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
<div class="ml-3">
|
|
32
|
+
<span class="group-hover:underline"><%= membership.label_string %></span>
|
|
33
|
+
<% if membership.unclaimed? %>
|
|
34
|
+
<span class="ml-1 px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
|
35
|
+
Invited
|
|
36
|
+
</span>
|
|
37
|
+
<% end %>
|
|
38
|
+
</div>
|
|
39
|
+
<% end %>
|
|
40
|
+
</td>
|
|
41
|
+
|
|
42
|
+
<td>
|
|
43
|
+
<% if membership.roles_without_defaults.any? %>
|
|
44
|
+
<%= membership.roles_without_defaults.map { |role| t("memberships.fields.role_ids.options.#{role.key}.label") }.to_sentence %>
|
|
45
|
+
<% else %>
|
|
46
|
+
<%= t("memberships.fields.role_ids.options.default.label") %>
|
|
47
|
+
<% end %>
|
|
48
|
+
</td>
|
|
49
|
+
<td class="text-right">
|
|
50
|
+
<%= link_to t('.buttons.reinvite'), [:reinvite, :account, membership], class: 'button-secondary button-smaller', method: :post, data: {confirm: t('.buttons.confirmations.reinvite', membership_name: membership.name)} if can? :edit, membership %>
|
|
51
|
+
<%= link_to t('.buttons.show'), [:account, membership], class: 'button-secondary button-smaller' %>
|
|
52
|
+
</td>
|
|
53
|
+
</tr>
|
|
54
|
+
<% end %>
|
|
55
|
+
</tbody>
|
|
56
|
+
</table>
|
|
57
|
+
<% end %>
|
|
58
|
+
<% end %>
|
|
59
|
+
<% end %>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<%= render 'account/shared/page' do |p| %>
|
|
2
|
+
<% p.content_for :title, t('.section') %>
|
|
3
|
+
<% p.content_for :body do %>
|
|
4
|
+
<%= render 'account/shared/box', divider: true do |p| %>
|
|
5
|
+
<% p.content_for :title, t('.header') %>
|
|
6
|
+
<% p.content_for :description, t('.description') %>
|
|
7
|
+
<% p.content_for :body do %>
|
|
8
|
+
<%= render 'form', membership: @membership %>
|
|
9
|
+
<% end %>
|
|
10
|
+
<% end %>
|
|
11
|
+
|
|
12
|
+
<% if can? :destroy, @membership %>
|
|
13
|
+
<%= render 'account/shared/box', divider: true do |p| %>
|
|
14
|
+
<% p.content_for :title, t('.remove_header') %>
|
|
15
|
+
<% p.content_for :description, t('.remove_description') %>
|
|
16
|
+
<% p.content_for :body do %>
|
|
17
|
+
<%= button_to t('.buttons.destroy'), [:account, @membership], method: :delete, data: { confirm: t('.buttons.confirmations.destroy') }, class: 'button' %>
|
|
18
|
+
<% end %>
|
|
19
|
+
<% end %>
|
|
20
|
+
<% end %>
|
|
21
|
+
<% end %>
|
|
22
|
+
<% end %>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<%= render 'account/shared/page' do |p| %>
|
|
2
|
+
<% p.content_for :title, t('.section') %>
|
|
3
|
+
<% p.content_for :body do %>
|
|
4
|
+
<%= render 'index', memberships: @memberships.current_and_invited.includes(:user) if @memberships.current_and_invited.any? %>
|
|
5
|
+
<%= render 'tombstones', memberships: @memberships.tombstones.includes(:user) if @memberships.tombstones.any? %>
|
|
6
|
+
<% end %>
|
|
7
|
+
<% end %>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<% scaffolding_completely_concrete_tangible_things_reassignment ||= @scaffolding_completely_concrete_tangible_things_reassignment %>
|
|
2
|
+
<% membership ||= @membership || scaffolding_completely_concrete_tangible_things_reassignment&.membership %>
|
|
3
|
+
<%= render 'account/memberships/breadcrumbs', membership: membership %>
|
|
4
|
+
<li class="breadcrumb-item">
|
|
5
|
+
<%= link_to t('.label'), [:account, membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments] %>
|
|
6
|
+
</li>
|
|
7
|
+
<% if scaffolding_completely_concrete_tangible_things_reassignment&.persisted? %>
|
|
8
|
+
<li class="breadcrumb-item"><%= scaffolding_completely_concrete_tangible_things_reassignment.label_string %></li>
|
|
9
|
+
<% end %>
|
|
10
|
+
<%= render 'account/shared/breadcrumbs/actions', only_for: 'memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments' %>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<%= form_with(model: scaffolding_completely_concrete_tangible_things_reassignment, url: (scaffolding_completely_concrete_tangible_things_reassignment.persisted? ? [:account, scaffolding_completely_concrete_tangible_things_reassignment] : [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments]), local: true) do |form| %>
|
|
2
|
+
<%= render 'account/shared/forms/errors', form: form %>
|
|
3
|
+
|
|
4
|
+
<%= render 'shared/fields/super_select', form: form, method: :membership_ids, options: {autofocus: true},
|
|
5
|
+
html_options: {multiple: true, class: can_invite? ? 'accepts-new' : ''},
|
|
6
|
+
choices: memberships_as_select_options(scaffolding_completely_concrete_tangible_things_reassignment.valid_memberships, form.object.membership_ids) do %>
|
|
7
|
+
<% if can_invite? %>
|
|
8
|
+
<% content_for :after_help do %>
|
|
9
|
+
<%= t('memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.fields.membership_ids.invite_help').html_safe %>
|
|
10
|
+
<% end %>
|
|
11
|
+
<% end %>
|
|
12
|
+
<% end %>
|
|
13
|
+
<%# 🚅 super scaffolding will insert new fields above this line. %>
|
|
14
|
+
|
|
15
|
+
<div class="form-buttons-w">
|
|
16
|
+
<%= form.submit (form.object.persisted? ? t('.buttons.update') : t('.buttons.create')), class: "btn btn-primary" %>
|
|
17
|
+
<% if form.object.persisted? %>
|
|
18
|
+
<%= link_to t('global.buttons.cancel'), [:account, scaffolding_completely_concrete_tangible_things_reassignment], class: "btn btn-link" %>
|
|
19
|
+
<% else %>
|
|
20
|
+
<%= link_to t('global.buttons.cancel'), [:account, @membership, :reassignments_scaffolding_completely_concrete_tangible_things_reassignments], class: "btn btn-link" %>
|
|
21
|
+
<% end %>
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<% end %>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
json.extract! scaffolding_completely_concrete_tangible_things_reassignment,
|
|
2
|
+
:id,
|
|
3
|
+
:membership_id,
|
|
4
|
+
:membership_ids,
|
|
5
|
+
# 🚅 super scaffolding will insert new fields above this line.
|
|
6
|
+
:created_at,
|
|
7
|
+
:updated_at
|
|
8
|
+
json.url account_memberships_reassignments_scaffolding_completely_concrete_tangible_things_reassignment_url(scaffolding_completely_concrete_tangible_things_reassignment, format: :json)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json.array! @scaffolding_completely_concrete_tangible_things_reassignments, partial: "memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/scaffolding_completely_concrete_tangible_things_reassignment", as: :scaffolding_completely_concrete_tangible_things_reassignment
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<div class="element-wrapper">
|
|
2
|
+
<h6 class="element-header"><%= t('.section') %></h6>
|
|
3
|
+
<%= render 'account/shared/notices' %>
|
|
4
|
+
<div class="padded-lg">
|
|
5
|
+
<div class="element-box">
|
|
6
|
+
<h5 class="form-header"><%= t('.header') %></h5>
|
|
7
|
+
<div class="form-desc"><%= t('.description') %></div>
|
|
8
|
+
<%= render 'form', scaffolding_completely_concrete_tangible_things_reassignment: @scaffolding_completely_concrete_tangible_things_reassignment %>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
json.partial! "memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/scaffolding_completely_concrete_tangible_things_reassignment", scaffolding_completely_concrete_tangible_things_reassignment: @scaffolding_completely_concrete_tangible_things_reassignment
|