@@ -31,6 +57,8 @@ export class ModerationArticles extends Component {
key={id}
publishedAt={publishedAt}
user={user}
+ articleOpened={id === selectedArticleId}
+ toggleArticle={this.toggleArticle}
/>
);
})}
diff --git a/app/javascript/modCenter/singleArticle/__tests__/singleArticle.test.jsx b/app/javascript/modCenter/singleArticle/__tests__/singleArticle.test.jsx
index 91236336c..2f4fc6fc1 100644
--- a/app/javascript/modCenter/singleArticle/__tests__/singleArticle.test.jsx
+++ b/app/javascript/modCenter/singleArticle/__tests__/singleArticle.test.jsx
@@ -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('
', () => {
- const renderSingleArticle = (article = testArticle1) =>
- render(
-
,
- );
-
it('should have no a11y violations', async () => {
- const { container } = renderSingleArticle();
+ const { container } = render(
+ <>
+
+ {/* Div below needed for this test to pass while preserve FlagUserModal functionality */}
+
+ >,
+ );
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('renders the article title', () => {
- const { queryByText } = renderSingleArticle();
+ const { queryByText } = render(
+ <>
+
+
+ >,
+ );
- expect(queryByText(testArticle1.title)).toBeDefined();
+ expect(queryByText(getTestArticle().title)).toBeDefined();
});
it('renders the tags', () => {
- const { queryByText } = renderSingleArticle();
+ const { queryByText } = render(
+ <>
+
+
+ >,
+ );
expect(queryByText('discuss')).toBeDefined();
expect(queryByText('javascript')).toBeDefined();
@@ -65,72 +63,116 @@ describe('
', () => {
});
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(
+ <>
+
+
+ >,
+ );
const text = getNodeText(container.querySelector('.article-title'));
expect(text).not.toContain('#');
});
it('renders the author name', () => {
- const { container } = renderSingleArticle();
+ const { container } = render(
+ <>
+
+
+ >,
+ );
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(
+ <>
+
+
+ >,
+ );
const text = getNodeText(container.querySelector('.article-author'));
expect(text).toContain('👋');
});
it('renders the correct formatted published date', () => {
- const { queryByText } = renderSingleArticle();
+ const { queryByText } = render(
+ <>
+
+
+ >,
+ );
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(
+ <>
+
+
+ >,
);
+
+ 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(
-
,
- );
- fireEvent.click(
- container.querySelector('button.moderation-single-article'),
+ const article = getTestArticle();
+ const { getByTestId } = render(
+ <>
+
+
+ >,
);
- expect(
- container.querySelector('.article-iframes-container').classList,
- ).toContain('opened');
+ const button = getByTestId(`mod-article-${article.id}`);
+ button.click();
+
+ expect(toggleArticle).toHaveBeenCalledTimes(1);
});
});
diff --git a/app/javascript/modCenter/singleArticle/index.jsx b/app/javascript/modCenter/singleArticle/index.jsx
index 45aa42d43..b56d5fcef 100644
--- a/app/javascript/modCenter/singleArticle/index.jsx
+++ b/app/javascript/modCenter/singleArticle/index.jsx
@@ -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 = `
`;
- }
+ 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 (
-
+ <>
+ {modContainer &&
+ createPortal(
+
,
+ document.querySelector('.flag-user-modal-container'),
+ )}
+
+ >
);
}
}
diff --git a/app/javascript/packs/articleModerationTools.js b/app/javascript/packs/articleModerationTools.js
index 261034ea9..064fcc0fc 100644
--- a/app/javascript/packs/articleModerationTools.js
+++ b/app/javascript/packs/articleModerationTools.js
@@ -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 (
diff --git a/app/javascript/packs/flagUserModal.js b/app/javascript/packs/flagUserModal.js
deleted file mode 100644
index 6933b99e4..000000000
--- a/app/javascript/packs/flagUserModal.js
+++ /dev/null
@@ -1,137 +0,0 @@
-import { request } from '../utilities/http';
-
-export default function initializeFlagUserModal(articleAuthorId) {
- const flagUserModalHTML = `
-
-
-
-
-
- Thanks for keeping DEV safe. Here is what you can do to flag this user:
-
-
-
-
-
-
-
Report other inappropriate conduct
-
-
-
-
-
-
-`;
-
- 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();
- });
-}
diff --git a/app/javascript/packs/flagUserModal.jsx b/app/javascript/packs/flagUserModal.jsx
new file mode 100644
index 000000000..f7e1b24bb
--- /dev/null
+++ b/app/javascript/packs/flagUserModal.jsx
@@ -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(
+
,
+ 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 (
+
+
+
+
+
+ Thanks for keeping DEV safe. Here is what you can do to flag this
+ user:
+
+
+
+
{
+ const { target } = event;
+
+ enableConfirmButton(target.checked);
+ }}
+ />
+
+
+
+ Report other inappropriate conduct
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+FlagUserModal.displayName = 'FlagUserModal';
+FlagUserModal.propTypes = {
+ moderationUrl: PropTypes.string,
+ authorId: PropTypes.number.isRequired,
+};
diff --git a/app/views/articles/show.html.erb b/app/views/articles/show.html.erb
index 99c3a453c..43572072d 100644
--- a/app/views/articles/show.html.erb
+++ b/app/views/articles/show.html.erb
@@ -243,7 +243,7 @@
-
+
<% cache("article-show-scripts", expires_in: 8.hours) do %>