docbrown/app/javascript/actionsPanel/actionsPanel.js
Andy Zhao 885e042d31
[deploy] New Feature: Mod Actions Panel (#7777)
* db

* Add an iframe to host mod actions menu

This iframe is probably the fastest way to get a working mod actions
menu without rewriting existing code. We may eventually re-write this,
but based on Ben's recommendation we are going to go with an iframe
because it gets us a working feature quickly and it seems to be serving
us well in other parts of the application.

* Add the mod actions menu button to articles

When the user is "trusted" they will see a small badge on the lower
right-hand side of the screen. Clicking the icon reveals the mod actions
menu.

* Add header to actions panel (#7395)

* Update iframe border

* Add 'Moderate Post' header and button

* Use inline SVG over <img src='svg'>

* Use Crayons variables for units

* Use flexbox to center chevron

* Add accidentally deleted positioning

* Add chevron-right.svg

* Use a partial for actions panel and revert /mod page

* Load actions panel in iframe and not /mod

* Style UpVote, DownVote and Vomit buttons (#7421)

* Add a iframe to host mod actions menu

* Up and Downvote Icons

* Starting with vomit button

* Completed UI; interactions pending

* Incorporated Andy changes; making mod action-panel

* space reactions away from header

* UI tweaks and corrections 1

* Fixing styles to match crayons

* choose pure white color css variable

* Add thumbs-up emoji; address Lisa comments

* Trying to fix reactions interactions

* remove clearThumbReactions function

Co-authored-by: jacobherrington <jacobherringtondeveloper@gmail.com>

* Add bottom layout for actions panel (#7501)

* WIP for Lisa

* Finalize mod actions bottom section

* Use showing instead of hidden for transition

* Add new optimized SVGs

* Fix weird height issues for actions panel

* Move hover styles below the cascade, yo

* Use margin-top for sticky footer over height: 100%

* Move initialize actions panel button to Webpack (#7506)

* Move actions panel JS code to webpack

* Refactor and use destructuring

Thanks @nickytonline!

Co-Authored-By: Nick Taylor <nick@iamdeveloper.com>

* Rename file

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* Configure Experience Level options for Mod Action Panel (#7507)

* Add a iframe to host mod actions menu

* Click interactions (no toggle of dropdowns yet)

Co-authored-by: jacobherrington <jacobherringtondeveloper@gmail.com>

* Fix the iframe's path (#7509)

* Toggle the 'Set Experience Level' menu on button-click (#7529)

* Add a iframe to host mod actions menu

* Add set-exp dropdown functionality

* WIP: making exp level updates async

* Completes async implementation

* Fix controller for "actions_panel" and "/mod"

Co-authored-by: jacobherrington <jacobherringtondeveloper@gmail.com>

* Add close button interaction (#7552)

* Rename actions panel pack file

* Add close button toggle to actions panel

* DB update

* Move more code to Webpack and add conditional tag adjustments section (#7652)

* Move JS code to Webpack

* Clean up a few style issues

* Add styling for tag adjustments

* Add active/inactive functionality

* Add conditional rendering for tag adjustments section

* Add missing variable oops (#7673)

* update with upstream

* Add missing subtract svg (#7695)

* Create 'Flag User' modal (#7626)

* Add a iframe to host mod actions menu

* Starting task

* Phase 1

* Fix "Close Modal" icon

* Implement open modal

* Working on all "Close Modal" actions

* Almost-complete implementation

* Remove Abuse Report snackbar

* Delete unneeded JS code

* Remove "How does this work?" links

* Link to Community Moderation page

* Addressing code review corrections

* Update api call to use "request" method

* Make else path clearer in api call

Co-authored-by: jacobherrington <jacobherringtondeveloper@gmail.com>

* Mod Actions Panel: Prevent nonsensical reaction pairs (#7699)

* Still implementing

* Completed Implementation

* Address code review feednack; refactor "create" method

* Move scss variables to native-css (for ModPanel code only)

* Correct missing native css

* update to native-css again

* More sccs -> native-css fixes

* Adjust ModPanel Button position on mobile (#7744)

* Adjusted ModPanel position on mobile

* Better adjustment

* Fix phantom schema.rb changes

* Fix phantom schema.rb changes

* Finalize tag adjustments (#7740)

* Finalize tag adjustments and flow

* Fix logic for rendering adjust tags options

* Clean up alert response

* Rename user to trusted user

* Use new CSS variables

* Add/remove tag on article's tag section after adjusting

* Fix logic for admin tag input and some other niceties

* Final bug fixes mod panel (#7772)

* Use correct boolean statement

* Clear the value of the textarea not div

* Use the correct attribute

* Use actions panel for /mod page (#7775)

* Fix loading logic with tag adjust button conditional render

* Link to tag moderation for adjust tags section

* Use top over window.parent

Thanks @nickytonline!

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* Use Webpack alias to import

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* Use place-items shorthand over align-items + justify-content

Thanks @nickytonline!

Co-authored-by: Nick Taylor <nick@iamdeveloper.com>

* Fix typo for utilities

* Add new line to end of file

* Disable linter for use for top function

* Use align-items and justify-content center

* Update SVGs to spec

* Remove unnecessary height and use spacing units for open button

* Remove accidentally added comment

* Error handling when selecting reactions; optimize "reactions.each" loop

* Add comment about clearing innerHTML

* Revert schema back to master

* Use each since there should only be two reactions

* Fix typo oops

* Add missing height oops

* Return if there are no reactions to be destroyed

* Center path with GUI tool shrug

* chose more appropriate name for JS pack; rewrote api calls using async-await

* Rename variables and methods and check categories before clearing

* Use addSnackbarItem instead of hardcoded version

* Place articleModerationTools pack behind user_signed_in

* Correct omission

* Implement dynamic imports for ModPanel tools

* Implemented async/await; added snackbar message for null response use-case

* Fix the "/article/mod" page; make sure all reactions, vomit-user and exp-level working as expected

* Update language to reflect new UI

* Close the modal after the "Confirm Action" button is clicked

* Change text for Spam/Abuse button

* Fix schema to use with master

* Update schema to master again

* Use crayons button for actions panel button

* UI tweak to play nicer with site themes

* Make actions panel work for dev.to/mod

* Add snackbar for reactions

* Update how does this work links

* Add "thumbsup" reactions to logging

* Add thumbsup to categories

* Add external link icon

* Changes to reflect "Privileged" and not "Negative" reactions; spec changes pending

* Updated instance variable

* Starting to update the specs

* Update "thumbsup" points from 10 to 5

* Still working on both requests

* Embracing an easier implemantation (thanks Andy!)

* Add tests for actions panel

* Add some basic tests for actions panel

* Change to flag to admins

* Indicate that 'Reason for Tag adjust' is required

Co-authored-by: Mac Siri <krairit.siri@gmail.com>

* Rename negative_reactions to privileged

* Update rating level to use correct values

* Use a different display value than backend value

* Don't show adjust tags for trusted users

* Add specs for moderation_routes for admin and super_admin

Co-authored-by: Arit Amana <msarit@gmail.com>
Co-authored-by: jacobherrington <jacobherringtondeveloper@gmail.com>
Co-authored-by: Arit Amana <32520970+msarit@users.noreply.github.com>
Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
Co-authored-by: Mac Siri <krairit.siri@gmail.com>
2020-06-08 13:24:18 -04:00

370 lines
10 KiB
JavaScript

import { request } from '@utilities/http';
export function addCloseListener() {
const button = document.querySelector('.close-actions-panel');
button.addEventListener('click', () => {
// getting the article show page document because this is called within an iframe
// eslint-disable-next-line no-restricted-globals
const articleDocument = top.document;
articleDocument
.querySelector('.mod-actions-menu')
.classList.toggle('showing');
articleDocument
.querySelector('.mod-actions-menu-btn')
.classList.toggle('hidden');
});
}
export function initializeHeight() {
document.documentElement.style.height = '100%';
document.body.style.cssText = 'height: 100%; margin: 0; padding-top: 0;';
document.getElementById('page-content').style.cssText =
'margin-top: 0 !important; margin-bottom: 0;';
}
function toggleDropdown(type) {
if (type === 'set-experience') {
document
.querySelector('.set-experience-options')
.classList.toggle('hidden');
} else if (type === 'adjust-tags') {
document.querySelector('.adjust-tags-options').classList.toggle('hidden');
}
}
function applyReactedClass(category) {
const upVote = document.querySelector("[data-category='thumbsup']");
const downVote = document.querySelector("[data-category='thumbsdown']");
const vomitVote = document.querySelector("[data-category='vomit']");
if (category === 'thumbsup') {
downVote.classList.remove('reacted');
vomitVote.classList.remove('reacted');
} else {
upVote.classList.remove('reacted');
}
}
export function addReactionButtonListeners() {
const butts = Array.from(
document.querySelectorAll('.reaction-button, .reaction-vomit-button'),
);
/* eslint-disable camelcase */
butts.forEach((butt) => {
butt.addEventListener('click', async (event) => {
event.preventDefault();
const {
reactableType: reactable_type,
category,
reactableId: reactable_id,
} = butt.dataset;
applyReactedClass(category);
butt.classList.toggle('reacted');
try {
const response = await request('/reactions', {
method: 'POST',
body: { reactable_type, category, reactable_id },
});
const outcome = await response.json();
let message;
/* eslint-disable no-restricted-globals */
if (outcome.result === 'create' && outcome.category === 'thumbsup') {
message = 'This post will be more visible.';
} else if (
outcome.result === 'create' &&
outcome.category === 'thumbsdown'
) {
message = 'This post will be less visible.';
} else if (
outcome.result === 'create' &&
outcome.category === 'vomit'
) {
message = "You've flagged this post as abusive or spam.";
} else if (outcome.result === 'destroy') {
message = 'Your quality rating was removed.';
} else if (outcome.error) {
message = `Error: ${outcome.error}`;
}
top.addSnackbarItem({
message,
addCloseButton: true,
});
/* eslint-enable no-restricted-globals */
} catch (error) {
// eslint-disable-next-line no-alert
alert(error);
}
});
});
/* eslint-enable camelcase */
}
function clearExpLevels() {
Array.from(
document.getElementsByClassName('level-rating-button selected'),
).forEach((el) => {
el.classList.remove('selected');
});
}
async function updateExperienceLevel(currentUserId, articleId, rating, group) {
try {
const response = await request('/rating_votes', {
method: 'POST',
body: JSON.stringify({
user_id: currentUserId,
article_id: articleId,
rating,
group,
}),
});
const outcome = await response.json();
if (outcome.result === 'Success') {
clearExpLevels();
document
.getElementById(`js__rating__vote__${rating}`)
.classList.add('selected');
} else {
// eslint-disable-next-line no-alert
alert(outcome.error);
}
} catch (error) {
// eslint-disable-next-line no-alert
alert(error);
}
}
function toggleSubmitContainer() {
document
.getElementById('adjustment-reason-container')
.classList.toggle('hidden');
}
function clearAdjustmentReason() {
document.getElementById('tag-adjustment-reason').value = '';
}
function renderTagOnArticle(tagName, colors) {
/* eslint-disable no-restricted-globals */
let articleTagsContainer;
if (top.document.location.pathname.endsWith('/mod')) {
[articleTagsContainer] = top.document
.getElementById('quick-mod-article')
.contentDocument.getElementsByClassName('tags');
} else {
[articleTagsContainer] = top.document.getElementsByClassName('tags');
}
/* eslint-enable no-restricted-globals */
const newTag = document.createElement('a');
newTag.innerText = `#${tagName}`;
newTag.setAttribute('class', 'tag');
newTag.setAttribute('href', `/t/${tagName}`);
newTag.style = `background-color: ${colors.bg}; color: ${colors.text};`;
articleTagsContainer.appendChild(newTag);
}
async function adjustTag(el) {
const reasonForAdjustment = document.getElementById('tag-adjustment-reason')
.value;
const body = {
tag_adjustment: {
// TODO: change to tag ID
tag_name: el.dataset.tagName || el.value,
article_id: el.dataset.articleId,
adjustment_type:
el.dataset.adjustmentType === 'subtract' ? 'removal' : 'addition',
reason_for_adjustment: reasonForAdjustment,
},
};
try {
const response = await request('/tag_adjustments', {
method: 'POST',
body: JSON.stringify(body),
});
const outcome = await response.json();
if (outcome.status === 'Success') {
let adjustedTagName;
if (el.tagName === 'BUTTON') {
adjustedTagName = el.dataset.tagName;
el.remove();
} else {
adjustedTagName = el.value;
// eslint-disable-next-line no-param-reassign
el.value = '';
}
toggleSubmitContainer();
clearAdjustmentReason();
if (outcome.result === 'addition') {
renderTagOnArticle(adjustedTagName, outcome.colors);
} else {
// eslint-disable-next-line no-restricted-globals
const tagOnArticle = top.document.querySelector(
`.tag[href="/t/${adjustedTagName}"]`,
);
tagOnArticle.remove();
}
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message: `The #${adjustedTagName} tag was ${
outcome.result === 'addition' ? 'added' : 'removed'
}.`,
addCloseButton: true,
});
} else {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message: `An error occurred: ${outcome.error}`,
addCloseButton: true,
});
}
} catch (error) {
// eslint-disable-next-line no-alert
alert(error);
}
}
export function handleAdjustTagBtn(btn) {
const currentActiveTags = document.querySelectorAll(
'button.adjustable-tag.active',
);
const adminTagInput = document.getElementById('admin-add-tag');
/* eslint-disable no-restricted-globals */
/* eslint-disable no-alert */
if (
adminTagInput &&
adminTagInput.value !== '' &&
confirm(
'This will clear your current "Add a tag" input. Do you want to continue?',
)
) {
/* eslint-enable no-restricted-globals */
/* eslint-enable no-alert */
adminTagInput.value = '';
} else if (currentActiveTags.length > 0) {
currentActiveTags.forEach((tag) => {
if (tag !== btn) {
tag.classList.remove('active');
}
btn.classList.toggle('active');
});
if (btn.classList.contains('active')) {
document
.getElementById('adjustment-reason-container')
.classList.remove('hidden');
} else {
document
.getElementById('adjustment-reason-container')
.classList.add('hidden');
}
} else {
btn.classList.toggle('active');
toggleSubmitContainer();
}
}
function handleAdminInput() {
const addTagInput = document.getElementById('admin-add-tag');
if (addTagInput) {
addTagInput.addEventListener('focus', () => {
document
.getElementById('adjustment-reason-container')
.classList.remove('hidden');
const activeTagBtns = Array.from(
document.querySelectorAll('button.adjustable-tag.active'),
);
activeTagBtns.forEach((btn) => {
btn.classList.remove('active');
});
});
addTagInput.addEventListener('focusout', () => {
if (addTagInput.value === '') {
toggleSubmitContainer();
}
});
}
}
export function addAdjustTagListeners() {
Array.from(document.getElementsByClassName('adjustable-tag')).forEach(
(btn) => {
btn.addEventListener('click', () => {
handleAdjustTagBtn(btn);
});
},
);
const adjustTagSubmitBtn = document.getElementById('tag-adjust-submit');
if (adjustTagSubmitBtn) {
adjustTagSubmitBtn.addEventListener('click', (e) => {
e.preventDefault();
const textArea = document.getElementById('tag-adjustment-reason');
const dataSource =
document.querySelector('button.adjustable-tag.active') ||
document.getElementById('admin-add-tag');
if (textArea.checkValidity()) {
adjustTag(dataSource);
}
});
handleAdminInput();
}
}
export function addBottomActionsListeners() {
addAdjustTagListeners();
Array.from(document.getElementsByClassName('other-things-btn')).forEach(
(btn) => {
btn.addEventListener('click', () => {
btn.classList.toggle('active');
const otherBtns = Array.from(
document.getElementsByClassName('other-things-btn'),
).filter((otherBtn) => otherBtn !== btn);
otherBtns.forEach((otherBtn) => {
otherBtn.classList.toggle('inactive');
});
btn.querySelector('.label-wrapper > .icon').classList.toggle('hidden');
btn
.querySelector('.toggle-chevron-container')
.classList.toggle('rotated');
toggleDropdown(btn.dataset.otherThingsType);
});
},
);
document.querySelectorAll('.level-rating-button').forEach((btn) => {
btn.addEventListener('click', () => {
updateExperienceLevel(
btn.dataset.userId,
btn.dataset.articleId,
btn.value,
btn.dataset.group,
);
});
});
}
export function initializeActionsPanel() {
initializeHeight();
addCloseListener();
addReactionButtonListeners();
addBottomActionsListeners();
}