Admin's Member view (#16174)

* init

* .

* partials

* few extras

* revert indicators in favor of separate PR

* stuff

* .

* stuff

* whoops

* .

* .

* .

* .

* opacity

* partial

* cosmetics

* move actions to a partial

* whoopsy

* Added an @admin alias for the frontend and removed the @admin-controllers alias.

* Added modal support for admin user actions.

* Added modal support for more parts of the user admin.

* pills

* pills

* pills

* Added optional chaining for when the overview panel isn't loaded.

* styling updates

* fix

* modals

* pass size

* tags

* .

* balance modal

* comment

* fixes

* Improved modal trigger implementation.

* Removed default value for dataset.

* Moved prevent default to after modal trigger check so forms still submit.

* Update app/views/admin/users/widgets/_credits.html.erb

Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com>

* Update app/views/admin/users/reports/_index.html.erb

* Apply suggestions from code review

* Apply suggestions from code review

* Update app/views/admin/users/actions/_export.html.erb

* Update app/views/admin/users/widgets/_tag_moderation.html.erb

* Update app/views/admin/users/actions/_merge.html.erb

* Removed <AdminUserActionsModal /> component as it wasn't necessary.

* Fixed duplicate ID errors detected by Axe.

* Redirects to the Show page rather than the Edit page

* Adjusts quotations within _social_accounts.html.erb

* Ensures that Learn More links open in new tab

* updates with latests and greatest

* .

* fixes

* wip. Fixed some issues in the credits form.

* Changed Subtract to Remove for credits dropdown.

* Cleaned up the edits form.

* Wraps functionality in feature flag and calls #set_user_details

* Wraps /admin/users/show.html.erb in feature flag

* wip: Got credit params form working.

* empty select

* responsiveness

* empty commiit

* Updates redirects in spec and controller to show rather than edit

* Updates tag moderation copy in Admin Member Detail View

* Now the note is saved when changing credits.

* Fixed label for attributes as it was affecting writing tests and a11y.

* Removes unused index.js component

* Refactored credit params.

* chore: move method under private and update comment

* verified email responsiveness

* better opacity variables

* better org logo

* indicators

* refactor the credits method

Co-authored-by: Nick Taylor <nick@dev.to>
Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
Co-authored-by: Julianna Tetreault <32834804+juliannatetreault@users.noreply.github.com>
Co-authored-by: Nick Taylor <nick@forem.com>
Co-authored-by: Ridhwana <ridhwana.khan16@gmail.com>
This commit is contained in:
ludwiczakpawel 2022-02-09 14:33:01 +01:00 committed by GitHub
parent 32e1b3f228
commit 50a4f0422b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 971 additions and 73 deletions

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M17 15.245v6.872a.5.5 0 0 1-.757.429L12 20l-4.243 2.546a.5.5 0 0 1-.757-.43v-6.87a8 8 0 1 1 10 0v-.001ZM12 15a6 6 0 1 0 0-12 6 6 0 0 0 0 12Zm0-2a4 4 0 1 1 0-8 4 4 0 0 1 0 8Z"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M16 9v10H8V9h8Zm-1.5-6h-5l-1 1H5v2h14V4h-3.5l-1-1ZM18 7H6v12c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7Z"/>
</svg>

After

Width:  |  Height:  |  Size: 202 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M6.414 16 16.556 5.858l-1.414-1.414L5 14.586V16h1.414Zm.829 2H3v-4.243L14.435 2.322a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414L7.243 18ZM3 20h18v2H3v-2Z"/>
</svg>

After

Width:  |  Height:  |  Size: 265 B

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="408" height="408" viewBox="0 0 408 408">
<path d="M346.8 61.2C311.1 22.95 260.1 0 204 0 91.8 0 0 91.8 0 204s91.8 204 204 204c94.35 0 173.4-66.3 196.35-153H346.8c-20.4 58.65-76.5 102-142.8 102-84.15 0-153-68.85-153-153S119.85 51 204 51c43.35 0 79.05 17.85 107.1 45.9l-81.6 81.6H408V0l-61.2 61.2z"/>
</svg>

Before

Width:  |  Height:  |  Size: 356 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M5.463 4.433A9.961 9.961 0 0 1 12 2c5.523 0 10 4.477 10 10 0 2.136-.67 4.116-1.81 5.74L17 12h3A8 8 0 0 0 6.46 6.228l-.997-1.795Zm13.074 15.134A9.961 9.961 0 0 1 12 22C6.477 22 2 17.523 2 12c0-2.136.67-4.116 1.81-5.74L7 12H4a8 8 0 0 0 13.54 5.772l.997 1.795Z"/>
</svg>

After

