[deploy] [ModCenter] Fix "FlagUser" Modal functionality (#8995)

* Make FlagUserModal show up on click

* Explain extra div

* Make sure "Vomit All" reaction fires accurately

* Make test-file comment clearer

* Fix bug when 2 articles are open and the FlagUser modal does not open for the first article

* Still working it out ...

* Tests broken; need help

* Get rid of debugging alerts

* Fixed broken tests for <SingleArticle />

* Write tests for ModerationArticles component

* Complete tests for ModerationArticles component

* Employ safer Preact testing techniques

* Query all articles using "data-testid"

* Revert changes

* Got it working with portals for non-iframe implementation.

* wip tests

* Got mod tools flag user modal wprking on article page again.

* Added some documentation the code.

* Now confirm is disabled until you select an item in the flag user modal.

* Revert "wip tests"

This reverts commit fb7a0825039fd377cad04d9dedad9d1146b03978.

* test prep

* Fixed broken test.

* Refactored to use useRef hook.

* Rename a variable

* remove unnecessary comments

Co-authored-by: Nick Taylor <nick@dev.to>
This commit is contained in:
Arit Amana 2020-07-15 13:54:52 -04:00 committed by GitHub
parent e11f4f321a
commit a78ccf2310
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 530 additions and 263 deletions

View file

@ -4,7 +4,7 @@ class ModerationsController < ApplicationController
JSON_OPTIONS = {
only: %i[id title published_at cached_tag_list path],
include: {
user: { only: %i[username name path articles_count] }
user: { only: %i[username name path articles_count id] }
}
}.freeze

View file

@ -0,0 +1,112 @@
import { h } from 'preact';
import { render, fireEvent } from '@testing-library/preact';
import { ModerationArticles } from '../moderationArticles';
const getTestArticles = () => {
const articles = [
{
id: 1,
title: 'An article title',
path: 'an-article-title-di3',
published_at: '2019-06-22T16:11:10.590Z',
cached_tag_list: 'discuss, javascript, beginners',
user: {
articles_count: 1,
name: 'hello',
id: 1,
},
},
{
id: 2,
title:
'An article title that is quite very actually rather extremely long with all things considered',
path:
'an-article-title-that-is-quite-very-actually-rather-extremely-long-with-all-things-considered-fi8',
published_at: '2019-06-24T09:32:10.590Z',
cached_tag_list: '',
user: {
articles_count: 3,
name: 'howdy',
id: 2,
},
},
];
return JSON.stringify(articles);
};
describe('<ModerationArticles />', () => {
beforeEach(() => {
render(
<div
class="mod-index-list"
id="mod-index-list"
data-articles={getTestArticles()}
>
<div
data-testid="flag-user-modal-container"
class="flag-user-modal-container hidden"
/>
</div>,
);
});
it('renders a list of 2 articles', () => {
render(<ModerationArticles />);
const listOfArticles = document.querySelectorAll(
'[data-testid^="mod-article-"]',
);
expect(listOfArticles.length).toEqual(2);
});
it('renders the iframes on click', () => {
const { getByTestId } = render(<ModerationArticles />);
const singleArticle = getByTestId('mod-article-1');
singleArticle.click();
const iframes = singleArticle.querySelectorAll('iframe');
expect(iframes.length).toEqual(2);
});
it('toggles the "opened" class when opening or closing an article', () => {
const { getByTestId } = render(<ModerationArticles />);
const singleArticle = getByTestId('mod-article-2');
fireEvent.click(singleArticle);
expect(
singleArticle.querySelector('.article-iframes-container').classList,
).toContain('opened');
fireEvent.click(singleArticle);
expect(
singleArticle.querySelector('.article-iframes-container').classList,
).not.toContain('opened');
});
it('adds the FlagUser Modal HTML associated with author when article opened', async () => {
const { getByTestId, findByTestId } = render(<ModerationArticles />);
const expectedArticleId = 2;
expect(
document.querySelector('[data-testid="flag-user-modal"]'),
).toBeNull();
const singleArticle = getByTestId(`mod-article-${expectedArticleId}`);
singleArticle.click();
// We need the iframe to load first before checking for the modal having been loaded.
await findByTestId(`mod-iframe-${expectedArticleId}`);
const flagUserModal = document.querySelector(
'[data-testid="flag-user-modal"]',
);
expect(flagUserModal).not.toBeNull();
const actualArticleId = Number(
flagUserModal.querySelector('input').dataset.reactableId,
);
expect(actualArticleId).toEqual(expectedArticleId);
});
});

View file

@ -6,10 +6,36 @@ export class ModerationArticles extends Component {
articles: JSON.parse(
document.getElementById('mod-index-list').dataset.articles,
),
prevSelectedArticleId: undefined,
selectedArticleId: undefined,
};
toggleArticle = (id, path) => {
const { prevSelectedArticleId } = this.state;
const selectedArticle = document.getElementById(`article-iframe-${id}`);
if (prevSelectedArticleId > 0) {
document.getElementById(
`article-iframe-${prevSelectedArticleId}`,
).innerHTML = '';
}
this.setState({ selectedArticleId: id, prevSelectedArticleId: id });
if (
id === prevSelectedArticleId &&
document.querySelectorAll('.opened').length > 0
) {
selectedArticle.classList.remove('opened');
return;
}
selectedArticle.classList.add('opened');
selectedArticle.innerHTML = `<iframe class="article-iframe" src="${path}"></iframe><iframe data-testid="mod-iframe-${id}" class="actions-panel-iframe" id="mod-iframe-${id}" src="${path}/actions_panel"></iframe>`;
};
render() {
const { articles } = this.state;
const { articles, selectedArticleId } = this.state;
return (
<div className="moderation-articles-list">
@ -31,6 +57,8 @@ export class ModerationArticles extends Component {
key={id}
publishedAt={publishedAt}
user={user}
articleOpened={id === selectedArticleId}
toggleArticle={this.toggleArticle}
/>
);
})}

View file

@ -1,11 +1,10 @@
/* eslint-disable jest/expect-expect */
import { h } from 'preact';
import { axe } from 'jest-axe';
import { render, getNodeText, fireEvent } from '@testing-library/preact';
import { render, getNodeText } from '@testing-library/preact';
import SingleArticle from '../index';
const testArticle1 = {
const getTestArticle = () => ({
id: 1,
title: 'An article title',
path: 'an-article-title-di3',
@ -15,49 +14,48 @@ const testArticle1 = {
articles_count: 1,
name: 'hello',
},
};
const testArticle2 = {
id: 2,
title:
'An article title that is quite very actually rather extremely long with all things considered',
path:
'an-article-title-that-is-quite-very-actually-rather-extremely-long-with-all-things-considered-fi8',
publishedAt: '2019-06-24T09:32:10.590Z',
cachedTagList: '',
user: {
articles_count: 1,
name: 'howdy',
},
};
});
describe('<SingleArticle />', () => {
const renderSingleArticle = (article = testArticle1) =>
render(
<SingleArticle
id={article.id}
title={article.title}
path={article.path}
publishedAt={article.publishedAt} // renders as Jun 28
cachedTagList={article.cachedTagList}
user={article.user}
/>,
);
it('should have no a11y violations', async () => {
const { container } = renderSingleArticle();
const { container } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
{/* Div below needed for this test to pass while preserve FlagUserModal functionality */}
<div
data-testid="flag-user-modal-container"
class="flag-user-modal-container hidden"
/>
</>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('renders the article title', () => {
const { queryByText } = renderSingleArticle();
const { queryByText } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
expect(queryByText(testArticle1.title)).toBeDefined();
expect(queryByText(getTestArticle().title)).toBeDefined();
});
it('renders the tags', () => {
const { queryByText } = renderSingleArticle();
const { queryByText } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
expect(queryByText('discuss')).toBeDefined();
expect(queryByText('javascript')).toBeDefined();
@ -65,72 +63,116 @@ describe('<SingleArticle />', () => {
});
it('renders no tags or # symbol when article has no tags', () => {
const { container } = renderSingleArticle(testArticle2);
const article = {
id: 2,
title:
'An article title that is quite very actually rather extremely long with all things considered',
path:
'an-article-title-that-is-quite-very-actually-rather-extremely-long-with-all-things-considered-fi8',
publishedAt: '2019-06-24T09:32:10.590Z',
cachedTagList: '',
user: {
articles_count: 1,
name: 'howdy',
},
};
const { container } = render(
<>
<SingleArticle {...article} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
const text = getNodeText(container.querySelector('.article-title'));
expect(text).not.toContain('#');
});
it('renders the author name', () => {
const { container } = renderSingleArticle();
const { container } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
const text = getNodeText(container.querySelector('.article-author'));
expect(text).toContain(testArticle1.user.name);
expect(text).toContain(getTestArticle().user.name);
});
it('renders the hand wave emoji if the author has less than 3 articles ', () => {
const { container } = renderSingleArticle();
const { container } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
const text = getNodeText(container.querySelector('.article-author'));
expect(text).toContain('👋');
});
it('renders the correct formatted published date', () => {
const { queryByText } = renderSingleArticle();
const { queryByText } = render(
<>
<SingleArticle {...getTestArticle()} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
expect(queryByText('Jun 22')).toBeDefined();
});
it('renders the correct formatted published date as a time if the date is the same day', () => {
const today = new Date();
today.setSeconds('00');
testArticle1.publishedAt = today.toISOString();
const article = getTestArticle();
const publishDate = new Date('Wed Jul 08 2020 12:11:27 GMT-0400');
article.publishedAt = publishDate.toISOString();
const { queryByText } = renderSingleArticle(testArticle1);
const readableTime = today.toLocaleTimeString().replace(':00 ', ' '); // looks like 8:05 PM
expect(queryByText(readableTime)).toBeDefined();
});
it('renders the iframes on click', () => {
const { container } = renderSingleArticle();
container.querySelector('button.moderation-single-article').click();
const iframes = container.querySelectorAll('iframe');
expect(iframes.length).toEqual(2);
const [articleIframe, actionPanelIframe] = iframes;
expect(articleIframe.src).toContain(testArticle1.path);
expect(actionPanelIframe.src).toContain(
`${testArticle1.path}/actions_panel`,
render(
<>
<SingleArticle {...article} toggleArticle={jest.fn()} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
const readableTime = publishDate
.toLocaleTimeString()
.replace(/:\d{2}\s/, ' '); // looks like 8:05 PM
expect(document.querySelector('time').getAttribute('datetime')).toEqual(
'2020-07-08T16:11:27.000Z',
);
expect(readableTime).toEqual('4:11 PM');
});
it('adds the opened class when opening an article', () => {
it('toggles the article when clicked', () => {
const toggleArticle = jest.fn();
const { container } = render(
<SingleArticle
id={testArticle1.id}
title={testArticle1.title}
path={testArticle1.path}
publishedAt={testArticle1.publishedAt} // renders as Jun 28
cachedTagList={testArticle1.cachedTagList}
user={testArticle1.user}
toggleArticle={toggleArticle}
/>,
);
fireEvent.click(
container.querySelector('button.moderation-single-article'),
const article = getTestArticle();
const { getByTestId } = render(
<>
<SingleArticle {...article} toggleArticle={toggleArticle} />
<div
data-testid="flag-user-modal"
class="flag-user-modal-container hidden"
/>
</>,
);
expect(
container.querySelector('.article-iframes-container').classList,
).toContain('opened');
const button = getByTestId(`mod-article-${article.id}`);
button.click();
expect(toggleArticle).toHaveBeenCalledTimes(1);
});
});

View file

@ -1,35 +1,28 @@
import PropTypes from 'prop-types';
import { h, Component } from 'preact';
import { createPortal } from 'preact/compat';
import { toggleFlagUserModal, FlagUserModal } from '../../packs/flagUserModal';
import { formatDate } from './util';
export default class SingleArticle extends Component {
constructor(props) {
super(props);
this.state = {
articleOpened: false,
};
}
toggleArticle = (e) => {
activateToggle = (e) => {
e.preventDefault();
const { id, path, toggleArticle } = this.props;
const { id, path } = this.props;
const { articleOpened } = this.state;
if (articleOpened) {
this.setState({ articleOpened: false });
document.getElementById(`article-iframe-${id}`).innerHTML = '';
} else {
this.setState({ articleOpened: true });
document.getElementById(
`article-iframe-${id}`,
).innerHTML = `<iframe class="article-iframe" src="${path}"></iframe><iframe class="actions-panel-iframe" src="${path}/actions_panel"></iframe>`;
}
toggleArticle(id, path);
};
render() {
const { articleOpened } = this.state;
const { id, title, publishedAt, cachedTagList, user, key } = this.props;
const {
id,
title,
publishedAt,
cachedTagList,
user,
key,
articleOpened,
path,
} = this.props;
const tags = cachedTagList.split(', ').map((tag) => {
if (tag) {
return (
@ -42,33 +35,53 @@ export default class SingleArticle extends Component {
});
const newAuthorNotification = user.articles_count <= 3 ? '👋 ' : '';
const modContainer = id
? document.getElementById(`mod-iframe-${id}`)
: document.getElementById('mod-container');
// Check whether context is ModCenter or Friday-Night-Mode
if (modContainer) {
modContainer.addEventListener('load', () => {
modContainer.contentWindow.document
.getElementById('open-flag-user-modal')
.addEventListener('click', toggleFlagUserModal);
});
}
return (
<button
type="button"
className="moderation-single-article"
onClick={this.toggleArticle}
>
<span className="article-title">
<header>
<h3 className="fs-base fw-bold lh-tight">{title}</h3>
</header>
{tags}
</span>
<span className="article-author fs-s lw-medium lh-tight">
{newAuthorNotification}
{user.name}
</span>
<span className="article-published-at fs-s fw-bold lh-tight">
<time dateTime={publishedAt}>{formatDate(publishedAt)}</time>
</span>
<div
className={`article-iframes-container ${
articleOpened ? 'opened' : ''
}`}
id={`article-iframe-${id}`}
/>
</button>
<>
{modContainer &&
createPortal(
<FlagUserModal moderationUrl={path} authorId={user.id} />,
document.querySelector('.flag-user-modal-container'),
)}
<button
data-testid={`mod-article-${id}`}
type="button"
className="moderation-single-article"
onClick={this.activateToggle}
>
<span className="article-title">
<header>
<h3 className="fs-base fw-bold lh-tight">{title}</h3>
</header>
{tags}
</span>
<span className="article-author fs-s lw-medium lh-tight">
{newAuthorNotification}
{user.name}
</span>
<span className="article-published-at fs-s fw-bold lh-tight">
<time dateTime={publishedAt}>{formatDate(publishedAt)}</time>
</span>
<div
className={`article-iframes-container ${
articleOpened ? 'opened' : ''
}`}
id={`article-iframe-${id}`}
/>
</button>
</>
);
}
}

View file

@ -9,7 +9,7 @@ const initializeModerationsTools = async () => {
const { default: initializeActionsPanel } = await import(
'../actionsPanel/initializeActionsPanelToggle'
);
const { default: initializeFlagUserModal } = await import('./flagUserModal');
const { initializeFlagUserModal } = await import('./flagUserModal');
// article show page
if (

View file

@ -1,137 +0,0 @@
import { request } from '../utilities/http';
export default function initializeFlagUserModal(articleAuthorId) {
const flagUserModalHTML = `
<div class="crayons-modal crayons-modal--s absolute flag-user-modal">
<div class="crayons-modal__box">
<header class="crayons-modal__box__header flag-user-modal-header">
<h2>Flag User</h2>
<button type="button" class="crayons-btn crayons-btn--icon crayons-btn--ghost modal-header-close-icon">
<svg width="24" height="24" viewBox="0 0 24 24" class="crayons-icon" xmlns="http://www.w3.org/2000/svg">
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
</svg>
</button>
</header>
<div class="crayons-modal__box__body flag-user-modal-body">
<span>
Thanks for keeping DEV safe. Here is what you can do to flag this user:
</span>
<div class="crayons-fields">
<div class="crayons-field crayons-field--radio">
<input
type="radio"
id="vomit-all"
name="flag-user"
class="crayons-radio"
data-reactable-id="${articleAuthorId}"
data-category="vomit"
data-reactable-type="User">
<label for="vomit-all" class="crayons-field__label">
Make all posts by this author less visible
<p class="crayons-field__description">
This author consistently posts content that violates DEV's code of conduct because it is harassing, offensive or spammy.
</p>
</label>
</div>
<a href="/report-abuse?url=${document.location}" class="fs-base abuse-report-link">Report other inappropriate conduct</a>
</div>
<div class="buttons-container">
<a href="#" class="crayons-btn crayons-btn--danger" id="confirm-flag-user-action">Confirm action</a>
<a href="#" class="crayons-btn crayons-btn--secondary" id="cancel-flag-user-action">Cancel</a>
</div>
</div>
</div>
<div class="crayons-modal__overlay"></div>
</div>
`;
const toggleFlagUserModal = () => {
const modalContainer = document.querySelector('.flag-user-modal-container');
modalContainer.classList.toggle('hidden');
if (!modalContainer.classList.contains('hidden')) {
window.scrollTo(0, 0);
document.querySelector('body').style.height = '100vh';
document.querySelector('body').style.overflowY = 'hidden';
} else {
document.querySelector('body').style.height = 'inherit';
document.querySelector('body').style.overflowY = 'inherit';
}
};
const modContainer = document.getElementById('mod-container');
document.querySelector(
'.flag-user-modal-container',
).innerHTML = flagUserModalHTML;
modContainer.addEventListener('load', () => {
modContainer.contentWindow.document
.getElementById('open-flag-user-modal')
.addEventListener('click', toggleFlagUserModal);
});
// Event listeners to Close the Modal
const closeModalElements = Array.from(
document.querySelectorAll(
'.crayons-modal__overlay, .modal-header-close-icon, #cancel-flag-user-action',
),
);
closeModalElements.forEach((element) => {
element.addEventListener('click', toggleFlagUserModal);
});
document
.getElementById('confirm-flag-user-action')
.addEventListener('click', async (e) => {
e.preventDefault();
const vomitAllOption = document.getElementById('vomit-all');
if (vomitAllOption.checked) {
const body = JSON.stringify({
reactable_type: vomitAllOption.dataset.reactableType,
category: vomitAllOption.dataset.category,
reactable_id: vomitAllOption.dataset.reactableId,
});
try {
const response = await request('/reactions', {
method: 'POST',
body,
});
const outcome = await response.json();
if (outcome.result === 'create') {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message: 'All posts by this author will be less visible.',
addCloseButton: true,
});
} else if (outcome.result === null) {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message:
"It seems you've already reduced the vibilsity of this author's posts.",
addCloseButton: true,
});
} else {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message: `Response from server: ${JSON.stringify(outcome)}`,
addCloseButton: true,
});
}
} catch (error) {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({ message: error, addCloseButton: true });
}
} else {
// eslint-disable-next-line no-restricted-globals
top.addSnackbarItem({
message: 'No selection made!',
addCloseButton: true,
});
}
toggleFlagUserModal();
});
}

View file

@ -0,0 +1,208 @@
import { h, render } from 'preact';
import { useState, useRef } from 'preact/hooks';
import PropTypes from 'prop-types';
import { request } from '../utilities/http';
import { Button } from '@crayons/Button/Button';
async function confirmFlagUser({ reactableType, category, reactableId }) {
const body = JSON.stringify({
reactable_type: reactableType,
category,
reactable_id: reactableId,
});
try {
const response = await request('/reactions', {
method: 'POST',
body,
});
const outcome = await response.json();
if (outcome.result === 'create') {
top.addSnackbarItem({
message: 'All posts by this author will be less visible.',
addCloseButton: true,
});
} else if (outcome.result === null) {
top.addSnackbarItem({
message:
"It seems you've already reduced the visibility of this author's posts.",
addCloseButton: true,
});
} else {
top.addSnackbarItem({
message: `Response from server: ${JSON.stringify(outcome)}`,
addCloseButton: true,
});
}
} catch (error) {
top.addSnackbarItem({
message: error,
addCloseButton: true,
});
}
toggleFlagUserModal();
}
/**
* Shows or hides the flag user modal.
*/
export function toggleFlagUserModal() {
const modalContainer = document.querySelector('.flag-user-modal-container');
modalContainer.classList.toggle('hidden');
if (!modalContainer.classList.contains('hidden')) {
window.scrollTo(0, 0);
document.body.style.height = '100vh';
document.body.style.overflowY = 'hidden';
} else {
document.body.style.height = 'inherit';
document.body.style.overflowY = 'inherit';
}
}
/**
* Initializes the flag user modal for the given author ID.
*
* @param {number} authorId
*/
export function initializeFlagUserModal(authorId) {
// Check whether context is ModCenter or Friday-Night-Mode
const modContainer = document.getElementById('mod-container');
if (!modContainer) {
return;
}
render(
<FlagUserModal authorId={authorId} />,
document.querySelector('.flag-user-modal-container'),
);
modContainer.addEventListener('load', () => {
modContainer.contentWindow.document
.getElementById('open-flag-user-modal')
.addEventListener('click', toggleFlagUserModal);
});
}
/**
* A modal for flagging a user and their content. This can be used in the moderation
* or on an article page.
*
* @param {string} props.modCenterUrl (optional) The article URL loaded when in the moderation center.
* @param {number} props.authorId The author ID associated to the content being moderated.
*/
export function FlagUserModal({ modCenterArticleUrl, authorId }) {
const [isConfirmButtonEnabled, enableConfirmButton] = useState(false);
const vomitAllRef = useRef(null);
return (
<div
data-testid="flag-user-modal"
class="crayons-modal crayons-modal--s absolute flag-user-modal"
>
<div class="crayons-modal__box">
<header class="crayons-modal__box__header flag-user-modal-header">
<h2>Flag User</h2>
<button
type="button"
class="crayons-btn crayons-btn--icon crayons-btn--ghost modal-header-close-icon"
onClick={toggleFlagUserModal}
>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
class="crayons-icon"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636l4.95 4.95z" />
</svg>
</button>
</header>
<div class="crayons-modal__box__body flag-user-modal-body">
<span>
Thanks for keeping DEV safe. Here is what you can do to flag this
user:
</span>
<div class="crayons-fields">
<div class="crayons-field crayons-field--radio">
<input
type="radio"
id="vomit-all"
ref={vomitAllRef}
name="flag-user"
class="crayons-radio"
data-reactable-id={authorId}
data-category="vomit"
data-reactable-type="User"
checked={isConfirmButtonEnabled}
onClick={(event) => {
const { target } = event;
enableConfirmButton(target.checked);
}}
/>
<label htmlFor="vomit-all" class="crayons-field__label">
Make all posts by this author less visible
<p class="crayons-field__description">
This author consistently posts content that violates DEV's
code of conduct because it is harassing, offensive or spammy.
</p>
</label>
</div>
<a
href={`/report-abuse?url=${
modCenterArticleUrl
? `${document.location.origin}${modCenterArticleUrl}`
: document.location
}`}
class="fs-base abuse-report-link"
>
Report other inappropriate conduct
</a>
</div>
<div class="buttons-container">
<Button
class="crayons-btn crayons-btn--danger mr-2"
id="confirm-flag-user-action"
onClick={(_event) => {
const {
current: { dataset: adminVomitReaction },
} = vomitAllRef;
confirmFlagUser(adminVomitReaction);
enableConfirmButton(false);
}}
disabled={!isConfirmButtonEnabled}
>
Confirm action
</Button>
<Button
class="crayons-btn crayons-btn--secondary"
id="cancel-flag-user-action"
onClick={toggleFlagUserModal}
>
Cancel
</Button>
</div>
</div>
</div>
<div
role="presentation"
class="crayons-modal__overlay"
onClick={toggleFlagUserModal}
onKeyUp={toggleFlagUserModal}
/>
</div>
);
}
FlagUserModal.displayName = 'FlagUserModal';
FlagUserModal.propTypes = {
moderationUrl: PropTypes.string,
authorId: PropTypes.number.isRequired,
};

View file

@ -243,7 +243,7 @@
<div class="mod-actions-menu"></div>
<div id="mod-actions-menu-btn-area"></div>
<div class="flag-user-modal-container hidden"></div>
<div data-testid="flag-user-modal-container" class="flag-user-modal-container hidden"></div>
<% cache("article-show-scripts", expires_in: 8.hours) do %>
<script async>

View file

@ -21,6 +21,7 @@
<main class="mod-index-list" id="mod-index-list" data-articles="<%= @articles %>">
</main>
</div>
<div data-testid="flag-user-modal-container" class="flag-user-modal-container hidden"></div>
</div>
<% else %>
<div class="container" style="margin-top: 90px;">