mod panel update unpublish all posts action 17709 (#17923)

This commit is contained in:
Dwight Scott 2022-06-27 16:50:31 -04:00 committed by GitHub
parent 3a1661690b
commit 50e2131079
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 167 additions and 8 deletions

View file

@ -25,16 +25,21 @@ module Admin
last_moderation_notification last_notification_activity
].freeze
MODROLE_ACTIONS_TO_POLICIES = {
user_status: :toggle_suspension_status?,
unpublish_all_articles: :unpublish_all_articles?
}.freeze
after_action only: %i[update user_status banish full_delete unpublish_all_articles merge] do
Audit::Logger.log(:moderator, current_user, params.dup)
end
# Having this method here (which also exists in admin/application_controller)
# allows us to authorize the moderator role specifically for the user_status
# action, while preserving the implementation for all other admin actions
# allows us to authorize the actions of the moderator role specifically,
# while preserving the implementation for all other admin actions
def authorize_admin
if action_name == "user_status"
authorize(User, :toggle_suspension_status?)
if MODROLE_ACTIONS_TO_POLICIES.key?(action_name.to_sym)
authorize(User, MODROLE_ACTIONS_TO_POLICIES[action_name.to_sym])
else
super
end
@ -184,8 +189,17 @@ module Admin
def unpublish_all_articles
Moderator::UnpublishAllArticlesWorker.perform_async(params[:id].to_i)
flash[:success] = I18n.t("admin.users_controller.unpublished")
redirect_to admin_user_path(params[:id])
message = I18n.t("admin.users_controller.unpublished")
respond_to do |format|
format.html do
flash[:success] = message
redirect_to admin_user_path(params[:id])
end
format.json do
render json: { message: message }
end
end
end
def merge

View file