Width:  |  Height:  |  Size: 365 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M10 19.748V16.4c0-1.283.995-2.292 2.467-2.868A8.482 8.482 0 0 0 9.5 13c-1.89 0-3.636.617-5.047 1.66A8.018 8.018 0 0 0 10 19.748Zm8.88-3.662C18.485 15.553 17.17 15 15.5 15c-2.006 0-3.5.797-3.5 1.4V20a7.995 7.995 0 0 0 6.88-3.914ZM9.55 11.5a2.25 2.25 0 1 0 0-4.5 2.25 2.25 0 0 0 0 4.5Zm5.95 1a2 2 0 1 0 0-4 2 2 0 0 0 0 4ZM12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10Z"/>
</svg>

After

Width:  |  Height:  |  Size: 500 B

View file

@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M22 6h-7a6 6 0 1 0 0 12h7v2a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h18a1 1 0 0 1 1 1v2Zm-7 2h8v8h-8a4 4 0 1 1 0-8Zm0 3v2h3v-2h-3Z"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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 **************************************
***********************************************/

View file

@ -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';

View file

@ -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 **************************************
***********************************************/

View file

@ -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

View file

@ -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(
<AdminModal
title={modalTitle}
size={modalSize}
onClose={() => {
render(null, modalContainer);
}}
>
<div
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: getModalContents(modalContentSelector),
}}
/>
</AdminModal>,
modalContainer,
);
};
document.body.addEventListener('click', openModal);

View file

@ -0,0 +1,41 @@
<%= javascript_packs_with_chunks_tag "admin/editUser", defer: true %>
<div class="flex relative justify-between s:justify-end gap-2 my-2 s:my-0">
<%= link_to "Visit profile", @user.path, class: "c-cta", target: "_blank", rel: "noopener" %>
<div class="dropdown-trigger-container">
<button type="button" class="c-btn c-btn--icon-alone dropdown-trigger" id="options-dropdown-trigger" aria-haspopup="true" aria-expanded="false" aria-controls="options-dropdown">
<%= crayons_icon_tag("overflow-vertical", title: "Options") %>
</button>
<div class="crayons-dropdown right-0 left-0 s:left-auto" id="options-dropdown">
<ul class="p-0">
<% if @user.access_locked? %>
<li><%= link_to "Unlock access", unlock_access_admin_user_path(@user), method: :patch, class: "c-link c-link--block" %></li>
<% 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 %>
<li><button type="button" class="c-btn w-100 align-left" data-modal-trigger data-modal-title="Export data" data-modal-size="small" data-modal-content-selector="#export-data">Export data</button></li>
<li><button type="button" class="c-btn w-100 align-left" data-modal-trigger data-modal-title="Merge accounts" data-modal-size="small" data-modal-content-selector="#merge-accounts">Merge accounts</button></li>
<% if @user.articles_count > 0 %>
<li><button type="button" class="c-btn w-100 align-left" data-modal-trigger data-modal-title="Unpublish all posts" data-modal-size="small" data-modal-content-selector="#unpublish-all-posts">Unpublish all posts</button></li>
<% end %>
<% if @user.identities.any? %>
<li><button type="button" class="c-btn w-100 align-left" data-modal-trigger data-modal-title="Remove social accounts" data-modal-size="medium" data-modal-content-selector="#remove-social-accounts">Remove social accounts</button></li>
<% end %>
<li><button type="button" class="c-btn c-btn--destructive w-100 align-left" data-modal-trigger data-modal-title="Banish for spam" data-modal-size="small" data-modal-content-selector="#banish-for-spam">Banish for spam</button></li>
<li><button type="button" class="c-btn c-btn--destructive w-100 align-left" data-modal-trigger data-modal-title="Delete user" data-modal-size="small" data-modal-content-selector="#delete-user">Delete user</button></li>
</ul>
</div>
</div>
</div>
<%# These are contents for modals %>
<div class="hidden">
<%= 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" %>
</div>

View file

