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" %> + +
+ +<%# These are contents for modals %> + 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 %> profile picture + +
+
+
+

+ <%= @user.name %> +

+ <%= render "status" %> +
+ <%= render "actions" %> +
+
+ @<%= @user.username %> ID <%= @user.id %> + <% if @user.registered? %> + Member since + + <% 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 %> + + <% 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 %> + + <% 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.

+
+ +
+ <% 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? +

+
    +
  1. To start, click on one of the links provided above to check the user's social account UID.
  2. +
  3. Next, confirm whether or not the user's social account UID matches the user's identity UID.
  4. +
  5. If the social account UID does not match, delete the respective identity.
  6. +
  7. Finally, ask the user to reauthorize their social account via <%= app_url(user_settings_path(:account)) %>
  8. +
+ + <% @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.

+
+ +
+ <% 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| %> +
+

+ + <%= email.subject %> + +

+ +
+ <% 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: + +

+ <%= 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" %> +

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

Add a note

+
+ <%= crayons_icon_tag(:lock, width: 18, height: 18, class: "mr-1") %> +

These notes will not be shared with the user.

+
+
+<%= 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 %> +

+
+ +
+ <% 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| %> +
+
+
+

+ + <%= report.category.capitalize %> + +

+ <%= "c-indicator--success" if report.status.casecmp("resolved").zero? %>"> + <%= report.status %> + +
+

<%= report.message %>

+

For <%= report.reported_url %>

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

+ <%= crayons_icon_tag(:wallet, title: "Wallet", class: "mr-2") %> + Credits +

+

Credits enable users to purchase classified listings from your site. Learn more

+
+
+

<%= @user.unspent_credits_count %>

+ +
+<% current_credits = @user.unspent_credits_count %> + 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 @@ +
+

+ <%= crayons_icon_tag(:team, title: "Organizations", class: "mr-2") %> + Organizations +

+

Assign roles to this member based on permissions. Learn more

+
+<% if @organization_memberships.load.empty? %> +
+

This user is not a part of any organization yet.

+ +
+<% else %> + <% @organization_memberships.each do |org_membership| %> +
+
+ +

+ + <%= org_membership.organization.name %> + + – <%= org_membership.type_of_user.capitalize %> +

+
+
+ + <%= 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 %> +
+
+ + + <% end %> + +<% 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 @@ +
+

+ <%= crayons_icon_tag(:badge, title: "Assigned roles", class: "mr-2") %> + Assigned roles +

+

Assign roles to this member based on permissions. Learn more

+
+<% unless @user.roles.any? %> +
+

No special roles assigned yet.

+ +
+<% else %> + +<% 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 @@ +
+

+ <%= crayons_icon_tag("mod-filled", title: "Mod", class: "mr-2") %> + Tag moderation +

+

Assign tags to this member to moderate. Learn more

+
+ +<% moderated_tags = @user.roles.select { |role| role.name == "tag_moderator" } %> + +<% if moderated_tags.any? %> + +<% 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