@ -1,6 +1,7 @@
import { toggleFlagUserModal } from '../packs/flagUserModal';
import { toggleModal } from '../packs/toggleUserSuspensionModal';
import { toggleUnpublishPostModal } from '../packs/unpublishPostModal';
import { toggleUnpublishAllPostsModal } from '../packs/modals/unpublishAllPosts';
import { request } from '@utilities/http';
export function addCloseListener() {
@ -404,6 +405,9 @@ export function addBottomActionsListeners() {
document
.getElementById('unsuspend-user-btn')
?.addEventListener('click', toggleModal);
document.getElementById('unpublish-all-posts-btn')
?.addEventListener('click', toggleUnpublishAllPostsModal);
}
export function initializeActionsPanel() {

View file

@ -0,0 +1,78 @@
import { closeWindowModal, showWindowModal } from '@utilities/showModal';
import { request } from '@utilities/http';
const unpublishAllPosts = async (event) => {
event.preventDefault();
const { userId } = event.target.dataset;
try {
const response = await request(
`/admin/member_manager/users/${userId}/unpublish_all_articles`,
{
method: 'POST',
body: JSON.stringify({ id: userId }),
credentials: 'same-origin',
},
);
const outcome = await response.json();
top.addSnackbarItem({
message: outcome.message,
addCloseButton: true,
});
} catch (error) {
top.addSnackbarItem({
message: `Error: ${error}`,
addCloseButton: true,
});
}
closeWindowModal(window.parent.document);
};
const modalContents = new Map();
/**
* Helper function to handle finding and caching modal content. Since our Preact modal helper works by duplicating HTML content,
* and our modals rely on IDs to label form controls, we remove the original hidden content from the DOM to avoid ID conflicts.
*
* @param {string} modalContentSelector The CSS selector used to identify the correct modal content
*/
function getModalContents(modalContentSelector) {
if (!modalContents.has(modalContentSelector)) {
const modalContentElement =
window.parent.document.querySelector(modalContentSelector);
const modalContent = modalContentElement.innerHTML;
modalContentElement.remove();
modalContents.set(modalContentSelector, modalContent);
}
return modalContents.get(modalContentSelector);
}
function activateUnpublishAllPostsBtn() {
const unpublishAllPostsBtn = window.parent.document.getElementById(
'unpublish-all-posts-submit-btn',
);
unpublishAllPostsBtn.addEventListener('click', unpublishAllPosts);
}
export function toggleUnpublishAllPostsModal() {
const unpublishAllPostsBtn = document.getElementById(
'unpublish-all-posts-btn',
);
const { modalTitle, modalSize, modalContentSelector } =
unpublishAllPostsBtn.dataset;
showWindowModal({
document: window.parent.document,
modalContent: getModalContents(modalContentSelector),
title: modalTitle,
size: modalSize,
onOpen: activateUnpublishAllPostsBtn,
});
}

View file

@ -94,6 +94,7 @@ class UserPolicy < ApplicationPolicy
end
alias toggle_suspension_status? elevated_user?
alias unpublish_all_articles? elevated_user?
def moderation_routes?
(user.has_trusted_role? || elevated_user?) && !user.suspended?

View file

@ -220,8 +220,9 @@
<div class="mod-actions-menu print-hidden"></div>
<div data-testid="flag-user-modal-container" class="flag-user-modal-container hidden"></div>
<div data-testid="unpublish-post-modal-container" class="unpublish-post-modal-container hidden"></div>
<%= render "moderations/suspend_user_modal" %>
<%= render "moderations/unsuspend_user_modal" %>
<%= render "moderations/modals/suspend_user_modal" %>
<%= render "moderations/modals/unsuspend_user_modal" %>
<%= render "moderations/modals/unpublish_all_posts" %>
<div class="fullscreen-code js-fullscreen-code"></div>
<%= javascript_packs_with_chunks_tag "followButtons", defer: true %>

View file

@ -211,6 +211,17 @@
data-modal-size="small" data-modal-content-selector="#<%= user_suspended ? "unsuspend" : "suspend" %>-modal-content" data-username="<%= user_username %>">
<%= t("views.moderations.actions.suspend.#{user_suspended ? 'unsuspend' : 'suspend'}_user", username: user_username) %>
</button>
<button
type="button"
id="unpublish-all-posts-btn"
class="c-btn c-btn--destructive align-center fs-s"
data-modal-title="Unpublish all posts"
data-modal-size="small"
data-modal-content-selector="#unpublish-all-posts"
data-username="<%= @moderatable.username %>">
Unpublish all posts for <%= @moderatable.username %>
</button>
</div>
<% end %>
</div>

View file

@ -0,0 +1,18 @@
<div id="unpublish-all-posts" class="hidden">
<p>
Once unpublished, all posts by <%= @article.username %> will become
hidden and only accessible to themselves.
</p>
<p class="mt-2">
If <%= @article.username %> is not suspended, they can still re-publish
their posts from their dashboard.
</p>
<div class="mt-4">
<button
id="unpublish-all-posts-submit-btn"
class="c-btn c-btn--destructive c-btn--primary"
data-user-id="<%= @article.user.id %>">
Unpublish all posts
</button>
</div>
</div>

View file

@ -152,6 +152,38 @@ describe('Moderation Tools for Posts', () => {
});
});
context('when unpublishing all posts', () => {
beforeEach(() => {
cy.get('@moderatorUser').then((user) => {
cy.loginAndVisit(user, '/series_user/series-test-article-slug');
cy.findByRole('heading', { level: 1, name: 'Series test article' });
cy.findByRole('button', { name: 'Moderation' }).click();
});
});
it('unpublishes all posts', () => {
cy.getIframeBody('[title="Moderation panel actions"]').within(() => {
cy.findByRole('button', { name: 'Open admin actions' })
.as('moderatingActionsButton')
.pipe(click)
.should('have.attr', 'aria-expanded', 'true');
cy.findByRole('button', {
name: /Unpublish all posts for series_user/i,
}).click();
});
cy.findByRole('dialog').within(() => {
cy.findByRole('button', { name: 'Unpublish all posts' }).click();
});
cy.findByTestId('snackbar')
.contains(
'Posts are being unpublished in the background. The job will complete soon.',
)
.should('exist');
});
});
context('when suspending user', () => {
beforeEach(() => {
cy.get('@moderatorUser').then((user) => {