@ -0,0 +1,63 @@
<% if @user.access_locked? %>
<div class="crayons-notice crayons-notice--danger py-2 pr-2 mb-3 flex justify-between items-center">
<p class="flex-1 flex">
<%= crayons_icon_tag(:lock, title: "Access locked", class: "mr-2") %>
This user has locked access.
</p>
<%= link_to "Unlock access", unlock_access_admin_user_path(@user), method: :patch, class: "c-link c-link--block c-link--branded" %>
</div>
<% end %>
<section class="crayons-card flex flex-col l:flex-row gap-2 m:gap-4 l:gap-6 mb-2 m:mb-3 p-3 s:p-4 m:p-7">
<span class="crayons-avatar crayons-avatar--xl m:crayons-avatar--2xl l:crayons-avatar--3xl">
<img src="<%= @user.profile_image_url_for(length: 192) %>" width="100%" height="100%" alt="<%= @user.name %> profile picture">
</span>
<div class="grid gap-2 flex-1">
<div class="s:flex items-center gap-5">
<div class="flex flex-1 items-center">
<h1 class="crayons-title lh-tight">
<%= @user.name %>
</h1>
<%= render "status" %>
</div>
<%= render "actions" %>
</div>
<div class="fs-s mb-1 color-secondary">
@<%= @user.username %> <span class="opacity-50">&bull;</span> ID <%= @user.id %> <span class="opacity-50">&bull;</span>
<% if @user.registered? %>
Member since
<time datetime="<%= @user.created_at.strftime("%Y-%m-%dT%H:%M:%S%z") %>" title="<%= @user.created_at.strftime("%Y-%m-%d, %H:%M") %>">
<%= @user.created_at.strftime("%b %e '%y") %>
</time>
<% else %>
<span class="color-accent-danger fw-medium">User has not accepted their invitation yet.</span>
<% end %>
</div>
<div>
<ul class="flex flex-col s:flex-row gap-3 s:gap-6 flex-wrap">
<li>
<a href="mailto:<%= @user.email %>" class="c-link c-link--icon-left inline-block" target="_blank">
<%= crayons_icon_tag(:email, title: "Email", class: "c-link__icon") %>
<%= @user.email %>
</a>
</li>
<% if @user.github_username %>
<li>
<a href="https://github.com/<%= @user.github_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
<%= crayons_icon_tag(:github, title: "GitHub") %>
<%= @user.github_username %>
</a>
</li>
<% end %>
<% if @user.twitter_username %>
<li>
<a href="https://twitter.com/<%= @user.twitter_username %>" class="c-link c-link--icon-left inline-block" target="_blank">
<%= crayons_icon_tag(:twitter, title: "GitHub") %>
<%= @user.twitter_username %>
</a>
</li>
<% end %>
</ul>
</div>
</div>
</section>

View file

@ -0,0 +1,9 @@
<nav class="mb-2">
<ul class="crayons-navigation crayons-navigation--horizontal">
<li><a href="#" class="crayons-navigation__item crayons-navigation__item--current" aria-current="page">Overview</a></li>
<li><a href="#" class="crayons-navigation__item">Notes</a></li>
<li><a href="#" class="crayons-navigation__item">Emails</a></li>
<li><a href="#" class="crayons-navigation__item">Reports</a></li>
<li><a href="#" class="crayons-navigation__item">Flags</a></li>
</ul>
</nav>

View file

@ -0,0 +1,30 @@
<section class="crayons-card mb-7 px-2 py-3 flex flex-col l:flex-row gap-4 l:gap-0">
<div class="flex flex-1">
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.comments_count %></strong>
<p class="fs-s l:fs-base color-secondary">Comments</p>
</div>
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.articles_count %></strong>
<p class="fs-s l:fs-base color-secondary">Posts</p>
</div>
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.reactions_count %></strong>
<p class="fs-s l:fs-base color-secondary">Reactions</p>
</div>
</div>
<div class="flex flex-1">
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.followers_count %></strong>
<p class="fs-s l:fs-base color-secondary">Followers</p>
</div>
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.following_users_count %></strong>
<p class="fs-s l:fs-base color-secondary">Following</p>
</div>
<div class="align-center flex-1">
<strong class="fs-xl l:fs-2xl fw-heavy lh-tight"><%= @user.badge_achievements_count %></strong>
<p class="fs-s l:fs-base color-secondary">Badges</p>
</div>
</div>
</section>

View file

@ -0,0 +1,13 @@
<span class="ml-3 whitespace-nowrap" aria-label="Current status" title="Current status">
<% if @user.suspended? %>
<span class="c-indicator c-indicator--danger c-indicator--relaxed">Suspended</span>
<% elsif @user.warned? %>
<span class="c-indicator c-indicator--warning c-indicator--relaxed">Warned</span>
<% elsif @user.comment_suspended? %>
<span class="c-indicator c-indicator--warning c-indicator--relaxed">Comment suspended</span>
<% elsif @user.trusted? %>
<span class="c-indicator c-indicator--success c-indicator--relaxed">Trusted</span>
<% else %>
<span class="c-indicator c-indicator--relaxed">Good standing</span>
<% end %>
</span>

View file

