[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:
parent
e11f4f321a
commit
a78ccf2310
10 changed files with 530 additions and 263 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
112
app/javascript/modCenter/__tests__/moderationArticles.test.jsx
Normal file
112
app/javascript/modCenter/__tests__/moderationArticles.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
208
app/javascript/packs/flagUserModal.jsx
Normal file
208
app/javascript/packs/flagUserModal.jsx
Normal 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,
|
||||
};
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue