diff --git a/app/assets/images/badge.svg b/app/assets/images/badge.svg
new file mode 100644
index 000000000..cdcf26016
--- /dev/null
+++ b/app/assets/images/badge.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/bin.svg b/app/assets/images/bin.svg
new file mode 100644
index 000000000..5969c82fa
--- /dev/null
+++ b/app/assets/images/bin.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/edit.svg b/app/assets/images/edit.svg
new file mode 100644
index 000000000..be8115d56
--- /dev/null
+++ b/app/assets/images/edit.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/refresh-button.svg b/app/assets/images/refresh-button.svg
deleted file mode 100644
index 05068aff0..000000000
--- a/app/assets/images/refresh-button.svg
+++ /dev/null
@@ -1,3 +0,0 @@
-
-
-
diff --git a/app/assets/images/refresh.svg b/app/assets/images/refresh.svg
new file mode 100644
index 000000000..6ffb91342
--- /dev/null
+++ b/app/assets/images/refresh.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/team.svg b/app/assets/images/team.svg
new file mode 100644
index 000000000..93e625365
--- /dev/null
+++ b/app/assets/images/team.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/images/wallet.svg b/app/assets/images/wallet.svg
new file mode 100644
index 000000000..9b14fec62
--- /dev/null
+++ b/app/assets/images/wallet.svg
@@ -0,0 +1,3 @@
+
+
+
diff --git a/app/assets/stylesheets/components/cards.scss b/app/assets/stylesheets/components/cards.scss
index 74d47fadd..4c3320efb 100644
--- a/app/assets/stylesheets/components/cards.scss
+++ b/app/assets/stylesheets/components/cards.scss
@@ -42,3 +42,28 @@
padding: var(--su-4);
}
}
+
+.c-card {
+ --revealable-opacity: var(--opacity-100);
+ @media (hover: hover) {
+ --revealable-opacity: var(--opacity-0);
+ }
+
+ border-radius: var(--radius-auto);
+ background: var(--card-bg);
+ box-shadow: 0 0 0 1px var(--card-border);
+ overflow-wrap: anywhere;
+
+ &__revealable {
+ opacity: var(--revealable-opacity);
+ }
+
+ &--interactive {
+ &:hover,
+ &:focus-within {
+ --card-border: var(--base-90);
+ --card-bg: var(--card-secondary-bg);
+ --revealable-opacity: var(--opacity-100);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/components/list-items.scss b/app/assets/stylesheets/components/list-items.scss
new file mode 100644
index 000000000..3feb3dd89
--- /dev/null
+++ b/app/assets/stylesheets/components/list-items.scss
@@ -0,0 +1,21 @@
+////////////////////////////////////////////////////////////
+// .c-list-item ////////////////////////////////////////////
+////////////////////////////////////////////////////////////
+
+.c-list-item {
+ padding: var(--su-3);
+ margin: 0 calc(var(--su-3) * -1);
+ border-bottom: 1px solid var(--divider);
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:not(&--inactive) {
+ &:hover,
+ &:focus-visible,
+ &:focus-within {
+ background: var(--list-item-bg-hover);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/config/_colors.css b/app/assets/stylesheets/config/_colors.css
index 38b3bb2d4..2693802d0 100644
--- a/app/assets/stylesheets/config/_colors.css
+++ b/app/assets/stylesheets/config/_colors.css
@@ -276,6 +276,12 @@
--indicator-info-bg: var(--accent-brand);
--indicator-info-color: rgb(var(--white));
+ /***********************************************
+ ** List items **********************************
+ ***********************************************/
+
+ --list-item-bg-hover: rgb(var(--grey-50));
+
/***********************************************
** Modals **************************************
***********************************************/
diff --git a/app/assets/stylesheets/crayons.scss b/app/assets/stylesheets/crayons.scss
index bd807f98f..08cd610f9 100644
--- a/app/assets/stylesheets/crayons.scss
+++ b/app/assets/stylesheets/crayons.scss
@@ -23,6 +23,7 @@
@import 'components/headers';
@import 'components/indicators';
@import 'components/links';
+@import 'components/list-items';
@import 'components/loaders';
@import 'components/media-cards';
@import 'components/modals';
diff --git a/app/assets/stylesheets/themes/dark.css b/app/assets/stylesheets/themes/dark.css
index 2817471c0..1c1fd7d77 100644
--- a/app/assets/stylesheets/themes/dark.css
+++ b/app/assets/stylesheets/themes/dark.css
@@ -285,6 +285,12 @@
--indicator-info-bg: rgb(var(--indigo-700));
--indicator-info-color: rgb(var(--indigo-100));
+ /***********************************************
+ ** List items **********************************
+ ***********************************************/
+
+ --list-item-bg-hover: rgb(var(--grey-800));
+
/***********************************************
** Modals **************************************
***********************************************/
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 691a30951..4fcc30f14 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -8,6 +8,7 @@ module Admin
merge_user_id add_credits remove_credits
add_org_credits remove_org_credits
organization_id identity_id
+ credit_action credit_amount
].freeze
EMAIL_ALLOWED_PARAMS = %i[
@@ -37,16 +38,12 @@ module Admin
if FeatureFlag.enabled?(:new_admin_members, current_user)
render "admin/users/new/show"
+ elsif FeatureFlag.enabled?(:admin_member_view)
+ set_feedback_messages
+ set_related_reactions
+ set_user_details
else
- @organizations = @user.organizations.order(:name)
- @notes = @user.notes.order(created_at: :desc).limit(10)
- @organization_memberships = @user.organization_memberships
- .joins(:organization)
- .order("organizations.name" => :asc)
- .includes(:organization)
- @last_email_verification_date = EmailAuthorization.last_verification_date(@user)
-
- render :show
+ set_user_details
end
end
@@ -56,7 +53,7 @@ module Admin
# TODO: [@rhymes] in the new Admin Member view this logic has been moved
# to Admin::Users::Tools::CreditsController and Admin::Users::Tools::NotesController#create.
# It can eventually be removed when we transition away from the old Admin UI
- Credits::Manage.call(@user, user_params)
+ Credits::Manage.call(@user, credit_params)
add_note if user_params[:new_note]
redirect_to admin_user_path(params[:id])
@@ -77,7 +74,7 @@ module Admin
else
flash[:danger] = response.error_message
end
- redirect_to edit_admin_user_path(@user.id)
+ redirect_to admin_user_path(params[:id])
end
def user_status
@@ -88,7 +85,7 @@ module Admin
rescue StandardError => e
flash[:danger] = e.message
end
- redirect_to edit_admin_user_path(@user.id)
+ redirect_to admin_user_path(params[:id])
end
def export_data
@@ -103,13 +100,13 @@ module Admin
end
ExportContentWorker.perform_async(user.id, email)
flash[:success] = "Data exported to the #{receiver}. The job will complete momentarily."
- redirect_to edit_admin_user_path(user.id)
+ redirect_to admin_user_path(params[:id])
end
def banish
Moderator::BanishUserWorker.perform_async(current_user.id, params[:id].to_i)
flash[:success] = "This user is being banished in the background. The job will complete soon."
- redirect_to edit_admin_user_path(params[:id])
+ redirect_to admin_user_path(params[:id])
end
def full_delete
@@ -131,7 +128,7 @@ module Admin
def unpublish_all_articles
Moderator::UnpublishAllArticlesWorker.perform_async(params[:id].to_i)
flash[:success] = "Posts are being unpublished in the background. The job will complete soon."
- redirect_to admin_users_path
+ redirect_to admin_user_path(params[:id])
end
def merge
@@ -142,7 +139,7 @@ module Admin
flash[:danger] = e.message
end
- redirect_to edit_admin_user_path(@user.id)
+ redirect_to admin_user_path(params[:id])
end
def remove_identity
@@ -163,7 +160,7 @@ module Admin
rescue StandardError => e
flash[:danger] = e.message
end
- redirect_to edit_admin_user_path(@user.id)
+ redirect_to admin_user_path(params[:id])
end
# NOTE: [@rhymes] This should be eventually moved in Admin::Users::Tools::EmailsController
@@ -181,7 +178,7 @@ module Admin
format.html do
flash[:success] = message
- redirect_back(fallback_location: admin_users_path)
+ redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { result: message }, content_type: "application/json" }
@@ -192,7 +189,7 @@ module Admin
format.html do
flash[:danger] = message
- redirect_back(fallback_location: admin_users_path)
+ redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js do
@@ -221,7 +218,7 @@ module Admin
format.html do
flash[:success] = message
- redirect_back(fallback_location: admin_users_path)
+ redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { result: message }, content_type: "application/json" }
@@ -232,7 +229,7 @@ module Admin
respond_to do |format|
format.html do
flash[:danger] = message
- redirect_back(fallback_location: admin_users_path)
+ redirect_back(fallback_location: admin_user_path(params[:id]))
end
format.js { render json: { error: message }, content_type: "application/json", status: :service_unavailable }
@@ -249,6 +246,18 @@ module Admin
private
+ def set_user_details
+ @organizations = @user.organizations.order(:name)
+ @notes = @user.notes.order(created_at: :desc).limit(10)
+ @organization_memberships = @user.organization_memberships
+ .joins(:organization)
+ .order("organizations.name" => :asc)
+ .includes(:organization)
+ @last_email_verification_date = EmailAuthorization.last_verification_date(@user)
+
+ render :show
+ end
+
def add_note
Note.create(
author_id: current_user.id,
@@ -285,5 +294,39 @@ module Admin
params.require(EMAIL_ALLOWED_PARAMS)
params.permit(EMAIL_ALLOWED_PARAMS)
end
+
+ def credit(_params)
+ return user_params unless FeatureFlag.enabled?(:admin_member_view)
+
+ credit_params = {}
+ if user_params[:credit_action] == "Add"
+ credit_params[:add_credits] = user_params[:credit_amount]
+ end
+
+ if user_params[:credit_action] == "Remove"
+ credit_params[:remove_credits] = user_params[:credit_amount]
+ end
+
+ credit_params
+ end
+
+ def credit_params
+ credit_params = nil
+ if FeatureFlag.enabled?(:admin_member_view)
+ credit_params = {}
+
+ if user_params[:credit_action] == "Add"
+ credit_params[:add_credits] = user_params[:credit_amount]
+ end
+
+ if user_params[:credit_action] == "Remove"
+ credit_params[:remove_credits] = user_params[:credit_amount]
+ end
+ else
+ credit_params = user_params
+ end
+
+ credit_params
+ end
end
end
diff --git a/app/javascript/packs/admin/editUser.jsx b/app/javascript/packs/admin/editUser.jsx
new file mode 100644
index 000000000..c71b37e87
--- /dev/null
+++ b/app/javascript/packs/admin/editUser.jsx
@@ -0,0 +1,70 @@
+import { initializeDropdown } from '@utilities/dropdownUtils';
+
+let preact;
+let AdminModal;
+const modalContents = new Map();
+
+function getModalContents(modalContentSelector) {
+ if (!modalContents.has(modalContentSelector)) {
+ const modelContentElement = document.querySelector(modalContentSelector);
+ const modalContent = modelContentElement.innerHTML;
+
+ // Remove the element from the DOM to avoid duplicate ID errors in regards to a11y.
+ modelContentElement.remove();
+ modalContents.set(modalContentSelector, modalContent);
+ }
+
+ return modalContents.get(modalContentSelector);
+}
+
+// Append an empty div to the end of the document so that is does not affect the layout.
+const modalContainer = document.createElement('div');
+document.body.appendChild(modalContainer);
+
+initializeDropdown({
+ triggerElementId: 'options-dropdown-trigger',
+ dropdownContentId: 'options-dropdown',
+});
+
+const openModal = async (event) => {
+ const { dataset } = event.target;
+
+ if (!Object.prototype.hasOwnProperty.call(dataset, 'modalTrigger')) {
+ // We're not trying to trigger a modal.
+ return;
+ }
+
+ event.preventDefault();
+
+ // Only load Preact if we haven't already.
+ if (!preact) {
+ [preact, { Modal: AdminModal }] = await Promise.all([
+ import('preact'),
+ import('@crayons/Modal/Modal'),
+ ]);
+ }
+
+ const { h, render } = preact;
+
+ const { modalTitle, modalSize, modalContentSelector } = dataset;
+
+ render(
+ {
+ render(null, modalContainer);
+ }}
+ >
+
+ ,
+ modalContainer,
+ );
+};
+
+document.body.addEventListener('click', openModal);
diff --git a/app/views/admin/users/_actions.html.erb b/app/views/admin/users/_actions.html.erb
new file mode 100644
index 000000000..690080729
--- /dev/null
+++ b/app/views/admin/users/_actions.html.erb
@@ -0,0 +1,41 @@
+<%= javascript_packs_with_chunks_tag "admin/editUser", defer: true %>
+
+ <%= link_to "Visit profile", @user.path, class: "c-cta", target: "_blank", rel: "noopener" %>
+
+
+ <%= crayons_icon_tag("overflow-vertical", title: "Options") %>
+
+
+
+ <% if @user.access_locked? %>
+ <%= link_to "Unlock access", unlock_access_admin_user_path(@user), method: :patch, class: "c-link c-link--block" %>
+ <% end %>
+ <% unless @last_email_verification_date %>
+ <%= form_with url: verify_email_ownership_admin_user_path(@user), local: true do |f| %>
+ <%= f.button "Verify email address", class: "c-btn w-100 align-left" %>
+ <% end %>
+ <% end %>
+ Export data
+ Merge accounts
+ <% if @user.articles_count > 0 %>
+ Unpublish all posts
+ <% end %>
+ <% if @user.identities.any? %>
+ Remove social accounts
+ <% end %>
+ Banish for spam
+ Delete user
+
+
+
+
+
+<%# These are contents for modals %>
+
+ <%= render "admin/users/actions/export" %>
+ <%= render "admin/users/actions/merge" %>
+ <%= render "admin/users/actions/unpublish" %>
+ <%= render "admin/users/actions/social_accounts" %>
+ <%= render "admin/users/actions/banish" %>
+ <%= render "admin/users/actions/delete" %>
+
diff --git a/app/views/admin/users/_header.html.erb b/app/views/admin/users/_header.html.erb
new file mode 100644
index 000000000..95e495402
--- /dev/null
+++ b/app/views/admin/users/_header.html.erb
@@ -0,0 +1,63 @@
+<% if @user.access_locked? %>
+
+
+ <%= crayons_icon_tag(:lock, title: "Access locked", class: "mr-2") %>
+ This user has locked access.
+
+ <%= link_to "Unlock access", unlock_access_admin_user_path(@user), method: :patch, class: "c-link c-link--block c-link--branded" %>
+
+<% end %>
+
+
+
+
+
+
+
+
+
+ <%= @user.name %>
+
+ <%= render "status" %>
+
+ <%= render "actions" %>
+
+
+ @<%= @user.username %> • ID <%= @user.id %> •
+ <% if @user.registered? %>
+ Member since
+ " title="<%= @user.created_at.strftime("%Y-%m-%d, %H:%M") %>">
+ <%= @user.created_at.strftime("%b %e '%y") %>
+
+ <% else %>
+ User has not accepted their invitation yet.
+ <% end %>
+
+
+
+
diff --git a/app/views/admin/users/_navigation.html.erb b/app/views/admin/users/_navigation.html.erb
new file mode 100644
index 000000000..8238d8525
--- /dev/null
+++ b/app/views/admin/users/_navigation.html.erb
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/views/admin/users/_stats.html.erb b/app/views/admin/users/_stats.html.erb
new file mode 100644
index 000000000..6153eef42
--- /dev/null
+++ b/app/views/admin/users/_stats.html.erb
@@ -0,0 +1,30 @@
+
+
+
+
<%= @user.comments_count %>
+
Comments
+
+
+
<%= @user.articles_count %>
+
Posts
+
+
+
<%= @user.reactions_count %>
+
Reactions
+
+
+
+
+
<%= @user.followers_count %>
+
Followers
+
+
+
<%= @user.following_users_count %>
+
Following
+
+
+
<%= @user.badge_achievements_count %>
+
Badges
+
+
+
diff --git a/app/views/admin/users/_status.html.erb b/app/views/admin/users/_status.html.erb
new file mode 100644
index 000000000..96b297a56
--- /dev/null
+++ b/app/views/admin/users/_status.html.erb
@@ -0,0 +1,13 @@
+
+ <% if @user.suspended? %>
+ Suspended
+ <% elsif @user.warned? %>
+ Warned
+ <% elsif @user.comment_suspended? %>
+ Comment suspended
+ <% elsif @user.trusted? %>
+ Trusted
+ <% else %>
+ Good standing
+ <% end %>
+
diff --git a/app/views/admin/users/actions/_banish.html.erb b/app/views/admin/users/actions/_banish.html.erb
new file mode 100644
index 000000000..366159f3b
--- /dev/null
+++ b/app/views/admin/users/actions/_banish.html.erb
@@ -0,0 +1,20 @@
+
+
+ <% if @user.comments.where("created_at < ?", 100.days.ago).empty? && @user.created_at < 100.days.ago %>
+
This is extremely destructive. Banishing will delete all the user's existing content and change their username to @spam_### .
+
Do not do this lightly.
+ <%= form_for(@user, url: banish_admin_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible. Banishing will delete all articles and turn their username into @spam_###')", id: nil }) do %>
+
Banish User for spam
+ <% end %>
+ <% elsif current_user.super_admin? || current_user.support_admin? %>
+
This is not a new user. You are only allowed to take this action because you are a Super Admin or a Support Admin.
+
This is extremely destructive. Banishing will delete all the user's existing content and change their username to @spam_### .
+
Do not do this lightly.
+ <%= form_for(@user, url: banish_admin_user_path(@user), html: { method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible. Banishing will delete all articles and turn their username into @spam_###')", id: nil }) do %>
+
Banish User for spam
+ <% end %>
+ <% else %>
+
This is not a new user. Only Super Admins or Support Admins are allowed to banish this user.
+ <% end %>
+
+
diff --git a/app/views/admin/users/actions/_delete.html.erb b/app/views/admin/users/actions/_delete.html.erb
new file mode 100644
index 000000000..dae886ced
--- /dev/null
+++ b/app/views/admin/users/actions/_delete.html.erb
@@ -0,0 +1,13 @@
+
+ <% if current_user.super_admin? %>
+ <%= form_for(@user, url: full_delete_admin_user_path(@user), html: { class: "flex flex-col gap-4", method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible.')", id: nil }) do |f| %>
+
This will completely destroy the user and all of their activity from our database. This action is irreversible.
+
Do not do this lightly.
+
+ Fully Delete User & All Activity
+
+ <% end %>
+ <% else %>
+
Only Super Admins are allowed to fully delete users.
+ <% end %>
+
diff --git a/app/views/admin/users/actions/_export.html.erb b/app/views/admin/users/actions/_export.html.erb
new file mode 100644
index 000000000..18ef91d13
--- /dev/null
+++ b/app/views/admin/users/actions/_export.html.erb
@@ -0,0 +1,19 @@
+
+
+
+ You can export a user's data here. Currently we only support exporting a user's posts and comments.
+
+
+ You have the option of exporting to your Admin account to send to the user, or exporting to the user directly.
+
+
+ Exporting the data to your Admin account will send it to your contact
+ Admin email (<%= ForemInstance.email %>),
+ and exporting to the user will send it to their email (<%= @user.email %>).
+
+
+ <%= button_to "Export to Admin", export_data_admin_user_path(@user.id), data: { confirm: "Are you sure you want to export this user's content to the ADMIN?" }, class: "c-btn c-btn--secondary", params: { send_to_admin: true } %>
+ <%= button_to "Export to User", export_data_admin_user_path(@user.id), data: { confirm: "Are you sure you want to export this user's content to the USER?" }, class: "c-btn c-btn--secondary", params: { send_to_admin: false } %>
+
+
+
diff --git a/app/views/admin/users/actions/_merge.html.erb b/app/views/admin/users/actions/_merge.html.erb
new file mode 100644
index 000000000..0102b51ba
--- /dev/null
+++ b/app/views/admin/users/actions/_merge.html.erb
@@ -0,0 +1,16 @@
+
+ <%= form_for(@user, url: merge_admin_user_path(@user),
+ html: { class: "flex flex-col gap-4", method: :post, onsubmit: "return confirm('Are you sure? This is extremely destructive and irreversible. Merging will delete all the other users content and combine it with this user')", id: nil }) do |f| %>
+
To merge a duplicate account, first make sure you are currently on the page of the user you want to keep . Below, add the user ID of the account to merge information from, and delete.
+
+ <%= f.label :merge_user_id, "User ID", class: "crayons-field__label" %>
+
+ User ID for the user that will be deleted and merged.
+
+ <%= f.text_field :merge_user_id, placeholder: "1234", class: "crayons-textfield", type: "number", inputmode: "numeric" %>
+
+
+ <%= f.button "Merge users", class: "c-btn c-btn--destructive c-btn--primary", type: "submit" %>
+
+ <% end %>
+
diff --git a/app/views/admin/users/actions/_social_accounts.html.erb b/app/views/admin/users/actions/_social_accounts.html.erb
new file mode 100644
index 000000000..8af24daae
--- /dev/null
+++ b/app/views/admin/users/actions/_social_accounts.html.erb
@@ -0,0 +1,39 @@
+
+
+
At times, removing a user's social account will resolve certain sign-in issues that a user may be experiencing.
+
For example, users may experience issues related to their UID when creating a <%= community_name %> account. If a user creates an account by, first, deleting their original social account and, then, creating their new account with the same Twitter/GitHub username,
+ the user may be unable to register their new account. This occurs when a user's Twitter/GitHub account's UID does not match their identity's UID. For more information on whether a user's UID does not match their identity UID, the following third party tools can be
+ helpful:
+ Tweeter ID for Twitter (username is in URL for reference)
+ or Caius's GitHub ID Lookup Tool
+
+
+
+ How do I remove a social account?
+
+
+ To start, click on one of the links provided above to check the user's social account UID.
+ Next, confirm whether or not the user's social account UID matches the user's identity UID.
+ If the social account UID does not match, delete the respective identity.
+ Finally, ask the user to reauthorize their social account via <%= app_url(user_settings_path(:account)) %>
+
+
+ <% @user.identities.each do |identity| %>
+ <%= form_for(@user, url: remove_identity_admin_user_path(@user),
+ html: { class: "fs-base p-4 py-3 c-card c-card--interactive flex gap-2 justify-between items-center", method: :delete, onsubmit: "return confirm('Are you sure? This should only be done as a solution for the listed example(s).)", id: nil }) do |f| %>
+ <%= f.hidden_field :identity_id, value: identity.id %>
+
+ <%= crayons_icon_tag(identity.provider, class: "shrink-0", width: 32, height: 32) %>
+
+
<%= identity.provider.capitalize %>
+
UID: <%= identity.uid %> • Username: <%= identity.auth_data_dump.present? ? identity.auth_data_dump["info"]["nickname"] : "No data available..." %>
+
+
+ <%= f.button class: "c-btn c-btn--icon-left c-btn--destructive", type: "submit" do %>
+ <%= crayons_icon_tag(:bin, class: "c-btn__icon") %>
+ Remove
+ <% end %>
+ <% end %>
+ <% end %>
+
+
diff --git a/app/views/admin/users/actions/_unpublish.html.erb b/app/views/admin/users/actions/_unpublish.html.erb
new file mode 100644
index 000000000..f722acd27
--- /dev/null
+++ b/app/views/admin/users/actions/_unpublish.html.erb
@@ -0,0 +1,8 @@
+
+ <%= form_for(@user, url: unpublish_all_articles_admin_user_path(@user), html: { class: "flex flex-col gap-4", method: :post, onsubmit: "return confirm('Are you sure? All posts will be unavailable to the community.')", id: nil }) do |f| %>
+
This will make any and all of the post links associated with this user unreachable. If the user is not suspended, they will have the ability to republish their posts.
+
+ Unpublish all posts
+
+ <% end %>
+
diff --git a/app/views/admin/users/emails/_form.html.erb b/app/views/admin/users/emails/_form.html.erb
new file mode 100644
index 000000000..715a5e681
--- /dev/null
+++ b/app/views/admin/users/emails/_form.html.erb
@@ -0,0 +1,18 @@
+Send an email
+<%= form_with url: send_email_admin_user_path(@user), local: true, class: "grid gap-4 pt-3" do |f| %>
+ <%= f.hidden_field(:user_id, value: @user.id) %>
+
+
+ <%= f.label :email_subject, "Subject", class: "crayons-field__label" %>
+ <%= f.text_field :email_subject, class: "crayons-textfield", placeholder: "Short and descriptive subject line..." %>
+
+
+
+ <%= f.label :email_body, "Body", class: "crayons-field__label" %>
+ <%= f.text_area :email_body, class: "crayons-textfield", rows: "7", placeholder: "What do you want to say?" %>
+
+
+
+ <%= f.button "Send email", class: "c-btn c-btn--primary", type: "submit" %>
+
+<% end %>
diff --git a/app/views/admin/users/emails/_index.html.erb b/app/views/admin/users/emails/_index.html.erb
new file mode 100644
index 000000000..96c47bef7
--- /dev/null
+++ b/app/views/admin/users/emails/_index.html.erb
@@ -0,0 +1,24 @@
+
+
Previous emails (<%= @user.email_messages.count %>)
+ <% if @user.email_messages.empty? %>
+
+
📭 No emails found.
+
Any emails sent to <%= @user.email %> can be found here.
+
+ <% else %>
+
+ <% @user.email_messages.order(sent_at: :desc).limit(50).each do |email| %>
+
+
+ " class="color-secondary fs-s shrink-0" title="<%= email.sent_at&.strftime("%Y-%m-%d, %H:%M") %>">
+ <%= email.sent_at&.strftime("%b %e '%y") %>
+
+
+ <% end %>
+
+ <% end %>
+
diff --git a/app/views/admin/users/emails/_verification.html.erb b/app/views/admin/users/emails/_verification.html.erb
new file mode 100644
index 000000000..801469021
--- /dev/null
+++ b/app/views/admin/users/emails/_verification.html.erb
@@ -0,0 +1,17 @@
+<%= form_with url: verify_email_ownership_admin_user_path(@user), local: true, class: "crayons-card mb-4 p-3 m:p-4 s:pl-4 m:pl-7 pr-4 flex flex-col s:flex-row justify-between gap-4 s:items-center" do |f| %>
+ <% unless @last_email_verification_date %>
+
+
Email not verified...
+
The user's email address, <%= @user.email %>, hasn't been verified yet.
+
+ <%= f.button "Verify email address", class: "c-btn c-btn--secondary whitespace-nowrap" %>
+ <% else %>
+
+ Email verified on:
+ " class="fw-medium whitespace-nowrap">
+ <%= @last_email_verification_date.strftime("%b %e '%y, %H:%M") %>
+
+
+ <%= f.button "Reverify", class: "c-btn c-btn--secondary whitespace-nowrap" %>
+ <% end %>
+<% end %>
diff --git a/app/views/admin/users/flags/_index.html.erb b/app/views/admin/users/flags/_index.html.erb
new file mode 100644
index 000000000..c78fa0a28
--- /dev/null
+++ b/app/views/admin/users/flags/_index.html.erb
@@ -0,0 +1,44 @@
+Flags received (<%= @related_vomit_reactions.count %>)
+
+<% unless @related_vomit_reactions.empty? %>
+
+ <% @related_vomit_reactions.each do |flag| %>
+
+
+ <% if flag.reactable_type == "Article" %>
+ <%= crayons_icon_tag("post") %>
+ <% elsif flag.reactable_type == "User" %>
+ <%= crayons_icon_tag("user-line") %>
+ <% elsif flag.reactable_type == "Comment" %>
+ <%= crayons_icon_tag("comment") %>
+ <% end %>
+
+
+
+ <%= link_to flag.reactable.path, class: "c-link" do %>
+ <% if flag.reactable_type == "Article" %>
+ Post "<%= flag.reactable.title %>"
+ <% elsif flag.reactable_type == "User" %>
+ User's profile
+ <% elsif flag.reactable_type == "Comment" %>
+ Comment on "<%= flag.reactable.title %>"
+ <% end %>
+ <% end %>
+
+
+ Flagged by
+ <%= link_to flag.user.username, admin_user_path(flag.user_id), class: "c-link c-link--branded" %>
+
+
+ " class="color-secondary fs-s shrink-0" title="<%= flag.created_at&.strftime("%Y-%m-%d, %H:%M") %>">
+ <%= flag.created_at&.strftime("%b %e '%y") %>
+
+
+ <% end %>
+
+<% else %>
+
+
😇 No flags!
+
No one flagged anything (profile, posts, or comments) negative about @<%= @user.username %>.
+
+<% end %>
diff --git a/app/views/admin/users/notes/_form.html.erb b/app/views/admin/users/notes/_form.html.erb
new file mode 100644
index 000000000..847c317b4
--- /dev/null
+++ b/app/views/admin/users/notes/_form.html.erb
@@ -0,0 +1,16 @@
+
+<%= form_with model: @user, url: admin_user_path(@user), method: :patch, html: { class: "grid gap-4 pt-3 m:sticky top-8" }, local: true do |f| %>
+
+ <%= f.label :new_note, "Note", class: "crayons-field__label" %>
+ <%= f.text_area :new_note, class: "crayons-textfield", rows: 5, placeholder: "Write your note here..." %>
+
+
+ <%= f.button "Submit", class: "c-btn c-btn--primary", type: "submit" %>
+
+<% end %>
diff --git a/app/views/admin/users/notes/_index.html.erb b/app/views/admin/users/notes/_index.html.erb
new file mode 100644
index 000000000..50b86449f
--- /dev/null
+++ b/app/views/admin/users/notes/_index.html.erb
@@ -0,0 +1,28 @@
+
+
Notes (<%= @notes.count %>)
+ <% if @notes.load.empty? %>
+
+
🗒 No notes found.
+
Any notes added for @<%= @user.name %> will be found here.
+
+ <% else %>
+
+ <% @notes.each do |note| %>
+
+
+
<%= note.content %>
+
+ <%= note.reason.presence %>
+ <% if note.author_id.present? %>
+ by <%= User.find(note.author_id).username %>
+ <% end %>
+
+
+ " class="color-secondary fs-s shrink-0" title="<%= note.created_at.strftime("%Y-%m-%d, %H:%M") %>">
+ <%= note.created_at.strftime("%b %e '%y") %>
+
+
+ <% end %>
+
+ <% end %>
+
diff --git a/app/views/admin/users/reports/_index.html.erb b/app/views/admin/users/reports/_index.html.erb
new file mode 100644
index 000000000..49aacf00f
--- /dev/null
+++ b/app/views/admin/users/reports/_index.html.erb
@@ -0,0 +1,32 @@
+Reports submitted by @<%= @user.username %> (<%= @related_reports.count %>)
+
+<% unless @related_reports.empty? %>
+
+ <% @related_reports.each do |report| %>
+
+
+
+
+ <%= "c-indicator--success" if report.status.casecmp("resolved").zero? %>">
+ <%= report.status %>
+
+
+
<%= report.message %>
+
For <%= report.reported_url %>
+
+ " class="color-secondary fs-s shrink-0" title="<%= report.created_at&.strftime("%Y-%m-%d, %H:%M") %>">
+ <%= report.created_at&.strftime("%b %e '%y") %>
+
+
+ <% end %>
+
+<% else %>
+
+
🧐 No reports submitted.
+
This user hasn't reported anything yet.
+
+<% end %>
diff --git a/app/views/admin/users/show.html.erb b/app/views/admin/users/show.html.erb
index d20e7f16e..f5a0f7e0d 100644
--- a/app/views/admin/users/show.html.erb
+++ b/app/views/admin/users/show.html.erb
@@ -1,50 +1,112 @@
-
-
-
+<%# TODO: incorporate a tab component. For now you have to manually change the value %>
+<% current_tab = "overview" # overview|emails|notes|reports|flags %>
+<% if FeatureFlag.enabled?(:admin_member_view) %>
+ <%= render "header" %>
+ <%= render "navigation" %>
+
+ <% if current_tab == 'overview' %>
+
+ <%= render "stats" if @user.registered %>
+
+
+
+
+ <%= render "admin/users/widgets/roles" %>
+
+
+
+ <%= render "admin/users/widgets/tag_moderation" %>
+
+
+
+
+
+ <%= render "admin/users/widgets/organizations" %>
+
+
+
+ <%= render "admin/users/widgets/credits" %>
+
+
+
+
+ <% elsif current_tab == 'notes' %>
+
+
+ <%= render "admin/users/notes/form" %>
+
+
+ <%= render "admin/users/notes/index" %>
+
+
+ <% elsif current_tab == 'emails' %>
+ <%= render "admin/users/emails/verification" %>
+
+
+ <%= render "admin/users/emails/form" %>
+
+
+ <%= render "admin/users/emails/index" %>
+
+
+ <% elsif current_tab == 'reports' %>
+
+ <%= render "admin/users/reports/index" %>
+
+ <% elsif current_tab == 'flags' %>
+
+ <%= render "admin/users/flags/index" %>
+
+ <% end %>
+<% else %>
+
+
+
+ <% if @user.registered? %>
+ <%= @user.name %><%= link_to "@#{@user.username}", @user.path, class: "ml-2", target: "_blank", rel: "noopener" %>
+ <% else %>
+ @<%= @user.username %> has not accepted their invitation yet.
+ <% end %>
+
<% if @user.registered? %>
- <%= @user.name %><%= link_to "@#{@user.username}", @user.path, class: "ml-2", target: "_blank", rel: "noopener" %>
- <% else %>
- @<%= @user.username %> has not accepted their invitation yet.
+
Manage User
+
Member since <%= @user.created_at.strftime("%b %e '%y") %>
<% end %>
-
- <% if @user.registered? %>
-
Manage User
-
Member since <%= @user.created_at.strftime("%b %e '%y") %>
- <% end %>
-
+
-
General Info
-
- ID:
- <%= @user.id %>
- Username:
- <%= @user.username %>
- Name:
- <%= @user.name %>
- Email:
- <%= @user.email %>
- Access Locked:
- <%= @user.access_locked? %>
- Twitter:
- <% if @user.twitter_username %>
- <%= link_to @user.twitter_username, "https://twitter.com/#{@user.twitter_username}" %>
- <% else %>
- N/A
- <% end %>
- GitHub:
- <% if @user.github_username %>
- <%= link_to @user.github_username, "https://github.com/#{@user.github_username}" %>
- <% else %>
- N/A
- <% end %>
-
-
+
General Info
+
+ ID:
+ <%= @user.id %>
+ Username:
+ <%= @user.username %>
+ Name:
+ <%= @user.name %>
+ Email:
+ <%= @user.email %>
+ Access Locked:
+ <%= @user.access_locked? %>
+ Twitter:
+ <% if @user.twitter_username %>
+ <%= link_to @user.twitter_username, "https://twitter.com/#{@user.twitter_username}" %>
+ <% else %>
+ N/A
+ <% end %>
+ GitHub:
+ <% if @user.github_username %>
+ <%= link_to @user.github_username, "https://github.com/#{@user.github_username}" %>
+ <% else %>
+ N/A
+ <% end %>
+
+
-<% if @user.registered? %>
- <%= render "activity" %>
- <%= render "credits" %>
- <%= render "notes" %>
- <%= render "user_organizations" %>
- <%= render "email_tools" %>
+ <% if @user.registered? %>
+ <%= render "activity" %>
+ <%= render "credits" %>
+ <%= render "notes" %>
+ <%= render "user_organizations" %>
+ <%= render "email_tools" %>
+ <% end %>
<% end %>
diff --git a/app/views/admin/users/widgets/_credits.html.erb b/app/views/admin/users/widgets/_credits.html.erb
new file mode 100644
index 000000000..467228b28
--- /dev/null
+++ b/app/views/admin/users/widgets/_credits.html.erb
@@ -0,0 +1,44 @@
+
+
+
<%= @user.unspent_credits_count %>
+
Adjust balance
+
+<% current_credits = @user.unspent_credits_count %>
+
+ <%= form_with scope: :user, url: admin_user_path(@user), method: :patch, local: true, html: { class: "flex flex-col gap-4 fs-base" } do |f| %>
+
+ <%= crayons_icon_tag(:wallet) %>
+
+ <%= @user.name %> currently has <%= @user.unspent_credits_count %> credits .
+
+
+
+ <%= f.label :credit_action, "Adjust balance", class: "crayons-field__label" %>
+
+ <%= f.select :credit_action, options_for_select(current_credits.positive? ? %w[Add Remove] : %w[Add]), {}, class: "crayons-select w-auto" %>
+ <%= f.text_field :credit_amount,
+ type: "number",
+ required: true,
+ class: "crayons-textfield w-auto",
+ value: 1,
+ size: 5,
+ min: 1,
+ max: 9999 %>
+ <%= f.label :credit_delta, "credits", class: "color-secondary flex-1", "aria-label": "Amount of credits to add or remove" %>
+
+
+
+ Why are you adjusting credits?
+ <%= f.text_area :new_note, id: "balance-note", size: 50, required: true, class: "crayons-textfield" %>
+
+
+ Adjust
+
+ <% end %>
+
diff --git a/app/views/admin/users/widgets/_organizations.html.erb b/app/views/admin/users/widgets/_organizations.html.erb
new file mode 100644
index 000000000..b06db2cef
--- /dev/null
+++ b/app/views/admin/users/widgets/_organizations.html.erb
@@ -0,0 +1,67 @@
+
+<% if @organization_memberships.load.empty? %>
+
+
This user is not a part of any organization yet.
+
Add organization
+
+<% else %>
+ <% @organization_memberships.each do |org_membership| %>
+
+
+
+
+ <%= crayons_icon_tag(:edit, class: "c-btn__icon") %>
+
+ <%= form_with model: [:admin, org_membership], method: :delete, local: true, html: { class: "inline" } do |f| %>
+ <%= f.button crayons_icon_tag(:x, class: "c-btn__icon"), class: "c-btn c-btn--destructive c-btn--icon-alone", data: { confirm: "Are you sure you want to remove them from #{org_membership.organization.name}?" } %>
+ <% end %>
+
+
+
+
+ <%= form_with model: [:admin, org_membership], method: :patch, local: true do |f| %>
+
+ <%= f.label "Permission level", class: "crayons-field__label" %>
+
Users can be assigned an Admin role or can remain a regular member of this organization.
+ <%= f.select("type_of_user", options_for_select(%w[member admin], selected: org_membership.type_of_user), {}, class: "crayons-select") %>
+
+ <%= f.button "Update", class: "c-btn c-btn--primary", type: "submit" %>
+ <% end %>
+
+ <% end %>
+ Add another organization
+<% end %>
+
+
+ <%= form_with model: [:admin, OrganizationMembership.new], local: true, html: { class: "flex flex-col gap-4" } do |f| %>
+ <%= f.hidden_field :user_id, value: @user.id %>
+
+ <%= f.label :organization_id, "Organization ID", class: "crayons-field__label" %>
+ <%= f.text_field :organization_id, class: "crayons-textfield", size: 8, required: true, inputmode: "numeric", type: "number", placeholder: "1234" %>
+
+
+ <%= f.label "Permission level", class: "crayons-field__label" %>
+
Users can be assigned an Admin role or can remain a regular member of this organization.
+ <%= f.select("type_of_user", options_for_select(%w[member admin]), {}, class: "crayons-select") %>
+
+
+ <%= f.submit "Add organization", class: "c-btn c-btn--primary" %>
+
+ <% end %>
+
diff --git a/app/views/admin/users/widgets/_roles.html.erb b/app/views/admin/users/widgets/_roles.html.erb
new file mode 100644
index 000000000..2034ec772
--- /dev/null
+++ b/app/views/admin/users/widgets/_roles.html.erb
@@ -0,0 +1,62 @@
+
+<% unless @user.roles.any? %>
+
+
No special roles assigned yet.
+
Add role
+
+<% else %>
+
+ <% @user.roles.reject { |role| role.name == "tag_moderator" }.each do |role| %>
+
+ <% if role.name == "banned" || role.name == "suspended" || role.name == "super_admin" || @user.id == current_user.id %>
+
+ <%= crayons_icon_tag(:lock, class: "c-pill__description-icon", width: 18, height: 18) %>
+ <% if role.resource_name %>
+ <%= role.resource_name.to_s %>
+ <% else %>
+ <%= role_display_name(role) %>
+ <% end %>
+ You can't remove this role...
+
+ <% else %>
+ <%= button_to url_for(action: :destroy, user_id: @user.id, role: role.name.to_sym, resource_type: role.resource_type), method: :delete, data: { confirm: "Are you sure?" }, class: "c-pill c-pill--action-icon" do %>
+ <% if role.resource_name %>
+ <%= role.resource_name.to_s %>
+ <% else %>
+ <%= role_display_name(role) %>
+ <% end %>
+ <%= crayons_icon_tag(:x, class: "c-pill__action-icon", width: 18, height: 18) %>
+ <% end %>
+ <% end %>
+
+ <% end %>
+ Add role
+
+<% end %>
+
+
+ <%= form_for(@user, url: user_status_admin_user_path(@user), html: { class: "flex flex-col gap-4", id: nil }) do |f| %>
+
+ <%= f.label :user_status, "Role", class: "crayons-field__label" %>
+ <% options = { "Base Roles" => Constants::Role::BASE_ROLES } %>
+ <% options["Special Roles"] = Constants::Role::SPECIAL_ROLES if current_user.super_admin? %>
+ <%= f.select(:user_status, grouped_options_for_select(options), { include_blank: "Select..." }, class: "crayons-select") %>
+
+
+ <%= f.label :note_for_current_role, "Reason", class: "crayons-field__label" %>
+ <%= f.text_area :note_for_current_role, required: true, class: "crayons-textfield", placeholder: "What's the reason for adding a role?" %>
+
+
+ <%= f.button "Add", class: "c-btn c-btn--primary" %>
+
+ <% end %>
+
diff --git a/app/views/admin/users/widgets/_tag_moderation.html.erb b/app/views/admin/users/widgets/_tag_moderation.html.erb
new file mode 100644
index 000000000..ea259785e
--- /dev/null
+++ b/app/views/admin/users/widgets/_tag_moderation.html.erb
@@ -0,0 +1,26 @@
+
+
+<% moderated_tags = @user.roles.select { |role| role.name == "tag_moderator" } %>
+
+<% if moderated_tags.any? %>
+
+ <% moderated_tags.each do |role| %>
+
+
+ #<%= role.resource_name.to_s %>
+ <%= crayons_icon_tag(:x, class: "c-pill__icon", width: 18, height: 18) %>
+
+
+ <% end %>
+
+<% end %>
+
+
+ Adding tags for moderation is currently only possible through a particular
tag's page , by providing the user's ID (
<%= @user.id %> ).
+
diff --git a/spec/requests/admin/users_spec.rb b/spec/requests/admin/users_spec.rb
index babeb4877..a5dd47e74 100644
--- a/spec/requests/admin/users_spec.rb
+++ b/spec/requests/admin/users_spec.rb
@@ -158,7 +158,7 @@ RSpec.describe "/admin/users", type: :request do
post send_email_admin_user_path(user.id), params: params
end
- expect(response).to redirect_to(admin_users_path)
+ expect(response).to redirect_to(admin_user_path)
expect(flash[:danger]).to include("failed")
end
@@ -167,7 +167,7 @@ RSpec.describe "/admin/users", type: :request do
post send_email_admin_user_path(user.id), params: params
end
- expect(response).to redirect_to(admin_users_path)
+ expect(response).to redirect_to(admin_user_path)
expect(flash[:success]).to include("sent")
email = ActionMailer::Base.deliveries.last
@@ -235,7 +235,7 @@ RSpec.describe "/admin/users", type: :request do
post verify_email_ownership_admin_user_path(user), params: { user_id: user.id }
end
- expect(response).to redirect_to(admin_users_path)
+ expect(response).to redirect_to(admin_user_path)
expect(flash[:danger]).to include("failed")
end
@@ -244,7 +244,7 @@ RSpec.describe "/admin/users", type: :request do
post verify_email_ownership_admin_user_path(user), params: { user_id: user.id }
end
- expect(response).to redirect_to(admin_users_path)
+ expect(response).to redirect_to(admin_user_path)
expect(flash[:success]).to include("sent")
end
@@ -394,7 +394,7 @@ RSpec.describe "/admin/users", type: :request do
it "redirects properly to the user edit page" do
sign_in admin
post export_data_admin_user_path(user), params: { send_to_admin: "true" }
- expect(response).to redirect_to edit_admin_user_path(user)
+ expect(response).to redirect_to admin_user_path(user)
end
end
end