@ -0,0 +1,20 @@
<div id="banish-for-spam">
<div class="flex flex-col gap-4">
<% if @user.comments.where("created_at < ?", 100.days.ago).empty? && @user.created_at < 100.days.ago %>
<p><strong>This is extremely destructive.</strong> Banishing will delete all the user's existing content and change their username to <em>@spam_###</em>.</p>
<p><strong>Do not do this lightly.</strong></p>
<%= 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 %>
<button class="c-btn c-btn--primary c-btn--destructive">Banish User for spam</button>
<% end %>
<% elsif current_user.super_admin? || current_user.support_admin? %>
<p><strong>This is not a new user.</strong> You are only allowed to take this action because you are a <strong>Super Admin or a Support Admin.</strong></p>
<p><strong>This is extremely destructive.</strong> Banishing will delete all the user's existing content and change their username to <em>@spam_###</em>.</p>
<p><strong>Do not do this lightly.</strong></p>
<%= 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 %>
<button class="c-btn c-btn--primary c-btn--destructive">Banish User for spam</button>
<% end %>
<% else %>
<div class="crayons-notice crayons-notice--danger">This is not a new user. Only Super Admins or Support Admins are allowed to banish this user.</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,13 @@
<div id="delete-user">
<% 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| %>
<p>This will <strong>completely destroy the user</strong> and all of their activity from our database. This action is irreversible.</p>
<p><strong>Do not do this lightly.</strong></p>
<div>
<button class="c-btn c-btn--primary c-btn--destructive">Fully Delete User & All Activity</button>
</div>
<% end %>
<% else %>
<div class="crayons-notice crayons-notice--danger">Only <strong>Super Admins</strong> are allowed to fully delete users.</div>
<% end %>
</div>

View file

@ -0,0 +1,19 @@
<div id="export-data">
<div class="flex flex-col gap-4">
<p>
You can export a user's data here. Currently we only support exporting a user's posts and comments.
</p>
<p>
You have the option of exporting to your Admin account to send to the user, or exporting to the user directly.
</p>
<p>
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 %>).
</p>
<div class="flex gap-2">
<%= 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 } %>
</div>
</div>
</div>

View file

@ -0,0 +1,16 @@
<div id="merge-accounts">
<%= 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| %>
<p>To merge a duplicate account, first make sure you are currently on the page of the user you want to <strong>keep</strong>. Below, add the user ID of the account to merge information from, and delete.</p>
<div class="crayons-field mb-4">
<%= f.label :merge_user_id, "User ID", class: "crayons-field__label" %>
<p class="crayons-field__description">
User ID for the user that will be <strong>deleted</strong> and merged.
</p>
<%= f.text_field :merge_user_id, placeholder: "1234", class: "crayons-textfield", type: "number", inputmode: "numeric" %>
</div>
<div>
<%= f.button "Merge users", class: "c-btn c-btn--destructive c-btn--primary", type: "submit" %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,39 @@
<div id="remove-social-accounts">
<div class="flex flex-col gap-4">
<p>At times, removing a user's social account will resolve certain sign-in issues that a user may be experiencing.</p>
<p>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:
<a href="https://tweeterid.com/?username=<%= @user.twitter_username %>" target="_blank" rel="noopener nofollow">Tweeter ID for Twitter (username is in URL for reference)</a>
or <a href="https://caius.github.io/github_id/#<%= @user.github_username %>" target="_blank" rel="noopener nofollow">Caius's GitHub ID Lookup Tool</a>
</p>
<h3 class="crayons-subtitle-2">
How do I remove a social account?
</h3>
<ol>
<li>To start, click on one of the links provided above to check the user's social account UID.</li>
<li>Next, confirm whether or not the user's social account UID matches the user's identity UID.</li>
<li>If the social account UID does not match, delete the respective identity.</li>
<li>Finally, ask the user to reauthorize their social account via <%= app_url(user_settings_path(:account)) %></li>
</ol>
<% @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 %>
<div class="flex-1 flex items-center gap-2">
<%= crayons_icon_tag(identity.provider, class: "shrink-0", width: 32, height: 32) %>
<div class="lh-tight">
<h4 class="crayons-subtitle-3"><%= identity.provider.capitalize %></h4>
<p class="fs-base color-secondary"><strong>UID:</strong> <%= identity.uid %> &bull; <strong>Username:</strong> <%= identity.auth_data_dump.present? ? identity.auth_data_dump["info"]["nickname"] : "No data available..." %></p>
</div>
</div>
<%= 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 %>
</div>
</div>

View file

@ -0,0 +1,8 @@
<div id="unpublish-all-posts">
<%= 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| %>
<p>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.</p>
<div>
<button class="c-btn c-btn--destructive c-btn--primary">Unpublish all posts</button>
</div>
<% end %>
</div>

View file

@ -0,0 +1,18 @@
<h2 class="crayons-subtitle-1">Send an email</h2>
<%= 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) %>
<div class="crayons-field">
<%= f.label :email_subject, "Subject", class: "crayons-field__label" %>
<%= f.text_field :email_subject, class: "crayons-textfield", placeholder: "Short and descriptive subject line..." %>
</div>
<div class="crayons-field">
<%= 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?" %>
</div>
<div>
<%= f.button "Send email", class: "c-btn c-btn--primary", type: "submit" %>
</div>
<% end %>

View file

