packages/lib/reporting-components/issue-link/issue-link.js (215 lines of code) (raw):

import React, {useEffect, useState} from 'react'; import fecha from 'fecha'; import PropTypes from 'prop-types'; import Link from '@jetbrains/ring-ui/components/link/link'; import classNames from 'classnames'; import Tooltip from '@jetbrains/ring-ui/components/tooltip/tooltip'; import LoaderInline from '@jetbrains/ring-ui/components/loader-inline/loader-inline'; import {i18n} from 'hub-dashboard-addons/dist/localization'; import {loadIssue} from '../resources/resources'; import './color-fields.scss'; import './issue-preview.scss'; const isPriorityField = issueField => { const fieldPrototype = issueField && (issueField.projectCustomField || {}).field; const fieldPrototypeName = fieldPrototype && fieldPrototype.name; return (fieldPrototypeName || '').toLowerCase() === 'priority'; }; const getPriorityIssueField = fields => (fields || []).filter(isPriorityField)[0]; const periodFormatter = value => { const EMPTY_PERIOD_VALUE = i18n('{{minCount}}m', {minCount: 0}); return value.presentation || value.minutes || EMPTY_PERIOD_VALUE; }; const dateFormatter = value => fecha.format(value, 'D-MM-YYYY'); const dateAndTimeFormatter = value => fecha.format(value, 'D-MM-YYYY HH:mm'); const floatFormatter = value => value; const fieldValueFormatters = { period: periodFormatter, date: dateFormatter, float: floatFormatter, 'date and time': dateAndTimeFormatter }; // eslint-disable-next-line complexity const formatAsIssueFieldValue = (value, fieldType) => { if (!value) { return ''; } const formatter = fieldValueFormatters[fieldType]; if (formatter) { return formatter(value); } return value.localizedName || value.name || value.fullName || value.login || value; }; // eslint-disable-next-line complexity const getCustomFieldTextPresentation = (field, noEmptyText) => { const values = Array.isArray(field.value) ? field.value : (field.value && [field.value] || null); if (values) { const cfPrototype = (field.projectCustomField || {}).field || {}; const cfPrototypeType = (cfPrototype.fieldType || {}).valueType; return values. map(value => formatAsIssueFieldValue(value, cfPrototypeType)). join(', '); } return noEmptyText ? '' : (field.projectCustomField || {}).emptyFieldText; }; const getColorsIds = field => { const values = field && field.value && ( Array.isArray(field.value) ? field.value : [field.value] ); return (values || []).map( val => val.color && val.color.id ).filter(val => !!val); }; const getFieldsValuesToDisplay = (fields, priorityField) => { const shownFieldsCount = 3; const values = (fields || []). filter(field => (!priorityField || field.id !== priorityField.id)). map(field => toFieldValue(field)). filter(it => !!it); return values.splice(0, Math.min(values.length, shownFieldsCount)); }; function toFieldValue(field, noEmptyText) { const presentation = field && getCustomFieldTextPresentation(field, noEmptyText); return presentation ? {id: field.id, colorsIds: getColorsIds(field), presentation} : undefined; } // eslint-disable-next-line complexity const IssueLink = ( {homeUrl, issue, fetchYouTrack, ...restProps} ) => { const [isLoading, setIsLoading] = useState( !issue.fields || !issue.fields.length ); const [priorityFieldValue, setPriorityFieldValue] = useState( toFieldValue(getPriorityIssueField(issue.fields), true) ); const [fieldsValuesToDisplay, setFieldsValuesToDisplay] = useState( getFieldsValuesToDisplay(issue.fields, priorityFieldValue) ); useEffect(() => { let subscribed = true; (async function load() { if (isLoading) { const loadedIssue = await ( loadIssue(fetchYouTrack, issue.id).catch(() => issue) ); const newFields = loadedIssue.fields; const newPriorityField = getPriorityIssueField(newFields); const newFieldsValuesToDisplay = getFieldsValuesToDisplay(newFields, newPriorityField); if (subscribed) { setIsLoading(false); setPriorityFieldValue(toFieldValue(newPriorityField, true)); setFieldsValuesToDisplay(newFieldsValuesToDisplay); } } }()); return () => { subscribed = false; }; }, [issue, fetchYouTrack, isLoading]); const priority = priorityFieldValue && ( <span className={classNames( 'yt-issue-preview__priority', `color-fields__field-${priorityFieldValue.colorsIds[0] || 'none'}` )} > { (priorityFieldValue.presentation || '')[0] } </span> ); const tooltip = ( <div className="yt-issue-preview"> { priority && <span className="yt-issue-preview__priority-wrapper"> {priority} </span> } <div className="yt-issue-preview__content"> <div className="yt-issue-preview__header"> <div> <Link href={`${homeUrl}issue/${issue.idReadable}`} target="_blank" className={classNames({ 'yt-issue-preview__id': true, 'yt-issue-preview__id_resolved': issue.resolved })} > {issue.idReadable} </Link> <span className="yt-issue-preview__summary"> {issue.summary} </span> </div> </div> <div className="yt-issue-preview__fields"> { isLoading && <center> <LoaderInline/> </center> } { (fieldsValuesToDisplay || []). map(({id, presentation, colorsIds}) => ( <span key={`field-value-${id}`} title={presentation} className={classNames( 'yt-issue-preview__field', colorsIds.length === 1 && `color-fields__plain-color-${colorsIds[0]}` )} > {presentation} <span className="yt-issue-preview__field-marker" > { colorsIds.map(colorId => ( <span key={`field-value-${id}-color-${colorId}`} className={classNames( 'yt-issue-preview__field-sample', `color-fields__field-${colorId}` )} /> )) } </span> </span> )) } </div> </div> </div> ); return ( <Tooltip title={tooltip} popupProps={{sidePadding: 24}} > <Link href={`${homeUrl}issue/${issue.idReadable}`} target="_blank" {...restProps} > {issue.idReadable} </Link> </Tooltip> ); }; IssueLink.propTypes = { issue: PropTypes.object.isRequired, homeUrl: PropTypes.string, fetchYouTrack: PropTypes.func.isRequired }; export default IssueLink;