bullet_train 1.0.0

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