@ -0,0 +1,24 @@
<div class="flex flex-col h-100">
<h2 class="crayons-subtitle-1">Previous emails (<%= @user.email_messages.count %>)</h2>
<% if @user.email_messages.empty? %>
<div class="align-center flex flex-col justify-center my-auto py-7">
<h3 class="fs-xl fw-normal mb-2">📭 No emails found.</h3>
<p class="color-secondary">Any emails sent to <%= @user.email %> can be found here.</p>
</div>
<% else %>
<div class="pt-3">
<% @user.email_messages.order(sent_at: :desc).limit(50).each do |email| %>
<article class="c-list-item flex justify-between gap-4">
<h3 class="flex-1 fw-medium lh-tight fs-base">
<a href="<%= admin_user_email_message_path(@user, email) %>" class="c-link" rel="noopener noreferer" target="_blank">
<%= email.subject %>
</a>
</h3>
<time datetime="<%= email.sent_at&.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= email.sent_at&.strftime("%Y-%m-%d, %H:%M") %>">
<%= email.sent_at&.strftime("%b %e '%y") %>
</time>
</article>
<% end %>
</div>
<% end %>
</div>

View file

@ -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 %>
<div>
<h2 class="crayons-subtitle-2">Email not verified...</h2>
<p class="color-secondary">The user's email address, <%= @user.email %>, hasn't been verified yet.</p>
</div>
<%= f.button "Verify email address", class: "c-btn c-btn--secondary whitespace-nowrap" %>
<% else %>
<p>
Email verified on:
<time datetime="<%= @last_email_verification_date.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="fw-medium whitespace-nowrap">
<%= @last_email_verification_date.strftime("%b %e '%y, %H:%M") %>
</time>
</p>
<%= f.button "Reverify", class: "c-btn c-btn--secondary whitespace-nowrap" %>
<% end %>
<% end %>

View file

@ -0,0 +1,44 @@
<h2 class="crayons-subtitle-1">Flags received (<%= @related_vomit_reactions.count %>)</h2>
<% unless @related_vomit_reactions.empty? %>
<div class="pt-3">
<% @related_vomit_reactions.each do |flag| %>
<article class="c-list-item flex gap-2">
<span class="color-secondary shrink-0">
<% 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 %>
</span>
<div class="flex-1">
<h3 class="crayons-subtitle-3 mb-1">
<%= 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 %>
</h3>
<p class="fs-s color-secondary">
Flagged by
<%= link_to flag.user.username, admin_user_path(flag.user_id), class: "c-link c-link--branded" %>
</p>
</div>
<time datetime="<%= flag.created_at&.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= flag.created_at&.strftime("%Y-%m-%d, %H:%M") %>">
<%= flag.created_at&.strftime("%b %e '%y") %>
</time>
</article>
<% end %>
</div>
<% else %>
<div class="align-center flex flex-col justify-center my-auto py-8">
<h3 class="fs-xl fw-normal mb-2">😇 No flags!</h3>
<p class="color-secondary">No one flagged anything (profile, posts, or comments) negative about @<%= @user.username %>.</p>
</div>
<% end %>

View file

@ -0,0 +1,16 @@
<header>
<h2 class="crayons-subtitle-1 mb-1">Add a note</h2>
<div class="color-secondary flex items-center">
<%= crayons_icon_tag(:lock, width: 18, height: 18, class: "mr-1") %>
<p>These notes will <strong>not</strong> be shared with the user.</p>
</div>
</header>
<%= 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| %>
<div class="crayons-field">
<%= f.label :new_note, "Note", class: "crayons-field__label" %>
<%= f.text_area :new_note, class: "crayons-textfield", rows: 5, placeholder: "Write your note here..." %>
</div>
<div>
<%= f.button "Submit", class: "c-btn c-btn--primary", type: "submit" %>
</div>
<% end %>

View file

