From 08b2dd7d6cd1f2cdfdd36a957eeb237ab4be6621 Mon Sep 17 00:00:00 2001 From: Mark Pittaway Date: Thu, 14 Mar 2024 17:11:38 +1100 Subject: [PATCH] [SDBELGA-778] fix: Show recurring Planning update method on Ignore|Cancel|Save modal Also improve look and feel when rendering Event & Planning metadata in the modal --- client/actions/events/api.ts | 5 +- client/actions/events/ui.ts | 1 + client/actions/main.ts | 8 +- client/components/ConfirmationModal.tsx | 54 ++-- .../Events/EventScheduleSummary/index.tsx | 43 ++- client/components/IgnoreCancelSaveModal.tsx | 190 ++++++----- .../forms/updateRecurringEventsForm.tsx | 306 ++++++++++-------- client/components/Main/ItemEditor/Editor.tsx | 12 +- .../components/Main/ItemEditor/ItemManager.ts | 19 +- .../Main/ItemEditor/tests/ItemManager_test.ts | 2 + .../RelatedPlanningListItem.tsx | 149 +++++---- client/components/fields/index.tsx | 4 +- .../fields/preview/EventSchedule.tsx | 9 +- .../preview/base/PreviewSimpleListItem.tsx | 32 +- client/components/fields/preview/index.ts | 7 + .../tests/IgnoreCancelSaveModal_test.tsx | 47 ++- client/selectors/general.ts | 17 + client/utils/events.ts | 10 +- 18 files changed, 546 insertions(+), 369 deletions(-) diff --git a/client/actions/events/api.ts b/client/actions/events/api.ts index c0c51cdfa..1f504bb4e 100644 --- a/client/actions/events/api.ts +++ b/client/actions/events/api.ts @@ -565,8 +565,9 @@ const save = (original, updates) => ( ) { delete eventUpdates.dates; } - eventUpdates.update_method = get(eventUpdates, 'update_method.value') || - EVENTS.UPDATE_METHODS[0].value; + eventUpdates.update_method = eventUpdates.update_method == null ? + EVENTS.UPDATE_METHODS[0].value : + eventUpdates.update_method?.value ?? eventUpdates.update_method; return originalEvent?._id != null ? planningApi.events.update(originalItem, eventUpdates) : diff --git a/client/actions/events/ui.ts b/client/actions/events/ui.ts index fc0003ce0..1bf43fea4 100644 --- a/client/actions/events/ui.ts +++ b/client/actions/events/ui.ts @@ -928,6 +928,7 @@ const save = (original, updates, confirmation, unlockOnClose) => ( { actionType: 'save', unlockOnClose: unlockOnClose, + large: true, } )); } diff --git a/client/actions/main.ts b/client/actions/main.ts index 302aee95f..9f9f18154 100644 --- a/client/actions/main.ts +++ b/client/actions/main.ts @@ -703,11 +703,7 @@ const openIgnoreCancelSaveModal = ({ const storedItems = itemType === ITEM_TYPE.EVENT ? selectors.events.storedEvents(getState()) : selectors.planning.storedPlannings(getState()); - - const item = { - ...get(storedItems, itemId) || {}, - ...autosaveData, - }; + const item = get(storedItems, itemId) || {}; if (!isExistingItem(item)) { delete item._id; @@ -736,7 +732,7 @@ const openIgnoreCancelSaveModal = ({ modalType: MODALS.IGNORE_CANCEL_SAVE, modalProps: { item: itemWithAssociatedData, - itemType: itemType, + updates: autosaveData, onCancel: onCancel, onIgnore: onIgnore, onSave: onSave, diff --git a/client/components/ConfirmationModal.tsx b/client/components/ConfirmationModal.tsx index 5c07b5763..b6485123a 100644 --- a/client/components/ConfirmationModal.tsx +++ b/client/components/ConfirmationModal.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {gettext} from '../utils'; @@ -7,7 +6,30 @@ import {Modal} from './index'; import {ButtonList, Icon} from './UI'; import {KEYCODES} from '../constants'; -export class ConfirmationModal extends React.Component { +interface IProps { + handleHide(itemType?: string): void; + modalProps: { + onCancel?(): void; + cancelText?: string; + ignore?(): void; + showIgnore?: boolean; + ignoreText?: string; + okText?: string; + action?(): void; + title?: string; + body: React.ReactNode; + itemType?: string; + autoClose?: boolean; + large?: boolean; + bodyClassname?: string; + }; +} + +interface IState { + submitting: boolean; +} + +export class ConfirmationModal extends React.Component { constructor(props) { super(props); @@ -96,14 +118,18 @@ export class ConfirmationModal extends React.Component { } return ( - +

{modalProps.title || gettext('Confirmation')}

- +
{modalProps.body || gettext('Are you sure ?')}
@@ -115,23 +141,3 @@ export class ConfirmationModal extends React.Component { ); } } - -ConfirmationModal.propTypes = { - handleHide: PropTypes.func.isRequired, - modalProps: PropTypes.shape({ - onCancel: PropTypes.func, - cancelText: PropTypes.string, - ignore: PropTypes.func, - showIgnore: PropTypes.bool, - ignoreText: PropTypes.string, - okText: PropTypes.string, - action: PropTypes.func, - title: PropTypes.string, - body: PropTypes.oneOfType([ - PropTypes.string, - PropTypes.element, - ]), - itemType: PropTypes.string, - autoClose: PropTypes.bool, - }), -}; diff --git a/client/components/Events/EventScheduleSummary/index.tsx b/client/components/Events/EventScheduleSummary/index.tsx index eb8e6ee59..a4fb349f6 100644 --- a/client/components/Events/EventScheduleSummary/index.tsx +++ b/client/components/Events/EventScheduleSummary/index.tsx @@ -1,24 +1,32 @@ import React from 'react'; -import PropTypes from 'prop-types'; import {get} from 'lodash'; +import {IEventItem} from 'interfaces'; +import {gettext, eventUtils, timeUtils} from '../../../utils'; + +import {FormLabel, Text, ContentDivider} from 'superdesk-ui-framework/react'; import {RepeatEventSummary} from '../RepeatEventSummary'; import {Row} from '../../UI/Preview'; -import {gettext, eventUtils, timeUtils} from '../../../utils'; + import './style.scss'; -import {IEventItem} from 'interfaces'; + + interface IProps { event: Partial, noPadding?: boolean, forUpdating?: boolean, useEventTimezone?: boolean + useFormLabelAndText?: boolean + addContentDivider?: boolean } export const EventScheduleSummary = ({ event, noPadding = false, forUpdating = false, - useEventTimezone = false + useEventTimezone = false, + useFormLabelAndText = false, + addContentDivider = false, }: IProps) => { if (!event) { return null; @@ -74,7 +82,32 @@ export const EventScheduleSummary = ({ currentDateLabel = gettext('Current Date (Based on Event timezone)'); } - return ( + return useFormLabelAndText ? ( + +
+ + + {currentDateText || ''} + +
+ {addContentDivider !== true ? null : ( + + )} + {doesRepeat !== true ? null : ( + +
+ + + {eventUtils.getRepeatSummaryForEvent(eventSchedule)} + +
+ {addContentDivider !== true ? null : ( + + )} +
+ )} +
+ ) : ( { + handleHide(itemType: IEventOrPlanningItem['_id']): void; + currentEditId: T['_id']; + modalProps: { + item: T; + updates: Partial; + onCancel(): void; + onIgnore(): void; + onSave( + withConfirmation: boolean, + updateMethod: string, + planningUpdateMethods: {[planningId: string]: IEventUpdateMethod} + ): void; + onGoTo(): void; + onSaveAndPost( + withConfirmation: boolean, + updateMethod: string, + planningUpdateMethods: {[planningId: string]: IEventUpdateMethod} + ): void; + title: string; + autoClose?: boolean; + bodyText?: string; + showIgnore?: boolean; + }; +} + +type IProps = IBaseProps | IBaseProps; -export class IgnoreCancelSaveModalComponent extends React.Component { - constructor(props) { +interface IState { + eventUpdateMethod: IEventUpdateMethod; + planningUpdateMethods: {[planningId: string]: IEventUpdateMethod}; +} + +export class IgnoreCancelSaveModalComponent extends React.Component { + constructor(props: IProps) { super(props); - this.state = {eventUpdateMethod: EVENTS.UPDATE_METHODS[0]}; + this.state = { + eventUpdateMethod: EVENTS.UPDATE_METHODS[0].value, + planningUpdateMethods: {}, + }; this.onEventUpdateMethodChange = this.onEventUpdateMethodChange.bind(this); + this.onPlanningUpdateMethodChange = this.onPlanningUpdateMethodChange.bind(this); this.onSubmit = this.onSubmit.bind(this); } - onEventUpdateMethodChange(field, option) { + onEventUpdateMethodChange(option: IEventUpdateMethod) { this.setState({eventUpdateMethod: option}); } - renderEvent() { - const {modalProps} = this.props; - - const { - item, - onSave, - } = modalProps || {}; - const {submitting} = this.state; - - const isRecurringEvent = eventUtils.isEventRecurring(item); - - return ( -
- - - - - - - {onSave && ( - - )} -
- ); + onPlanningUpdateMethodChange(planningId: IPlanningItem['_id'], updateMethod: IEventUpdateMethod) { + this.setState((prevState) => ({ + planningUpdateMethods: { + ...prevState.planningUpdateMethods, + [planningId]: updateMethod, + }, + })); } - renderPlanning() { - const {item} = get(this.props, 'modalProps') || {}; - - return ( -
- + +
+ ); + } else if (this.props.modalProps.item.type === 'event') { + return ( + { + this.props.handleHide(this.props.modalProps.item.type); + }, + unlockOnClose: false, + }} /> - - ); + ); + } + + return null; } onSubmit() { @@ -94,13 +110,15 @@ export class IgnoreCancelSaveModalComponent extends React.Component { } else if (onSaveAndPost) { return onSaveAndPost( false, - this.state.eventUpdateMethod + this.state.eventUpdateMethod, + this.state.planningUpdateMethods, ); } return onSave( false, - this.state.eventUpdateMethod + this.state.eventUpdateMethod, + this.state.planningUpdateMethods, ); } @@ -128,22 +146,9 @@ export class IgnoreCancelSaveModalComponent extends React.Component { return okText; } - getRenderItem(itemType) { - switch (itemType) { - case ITEM_TYPE.EVENT: - return this.renderEvent(); - - case ITEM_TYPE.PLANNING: - return this.renderPlanning(); - } - - return null; - } - render() { const {handleHide, modalProps} = this.props; const { - itemType, title, onIgnore, onCancel, @@ -163,37 +168,22 @@ export class IgnoreCancelSaveModalComponent extends React.Component { modalProps={{ onCancel: onCancel, cancelText: gettext('Cancel'), - showIgnore: isNil(showIgnore) ? true : showIgnore, + showIgnore: showIgnore !== true, ignore: onIgnore, ignoreText: gettext('Ignore'), action: (onGoTo || onSave || onSaveAndPost) ? this.onSubmit : null, okText: okText, title: title || gettext('Save Changes?'), - body: bodyText || this.getRenderItem(itemType), + body: bodyText || this.renderItemDetails(), autoClose: autoClose, + large: true, + bodyClassname: 'p-3', }} /> ); } } -IgnoreCancelSaveModalComponent.propTypes = { - handleHide: PropTypes.func.isRequired, - modalProps: PropTypes.shape({ - item: PropTypes.object, - itemType: PropTypes.string, - onCancel: PropTypes.func, - onIgnore: PropTypes.func, - onSave: PropTypes.func, - onGoTo: PropTypes.func, - onSaveAndPost: PropTypes.func, - title: PropTypes.string, - autoClose: PropTypes.bool, - bodyText: PropTypes.string, - }), - currentEditId: PropTypes.string, -}; - const mapStateToProps = (state) => ({ currentEditId: selectors.forms.currentItemId(state), }); diff --git a/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx b/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx index f26febe90..9284b4193 100644 --- a/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx +++ b/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx @@ -2,33 +2,42 @@ import React from 'react'; import {connect} from 'react-redux'; import {cloneDeep, isEqual} from 'lodash'; -import {IEventItem, IPlanningItem, IEventUpdateMethod, IEmbeddedCoverageItem} from '../../../interfaces'; +import { + IEmbeddedCoverageItem, + IEventFormProfile, + IEventItem, + IEventUpdateMethod, + IPlanningItem, + PREVIEW_PANEL, +} from '../../../interfaces'; import {planningApi} from '../../../superdeskApi'; import * as actions from '../../../actions'; -import {UpdateMethodSelection} from '../UpdateMethodSelection'; import {EVENTS, TEMP_ID_PREFIX} from '../../../constants'; -import {EventScheduleSummary} from '../../Events'; import {eventUtils, gettext} from '../../../utils'; -import {onItemActionModalHide, IModalProps} from './utils'; +import {IModalProps, onItemActionModalHide} from './utils'; import {storedPlannings} from '../../../selectors/planning'; +import {eventProfile} from '../../../selectors/forms'; -import {Row} from '../../UI/Preview'; -import {Select, Option} from 'superdesk-ui-framework/react'; +import {ContentDivider, Heading, Option, Select, Text, FormLabel} from 'superdesk-ui-framework/react'; import {PlanningMetaData} from '../../RelatedPlannings/PlanningMetaData'; +import {previewGroupToProfile, renderGroupedFieldsForPanel} from '../../fields'; +import {getUserInterfaceLanguageFromCV} from '../../../utils/users'; import '../style.scss'; interface IOwnProps { original: IEventItem; updates: Partial; - submitting: boolean; modalProps: IModalProps; - enableSaveInModal(): void; - resolve(item?: IEventItem): void; + enableSaveInModal?(): void; + resolve?(item?: IEventItem): void; + onEventUpdateMethodChange?(option: IEventUpdateMethod): void; + onPlanningUpdateMethodChange?(planningId: IPlanningItem['_id'], updateMethod: IEventUpdateMethod): void; } interface IStateProps { originalPlanningItems: {[planningId: string]: IPlanningItem}; + eventProfile: IEventFormProfile; } interface IDispatchProps { @@ -39,13 +48,8 @@ interface IDispatchProps { type IProps = IOwnProps & IStateProps & IDispatchProps; type IPlanningEmbeddedCoverageMap = {[planningId: string]: {[coverageId: string]: IEmbeddedCoverageItem}}; -interface IRecurringItemUpdateMethodOption { - name: string; - value: IEventUpdateMethod; -} - interface IState { - eventUpdateMethod: IRecurringItemUpdateMethodOption; + eventUpdateMethod: IEventUpdateMethod; relatedEvents: Array; relatedPlannings: Array; posting: boolean; @@ -60,7 +64,7 @@ function eventWasUpdated(original: IEventItem, updates: Partial): bo const originalItem = eventUtils.modifyForServer(cloneDeep(original)); const eventUpdates = eventUtils.getEventDiff(originalItem, updates); const eventFields = Object.keys(eventUpdates).filter( - (field) => !['update_method', 'dates'].includes(field) + (field) => !['update_method', 'dates', 'associated_plannings'].includes(field) ); return eventFields.length > 0; @@ -134,12 +138,12 @@ export class UpdateRecurringEventsComponent extends React.Component 0) { - return this.props.updates.associated_plannings - .filter((planningItem) => ( - this.state.recurringPlanningItemsToCreate.includes(planningItem._id) - )) - .map((planningItem) => ( -
- - -
- )); + renderModifiedPlanningItems() { + const planningsToCreate = (this.props.updates.associated_plannings || []) + .filter((planningItem) => ( + this.state.recurringPlanningItemsToCreate.includes(planningItem._id) + )); + const planningsToUpdate = (this.props.updates.associated_plannings || []) + .filter((planningItem) => ( + this.state.recurringPlanningItemsToUpdate.includes(planningItem._id) + )); + + if (planningsToCreate.length === 0 && planningsToUpdate.length === 0) { + return null; } - return null; + return ( + + + {gettext('Related Planning(s)')} + + {planningsToCreate.map((item, index) => ( + this.renderPlanningItem(item, false, index === planningsToCreate.length - 1) + ))} + {planningsToCreate.length === 0 || planningsToUpdate.length === 0 ? null : ( + + )} + {planningsToUpdate.map((item, index) => ( + this.renderPlanningItem(item, true, index === planningsToUpdate.length - 1) + ))} + + ); } - renderPlanningUpdateForm() { - if (this.state.recurringPlanningItemsToUpdate.length > 0) { - return this.props.updates.associated_plannings - .filter((planningItem) => ( - this.state.recurringPlanningItemsToUpdate.includes(planningItem._id) - )) - .map((planningItem) => ( -
- - -
- )); - } - - return null; + renderPlanningItem(item: Partial, planningExists: boolean, lastItem: boolean) { + return ( + + + {planningExists === true ? ( + + + {gettext('You made changes to this planning item that is part of a recurring event.')} + +  {gettext('Apply the changes to all recurring planning items or just this one?')} + + ) : ( + + + {gettext('You are creating a new planning item.')} + +  {gettext('Add this item to all recurring events or just this one?')} + + )} + + + + {lastItem === true ? null : ( + + )} + + ); } - onPlanningUpdateMethodChange(planningId: string, updateMethod: IEventUpdateMethod) { + onPlanningUpdateMethodChange(planningId: IPlanningItem['_id'], updateMethod: IEventUpdateMethod) { this.setState((prevState) => ({ planningUpdateMethods: { ...prevState.planningUpdateMethods, [planningId]: updateMethod, }, })); + if (this.props.onPlanningUpdateMethodChange != null) { + this.props.onPlanningUpdateMethodChange(planningId, updateMethod); + } } render() { - const {original, submitting} = this.props; + const {original} = this.props; const isRecurring = !!original.recurrence_id; const eventsInUse = this.state.relatedEvents.filter((e) => ( (e.planning_ids?.length ?? 0) > 0 || e.pubstatus != null @@ -276,50 +313,64 @@ export class UpdateRecurringEventsComponent extends React.Component +
    + {renderGroupedFieldsForPanel( + 'simple-preview', + previewGroupToProfile(PREVIEW_PANEL.EVENT, this.props.eventProfile, false), + { + item: { + ...this.props.original, + ...this.props.updates, + }, + language: this.props.updates.language ?? + this.props.original.language ?? + getUserInterfaceLanguageFromCV(), + useFormLabelAndText: true, + schema: this.props.eventProfile.schema, + profile: this.props.eventProfile, + addContentDivider: true, + }, + {}, + )} + {this.state.eventModified === false || isRecurring !== true ? null : ( + +
    + + + {numEvents} + +
    + +
    + )} +
+ {this.state.eventModified === false ? null : ( -
- - - - - - - - - + + {gettext('This is a recurring event.')} + {gettext('Update all recurring events or just this one?')} + + + )} - {this.renderPlanningCreateForm()} - {this.renderPlanningUpdateForm()} + {this.renderModifiedPlanningItems()} ); } @@ -327,6 +378,7 @@ export class UpdateRecurringEventsComponent extends React.Component ({ originalPlanningItems: storedPlannings(state), + eventProfile: eventProfile(state), }); const mapDispatchToProps = (dispatch: any, ownProps: IOwnProps) => ({ @@ -337,7 +389,7 @@ const mapDispatchToProps = (dispatch: any, ownProps: IOwnProps) => ({ planningApi.locks.unlockItem(savedItem); } - if (ownProps.resolve) { + if (ownProps.resolve != null) { ownProps.resolve(savedItem); } }) diff --git a/client/components/Main/ItemEditor/Editor.tsx b/client/components/Main/ItemEditor/Editor.tsx index 3d5997db0..a125c063e 100644 --- a/client/components/Main/ItemEditor/Editor.tsx +++ b/client/components/Main/ItemEditor/Editor.tsx @@ -225,22 +225,24 @@ export class EditorComponent extends React.Component }; const onSave = (isKilled || hasErrors) ? null : - (withConfirmation, updateMethod) => ( + (withConfirmation, updateMethod, planningUpdateMethods) => ( this.itemManager.save( withConfirmation, - updateMethod, + {name: updateMethod, value: updateMethod}, true, - updateStates + updateStates, + planningUpdateMethods ) ); const onSaveAndPost = (!isKilled || hasErrors) ? null : - (withConfirmation, updateMethod) => ( + (withConfirmation, updateMethod, planningUpdateMethods) => ( this.itemManager.saveAndPost( withConfirmation, updateMethod, true, - updateStates + updateStates, + planningUpdateMethods ) ); diff --git a/client/components/Main/ItemEditor/ItemManager.ts b/client/components/Main/ItemEditor/ItemManager.ts index 195dc6e83..ee91f7178 100644 --- a/client/components/Main/ItemEditor/ItemManager.ts +++ b/client/components/Main/ItemEditor/ItemManager.ts @@ -566,7 +566,8 @@ export class ItemManager { withConfirmation = true, updateMethod = EVENTS.UPDATE_METHODS[0], closeAfter = false, - updateStates = true + updateStates = true, + planningUpdateMethods = {} ) { return this._save({ post: false, @@ -575,6 +576,7 @@ export class ItemManager { updateMethod: updateMethod, closeAfter: closeAfter || this.shouldClose(), updateStates: updateStates, + planningUpdateMethods: planningUpdateMethods, }); } @@ -582,7 +584,8 @@ export class ItemManager { withConfirmation = true, updateMethod = EVENTS.UPDATE_METHODS[0], closeAfter = false, - updateStates = true + updateStates = true, + planningUpdateMethods = {} ) { return this._save({ post: true, @@ -591,6 +594,7 @@ export class ItemManager { updateMethod: updateMethod, closeAfter: closeAfter || this.shouldClose(), updateStates: updateStates, + planningUpdateMethods: planningUpdateMethods, }); } @@ -644,6 +648,7 @@ export class ItemManager { updateMethod = EVENTS.UPDATE_METHODS[0], closeAfter = false, updateStates = true, + planningUpdateMethods = {} } = {}) { if (!isEqual(this.state.errorMessages, [])) { return this.setState({ @@ -684,8 +689,16 @@ export class ItemManager { updates.pubstatus = POST_STATE.CANCELLED; } - if (this.props.itemType === ITEM_TYPE.EVENT) { + if (updates.type === 'event') { updates.update_method = updateMethod; + + if (Object.keys(planningUpdateMethods).length > 0) { + updates.associated_plannings?.forEach((planningItem) => { + if (planningUpdateMethods[planningItem._id] != null) { + planningItem.update_method = planningUpdateMethods[planningItem._id]; + } + }); + } } return promise.then(() => this.autoSave.flushAutosave()) diff --git a/client/components/Main/ItemEditor/tests/ItemManager_test.ts b/client/components/Main/ItemEditor/tests/ItemManager_test.ts index 2b83eba8b..d9d36b7b0 100644 --- a/client/components/Main/ItemEditor/tests/ItemManager_test.ts +++ b/client/components/Main/ItemEditor/tests/ItemManager_test.ts @@ -1391,6 +1391,7 @@ describe('components.Main.ItemManager', () => { updateMethod: EVENTS.UPDATE_METHODS[0], closeAfter: false, updateStates: true, + planningUpdateMethods: {}, }]); }); @@ -1404,6 +1405,7 @@ describe('components.Main.ItemManager', () => { updateMethod: EVENTS.UPDATE_METHODS[0], closeAfter: false, updateStates: true, + planningUpdateMethods: {}, }]); }); diff --git a/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx b/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx index 8a8fd19d4..9379184d0 100644 --- a/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx +++ b/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx @@ -1,21 +1,19 @@ import * as React from 'react'; import {connect} from 'react-redux'; -import {IPlanningItem, IG2ContentType, ILockedItems} from '../../../interfaces'; +import {IPlanningItem, IG2ContentType, ILockedItems, IAgenda} from '../../../interfaces'; import {IDesk, IUser} from 'superdesk-api'; import {superdeskApi} from '../../../superdeskApi'; -import {ICON_COLORS} from '../../../constants'; -import {planningUtils, lockUtils} from '../../../utils'; +import {lockUtils, getItemWorkflowStateLabel} from '../../../utils'; import * as selectors from '../../../selectors'; +import {Label} from 'superdesk-ui-framework/react'; import * as List from '../../UI/List'; -import {ItemIcon} from '../../ItemIcon'; import {AgendaNameList} from '../../Agendas'; -import {StateLabel} from '../../StateLabel'; import {CoverageIcons} from '../../Coverages/CoverageIcons'; -interface IProps { +interface IOwnProps { item: DeepPartial; active?: boolean; noBg?: boolean; @@ -23,21 +21,26 @@ interface IProps { showIcon?: boolean; shadow?: number; editPlanningComponent?: React.ReactNode; + isAgendaEnabled: boolean; onClick?(): void; +} - // Redux Store +interface IStateProps { users: Array; desks: Array; contentTypes: Array; lockedItems: ILockedItems; - isAgendaEnabled: boolean; + agendas: {[agendaId: string]: IAgenda}; } +type IProps = IOwnProps & IStateProps; + const mapStateToProps = (state) => ({ users: selectors.general.users(state), desks: selectors.general.desks(state), contentTypes: selectors.general.contentTypes(state), lockedItems: selectors.locks.getLockedItems(state), + agendas: selectors.general.agendasById(state), }); class RelatedPlanningListItemComponent extends React.PureComponent { @@ -47,74 +50,86 @@ class RelatedPlanningListItemComponent extends React.PureComponent { this.props.item, this.props.lockedItems ); + const stateLabel = getItemWorkflowStateLabel(this.props.item); + const agendas = (this.props.item.agendas ?? []) + .map((agendaId) => this.props.agendas[agendaId]) + .filter((agenda) => agenda != null); + const itemDescription = this.props.item.name || this.props.item.description_text || ''; return ( - - {!(this.props.showBorder && isItemLocked) ? null : ( - - )} -
- {!this.props.showIcon ? null : ( - - - - )} - + - - - {this.props.item.slugline} - - - {this.props.isAgendaEnabled && ( + {!(this.props.showBorder && isItemLocked) ? null : ( + + )} + - - {gettext('Agenda:')} - - + {this.props.showIcon !== true ? null : ( + + )} + {(this.props.item.slugline?.length ?? 0) === 0 ? null : ( + + {this.props.item.slugline} + )} + {itemDescription.length === 0 ? null : ( + + {this.props.item.name || this.props.item.description_text} + + )} + + + + + {this.props.editPlanningComponent == null ? null : ( + + {this.props.editPlanningComponent} + )} - - - - - - - - - - {!this.props.editPlanningComponent ? null : ( - - {this.props.editPlanningComponent} - - )} - + + ); } } -export const RelatedPlanningListItem = connect(mapStateToProps)(RelatedPlanningListItemComponent); +export const RelatedPlanningListItem = connect(mapStateToProps)(RelatedPlanningListItemComponent); diff --git a/client/components/fields/index.tsx b/client/components/fields/index.tsx index 357d9c32b..8446cf3b8 100644 --- a/client/components/fields/index.tsx +++ b/client/components/fields/index.tsx @@ -112,7 +112,7 @@ export function renderFieldsForPanel( profile: ISearchProfile, globalProps: {[key: string]: any}, fieldProps: {[key: string]: any}, - Container?: React.ComponentClass, + Container?: React.ComponentType, groupName?: string, enabledField: string = 'enabled', refs: {[key: string]: React.RefObject} = {}, @@ -182,7 +182,7 @@ export function renderGroupedFieldsForPanel( profile: ISearchProfile, globalProps: {[key: string]: any}, fieldProps: {[key: string]: any}, - Container?: React.ComponentClass, + Container?: React.ComponentType, enabledField: string = 'enabled' ) { const {gettext} = superdeskApi.localization; diff --git a/client/components/fields/preview/EventSchedule.tsx b/client/components/fields/preview/EventSchedule.tsx index ea0931191..2ec544d0f 100644 --- a/client/components/fields/preview/EventSchedule.tsx +++ b/client/components/fields/preview/EventSchedule.tsx @@ -5,7 +5,12 @@ import {TO_BE_CONFIRMED_FIELD} from '../../../constants'; import {EventScheduleSummary} from '../../Events/EventScheduleSummary'; -export class PreviewFieldEventSchedule extends React.PureComponent { +interface IProps extends IListFieldProps { + useFormLabelAndText?: boolean + addContentDivider?: boolean +} + +export class PreviewFieldEventSchedule extends React.PureComponent { render() { const item = this.props.item as IEventItem; @@ -15,6 +20,8 @@ export class PreviewFieldEventSchedule extends React.PureComponent ); } diff --git a/client/components/fields/preview/base/PreviewSimpleListItem.tsx b/client/components/fields/preview/base/PreviewSimpleListItem.tsx index 400df3601..74bcf7580 100644 --- a/client/components/fields/preview/base/PreviewSimpleListItem.tsx +++ b/client/components/fields/preview/base/PreviewSimpleListItem.tsx @@ -1,10 +1,16 @@ import * as React from 'react'; import {IBasePreviewProps} from './PreviewHoc'; import {stringUtils} from '../../../../utils'; +import {FormLabel, Text, ContentDivider} from 'superdesk-ui-framework/react'; -export class PreviewSimpleListItem extends React.PureComponent { +interface IProps extends IBasePreviewProps { + useFormLabelAndText?: boolean; + addContentDivider?: boolean; +} + +export class PreviewSimpleListItem extends React.PureComponent { render() { - if (this.props.value == undefined && !this.props.renderEmpty) { + if ((this.props.value?.length ?? 0) == 0 && this.props.renderEmpty !== true) { return null; } @@ -15,10 +21,24 @@ export class PreviewSimpleListItem extends React.PureComponent - {this.props.label} - {children} - + + {this.props.useFormLabelAndText ? ( +
+ + + {children} + +
+ ) : ( +
  • + {this.props.label} + {children} +
  • + )} + {this.props.addContentDivider !== true ? null : ( + + )} +
    ); } } diff --git a/client/components/fields/preview/index.ts b/client/components/fields/preview/index.ts index 15d3e207f..f152102ba 100644 --- a/client/components/fields/preview/index.ts +++ b/client/components/fields/preview/index.ts @@ -301,9 +301,16 @@ Object.keys(multilingualFieldOptions).forEach((field) => { FIELD_TO_PREVIEW_COMPONENT.filter_schedule = PreviewFieldFilterSchedule; FIELD_TO_FORM_PREVIEW_COMPONENT.dates = PreviewFieldEventSchedule; +FIELD_TO_PREVIEW_COMPONENT.dates = PreviewFieldEventSchedule; + FIELD_TO_FORM_PREVIEW_COMPONENT.location = PreviewFieldLocation; + FIELD_TO_FORM_PREVIEW_COMPONENT.event_contact_info = PreviewFieldContacts; +FIELD_TO_PREVIEW_COMPONENT.event_contact_info = PreviewFieldContacts; + FIELD_TO_FORM_PREVIEW_COMPONENT.custom_vocabularies = PreviewFieldCustomVocabularies; +FIELD_TO_PREVIEW_COMPONENT.custom_vocabularies = PreviewFieldCustomVocabularies; + FIELD_TO_FORM_PREVIEW_COMPONENT.urgency = PreviewFieldUrgency; FIELD_TO_FORM_PREVIEW_COMPONENT.flags = PreviewFieldFlags; diff --git a/client/components/tests/IgnoreCancelSaveModal_test.tsx b/client/components/tests/IgnoreCancelSaveModal_test.tsx index 625559f08..2374ec030 100644 --- a/client/components/tests/IgnoreCancelSaveModal_test.tsx +++ b/client/components/tests/IgnoreCancelSaveModal_test.tsx @@ -1,18 +1,23 @@ import React from 'react'; +import {Provider} from 'react-redux'; import {mount} from 'enzyme'; import sinon from 'sinon'; import {cloneDeep} from 'lodash'; -import {eventUtils, generateTempId} from '../../utils'; +import {createTestStore, eventUtils, generateTempId} from '../../utils'; import * as testData from '../../utils/testData'; import * as helpers from './helpers'; +import {getTestActionStore} from '../../utils/testUtils'; import {IgnoreCancelSaveModalComponent} from '../IgnoreCancelSaveModal'; describe('', () => { + let astore; + let store; let wrapper; let handleHide; let item; + let updates; let itemType; let onCancel; let onIgnore; @@ -24,7 +29,9 @@ describe('', () => { let buttons; beforeEach(() => { + astore = getTestActionStore(); item = eventUtils.modifyForClient(cloneDeep(testData.events[0])); + updates = {}; itemType = item.type; handleHide = sinon.spy(); onCancel = sinon.spy(); @@ -33,26 +40,32 @@ describe('', () => { onGoTo = null; title = 'Test Modal'; autoClose = false; + + astore.init(); + store = createTestStore({initialState: astore.initialState}); }); const setWrapper = () => { wrapper = mount( - + + + ); resetButtons(); diff --git a/client/selectors/general.ts b/client/selectors/general.ts index 3023d705b..44cc867eb 100644 --- a/client/selectors/general.ts +++ b/client/selectors/general.ts @@ -1,5 +1,7 @@ import {get, keyBy} from 'lodash'; import {createSelector} from 'reselect'; + +import {IAgenda, IPlanningAppState} from '../interfaces'; import {getEnabledAgendas, getDisabledAgendas, getItemInArrayById} from '../utils'; import {ITEM_TYPE, COVERAGES, ASSIGNMENTS} from '../constants/index'; @@ -42,6 +44,21 @@ export const enabledAgendas = createSelector( [agendas], (agendas) => getEnabledAgendas(agendas) ); +export const agendasById = createSelector< + IPlanningAppState, + Array, + {[agendaId: string]: IAgenda} +>( + [agendas], + (agendas) => agendas.reduce( + (agendaList, agenda) => { + agendaList[agenda._id] = agenda; + + return agendaList; + }, + {} + ) +); export const disabledAgendas = createSelector( [agendas], diff --git a/client/utils/events.ts b/client/utils/events.ts index 43b52c09f..e49bf4aab 100644 --- a/client/utils/events.ts +++ b/client/utils/events.ts @@ -1357,11 +1357,13 @@ function convertCoverageToEventEmbedded(coverage: IPlanningCoverageItem): IEmbed return { coverage_id: coverage.coverage_id, g2_content_type: coverage.planning.g2_content_type, - desk: coverage.assigned_to.desk, - user: coverage.assigned_to.user, + desk: coverage.assigned_to?.desk, + user: coverage.assigned_to?.user, language: coverage.planning.language, - news_coverage_status: coverage.news_coverage_status.qcode, - scheduled: coverage.planning.scheduled, + news_coverage_status: coverage.news_coverage_status?.qcode ?? 'ncostat:int', + scheduled: coverage.planning.scheduled != null ? + moment(coverage.planning.scheduled).toDate() : + undefined, genre: coverage.planning.genre?.qcode, slugline: coverage.planning.slugline, headline: coverage.planning.headline,