diff --git a/client/components/Coverages/CoverageHistory.tsx b/client/components/Coverages/CoverageHistory.tsx index ad33987c5..0504b0b62 100644 --- a/client/components/Coverages/CoverageHistory.tsx +++ b/client/components/Coverages/CoverageHistory.tsx @@ -9,7 +9,7 @@ import {stringUtils, historyUtils, getDateTimeString, getItemInArrayById, gettex import {Item, Column, Row, Border} from '../UI/List'; import {ContentBlock} from '../UI/SidePanel'; import {CollapseBox} from '../UI'; -import {CoverageIcon} from './index'; +import {CoverageIcons} from './CoverageIcons'; export class CoverageHistory extends React.Component { getHistoryActionElement(historyItem) { @@ -143,8 +143,8 @@ export class CoverageHistory extends React.Component { - ; - users: Array; - desks: Array; - contentTypes: Array; - contacts: Dictionary; - tooltipDirection?: 'top' | 'right' | 'bottom' | 'left'; // defaults to 'right' - iconWrapper?(children: React.ReactNode): React.ReactNode; -} - -export class CoverageIcon extends React.PureComponent { - render() { - const {gettext} = superdeskApi.localization; - const language = this.props.coverage.planning?.language ?? getUserInterfaceLanguageFromCV(); - const user = this.props.users.find( - (u) => u._id === this.props.coverage.assigned_to?.user, - ); - const desk = this.props.desks.find( - (d) => d._id === this.props.coverage.assigned_to?.desk, - ); - const dateFormat = appConfig.planning.dateformat; - const timeFormat = appConfig.planning.timeformat; - let provider = this.props.coverage.assigned_to?.coverage_provider?.name; - const contactId = this.props.coverage.assigned_to?.contact; - - if (contactId != null && this.props.contacts?.[contactId] != null) { - const contact = this.props.contacts[contactId]; - - provider = contact.first_name ? - `${contact.last_name}, ${contact.first_name}` : - contact.organisation; - } - - const assignmentStr = desk ? - gettext('Desk: {{ desk }}', {desk: desk.name}) : - gettext('Status: Unassigned'); - let scheduledStr = this.props.coverage.planning?.scheduled != null && dateFormat && timeFormat ? - moment(this.props.coverage.planning.scheduled).format(dateFormat + ' ' + timeFormat) : - null; - - if (this.props.coverage._time_to_be_confirmed) { - scheduledStr = moment(this.props.coverage.planning.scheduled) - .format(dateFormat + ` @ ${gettext('TBC')}`); - } - const state = getItemWorkflowStateLabel(this.props.coverage.assigned_to); - const genre = getVocabularyItemFieldTranslated( - this.props.coverage.planning?.genre, - 'name', - language - ); - const slugline = this.props.coverage.planning?.slugline ?? ''; - const contentType = getVocabularyItemFieldTranslated( - this.props.contentTypes.find( - (type) => type.qcode === this.props.coverage.planning?.g2_content_type - ), - 'name', - language - ); - const icons = ( - - - - - ); - const ContentWrapper = this.props.iconWrapper != null ? - this.props.iconWrapper : - () => icons; - - return ( - - {!contentType?.length ? null : ( - - {gettext('Type: {{ type }}', {type: contentType})}
-
- )} - {!desk ? null : ( - - {gettext('Status: {{ state }}', {state: state.label})}
-
- )} - {assignmentStr} - {!user ? null : ( - -
{gettext('User: {{ user }}', {user: user.display_name})} -
- )} - {!provider ? null : ( - -
{gettext('Provider: {{ provider }}', {provider: provider})} -
- )} - {!genre ? null : ( - -
{gettext('Genre: {{ genre }}', {genre: genre})} -
- )} - {!slugline ? null : ( - -
{gettext('Slugline: {{ slugline }}', {slugline: slugline})} -
- )} - {!scheduledStr ? null : ( - -
{gettext('Due: {{ date }}', {date: scheduledStr})} -
- )} - {(this.props.coverage.scheduled_updates ?? []).map((s) => { - if (s.planning?.scheduled != null) { - scheduledStr = dateFormat && timeFormat ? - moment(s.planning.scheduled).format(dateFormat + ' ' + timeFormat) : - null; - return ( - -
{gettext('Update Due: {{ date }}', {date: scheduledStr})} -
- ); - } - - return null; - })} - - )} - > - {ContentWrapper(icons)} -
- ); - } -} diff --git a/client/components/Coverages/CoverageIcons.tsx b/client/components/Coverages/CoverageIcons.tsx new file mode 100644 index 000000000..4662d6325 --- /dev/null +++ b/client/components/Coverages/CoverageIcons.tsx @@ -0,0 +1,275 @@ +import * as React from 'react'; +import moment from 'moment-timezone'; +import {getUserInitials} from './../../components/UserAvatar'; +import * as config from 'appConfig'; +import {IPlanningCoverageItem, IG2ContentType, IContactItem, IPlanningConfig} from '../../interfaces'; +import {IUser, IDesk} from 'superdesk-api'; +import {superdeskApi} from '../../superdeskApi'; +import { + AvatarGroup, + ContentDivider, + Icon, + WithPopover, + Avatar, + AvatarPlaceholder, + Spacer, +} from 'superdesk-ui-framework/react'; +import {IPropsAvatarPlaceholder} from 'superdesk-ui-framework/react/components/avatar/avatar-placeholder'; +import {IPropsAvatar} from 'superdesk-ui-framework/react/components/avatar/avatar'; +import {trimStartExact} from 'superdesk-core/scripts/core/helpers/utils'; +import {getItemWorkflowStateLabel, planningUtils} from '../../utils'; +import {getVocabularyItemFieldTranslated} from '../../utils/vocabularies'; +import {getUserInterfaceLanguageFromCV} from '../../utils/users'; +import './coverage-icons.scss'; +import classNames from 'classnames'; +import {noop} from 'lodash'; + +interface IProps { + coverages: Array>; + users: Array; + desks: Array; + contentTypes: Array; + contacts?: Dictionary; + tooltipDirection?: 'top' | 'right' | 'bottom' | 'left'; // defaults to 'right' + iconWrapper?(children: React.ReactNode): React.ReactNode; +} + +const appConfig = config.appConfig as IPlanningConfig; + +export function isAvatarPlaceholder( + item: Omit | Omit +): item is Omit { + return (item as any)['kind'] != null; +} + +export function getAvatarForCoverage( + coverage: DeepPartial, + users: Array, + contentTypes: Array, + noIcon: boolean = false, +): Omit | Omit { + const user = users.find((u) => u._id === coverage.assigned_to?.user); + + const icon: {name: string; color: string} | undefined = + noIcon === true || coverage.planning?.g2_content_type == null ? undefined : { + name: trimStartExact( + planningUtils.getCoverageIcon( + planningUtils.getCoverageContentType( + coverage, + contentTypes, + ) || coverage.planning?.g2_content_type, + coverage, + ), + 'icon-', + ), + color: planningUtils.getCoverageIconColor(coverage), + }; + + if (user == null) { + const placeholder: Omit = { + kind: 'plus-button', + icon: icon, + }; + + return placeholder; + } else { + const avatar: Omit = { + initials: getUserInitials(user.display_name), + imageUrl: user.picture_url, + displayName: user.display_name, + icon: icon, + }; + + return avatar; + } +} + +export class CoverageIcons extends React.PureComponent { + render() { + const {coverages, users} = this.props; + const {gettext} = superdeskApi.localization; + + return ( + ( +
+ + {this.props.coverages.map((coverage, i) => { + const language = coverage.planning?.language ?? getUserInterfaceLanguageFromCV(); + const desk = this.props.desks.find( + (d) => d._id === coverage.assigned_to?.desk, + ); + const dateFormat = appConfig.planning.dateformat; + const timeFormat = appConfig.planning.timeformat; + + const assignmentStr = desk ? + gettext('Desk: {{ desk }}', {desk: desk.name}) : + gettext('Status: Unassigned'); + let scheduledStr = coverage.planning?.scheduled != null && dateFormat && timeFormat ? + moment(coverage.planning.scheduled).format(dateFormat + ' ' + timeFormat) : + null; + + if (coverage._time_to_be_confirmed) { + scheduledStr = moment(coverage.planning.scheduled) + .format(dateFormat + ` @ ${gettext('TBC')}`); + } + const slugline = coverage.planning?.slugline ?? ''; + const contentType = getVocabularyItemFieldTranslated( + this.props.contentTypes.find( + (type) => type.qcode === coverage.planning?.g2_content_type + ), + 'name', + language + ); + + const maybeAvatar = getAvatarForCoverage( + coverage, + users, + this.props.contentTypes, + true, + ); + const state = getItemWorkflowStateLabel(coverage.assigned_to); + + const iconTooltipInfo: Array = [ + gettext('Type: {{ type }}', {type: contentType}), + ]; + + if (desk != null) { + iconTooltipInfo.push(gettext('Status: {{ state }}', {state: state.label})); + } + + return ( + + +
+ + + +
+ +
+
+ + {gettext('Due:')} + + {scheduledStr} + + +
+ + {(coverage.scheduled_updates ?? []).map((s) => { + if (s.planning?.scheduled != null) { + const scheduledStr2 = dateFormat && timeFormat ? + moment(s.planning.scheduled) + .format(dateFormat + ' ' + timeFormat) : + null; + + return ( +
+ + {gettext('Update Due:')} + + {scheduledStr2} + + +
+ ); + } + + return null; + })} + + +
{assignmentStr}
+ +
+ +
+ + {!slugline ? null : ( +
+ + {slugline} + +
+ )} +
+
+
+ +
+ { + isAvatarPlaceholder(maybeAvatar) + ? ( + + ) + : ( + + ) + } +
+
+ ); + })} +
+
+ )} + > + {(onToggle) => ( +
{ + event.stopPropagation(); + onToggle(event.target as HTMLElement); + }} + > + getAvatarForCoverage(coverage, users, this.props.contentTypes), + )} + onClick={noop} // can move code from onClick, because event is not available + /> +
+ )} +
+ ); + } +} diff --git a/client/components/Coverages/CoverageItem.tsx b/client/components/Coverages/CoverageItem.tsx index f8aace117..dfd12729d 100644 --- a/client/components/Coverages/CoverageItem.tsx +++ b/client/components/Coverages/CoverageItem.tsx @@ -19,7 +19,7 @@ import {getUserInterfaceLanguageFromCV} from '../../utils/users'; import {Item, Column, Row, Border, ActionMenu} from '../UI/List'; import {StateLabel, InternalNoteLabel} from '../../components'; -import {CoverageIcon} from './CoverageIcon'; +import {CoverageIcons} from './CoverageIcons'; import {UserAvatar} from '../UserAvatar'; interface IProps { @@ -187,8 +187,8 @@ export class CoverageItemComponent extends React.Component { renderFirstRow() { return ( - ; @@ -66,34 +68,37 @@ class CoveragesBookmarkComponent extends React.Component { } renderForPanel() { - return (this.props.item?.coverages ?? []).map((coverage) => ( - ( - - )} - /> - )); + return (this.props.item?.coverages ?? []).map((coverage) => { + const {users} = this.props; + const maybeAvatar = getAvatarForCoverage(coverage, users, this.props.contentTypes); + + return ( + + ); + }); } renderForPopup() { @@ -150,9 +155,9 @@ class CoveragesBookmarkComponent extends React.Component { return null; } - return this.props.editorType === EDITOR_TYPE.POPUP ? - this.renderForPopup() : - this.renderForPanel(); + return this.props.editorType === EDITOR_TYPE.POPUP + ? this.renderForPopup() + : this.renderForPanel(); } } diff --git a/client/components/Planning/PlanningDateTime.tsx b/client/components/Planning/PlanningDateTime.tsx index 2985c8576..204514943 100644 --- a/client/components/Planning/PlanningDateTime.tsx +++ b/client/components/Planning/PlanningDateTime.tsx @@ -2,10 +2,9 @@ import React from 'react'; import PropTypes from 'prop-types'; import {get} from 'lodash'; import moment from 'moment'; - import {planningUtils} from '../../utils/index'; import {MAIN} from '../../constants'; -import {CoverageIcon} from '../Coverages/'; +import {CoverageIcons} from '../Coverages/CoverageIcons'; export const PlanningDateTime = ({ item, @@ -46,22 +45,12 @@ export const PlanningDateTime = ({ }); return ( - - {coverageToDisplay.map((coverage, i) => ( - - ) - )} - + ); }; diff --git a/client/components/Planning/PlanningItem.tsx b/client/components/Planning/PlanningItem.tsx index fac214669..1f78be4be 100644 --- a/client/components/Planning/PlanningItem.tsx +++ b/client/components/Planning/PlanningItem.tsx @@ -245,7 +245,7 @@ class PlanningItemComponent extends React.Component { )} - + {/** overflow is needed for coverage icons */} {isExpired && (