@ -0,0 +1,28 @@
<div class="flex flex-col h-100">
<h2 class="crayons-subtitle-1">Notes (<%= @notes.count %>)</h2>
<% if @notes.load.empty? %>
<div class="align-center flex flex-col justify-center my-auto py-7">
<h3 class="fs-xl fw-normal mb-2">🗒 No notes found.</h3>
<p class="color-secondary">Any notes added for @<%= @user.name %> will be found here. </p>
</div>
<% else %>
<div class="pt-3">
<% @notes.each do |note| %>
<article class="c-list-item flex justify-between gap-4">
<div>
<p><%= note.content %></p>
<p class="fs-s color-secondary">
<%= note.reason.presence %>
<% if note.author_id.present? %>
by <span class="fw-medium"><%= User.find(note.author_id).username %></span>
<% end %>
</p>
</div>
<time datetime="<%= note.created_at.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= note.created_at.strftime("%Y-%m-%d, %H:%M") %>">
<%= note.created_at.strftime("%b %e '%y") %>
</time>
</article>
<% end %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,32 @@
<h2 class="crayons-subtitle-1">Reports submitted by @<%= @user.username %> (<%= @related_reports.count %>)</h2>
<% unless @related_reports.empty? %>
<div class="pt-3">
<% @related_reports.each do |report| %>
<article class="c-list-item c-list-item--interactive flex justify-between gap-4">
<div>
<header class="flex gap-3 items-center">
<h3 class="crayons-subtitle-3">
<a href="<%= admin_report_path(report.id) %>" class="c-link">
<%= report.category.capitalize %>
</a>
</h3>
<span class="c-indicator c-indicator--relaxed <%= "c-indicator--danger" if report.status.casecmp("open").zero? %> <%= "c-indicator--success" if report.status.casecmp("resolved").zero? %>">
<%= report.status %>
</span>
</header>
<p class="mb-1"><%= report.message %></p>
<p class="fs-s color-secondary">For <a href="<%= report.reported_url %>" class="c-link" target="_blank" rel="noopener"><%= report.reported_url %></a></p>
</div>
<time datetime="<%= report.created_at&.strftime("%Y-%m-%dT%H:%M:%S%z") %>" class="color-secondary fs-s shrink-0" title="<%= report.created_at&.strftime("%Y-%m-%d, %H:%M") %>">
<%= report.created_at&.strftime("%b %e '%y") %>
</time>
</article>
<% end %>
</div>
<% else %>
<div class="align-center flex flex-col justify-center my-auto py-8">
<h3 class="fs-xl fw-normal mb-2">🧐 No reports submitted.</h3>
<p class="color-secondary">This user hasn't reported anything yet.</p>
</div>
<% end %>

View file

@ -1,50 +1,112 @@
<div class="grid gap-6">
<div class="crayons-card p-6">
<h2 class="d-inline">
<%# 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' %>
<div class="crayons-card p-3 s:p-4 m:p-7">
<%= render "stats" if @user.registered %>
<div class="l:grid gap-8 grid-cols-2">
<div>
<section class="mb-7">
<%= render "admin/users/widgets/roles" %>
</section>
<section class="mb-7">
<%= render "admin/users/widgets/tag_moderation" %>
</section>
</div>
<div>
<section class="mb-7">
<%= render "admin/users/widgets/organizations" %>
</section>
<section class="mb-7">
<%= render "admin/users/widgets/credits" %>
</section>
</div>
</div>
</div>
<% elsif current_tab == 'notes' %>
<section class="flex flex-col m:flex-row gap-4">
<div class="crayons-card p-3 s:p-4 m:p-7 flex-1">
<%= render "admin/users/notes/form" %>
</div>
<div class="crayons-card p-3 s:p-4 m:p-7 flex-1">
<%= render "admin/users/notes/index" %>
</div>
</section>
<% elsif current_tab == 'emails' %>
<%= render "admin/users/emails/verification" %>
<section class="flex flex-col m:flex-row gap-4">
<div class="crayons-card p-3 s:p-4 m:p-7 flex-1">
<%= render "admin/users/emails/form" %>
</div>
<div class="crayons-card p-3 s:p-4 m:p-7 flex-1">
<%= render "admin/users/emails/index" %>
</div>
</section>
<% elsif current_tab == 'reports' %>
<section class="crayons-card p-3 s:p-4 m:p-7">
<%= render "admin/users/reports/index" %>
</section>
<% elsif current_tab == 'flags' %>
<section class="crayons-card p-3 s:p-4 m:p-7">
<%= render "admin/users/flags/index" %>
</section>
<% end %>
<% else %>
<div class="grid gap-6">
<div class="crayons-card p-6">
<h2 class="d-inline">
<% 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 %>
</h2>
<% 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.
<a href="<%= edit_admin_user_path(@user) %>" class="btn btn-primary float-right">Manage User</a>
<p class="font-italic">Member since <%= @user.created_at.strftime("%b %e '%y") %></p>
<% end %>
</h2>
<% if @user.registered? %>
<a href="<%= edit_admin_user_path(@user) %>" class="btn btn-primary float-right">Manage User</a>
<p class="font-italic">Member since <%= @user.created_at.strftime("%b %e '%y") %></p>
<% end %>
<hr>
<hr>
<h2>General Info</h2>
<dl>
<dt>ID:</dt>
<dd><%= @user.id %></dd>
<dt>Username:</dt>
<dd><%= @user.username %></dd>
<dt>Name:</dt>
<dd><%= @user.name %></dd>
<dt>Email:</dt>
<dd><%= @user.email %></dd>
<dt>Access Locked:</dt>
<dd><%= @user.access_locked? %></dd>
<dt>Twitter:</dt>
<% if @user.twitter_username %>
<dd><%= link_to @user.twitter_username, "https://twitter.com/#{@user.twitter_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
<dt>GitHub:</dt>
<% if @user.github_username %>
<dd><%= link_to @user.github_username, "https://github.com/#{@user.github_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
</dl>
</div>
<h2>General Info</h2>
<dl>
<dt>ID:</dt>
<dd><%= @user.id %></dd>
<dt>Username:</dt>
<dd><%= @user.username %></dd>
<dt>Name:</dt>
<dd><%= @user.name %></dd>
<dt>Email:</dt>
<dd><%= @user.email %></dd>
<dt>Access Locked:</dt>
<dd><%= @user.access_locked? %></dd>
<dt>Twitter:</dt>
<% if @user.twitter_username %>
<dd><%= link_to @user.twitter_username, "https://twitter.com/#{@user.twitter_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
<dt>GitHub:</dt>
<% if @user.github_username %>
<dd><%= link_to @user.github_username, "https://github.com/#{@user.github_username}" %></dd>
<% else %>
<dd>N/A</dd>
<% end %>
</dl>
</div>
<% 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 %>

