app/assets/javascripts/components/common/AssignCell/AssignButton.jsx
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useDispatch } from 'react-redux';
import { Link } from 'react-router-dom';
import Popover from '../popover.jsx';
import { initiateConfirm } from '../../../actions/confirm_actions';
import { addAssignment, deleteAssignment, claimAssignment } from '../../../actions/assignment_actions';
import useExpandablePopover from '../../../hooks/useExpandablePopover';
import CourseUtils from '../../../utils/course_utils.js';
import AddAvailableArticles from '../../articles/add_available_articles';
import NewAssignmentInput from '../../assignments/new_assignment_input';
import { ASSIGNED_ROLE, REVIEWING_ROLE } from '~/app/assets/javascripts/constants';
import SelectedWikiOption from '../selected_wiki_option';
import { trackedWikisMaker } from '../../../utils/wiki_utils';
import ArticleUtils from '../../../utils/article_utils';
// Helper Components
// Button to show the static list
const ShowButton = ({ is_open, open }) => {
let buttonText = '…';
if (is_open) buttonText = I18n.t('users.assign_articles_done');
return (
<button
className={`button border small assign-button ${is_open ? 'dark' : ''}`}
onClick={open}
>
{buttonText}
</button>
);
};
const AddAssignmentButton = ({ assignment, assign, reviewing = false }) => {
const text = reviewing ? 'Review' : 'Select';
return (
<span>
<button
aria-label="Add"
className="button border assign-selection-button"
onClick={e => assign(e, assignment)}
>
{text}
</button>
</span>
);
};
const RemoveAssignmentButton = ({ assignment, unassign }) => {
return (
<span>
<button
aria-label="Remove"
className="button border assign-selection-button"
onClick={() => unassign(assignment)}
>
Remove
</button>
</span>
);
};
const ArticleLink = ({ articleUrl, title }) => {
if (!articleUrl) return (<span>{title}</span>);
return (
<a href={articleUrl} target="_blank" className="inline" aria-label={`View ${title} on Wikipedia`}>{title}</a>
);
};
const getArticle = (assignment, course, labels) => {
const article = CourseUtils.articleFromAssignment(assignment, course.home_wiki);
const label = labels[article.title];
article.title = CourseUtils.formattedArticleTitle(article, course.home_wiki, label);
return article;
};
const AssignedAssignmentRows = ({
assignments = [], course, permitted, role, wikidataLabels, project, unassign // functions
}) => {
const elements = assignments.map((assignment) => {
const article = getArticle(assignment, course, wikidataLabels);
return (
<tr key={assignment.id} className="assignment">
<td>
<ArticleLink articleUrl={article.url} title={article.title} />
{
permitted
&& <RemoveAssignmentButton
assignment={assignment}
unassign={unassign}
/>
}
</td>
</tr>
);
});
const text = role === ASSIGNED_ROLE
? I18n.t(`courses.assignment_headings.${ArticleUtils.projectSuffix(project, 'assigned_articles')}`)
: I18n.t('courses.assignment_headings.assigned_reviews');
const title = (
<tr key="assigned" className="assignment-section-header">
<td>
<h3>{text}</h3>
</td>
</tr>
);
return elements.length ? [title].concat(elements) : [];
};
const PotentialAssignmentRows = ({
assignments = [], course, permitted, role, wikidataLabels,
assign, // functions
project
}) => {
const elements = assignments.map((assignment) => {
const article = getArticle(assignment, course, wikidataLabels);
return (
<tr key={assignment.id} className="assignment">
<td>
<ArticleLink articleUrl={article.url} title={article.title} />
{
permitted
&& <AddAssignmentButton
assignment={assignment}
assign={assign}
reviewing={role === REVIEWING_ROLE}
/>
}
</td>
</tr>
);
});
const text = role === ASSIGNED_ROLE
? I18n.t(`courses.assignment_headings.${ArticleUtils.projectSuffix(project, 'available_articles')}`)
: CourseUtils.i18n(`assignment_headings.${ArticleUtils.projectSuffix(project, 'available_reviews')}`, course.string_prefix);
const title = (
<tr key="available" className="assignment-section-header">
<td>
<h3>{text}</h3>
</td>
</tr>
);
return elements.length ? [title].concat(elements) : [];
};
const Tooltip = ({ message }) => {
return (
<div className="tooltip">
<p>
{message}
</p>
</div>
);
};
// Button to add new assignments
const EditButton = ({
allowMultipleArticles, current_user, is_open, setHover, open, role, student,
tooltip, tooltipIndicator, assignmentLength, project
}) => {
let assignText;
let reviewText;
if (allowMultipleArticles) {
assignText = I18n.t(`assignments.${ArticleUtils.projectSuffix(project, 'add_available')}`);
} else if (assignmentLength) {
assignText = '+/-';
reviewText = '+/-';
} else if (student && current_user.id === student.id) {
assignText = I18n.t(`assignments.${ArticleUtils.projectSuffix(project, 'assign_self')}`);
reviewText = I18n.t(`assignments.${ArticleUtils.projectSuffix(project, 'review_self')}`);
} else if (current_user.role > 0 || current_user.admin) {
assignText = I18n.t(`assignments.${ArticleUtils.projectSuffix(project, 'assign_other')}`);
reviewText = I18n.t('assignments.review_other');
}
let finalText = role === ASSIGNED_ROLE ? assignText : reviewText;
if (is_open) finalText = I18n.t('users.assign_articles_done');
return (
<div className="tooltip-trigger" onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}>
<button
className={`button border small assign-button ${is_open ? 'dark' : ''}`}
onClick={open}
>
{finalText} {tooltipIndicator}
</button>
{tooltip}
</div>
);
};
const FindArticles = ({ course, open, project, language }) => {
const btnText = project === 'wikidata' ? I18n.t('items.search') : I18n.t('articles.search');
return (
<tr className="assignment find-articles-section">
<td>
<Link to={{ pathname: `/courses/${course.slug}/article_finder`, project: `${project}`, language: `${language}` }}>
<button className="button border small link" onClick={open}>
{btnText}
</button>
</Link>
</td>
</tr>
);
};
// Main Component
const AssignButton = ({ course, role, course_id, wikidataLabels = {}, hideAssignedArticles,
assignments, unassigned, student, allowMultipleArticles, current_user, isStudentsPage,
permitted, tooltip_message }) => {
const dispatch = useDispatch();
const [language, setLanguage] = useState('');
const [project, setProject] = useState('');
const [title, setTitle] = useState('');
useEffect(() => {
setLanguage(course.home_wiki.language || 'www');
setProject(course.home_wiki.project);
}, []);
const getKey = () => {
const tag = role === ASSIGNED_ROLE ? 'assign_' : 'review_';
return student ? tag + student.id : tag;
};
const { isOpen, ref, open } = useExpandablePopover(getKey);
const stop = (e) => {
return e.stopPropagation();
};
const handleChangeTitle = (e) => {
e.preventDefault();
// this text contains the titles/links of the article separated by new lines
const text = e.target.value;
const articlesTitles = [];
let articleLanguage;
let articleProject;
// loop for each individual article
text.split('\n').forEach((articleTitle) => {
// if the article title is empty, then skip it
if (!articleTitle) {
// add an empty string to the array so that new lines are preserved
articlesTitles.push('');
return;
}
const article = CourseUtils.articleFromTitleInput(articleTitle);
articlesTitles.push(article.title);
articleLanguage = article.language;
articleProject = article.project;
});
setTitle(articlesTitles.join('\n'));
setProject(articleProject || project);
setLanguage(articleLanguage || language);
};
const handleWikiChange = (chosenWiki) => {
setLanguage(chosenWiki.value.language);
setProject(chosenWiki.value.project);
};
const _onConfirmHandler = ({ action, assignment, isInTrackedWiki = true, title: confirmedTitle }) => {
const studentId = (student && student.id) || null;
const onConfirm = (e) => {
open(e);
setTitle('');
dispatch(action({
...assignment,
user_id: studentId
}));
};
let confirmMessage;
// Confirm for assigning an article to a student
if (student) {
confirmMessage = I18n.t('assignments.confirm_addition', {
title: confirmedTitle,
username: student.username
});
// Confirm for adding an unassigned available article
} else {
confirmMessage = I18n.t('assignments.confirm_add_available', {
title: confirmedTitle
});
}
// If the article is not from a tracked wiki, add a warning message.
let warningMessage;
if (!isInTrackedWiki) {
const wiki = `${assignment.language}.${assignment.project}.org`;
warningMessage = I18n.t('assignments.warning_untracked_wiki', { wiki });
}
return dispatch(initiateConfirm({ confirmMessage, onConfirm, warningMessage }));
};
const assign = (e) => {
e.preventDefault();
title.split('\n').filter(Boolean).forEach((assignment_title) => {
const assignment = {
title: decodeURIComponent(assignment_title).trim(),
project: project,
language: language,
course_slug: course.slug,
role: role
};
if (!assignment.title || assignment.title === 'undefined') return;
if (assignment.title.length > 255) {
// Title shouldn't exceed 255 chars to prevent mysql errors
return alert(I18n.t('assignments.title_too_large'));
}
const studentId = (student && student.id) || null;
dispatch(addAssignment({
...assignment,
user_id: studentId
}));
});
};
const review = (e, assignment) => {
e.preventDefault();
const reviewing = {
title: assignment.article_title,
course_slug: course.slug,
role
};
return _onConfirmHandler({
action: addAssignment,
assignment: reviewing,
title: reviewing.title
});
};
const update = (e, assignment) => {
e.preventDefault();
return _onConfirmHandler({
action: claimAssignment,
assignment: {
id: assignment.id,
role: role
},
title: assignment.article_title
});
};
const unassign = (assignment) => {
const confirmMessage = I18n.t('assignments.confirm_deletion');
const onConfirm = () => {
dispatch(deleteAssignment({ course_slug: course.slug, ...assignment }));
};
dispatch(initiateConfirm({ confirmMessage, onConfirm }));
};
let showButton;
if (!permitted && assignments.length > 1) {
showButton = (
<ShowButton
is_open={isOpen}
open={open}
/>
);
}
const [hover, setHover] = useState();
let editButton;
if (!showButton && permitted) {
let tooltip;
let tooltipIndicator;
if (tooltip_message && !isOpen) {
tooltipIndicator = (<span className={`${hover ? 'tooltip-indicator-hover' : 'tooltip-indicator'}`}/>);
tooltip = (<Tooltip message={tooltip_message} />);
}
editButton = (
<EditButton
allowMultipleArticles={allowMultipleArticles}
current_user={current_user}
is_open={isOpen}
open={open}
setHover={setHover}
role={role}
student={student}
tooltip={tooltip}
tooltipIndicator={tooltipIndicator}
assignmentLength={isStudentsPage && assignments.length}
project={project}
/>
);
}
const trackedWikis = trackedWikisMaker(course);
let editRow;
if (permitted) {
let assignmentInput;
// Add multiple at once via AddAvailableArticles
if (allowMultipleArticles) {
assignmentInput = (
<td>
<AddAvailableArticles
language={language} project={project} title={title} role={role}
course_id={course_id} addAssignment={assignment => dispatch(addAssignment(assignment))} open={open}
/>
<br />
<SelectedWikiOption
language={language}
trackedWikis={trackedWikis}
project={project}
handleWikiChange={handleWikiChange}
/>
</td>
);
// Add a single assignment
} else {
const onSubmit = (e, ...args) => {
assign(e, ...args);
setTitle('');
};
assignmentInput = (
<td>
<NewAssignmentInput
language={language}
project={project}
title={title}
assign={onSubmit}
trackedWikis={trackedWikis}
handleChangeTitle={handleChangeTitle}
handleWikiChange={handleWikiChange}
/>
</td>
);
}
editRow = (
<tr className="edit">
{assignmentInput}
</tr>
);
}
const assignmentRows = [];
// hideAssignedArticles will always be false except in the case
// of the my_articles.jsx view
if (!hideAssignedArticles) {
assignmentRows.push(
<AssignedAssignmentRows
assignments={assignments}
key="assigned"
unassign={unassign}
course={course}
permitted={permitted}
role={role}
wikidataLabels={wikidataLabels}
project={project}
/>
);
}
// If you are allowed to edit the assignments generally,
// either as an instructor or student
if (permitted) {
const action = role === REVIEWING_ROLE ? review : update;
assignmentRows.push(
<PotentialAssignmentRows
assignments={unassigned}
assign={action}
course={course}
key="potential"
permitted={permitted}
role={role}
wikidataLabels={wikidataLabels}
project={project}
/>
);
}
// Add the FindArticles button
if (role === ASSIGNED_ROLE && !isStudentsPage) {
const wikiLanguage = language === null ? 'www' : language;
assignmentRows.push(<FindArticles course={course} open={open} project={project} language={wikiLanguage} key="find-articles-link" />);
}
return (
<div className="pop__container assignment-popover" onClick={stop} ref={ref}>
{showButton}
{editButton}
<Popover
edit_row={editRow}
is_open={isOpen}
rows={assignmentRows}
/>
</div>
);
};
AssignButton.propTypes = {
allowMultipleArticles: PropTypes.bool,
assignments: PropTypes.array,
course: PropTypes.object.isRequired,
current_user: PropTypes.object,
role: PropTypes.number.isRequired,
is_open: PropTypes.bool,
permitted: PropTypes.bool,
student: PropTypes.object,
tooltip_message: PropTypes.string,
wikidataLabels: PropTypes.object,
unassigned: PropTypes.array
};
export default (AssignButton);