bullet_train 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|