View file

@ -0,0 +1,44 @@
<header class="mb-4">
<h2 class="crayons-subtitle-2 mb-1 flex items-center">
<%= crayons_icon_tag(:wallet, title: "Wallet", class: "mr-2") %>
Credits
</h2>
<p class="color-secondary">Credits enable users to purchase classified listings from your site. <a href="https://admin.forem.com/docs/advanced-customization/config/credits" rel="noopener noreferer" target="_blank" class="c-link c-link--branded">Learn more</a></p>
</header>
<div class="flex justify-between items-center">
<p class="fs-2xl fw-heavy"><%= @user.unspent_credits_count %></p>
<button class="c-btn c-btn--secondary whitespace-nowrap" type="button" data-modal-trigger data-modal-title="Adjust balance" data-modal-size="small" data-modal-content-selector="#adjust-balance">Adjust balance</button>
</div>
<% current_credits = @user.unspent_credits_count %>
<div id="adjust-balance" class="hidden">
<%= form_with scope: :user, url: admin_user_path(@user), method: :patch, local: true, html: { class: "flex flex-col gap-4 fs-base" } do |f| %>
<div class="flex items-center gap-2 color-secondary">
<%= crayons_icon_tag(:wallet) %>
<p>
<%= @user.name %> currently has <span class="color-primary"><%= @user.unspent_credits_count %> credits</span>.
</p>
</div>
<div class="crayons-field">
<%= f.label :credit_action, "Adjust balance", class: "crayons-field__label" %>
<div class="flex gap-2 items-center">
<%= 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" %>
</div>
</div>
<div class="crayons-field">
<label for="balance-note" class="crayons-field__label">Why are you adjusting credits?</label>
<%= f.text_area :new_note, id: "balance-note", size: 50, required: true, class: "crayons-textfield" %>
</div>
<div>
<button type="submit" class="c-btn c-btn--primary">Adjust</button>
</div>
<% end %>
</div>

View file

