ui/job-view/details/tabs/AnnotationsTab.jsx (246 lines of code) (raw):

import React from 'react'; import PropTypes from 'prop-types'; import { connect } from 'react-redux'; import { Button } from 'reactstrap'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faStar as faStarRegular } from '@fortawesome/free-regular-svg-icons'; import { faStar as faStarSolid, faTimesCircle, } from '@fortawesome/free-solid-svg-icons'; import { thEvents } from '../../../helpers/constants'; import { getBugUrl } from '../../../helpers/url'; import { longDateFormat } from '../../../helpers/display'; import { notify } from '../../redux/stores/notifications'; import { recalculateUnclassifiedCounts } from '../../redux/stores/pushes'; function RelatedBugSaved(props) { const { deleteBug, bug } = props; return ( <span className="btn-group pinboard-related-bugs-btn"> {!bug.bug_id && ( <span className="btn btn-xs"> <em>i{bug.bug_internal_id}</em> </span> )} {bug.bug_id && ( <a className="btn btn-xs annotations-bug related-bugs-link" href={getBugUrl(bug.bug_id)} target="_blank" rel="noopener noreferrer" title={`View bug ${bug.bug_id}`} > <em>{bug.bug_id}</em> </a> )} <Button color="link" size="xs" className="classification-delete-icon hover-warning pinned-job-close-btn annotations-bug" onClick={() => deleteBug(bug)} title={`Delete relation to bug ${bug.bug_internal_id ?? bug.bug_id}`} > <FontAwesomeIcon icon={faTimesCircle} title="Delete" /> </Button> </span> ); } RelatedBugSaved.propTypes = { deleteBug: PropTypes.func.isRequired, bug: PropTypes.shape({}).isRequired, }; function RelatedBug(props) { const { bugs, deleteBug } = props; return ( <span> <p className="annotations-bug-header font-weight-bold">Bugs</p> <ul className="annotations-bug-list"> {bugs.map((bug) => ( <li key={bug.internal_id}> <RelatedBugSaved bug={bug} deleteBug={() => deleteBug(bug)} /> </li> ))} </ul> </span> ); } RelatedBug.propTypes = { deleteBug: PropTypes.func.isRequired, bugs: PropTypes.arrayOf(PropTypes.shape({})).isRequired, }; function TableRow(props) { const { deleteClassification, classification, classificationMap } = props; const { created, who, name, text } = classification; const deleteEvent = () => { deleteClassification(classification); }; const failureId = classification.failure_classification_id; const icon = failureId === 7 ? faStarRegular : faStarSolid; const classificationName = classificationMap[failureId]; return ( <tr> <td>{new Date(created).toLocaleString('en-US', longDateFormat)}</td> <td>{who}</td> <td> {/* TODO: the classification label & star has been used in the job_details_pane.jxs so it should probably be made its own component when we start using import */} <span title={name}> <FontAwesomeIcon icon={icon} title={failureId === 7 ? 'Auto classified' : 'Classified'} /> <span className="ml-1">{classificationName.name}</span> </span> </td> <td>{text}</td> <td> <Button color="link" onClick={deleteEvent} className="classification-delete-icon hover-warning pointable" title="Delete this classification" > <FontAwesomeIcon icon={faTimesCircle} title="Delete classification" /> </Button> </td> </tr> ); } TableRow.propTypes = { deleteClassification: PropTypes.func.isRequired, classification: PropTypes.shape({}).isRequired, classificationMap: PropTypes.shape({}).isRequired, }; function AnnotationsTable(props) { const { classifications, deleteClassification, classificationMap } = props; return ( <table className="table-super-condensed table-hover"> <thead> <tr> <th>Classified</th> <th>Author</th> <th>Classification</th> <th>Comment</th> </tr> </thead> <tbody> {classifications.map((classification) => ( <TableRow key={classification.id} classification={classification} deleteClassification={deleteClassification} classificationMap={classificationMap} /> ))} </tbody> </table> ); } AnnotationsTable.propTypes = { deleteClassification: PropTypes.func.isRequired, classifications: PropTypes.arrayOf(PropTypes.shape({})).isRequired, classificationMap: PropTypes.shape({}).isRequired, }; class AnnotationsTab extends React.Component { componentDidMount() { window.addEventListener( thEvents.deleteClassification, this.onDeleteClassification, ); } componentWillUnmount() { window.removeEventListener( thEvents.deleteClassification, this.onDeleteClassification, ); } onDeleteClassification = () => { const { classifications, bugs, notify } = this.props; if (classifications.length) { this.deleteClassification(classifications[0]); // Delete any number of bugs if they exist bugs.forEach((bug) => { this.deleteBug(bug); }); } else { notify('No classification on this job to delete', 'warning'); } }; deleteClassification = async (classification) => { const { selectedJobFull, recalculateUnclassifiedCounts, notify, } = this.props; selectedJobFull.failure_classification_id = 1; recalculateUnclassifiedCounts(); const { failureStatus } = await classification.destroy(); if (!failureStatus) { notify('Classification successfully deleted', 'success'); // also be sure the job object in question gets updated to the latest // classification state (in case one was added or removed). window.dispatchEvent(new CustomEvent(thEvents.classificationChanged)); } else { notify('Classification deletion failed', 'danger', { sticky: true }); } }; deleteBug = async (bug) => { const { notify } = this.props; const { failureStatus } = await bug.destroy(); if (!failureStatus) { notify( `Association to bug ${ bug.bug_id ?? bug.bug_internal_id } successfully deleted`, 'success', ); window.dispatchEvent(new CustomEvent(thEvents.classificationChanged)); } else { notify( `Association to bug ${ bug.bug_id ?? bug.bug_internal_id } deletion failed`, 'danger', { sticky: true, }, ); } }; render() { const { classifications, classificationMap, bugs } = this.props; return ( <div className="container-fluid" role="region" aria-label="Annotations"> <div className="row h-100"> <div className="col-sm-10 classifications-pane"> {classifications.length ? ( <AnnotationsTable classifications={classifications} deleteClassification={this.deleteClassification} classificationMap={classificationMap} /> ) : ( <p>This job has not been classified</p> )} </div> {!!classifications.length && !!bugs.length && ( <div className="col-sm-2 bug-list-pane"> <RelatedBug bugs={bugs} deleteBug={this.deleteBug} /> </div> )} </div> </div> ); } } AnnotationsTab.propTypes = { classificationMap: PropTypes.shape({}).isRequired, bugs: PropTypes.arrayOf(PropTypes.shape({})).isRequired, classifications: PropTypes.arrayOf(PropTypes.shape({})).isRequired, recalculateUnclassifiedCounts: PropTypes.func.isRequired, notify: PropTypes.func.isRequired, selectedJobFull: PropTypes.shape({}).isRequired, }; export default connect(null, { notify, recalculateUnclassifiedCounts })( AnnotationsTab, );