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.
Files changed (111) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +28 -0
  4. data/Rakefile +8 -0
  5. data/app/assets/config/bullet_train_manifest.js +0 -0
  6. data/app/controllers/account/invitations_controller.rb +146 -0
  7. data/app/controllers/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments_controller.rb +83 -0
  8. data/app/controllers/account/memberships_controller.rb +132 -0
  9. data/app/controllers/account/onboarding/user_details_controller.rb +63 -0
  10. data/app/controllers/account/onboarding/user_email_controller.rb +65 -0
  11. data/app/controllers/account/teams_controller.rb +122 -0
  12. data/app/controllers/account/users_controller.rb +59 -0
  13. data/app/helpers/account/invitations_helper.rb +2 -0
  14. data/app/helpers/account/memberships_helper.rb +9 -0
  15. data/app/helpers/account/teams_helper.rb +80 -0
  16. data/app/helpers/account/users_helper.rb +81 -0
  17. data/app/helpers/invitation_only_helper.rb +10 -0
  18. data/app/helpers/invitations_helper.rb +17 -0
  19. data/app/models/invitation.rb +73 -0
  20. data/app/models/membership.rb +164 -0
  21. data/app/models/memberships/reassignments/assignment.rb +12 -0
  22. data/app/models/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignment.rb +38 -0
  23. data/app/models/memberships/reassignments.rb +5 -0
  24. data/app/models/team.rb +81 -0
  25. data/app/models/user.rb +191 -0
  26. data/app/views/account/invitations/_breadcrumbs.html.erb +9 -0
  27. data/app/views/account/invitations/_form.html.erb +49 -0
  28. data/app/views/account/invitations/_invitation.json.jbuilder +7 -0
  29. data/app/views/account/invitations/index.json.jbuilder +1 -0
  30. data/app/views/account/invitations/new.html.erb +12 -0
  31. data/app/views/account/invitations/show.html.erb +30 -0
  32. data/app/views/account/invitations/show.json.jbuilder +1 -0
  33. data/app/views/account/memberships/_breadcrumbs.html.erb +8 -0
  34. data/app/views/account/memberships/_form.html.erb +45 -0
  35. data/app/views/account/memberships/_index.html.erb +66 -0
  36. data/app/views/account/memberships/_menu_item.html.erb +8 -0
  37. data/app/views/account/memberships/_tombstones.html.erb +59 -0
  38. data/app/views/account/memberships/edit.html.erb +22 -0
  39. data/app/views/account/memberships/index.html.erb +7 -0
  40. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_breadcrumbs.html.erb +10 -0
  41. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_form.html.erb +24 -0
  42. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/_scaffolding_completely_concrete_tangible_things_reassignment.json.jbuilder +8 -0
  43. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/index.json.jbuilder +1 -0
  44. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/new.html.erb +11 -0
  45. data/app/views/account/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments/show.json.jbuilder +1 -0
  46. data/app/views/account/memberships/show.html.erb +60 -0
  47. data/app/views/account/onboarding/user_details/edit.html.erb +72 -0
  48. data/app/views/account/onboarding/user_email/edit.html.erb +33 -0
  49. data/app/views/account/teams/_breadcrumbs.html.erb +11 -0
  50. data/app/views/account/teams/_form.html.erb +22 -0
  51. data/app/views/account/teams/_index.html.erb +33 -0
  52. data/app/views/account/teams/_menu_item.html.erb +8 -0
  53. data/app/views/account/teams/_team.json.jbuilder +9 -0
  54. data/app/views/account/teams/edit.html.erb +12 -0
  55. data/app/views/account/teams/index.html.erb +6 -0
  56. data/app/views/account/teams/index.json.jbuilder +1 -0
  57. data/app/views/account/teams/new.html.erb +88 -0
  58. data/app/views/account/teams/show.html.erb +25 -0
  59. data/app/views/account/teams/show.json.jbuilder +1 -0
  60. data/app/views/account/users/_breadcrumbs.html.erb +4 -0
  61. data/app/views/account/users/_form.html.erb +39 -0
  62. data/app/views/account/users/edit.html.erb +50 -0
  63. data/app/views/account/users/show.html.erb +17 -0
  64. data/config/locales/en/invitations.en.yml +69 -0
  65. data/config/locales/en/memberships/reassignments/scaffolding_completely_concrete_tangible_things_reassignments.en.yml +42 -0
  66. data/config/locales/en/memberships.en.yml +99 -0
  67. data/config/locales/en/onboarding/user_details.en.yml +11 -0
  68. data/config/locales/en/onboarding/user_email.en.yml +13 -0
  69. data/config/locales/en/teams.en.yml +85 -0
  70. data/config/locales/en/users.en.yml +110 -0
  71. data/config/routes.rb +2 -0
  72. data/db/migrate/20161115160419_devise_create_users.rb +41 -0
  73. data/db/migrate/20161116003852_add_api_key_to_user.rb +6 -0
  74. data/db/migrate/20161117154605_create_teams.rb +10 -0
  75. data/db/migrate/20161117154709_create_memberships.rb +11 -0
  76. data/db/migrate/20161203193930_add_current_team_to_user.rb +5 -0
  77. data/db/migrate/20161204234150_create_invitations.rb +11 -0
  78. data/db/migrate/20161205154821_add_team_to_invitation.rb +5 -0
  79. data/db/migrate/20161205164613_add_admin_to_invitation.rb +5 -0
  80. data/db/migrate/20170908205756_add_names_to_user.rb +6 -0
  81. data/db/migrate/20170915215309_add_team_to_thing.rb +5 -0
  82. data/db/migrate/20171105001408_remove_api_key_from_user.rb +6 -0
  83. data/db/migrate/20180326124105_add_timezone_to_user.rb +5 -0
  84. data/db/migrate/20180902142350_create_membership_roles.rb +10 -0
  85. data/db/migrate/20180902143758_remove_admin_from_membership.rb +5 -0
  86. data/db/migrate/20180902154611_create_invitation_roles.rb +10 -0
  87. data/db/migrate/20180902154652_migrate_admin_flag_on_invitations.rb +14 -0
  88. data/db/migrate/20180902195848_remove_admin_from_invitation.rb +5 -0
  89. data/db/migrate/20180903101707_add_last_seen_at_to_users.rb +5 -0
  90. data/db/migrate/20190321203224_add_profile_photo_id_to_user.rb +5 -0
  91. data/db/migrate/20190519230202_add_ability_cache_to_user.rb +5 -0
  92. data/db/migrate/20190628194704_add_last_notification_email_sent_at_to_user.rb +5 -0
  93. data/db/migrate/20200211034208_add_invitation_to_membership.rb +5 -0
  94. data/db/migrate/20200211044616_drop_invitation_roles_table.rb +10 -0
  95. data/db/migrate/20200213052748_add_former_user_fields_to_membership.rb +8 -0
  96. data/db/migrate/20200213235037_add_former_user_to_user.rb +7 -0
  97. data/db/migrate/20200219013834_add_added_by_to_membership.rb +5 -0
  98. data/db/migrate/20200219015116_rename_from_user_to_from_membership.rb +5 -0
  99. data/db/migrate/20200726222314_add_being_destroyed_to_team.rb +5 -0
  100. data/db/migrate/20200727171308_add_devise_two_factor_to_users.rb +9 -0
  101. data/db/migrate/20200727175949_add_devise_two_factor_backupable_to_users.rb +5 -0
  102. data/db/migrate/20210304133200_add_time_zone_to_team.rb +5 -0
  103. data/db/migrate/20210816072419_add_locale_to_users.rb +5 -0
  104. data/db/migrate/20210816072508_add_locale_to_teams.rb +5 -0
  105. data/db/migrate/20211020200855_add_doorkeeper_application_to_memberships.rb +5 -0
  106. data/db/migrate/20211027002944_add_doorkeeper_application_to_users.rb +5 -0
  107. data/lib/bullet_train/engine.rb +4 -0
  108. data/lib/bullet_train/version.rb +3 -0
  109. data/lib/bullet_train.rb +6 -0
  110. data/lib/tasks/bullet_train_tasks.rake +4 -0
  111. metadata +168 -0
@@ -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,7 @@
1
+ json.extract! invitation,
2
+ :id,
3
+ :email,
4
+ # 🚅 super scaffolding will insert new fields above this line.
5
+ :created_at,
6
+ :updated_at
7
+ json.url account_invitation_url(invitation, format: :json)
@@ -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