@ -0,0 +1,67 @@
<header class="mb-4">
<h2 class="crayons-subtitle-2 mb-1 flex items-center">
<%= crayons_icon_tag(:team, title: "Organizations", class: "mr-2") %>
Organizations
</h2>
<p class="color-secondary">Assign roles to this member based on permissions. <a href="https://admin.forem.com/docs/managing-your-community/organization-pages#organization-roles" rel="noopener noreferer" target="_blank" class="c-link c-link--branded">Learn more</a></p>
</header>
<% if @organization_memberships.load.empty? %>
<div class="flex justify-between items-center gap-4">
<p>This user is not a part of any organization yet.</p>
<button class="c-btn c-btn--secondary whitespace-nowrap" type="button" data-modal-trigger data-modal-title="Add organization" data-modal-size="small" data-modal-content-selector="#add-organization">Add organization</button>
</div>
<% else %>
<% @organization_memberships.each do |org_membership| %>
<div class="c-card c-card--interactive flex p-2 relative items-center mb-2 pl-3">
<div class="flex items-center gap-2 flex-1">
<a href="<%= org_membership.organization.path %>" target="_blank" rel="noopener" class="c-link crayons-logo crayons-logo--l">
<img src="<%= org_membership.organization.profile_image_url_for(length: 64) %>" width="32" height="32" alt="<%= org_membership.organization.name %> logo" class="crayons-logo__image">
</a>
<h3 class="crayons-subtitle-3">
<a href="<%= org_membership.organization.path %>" target="_blank" rel="noopener" class="c-link">
<%= org_membership.organization.name %>
</a>
<span class="color-secondary fw-normal"> <%= org_membership.type_of_user.capitalize %></span>
</h3>
</div>
<div class="c-card__revealable shrink-0">
<button type="button" class="c-btn c-btn--icon-alone" data-modal-trigger data-modal-title="Edit organization" data-modal-size="small" data-modal-content-selector="#edit-organization-<%= org_membership.organization.id %>">
<%= crayons_icon_tag(:edit, class: "c-btn__icon") %>
</button>
<%= 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 %>
</div>
</div>
<div id="edit-organization-<%= org_membership.organization.id %>" class="hidden">
<%= form_with model: [:admin, org_membership], method: :patch, local: true do |f| %>
<div class="crayons-field mb-4">
<%= f.label "Permission level", class: "crayons-field__label" %>
<p class="crayons-field__description">Users can be assigned an Admin role or can remain a regular member of this organization.</p>
<%= f.select("type_of_user", options_for_select(%w[member admin], selected: org_membership.type_of_user), {}, class: "crayons-select") %>
</div>
<%= f.button "Update", class: "c-btn c-btn--primary", type: "submit" %>
<% end %>
</div>
<% end %>
<button class="c-btn c-btn--secondary whitespace-nowrap w-100" type="button" data-modal-trigger data-modal-title="Add organization" data-modal-size="small" data-modal-content-selector="#add-organization">Add another organization</button>
<% end %>
<div id="add-organization" class="hidden">
<%= 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 %>
<div class="crayons-field">
<%= 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" %>
</div>
<div class="crayons-field">
<%= f.label "Permission level", class: "crayons-field__label" %>
<p class="crayons-field__description">Users can be assigned an Admin role or can remain a regular member of this organization.</p>
<%= f.select("type_of_user", options_for_select(%w[member admin]), {}, class: "crayons-select") %>
</div>
<div>
<%= f.submit "Add organization", class: "c-btn c-btn--primary" %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,62 @@
<header class="mb-4">
<h2 class="crayons-subtitle-2 mb-1 flex items-center">
<%= crayons_icon_tag(:badge, title: "Assigned roles", class: "mr-2") %>
Assigned roles
</h2>
<p class="color-secondary">Assign roles to this member based on permissions. <a href="https://admin.forem.com/docs/forem-basics/user-roles" rel="noopener noreferer" target="_blank" class="c-link c-link--branded">Learn more</a></p>
</header>
<% unless @user.roles.any? %>
<div class="flex justify-between items-center">
<p>No special roles assigned yet.</p>
<button class="c-btn c-btn--secondary whitespace-nowrap" type="button" data-modal-trigger data-modal-title="Add role" data-modal-size="m" data-modal-content-selector="#add-role">Add role</button>
</div>
<% else %>
<ul class="flex flex-wrap gap-2">
<% @user.roles.reject { |role| role.name == "tag_moderator" }.each do |role| %>
<li>
<% if role.name == "banned" || role.name == "suspended" || role.name == "super_admin" || @user.id == current_user.id %>
<button
class="c-pill c-pill--description-icon crayons-tooltip__activator cursor-help"
type="button"
aria-disabled="true">
<%= 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 %>
<span data-testid="tooltip" class="crayons-tooltip__content">You can't remove this role...</span>
</button>
<% 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 %>
</li>
<% end %>
<li><button class="c-btn c-btn--secondary whitespace-nowrap" type="button" data-modal-trigger data-modal-title="Add role" data-modal-size="small" data-modal-content-selector="#add-role">Add role</button></li>
</ul>
<% end %>
<div id="add-role" class="hidden">
<%= form_for(@user, url: user_status_admin_user_path(@user), html: { class: "flex flex-col gap-4", id: nil }) do |f| %>
<div class="crayons-field">
<%= 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") %>
</div>
<div class="crayons-field">
<%= 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?" %>
</div>
<div>
<%= f.button "Add", class: "c-btn c-btn--primary" %>
</div>
<% end %>
</div>

View file

@ -0,0 +1,26 @@
<header class="mb-4">
<h2 class="crayons-subtitle-2 mb-1 flex items-center">
<%= crayons_icon_tag("mod-filled", title: "Mod", class: "mr-2") %>
Tag moderation
</h2>
<p class="color-secondary">Assign tags to this member to moderate. <a href="https://admin.forem.com/docs/managing-your-community/tag-moderator-features" rel="noopener noreferer" target="_blank" class="c-link c-link--branded">Learn more</a></p>
</header>
<% moderated_tags = @user.roles.select { |role| role.name == "tag_moderator" } %>
<% if moderated_tags.any? %>
<ul class="flex flex-wrap gap-2 mb-4">
<% moderated_tags.each do |role| %>
<li>
<button class="c-pill c-pill--icon-right" style="--icon-right-color: var(--color-secondary); --icon-right-color-hover: var(--accent-danger);" type="button">
#<%= role.resource_name.to_s %>
<%= crayons_icon_tag(:x, class: "c-pill__icon", width: 18, height: 18) %>
</button>
</li>
<% end %>
</ul>
<% end %>
<div class="crayons-notice crayons-notice--warning">
Adding tags for moderation is currently only possible through a particular <a href="<%= admin_tags_path %>" class="c-link c-link--branded">tag's page</a>, by providing the user's ID (<strong><%= @user.id %></strong>).
</div>

View file

@ -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