diff --git a/client/actions/agenda.ts b/client/actions/agenda.ts index e2157511b..8b10fa4a8 100644 --- a/client/actions/agenda.ts +++ b/client/actions/agenda.ts @@ -2,7 +2,7 @@ import * as selectors from '../selectors'; import {cloneDeep, pick, get, sortBy, findIndex} from 'lodash'; import {Moment} from 'moment'; -import {IEventItem, IPlanningItem, IAgenda} from '../interfaces'; +import {IEventItem, IPlanningItem, IAgenda, IPlanningRelatedEventLink} from '../interfaces'; import {planningApi} from '../superdeskApi'; import {AGENDA, MODALS, EVENTS} from '../constants'; @@ -243,11 +243,19 @@ const addEventToCurrentAgenda = ( export function convertEventToPlanningItem(event: IEventItem): Partial { const defaultPlace = selectors.general.defaultPlaceList(planningApi.redux.store.getState()); const defaultValues = planningUtils.defaultPlanningValues(null, defaultPlace); + const eventLink: IPlanningRelatedEventLink = { + _id: event._id, + link_type: 'primary', + }; + + if (event.recurrence_id != null) { + eventLink.recurrence_id = event.recurrence_id; + } let newPlanningItem: Partial = { ...defaultValues, type: 'planning', - event_item: event._id, + related_events: [eventLink], planning_date: event._sortDate || event.dates?.start, place: event.place || defaultPlace, subject: event.subject, diff --git a/client/actions/events/api.ts b/client/actions/events/api.ts index 86a17af1a..0249c3717 100644 --- a/client/actions/events/api.ts +++ b/client/actions/events/api.ts @@ -26,6 +26,7 @@ import planningApis from '../planning/api'; import eventsUi from './ui'; import main from '../main'; import {eventParamsToSearchParams} from '../../utils/search'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; /** * Action dispatcher to load a series of recurring events into the local store. @@ -213,7 +214,7 @@ function loadEventDataForAction( _relatedPlannings: loadEveryRecurringPlanning ? items.plannings : items.plannings.filter( - (item) => item.event_item === event._id + (item) => getRelatedEventIdsForPlanning(item, 'primary').includes(event._id) ), })); } @@ -482,7 +483,7 @@ function markEventPostponed(event: IEventItem, reason: string, actionedDate: str const markEventHasPlannings = (event, planning) => ({ type: EVENTS.ACTIONS.MARK_EVENT_HAS_PLANNINGS, payload: { - event_item: event, + event_id: event, planning_item: planning, }, }); @@ -569,8 +570,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..8904ac5f7 100644 --- a/client/actions/events/ui.ts +++ b/client/actions/events/ui.ts @@ -20,9 +20,9 @@ import { timeUtils, getItemId, isItemPublic, - stringUtils, } from '../../utils'; import {convertStringFields} from '../../utils/strings'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; /** * Action Dispatcher to fetch events from the server @@ -590,13 +590,15 @@ const openEventPostModal = ( let promise = Promise.resolve(original); if (planningItem) { + const primaryEventIds = getRelatedEventIdsForPlanning(planningItem, 'primary'); + // Actually posting a planning item - if (!planningItem.event_item || !planningItem.recurrence_id) { + if (primaryEventIds.length === 0 || !planningItem.recurrence_id) { // Adhoc planning item or does not belong to recurring series return dispatch(planningAction()).then((p) => Promise.resolve(p)); } - promise = dispatch(eventsApi.fetchById(planningItem.event_item, {force: true, loadPlanning: false})); + promise = dispatch(eventsApi.fetchById(primaryEventIds[0], {force: true, loadPlanning: false})); } return promise.then((fetchedEvent) => { @@ -928,6 +930,7 @@ const save = (original, updates, confirmation, unlockOnClose) => ( { actionType: 'save', unlockOnClose: unlockOnClose, + large: true, } )); } diff --git a/client/actions/eventsPlanning/ui.ts b/client/actions/eventsPlanning/ui.ts index cf96e623d..e1f27a1c9 100644 --- a/client/actions/eventsPlanning/ui.ts +++ b/client/actions/eventsPlanning/ui.ts @@ -4,9 +4,9 @@ import eventsApi from '../events/api'; import planningApi from '../planning/api'; import {EVENTS_PLANNING, MAIN, ITEM_TYPE, MODALS} from '../../constants'; import * as selectors from '../../selectors'; -import {getItemType, dispatchUtils, getErrorMessage} from '../../utils'; +import {getItemType, dispatchUtils, getErrorMessage, gettext} from '../../utils'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; import main from '../main'; -import {gettext} from '../../utils'; import {showModal} from '../index'; /** @@ -90,7 +90,8 @@ const refetchPlanning = (planningId) => ( (dispatch, getState) => { const storedPlannings = selectors.planning.storedPlannings(getState()); const plan = get(storedPlannings, planningId); - const eventId = get(plan, 'event_item'); + const relatedEventIds = getRelatedEventIdsForPlanning(plan, 'primary'); + const eventId = relatedEventIds.length > 0 ? relatedEventIds[0] : undefined; const events = selectors.eventsPlanning.getRelatedPlanningsList(getState()) || {}; if (!selectors.main.isEventsPlanningView(getState()) || !eventId || diff --git a/client/actions/main.ts b/client/actions/main.ts index 5ee0ee992..95718be86 100644 --- a/client/actions/main.ts +++ b/client/actions/main.ts @@ -716,11 +716,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; @@ -749,7 +745,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/actions/planning/api.ts b/client/actions/planning/api.ts index 3fa242e1c..9a31fd0ce 100644 --- a/client/actions/planning/api.ts +++ b/client/actions/planning/api.ts @@ -24,6 +24,7 @@ import { } from '../../constants'; import main from '../main'; import {planningParamsToSearchParams} from '../../utils/search'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; /** * Action dispatcher that marks a Planning item as spiked @@ -181,24 +182,19 @@ const refetch = (page = 1, plannings = []) => ( * @param {Array} plannings - An array of Planning items * @return Promise */ -const fetchPlanningsEvents = (plannings) => ( +const fetchPlanningsEvents = (plannings: Array) => ( (dispatch, getState) => { const loadedEvents = selectors.events.storedEvents(getState()); - const linkedEvents = plannings - .map((p) => p.event_item) - .filter((eid) => ( - eid && !has(loadedEvents, eid) - )); - // load missing events, if there are any - if (get(linkedEvents, 'length', 0) > 0) { - return dispatch(actions.events.api.silentlyFetchEventsById( - linkedEvents, - 'both' - )); - } + const linkedEventIds = plannings + .map((plan) => getRelatedEventIdsForPlanning(plan, 'primary')) + .flat() + .filter((eventId) => loadedEvents[eventId] == null); - return Promise.resolve([]); + // load missing events, if there are any + return linkedEventIds.length > 0 ? + dispatch(actions.events.api.silentlyFetchEventsById(linkedEventIds, 'both')) : + Promise.resolve([]); } ); diff --git a/client/actions/planning/featuredPlanning.ts b/client/actions/planning/featuredPlanning.ts index 798766402..d2fee1a0e 100644 --- a/client/actions/planning/featuredPlanning.ts +++ b/client/actions/planning/featuredPlanning.ts @@ -132,13 +132,15 @@ function movePlanningToUnselectedList(item: IPlanningItem) { function getAndUpdateStoredPlanningItem(itemId: IPlanningItem['_id']) { return (dispatch, getState) => { if (selectors.featuredPlanning.inUse(getState())) { - planningApi.planning.getById(itemId, false, true).then((item) => { + return planningApi.planning.getById(itemId, false, true).then((item) => { dispatch({ type: FEATURED_PLANNING.ACTIONS.UPDATE_PLANNING_AND_LISTS, payload: item, }); }); } + + return Promise.resolve(); }; } diff --git a/client/actions/planning/notifications.ts b/client/actions/planning/notifications.ts index cfae83f1b..de014b791 100644 --- a/client/actions/planning/notifications.ts +++ b/client/actions/planning/notifications.ts @@ -20,86 +20,89 @@ import eventsPlanning from '../eventsPlanning'; * @param {object} _e - Event object * @param {object} data - Planning and User IDs */ -const onPlanningCreated = (_e, data) => ( +const onPlanningCreated = (_e: {}, data: IWebsocketMessageData['PLANNING_CREATED']) => ( (dispatch, getState) => { - // If this planning item was created by this user in AddToPlanning Modal - // Then ignore this notification - if (selectors.general.sessionId(getState()) === data.session && ( + if (data.item == null) { + return Promise.resolve(); + } else if (selectors.general.sessionId(getState()) === data.session && ( selectors.general.modalType(getState()) === MODALS.ADD_TO_PLANNING || selectors.general.previousModalType(getState()) === MODALS.ADD_TO_PLANNING )) { - return; + // If this planning item was created by this user in AddToPlanning Modal + // Then ignore this notification + return Promise.resolve(); } - if (get(data, 'item')) { - if (get(data, 'event_item', null) !== null) { - dispatch(events.api.markEventHasPlannings( - data.event_item, - data.item - )); - dispatch(main.fetchItemHistory({_id: data.event_item, type: ITEM_TYPE.EVENT})); - } - - dispatch(main.setUnsetLoadingIndicator(true)); - return dispatch(planning.ui.scheduleRefetch()) - .then(() => dispatch(eventsPlanning.ui.scheduleRefetch())) - .finally(() => dispatch(main.setUnsetLoadingIndicator(false))); + // Update Redux store to mark Event's to have Planning items + for (let eventId of data.event_ids) { + dispatch(events.api.markEventHasPlannings(eventId, data.item)); + dispatch(main.fetchItemHistory({_id: eventId, type: ITEM_TYPE.EVENT})); } - return Promise.resolve(); + dispatch(main.setUnsetLoadingIndicator(true)); + return dispatch(planning.ui.scheduleRefetch()) + .then(() => dispatch(eventsPlanning.ui.scheduleRefetch())) + .finally(() => dispatch(main.setUnsetLoadingIndicator(false))); } ); /** * WS Action when a Planning item gets updated, spiked or unspiked * If the Planning Item is not loaded, silently discard this notification - * @param {object} _e - Event object - * @param {object} data - Planning and User IDs */ -const onPlanningUpdated = (_e, data) => ( +const onPlanningUpdated = (_e: {}, data: IWebsocketMessageData['PLANNING_UPDATED']) => ( (dispatch, getState) => { - // If this planning item was update by this user in AddToPlanning Modal - // Then ignore this notification - if (selectors.general.sessionId(getState()) === data.session && ( + if (data.item == null) { + return Promise.resolve(); + } else if (selectors.general.sessionId(getState()) === data.session && ( selectors.general.modalType(getState()) === MODALS.ADD_TO_PLANNING || selectors.general.previousModalType(getState()) === MODALS.ADD_TO_PLANNING )) { - return; + // If this planning item was update by this user in AddToPlanning Modal + // Then ignore this notification + return Promise.resolve(); } - if (get(data, 'item')) { - dispatch(planning.ui.scheduleRefetch()) - .then((results) => { - if (selectors.general.currentWorkspace(getState()) === WORKSPACE.ASSIGNMENTS) { - const selectedItems = selectors.multiSelect.selectedPlannings(getState()); - const currentPreviewId = selectors.main.previewId(getState()); - - const loadedFromRefetch = selectedItems.indexOf(data.item) !== -1 && - !get(results, '[0]._items').find((plan) => plan._id === data.item); - - if (!loadedFromRefetch && currentPreviewId === data.item) { - dispatch(planning.api.fetchById(data.item, {force: true})); - } + // Update Redux store to mark Event's to have Planning items + for (let eventId of data.event_ids) { + dispatch(events.api.markEventHasPlannings(eventId, data.item)); + dispatch(main.fetchItemHistory({_id: eventId, type: ITEM_TYPE.EVENT})); + } + + const promises = []; + + promises.push(dispatch(planning.ui.scheduleRefetch()) + .then((results) => { + if (selectors.general.currentWorkspace(getState()) === WORKSPACE.ASSIGNMENTS) { + const selectedItems = selectors.multiSelect.selectedPlannings(getState()); + const currentPreviewId = selectors.main.previewId(getState()); + + const loadedFromRefetch = selectedItems.indexOf(data.item) !== -1 && + !get(results, '[0]._items').find((plan) => plan._id === data.item); + + if (!loadedFromRefetch && currentPreviewId === data.item) { + dispatch(planning.api.fetchById(data.item, {force: true})); } + } - dispatch(eventsPlanning.ui.scheduleRefetch()); - }); + dispatch(eventsPlanning.ui.scheduleRefetch()); + })); - if (get(data, 'added_agendas.length', 0) > 0 || get(data, 'removed_agendas.length', 0) > 0) { - dispatch(fetchAgendas()); - } - dispatch(main.fetchItemHistory({_id: data.item, type: ITEM_TYPE.PLANNING})); - dispatch(udpateAssignment(data.item)); - dispatch(planning.featuredPlanning.getAndUpdateStoredPlanningItem(data.item)); + if (data.added_agendas.length > 0 || data.removed_agendas.length > 0) { + promises.push(dispatch(fetchAgendas())); } - return Promise.resolve(); + promises.push(dispatch(main.fetchItemHistory({_id: data.item, type: ITEM_TYPE.PLANNING}))); + promises.push(dispatch(udpateAssignment(data.item))); + promises.push(dispatch(planning.featuredPlanning.getAndUpdateStoredPlanningItem(data.item))); + + return Promise.all(promises); } ); -const onPlanningLocked = (e, data) => ( +const onPlanningLocked = (e: {}, data: IWebsocketMessageData['ITEM_LOCKED']) => ( (dispatch, getState) => { - if (get(data, 'item')) { + if (data.item != null) { planningApi.locks.setItemAsLocked(data); const sessionId = selectors.general.session(getState()).sessionId; @@ -158,8 +161,6 @@ function onPlanningUnlocked(_e: {}, data: IWebsocketMessageData['ITEM_UNLOCKED'] } planningItem = { - event_item: get(data, 'event_item') || null, - recurrence_id: get(data, 'recurrence_id') || null, ...planningItem, _id: data.item, lock_action: null, @@ -301,13 +302,20 @@ const udpateAssignment = (planningId) => ( } const planningItem = selectors.planning.storedPlannings(getState())[planningId]; + const promises = []; get(planningItem, 'coverages', []).forEach((cov) => { if (get(cov, 'assigned_to.assignment_id')) { - dispatch(assignments.api.fetchAssignmentById(cov.assigned_to.assignment_id, true)); - dispatch(assignments.api.fetchAssignmentHistory({_id: cov.assigned_to.assignment_id})); + promises.push( + dispatch(assignments.api.fetchAssignmentById(cov.assigned_to.assignment_id, true)) + ); + promises.push( + dispatch(assignments.api.fetchAssignmentHistory({_id: cov.assigned_to.assignment_id})) + ); } }); + + return Promise.all(promises); } ); diff --git a/client/actions/planning/tests/notifications_test.ts b/client/actions/planning/tests/notifications_test.ts index 79fcfbff2..fea23eb8e 100644 --- a/client/actions/planning/tests/notifications_test.ts +++ b/client/actions/planning/tests/notifications_test.ts @@ -168,15 +168,17 @@ describe('actions.planning.notifications', () => { }); it('calls refetch on create', (done) => { + const eventId = data.plannings[1].related_events[0]._id; + store.initialState.main.filter = MAIN.FILTERS.PLANNING; return store.test(done, planningNotifications.onPlanningCreated({}, { item: data.plannings[1]._id, - event_item: data.plannings[1].event_item, + event_ids: [eventId] })) .then(() => { expect(eventsApi.markEventHasPlannings.callCount).toBe(1); expect(eventsApi.markEventHasPlannings.args[0]).toEqual([ - data.plannings[1].event_item, + eventId, data.plannings[1]._id, ]); @@ -323,8 +325,6 @@ describe('actions.planning.notifications', () => { lock_session: null, lock_time: null, _etag: 'e123', - event_item: null, - recurrence_id: null, }, }, }]); @@ -494,7 +494,12 @@ describe('actions.planning.notifications', () => { item_type: 'planning', }; - return store.test(done, planningNotifications.onPlanningUpdated({}, {item: data.plannings[0]._id})) + return store.test(done, planningNotifications.onPlanningUpdated({}, { + item: data.plannings[0]._id, + event_ids: [], + added_agendas: [], + removed_agendas: [], + })) .then(() => { expect(planningUi.scheduleRefetch.callCount).toBe(1); expect(eventsPlanningUi.scheduleRefetch.callCount).toBe(1); @@ -504,7 +509,12 @@ describe('actions.planning.notifications', () => { }); it('onPlanningUpdated does calls scheduleRefetch if item is not being edited', (done) => ( - store.test(done, planningNotifications.onPlanningUpdated({}, {item: data.plannings[0]._id})) + store.test(done, planningNotifications.onPlanningUpdated({}, { + item: data.plannings[0]._id, + event_ids: [], + added_agendas: [], + removed_agendas: [], + })) .then(() => { expect(planningUi.scheduleRefetch.callCount).toBe(1); expect(eventsPlanningUi.scheduleRefetch.callCount).toBe(1); diff --git a/client/actions/planning/ui.ts b/client/actions/planning/ui.ts index 2c0088a88..00f18de1f 100644 --- a/client/actions/planning/ui.ts +++ b/client/actions/planning/ui.ts @@ -1,3 +1,5 @@ +import {get, orderBy, cloneDeep} from 'lodash'; + import {IPlanningSearchParams} from '../../interfaces'; import {planningApi} from '../../superdeskApi'; @@ -16,11 +18,11 @@ import { isExistingItem, planningUtils, } from '../../utils'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; import * as selectors from '../../selectors'; import {PLANNING, WORKSPACE, MODALS, MAIN, COVERAGES} from '../../constants'; import * as actions from '../index'; -import {get, orderBy, cloneDeep} from 'lodash'; /** * Action dispatcher that marks a Planning item as spiked @@ -207,8 +209,9 @@ const duplicate = (plan) => ( .then((newPlan) => { notify.success(gettext('Planning duplicated')); const openInModal = selectors.forms.currentItemIdModal(getState()); + const relatedEventIds = getRelatedEventIdsForPlanning(plan, 'primary'); - if (get(plan, 'event_item')) { + if (relatedEventIds.length > 0) { dispatch(main.unlockAndCancel(plan)).then(() => { dispatch(main.openForEdit(newPlan, !openInModal, openInModal)); }); diff --git a/client/actions/tests/agenda_test.ts b/client/actions/tests/agenda_test.ts index addd07b89..22ce18c68 100644 --- a/client/actions/tests/agenda_test.ts +++ b/client/actions/tests/agenda_test.ts @@ -336,7 +336,10 @@ describe('agenda', () => { overide_auto_assign_to_workflow: false, }, coverages: [], - event_item: events[0]._id, + related_events: [{ + _id: events[0]._id, + link_type: 'primary', + }], planning_date: events[0].dates.start, slugline: events[0].slugline, name: events[0].name, diff --git a/client/api/editor/events.ts b/client/api/editor/events.ts index b70e8aeb8..2052a5456 100644 --- a/client/api/editor/events.ts +++ b/client/api/editor/events.ts @@ -7,9 +7,11 @@ import { IEditorBookmark, IEditorFormGroup, IEditorState, + IEventItem, IEventOrPlanningItem, IFormAutosave, IFormItemManager, + IPlanningItem, } from '../../interfaces'; import {planningApi} from '../../superdeskApi'; @@ -94,8 +96,8 @@ export function getEventsInstance(type: EDITOR_TYPE): IEditorAPI['events'] { function registerFormComponents(newState: Partial) { const editor = planningApi.editor(type); const parts = newState.diff.type === 'event' ? - editor.item.events.getGroupsForItem(newState.diff) : - editor.item.planning.getGroupsForItem(newState.diff); + editor.item.events.getGroupsForItem(newState.diff as Partial) : + editor.item.planning.getGroupsForItem(newState.diff as Partial); registerFormGroups(newState, parts.groups); registerFormBookmarks(newState, parts.bookmarks); diff --git a/client/api/editor/item.ts b/client/api/editor/item.ts index 93ee85933..da9cb5b9a 100644 --- a/client/api/editor/item.ts +++ b/client/api/editor/item.ts @@ -5,6 +5,7 @@ import {planningApi} from '../../superdeskApi'; import * as selectors from '../../selectors'; import {getPlanningInstance} from './item_planning'; import {getEventsInstance} from './item_events'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; export function getItemInstance(type: EDITOR_TYPE): IEditorAPI['item'] { const events = getEventsInstance(type); @@ -24,7 +25,10 @@ export function getItemInstance(type: EDITOR_TYPE): IEditorAPI['item'] { const plans = selectors.planning.storedPlannings(state); return Object.keys(plans) - .filter((planId) => plans[planId].event_item === eventId) + .filter((planId) => ( + plans[planId] != null && + getRelatedEventIdsForPlanning(plans[planId], 'primary').includes(eventId)) + ) .map((planId) => cloneDeep(plans[planId])); } diff --git a/client/api/editor/item_planning.ts b/client/api/editor/item_planning.ts index 2c8cb117c..76956f431 100644 --- a/client/api/editor/item_planning.ts +++ b/client/api/editor/item_planning.ts @@ -18,6 +18,7 @@ import { getEditorFormGroupsFromProfile, getGroupFieldsSorted, } from '../../utils/contentProfiles'; +import {getRelatedEventLinksForPlanning} from '../../utils/planning'; import {CoveragesBookmark, AddCoverageBookmark} from '../../components/Editor/bookmarks'; @@ -39,14 +40,14 @@ export function getPlanningInstance(type: EDITOR_TYPE): IEditorAPI['item']['plan return profile; } - function getGroupsForItem(item: DeepPartial): { + function getGroupsForItem(item: Partial): { bookmarks: Array, groups: Array } { const profile = planningApi.contentProfiles.get('planning'); const groups = getEditorFormGroupsFromProfile(profile); - if (item.event_item == null) { + if (getRelatedEventLinksForPlanning(item, 'primary').length === 0) { delete groups['associated_event']; } const bookmarks = getBookmarksFromFormGroups(groups); diff --git a/client/api/locks.ts b/client/api/locks.ts index aae3f29ff..39102411f 100644 --- a/client/api/locks.ts +++ b/client/api/locks.ts @@ -12,6 +12,7 @@ import {EVENTS, LOCKS, PLANNING, WORKSPACE, ASSIGNMENTS} from '../constants'; import featuredPlanning from '../actions/planning/featuredPlanning'; import {lockUtils, getErrorMessage, eventUtils, planningUtils, isExistingItem} from '../utils'; +import {getRelatedEventIdsForPlanning, getRelatedEventLinksForPlanning} from '../utils/planning'; import {currentWorkspace as getCurrentWorkspace} from '../selectors/general'; import {getLockedItems} from '../selectors/locks'; @@ -140,7 +141,9 @@ function lockItem(item: T, action?: string) locks.setItemAsLocked({ item: lockedItem._id, type: lockedItem.type, - event_item: lockedItem.type === 'planning' ? lockedItem.event_item : undefined, + event_ids: lockedItem.type === 'planning' ? + getRelatedEventIdsForPlanning(lockedItem, 'primary') : + [], recurrence_id: lockedItem.type !== 'assignment' ? lockedItem.recurrence_id : undefined, etag: lockedItem._etag, user: lockedItem.lock_user, @@ -239,7 +242,13 @@ function unlockItem(item: T, reloadLocksIfN if (item.type === 'event' && item.recurrence_id === currentLock.item_id) { lockedItemId = item._id; } else if (item.type === 'planning' && item.recurrence_id === currentLock.item_id) { - lockedItemId = item.event_item; + const relatedEventIds = getRelatedEventIdsForPlanning(item, 'primary'); + + if (relatedEventIds.length === 0) { + throw new Error('Planning is associated with an Event series, but original Event not linked'); + } + + lockedItemId = getRelatedEventIdsForPlanning(item, 'primary')[0]; } else { lockedItemId = currentLock.item_id; } @@ -258,7 +267,9 @@ function unlockItem(item: T, reloadLocksIfN locks.setItemAsUnlocked({ item: unlockedItem._id, type: unlockedItem.type, - event_item: unlockedItem.type === 'planning' ? unlockedItem.event_item : undefined, + event_ids: unlockedItem.type === 'planning' ? + getRelatedEventIdsForPlanning(unlockedItem, 'primary') : + [], recurrence_id: unlockedItem.type !== 'assignment' ? unlockedItem.recurrence_id : undefined, etag: unlockedItem._etag, from_ingest: false, diff --git a/client/api/planning.ts b/client/api/planning.ts index 56f44e960..98cfc1092 100644 --- a/client/api/planning.ts +++ b/client/api/planning.ts @@ -9,7 +9,7 @@ import { ISearchAPIParams, ISearchParams, ISearchSpikeState, - IPlanningConfig, + IPlanningConfig, IPlanningRelatedEventLink, } from '../interfaces'; import {appConfig as config} from 'appConfig'; @@ -162,6 +162,15 @@ function createFromEvent(event: IEventItem, updates: Partial): Pr updates.update_method = 'all'; } + const eventLink: IPlanningRelatedEventLink = { + _id: event._id, + link_type: 'primary', + }; + + if (event.recurrence_id != null) { + eventLink.recurrence_id = event.recurrence_id; + } + return create( planningUtils.modifyForServer({ slugline: event.slugline, @@ -175,7 +184,7 @@ function createFromEvent(event: IEventItem, updates: Partial): Pr ednote: event.ednote, language: event.language, ...updates, - event_item: event._id, + related_events: [eventLink], }), ); } diff --git a/client/api/tests/api_locks_test.ts b/client/api/tests/api_locks_test.ts index 3f2f74b36..bcebc776a 100644 --- a/client/api/tests/api_locks_test.ts +++ b/client/api/tests/api_locks_test.ts @@ -29,7 +29,7 @@ describe('planningApi.locks', () => { const itemLock = { item: testData.events[0]._id, type: testData.events[0].type, - event_item: undefined, + event_ids: [], etag: testData.events[0]._etag, user: testData.lockedEvents[0].lock_user, lock_session: testData.lockedEvents[0].lock_session, 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/EventEditor/index.tsx b/client/components/Events/EventEditor/index.tsx index 45cbafa12..ab4779001 100644 --- a/client/components/Events/EventEditor/index.tsx +++ b/client/components/Events/EventEditor/index.tsx @@ -96,12 +96,6 @@ class EventEditorComponent extends React.PureComponent { } } - getRelatedPlanningsForEvent(): Array { - return this.props.plannings?.filter( - (plan) => plan.event_item === this.props.item?._id - ); - } - showAddLocationForm(props: any): Promise { const editor = planningApi.editor(this.props.editorType); 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/ItemActionConfirmation/tests/spikeEventForm_test.tsx b/client/components/ItemActionConfirmation/tests/spikeEventForm_test.tsx index 6466ccc5e..e1f6cad96 100644 --- a/client/components/ItemActionConfirmation/tests/spikeEventForm_test.tsx +++ b/client/components/ItemActionConfirmation/tests/spikeEventForm_test.tsx @@ -79,7 +79,10 @@ xdescribe('', () => { headline: 'Some Plan 1', agendas: [data.agendas[1]._id], coverages: [], - event_item: 'e5', + related_events: [{ + _id: 'e5', + link_type: 'primary', + }], original_creator: {display_name: 'Hue Man'}, _agendas: [data.agendas[1]], }]; 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..fb402908b 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()) @@ -808,16 +821,6 @@ export class ItemManager { return this.setState({initialValues}).then(() => this.editor.onChangeHandler(diff, null, false)); } - // TODO: Is this used anywhere - // lock(item: IEventOrPlanningItem) { - // return planningApi.locks.lockItem(item); - // } - - // TODO: Is this used anywhere - // unlock() { - // return planningApi.locks.unlockItem(this.props.item); - // } - unlockThenLock(item: IEventOrPlanningItem) { return this.setState({ itemReady: false, 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/Planning/PlanningDateTime.tsx b/client/components/Planning/PlanningDateTime.tsx index 204514943..794ef7b24 100644 --- a/client/components/Planning/PlanningDateTime.tsx +++ b/client/components/Planning/PlanningDateTime.tsx @@ -2,7 +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 {getRelatedEventIdsForPlanning} from '../../utils/planning'; import {MAIN} from '../../constants'; import {CoverageIcons} from '../Coverages/CoverageIcons'; @@ -18,7 +20,7 @@ export const PlanningDateTime = ({ }) => { const coverages = get(item, 'coverages', []); const coverageTypes = planningUtils.mapCoverageByDate(coverages); - const hasAssociatedEvent = !!get(item, 'event_item'); + const hasAssociatedEvent = getRelatedEventIdsForPlanning(item, 'primary').length > 0; const isSameDay = (scheduled) => scheduled && (date == null || moment(scheduled).format('YYYY-MM-DD') === date); const coverageToDisplay = coverageTypes.filter((coverage) => { const scheduled = get(coverage, 'planning.scheduled'); diff --git a/client/components/Planning/PlanningHistory.tsx b/client/components/Planning/PlanningHistory.tsx index 316684546..d33342992 100644 --- a/client/components/Planning/PlanningHistory.tsx +++ b/client/components/Planning/PlanningHistory.tsx @@ -1,11 +1,20 @@ import React from 'react'; import PropTypes from 'prop-types'; +import {get} from 'lodash'; + +import {IEventItem} from '../../interfaces'; import {PLANNING, HISTORY_OPERATIONS, ITEM_TYPE} from '../../constants'; import {getItemInArrayById, gettext, historyUtils} from '../../utils'; -import {get} from 'lodash'; +import {getRelatedEventIdsForPlanning} from '../../utils/planning'; + + import {ContentBlock} from '../UI/SidePanel'; import {CoverageHistory} from '../Coverages'; +function getFirstPrimaryEventId(historyItem: any): IEventItem['_id'] | undefined { + return getRelatedEventIdsForPlanning(historyItem.update, 'primary')[0]; +} + export class PlanningHistory extends React.Component { closeAndOpenDuplicate(duplicateId, type = ITEM_TYPE.PLANNING) { this.props.openItemPreview(duplicateId, type); @@ -19,7 +28,8 @@ export class PlanningHistory extends React.Component { text = gettext('Ingested'); break; case HISTORY_OPERATIONS.CREATE: - text = get(historyItem, 'update.event_item') ? gettext('Created from event') : + text = getFirstPrimaryEventId(historyItem) != null ? + gettext('Created from event') : gettext('Created'); break; @@ -99,6 +109,7 @@ export class PlanningHistory extends React.Component { const postElement = historyUtils.getPostedHistoryElement( index, this.props.historyItems, this.props.users); const historyElement = this.getHistoryActionElement(historyItem); + const primaryEventId = getFirstPrimaryEventId(historyItem); if (postElement || historyElement) { return ( @@ -142,14 +153,15 @@ export class PlanningHistory extends React.Component {
)} - {(historyItem.operation === PLANNING.HISTORY_OPERATIONS.CREATE_EVENT || - historyItem.operation === HISTORY_OPERATIONS.CREATE) && - get(historyItem, 'update.event_item') && ( + {( + historyItem.operation === PLANNING.HISTORY_OPERATIONS.CREATE_EVENT || + historyItem.operation === HISTORY_OPERATIONS.CREATE + ) && primaryEventId != null && (
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/RelatedPlannings/RelatedPlannings_test.tsx b/client/components/RelatedPlannings/RelatedPlannings_test.tsx index eaecd4f98..abe0b44ed 100644 --- a/client/components/RelatedPlannings/RelatedPlannings_test.tsx +++ b/client/components/RelatedPlannings/RelatedPlannings_test.tsx @@ -15,7 +15,10 @@ describe('', () => { slugline: 'planning 3', original_creator: {display_name: 'ABC'}, agendas: ['1', '2'], - event_item: 'event1', + related_events: [{ + _id: 'event1', + link_type: 'primary', + }], }, }, }, diff --git a/client/components/WorkqueueContainer/WorkqueueContainer_test.tsx b/client/components/WorkqueueContainer/WorkqueueContainer_test.tsx index 996b928b5..ab8628512 100644 --- a/client/components/WorkqueueContainer/WorkqueueContainer_test.tsx +++ b/client/components/WorkqueueContainer/WorkqueueContainer_test.tsx @@ -49,7 +49,10 @@ describe('', () => { _id: 'p2', slugline: 'Planning2', headline: 'Some Plan 2', - event_item: 'e1', + related_events: [{ + _id: 'e1', + link_type: 'primary', + }], coverages: [], agendas: ['agenda1'], lock_action: 'edit', diff --git a/client/components/fields/index.tsx b/client/components/fields/index.tsx index 431682865..be3b81eff 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 0e697a419..4fa9b968c 100644 --- a/client/components/fields/preview/index.ts +++ b/client/components/fields/preview/index.ts @@ -302,9 +302,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/interfaces.ts b/client/interfaces.ts index 07a5e666a..76551680b 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -694,6 +694,17 @@ export interface IPlanningCoverageItem { scheduled_updates: Array; } +// An Event that is linked with 'primary' has side effects with the Planning and vice-versa +// such as locks, cancelling, postponing etc. +// Event's linked with a 'secondary' link have no side effects, ever, the link is purely informational. +export type IPlanningRelatedEventLinkType = 'primary' | 'secondary'; + +export interface IPlanningRelatedEventLink { + _id: string; + link_type: IPlanningRelatedEventLinkType; + recurrence_id?: string; +} + export interface IPlanningItem extends IBaseRestApiResponse { guid: string; original_creator: string; @@ -701,7 +712,7 @@ export interface IPlanningItem extends IBaseRestApiResponse { firstcreated: string; versioncreated: string; agendas: Array; - event_item: string; + related_events?: Array; recurrence_id: string; planning_recurrence_id: string; item_class: string; @@ -2051,7 +2062,6 @@ export interface IWebsocketMessageData { user?: IEventOrPlanningItem['lock_user']; lock_session?: IEventOrPlanningItem['lock_session']; recurrence_id?: IEventItem['recurrence_id']; - event_item?: IEventItem['_id']; type: IEventOrPlanningItem['type'] | IAssignmentItem['type']; }; ITEM_LOCKED: { @@ -2063,7 +2073,23 @@ export interface IWebsocketMessageData { lock_time: IEventOrPlanningItem['lock_time']; recurrence_id?: IEventOrPlanningItem['recurrence_id']; type: IEventOrPlanningItem['type'] | IAssignmentItem['type']; - event_item?: IEventItem['_id']; + }; + + PLANNING_CREATED: { + item: IPlanningItem['_id']; + user: IUser['_id']; + added_agendas: Array; + removed_agendas: Array; + session: ISession['sessionId']; + event_ids: Array; + }; + PLANNING_UPDATED: { + item: IPlanningItem['_id']; + user: IUser['_id']; + added_agendas: Array; + removed_agendas: Array; + session: ISession['sessionId']; + event_ids: Array; }; } diff --git a/client/reducers/events.ts b/client/reducers/events.ts index 055aa81b8..791831867 100644 --- a/client/reducers/events.ts +++ b/client/reducers/events.ts @@ -159,10 +159,10 @@ const eventsReducer = createReducer(initialState, { [EVENTS.ACTIONS.MARK_EVENT_HAS_PLANNINGS]: (state, payload) => { // If the event is not loaded, disregard this action - if (!(payload.event_item in state.events)) return state; + if (!(payload.event_id in state.events)) return state; let events = cloneDeep(state.events); - let event = events[payload.event_item]; + let event = events[payload.event_id]; const planningIds = get(event, 'planning_ids', []); diff --git a/client/reducers/locks.ts b/client/reducers/locks.ts index dba5ca3a0..d52baf3ef 100644 --- a/client/reducers/locks.ts +++ b/client/reducers/locks.ts @@ -13,8 +13,9 @@ const initialLockState: ILockedItems = { function removeLock(state: ILockedItems, data: IWebsocketMessageData['ITEM_UNLOCKED']) { if (data.recurrence_id != null) { delete state.recurring[data.recurrence_id]; - } else if (data.event_item != null) { - delete state.event[data.event_item]; + } else if ((data.event_ids?.length ?? 0) > 0) { + // For now, only support 1 primary event link for locks + delete state.event[data.event_ids[0]]; } // Always try and delete a lock direclty on the supplied item @@ -38,8 +39,9 @@ function addLock(state: ILockedItems, data: IWebsocketMessageData['ITEM_LOCKED'] if (data.recurrence_id != null) { state.recurring[data.recurrence_id] = lockData; - } else if (data.event_item != null) { - state.event[data.event_item] = lockData; + } else if ((data.event_ids?.length ?? 0) > 0) { + // For now, only support 1 primary event link for locks + state.event[data.event_ids[0]] = lockData; } else { state[data.type][data.item] = lockData; } diff --git a/client/reducers/tests/locks_test.ts b/client/reducers/tests/locks_test.ts index 1cd5c2bc4..7fbb5d6f9 100644 --- a/client/reducers/tests/locks_test.ts +++ b/client/reducers/tests/locks_test.ts @@ -42,11 +42,12 @@ describe('lock reducers', () => { lock_session: 'sess123', lock_user: 'user123', lock_time: '2099-10-15T14:33+0000', + event_ids: [], }, event: { _id: 'p2', type: 'planning', - event_item: 'e3', + event_ids: ['e3'], lock_action: 'reschedule', lock_session: 'sess123', lock_user: 'user123', @@ -55,7 +56,7 @@ describe('lock reducers', () => { recurring: { _id: 'p3', type: 'planning', - event_item: 'e7', + event_ids: ['e7'], recurrence_id: 'r2', lock_action: 'edit', lock_session: 'sess123', diff --git a/client/selectors/assignments.ts b/client/selectors/assignments.ts index a7af42e1f..a5a6638ae 100644 --- a/client/selectors/assignments.ts +++ b/client/selectors/assignments.ts @@ -1,10 +1,12 @@ import {get} from 'lodash'; import {createSelector} from 'reselect'; +import {IEventItem, IPlanningAppState, IPlanningItem} from '../interfaces'; import {storedEvents} from './events'; import {storedPlannings} from './planning'; import {currentDeskId, currentUserId, currentWorkspace} from './general'; import {getItemsById} from '../utils'; +import {getRelatedEventIdsForPlanning} from '../utils/planning'; import {ASSIGNMENTS, SORT_DIRECTION} from '../constants'; export const getStoredAssignments = (state) => get(state, 'assignment.assignments', {}); @@ -198,13 +200,22 @@ export const getCurrentAssignmentPlanningItem = createSelector( ) ); -export const getCurrentAssignmentEventItem = createSelector( +export const getCurrentAssignmentEventItem = createSelector< + IPlanningAppState, + IPlanningItem | null, + {[eventId: string]: IEventItem}, + IEventItem | null +>( [getCurrentAssignmentPlanningItem, storedEvents], - (planning, events) => ( - planning ? - get(events, planning.event_item) : - null - ) + (planning, events) => { + if (planning == null) { + return null; + } + + const relatedEventIds = getRelatedEventIdsForPlanning(planning, 'primary'); + + return relatedEventIds.length > 0 ? events[relatedEventIds[0]] : null; + } ); export const getCurrentAssignmentArchiveItem = createSelector( @@ -262,4 +273,4 @@ export const getAssignmentGroupSelectors = { Object.keys(getAssignmentGroupSelectors).forEach((groupKey) => { getAssignmentGroupSelectors[groupKey].isLoading = (state) => getList(state, groupKey).isLoading; -}); \ No newline at end of file +}); diff --git a/client/selectors/events.ts b/client/selectors/events.ts index 0b848a8d5..e9f9840b0 100644 --- a/client/selectors/events.ts +++ b/client/selectors/events.ts @@ -2,12 +2,21 @@ import {createSelector} from 'reselect'; import {get, sortBy} from 'lodash'; import {appConfig} from 'appConfig'; -import {IEventItem, IEventState, IEventTemplate, IPlanningAppState, LIST_VIEW_TYPE} from '../interfaces'; +import { + IEventItem, + IEventOrPlanningItem, + IEventState, + IEventTemplate, + IPlanningAppState, + IPlanningItem, + LIST_VIEW_TYPE +} from '../interfaces'; import {currentPlanning, storedPlannings} from './planning'; import {agendas, userPreferences} from './general'; import {currentItem, currentItemModal} from './forms'; import {eventUtils, getSearchDateRange} from '../utils'; +import {getRelatedEventIdsForPlanning} from '../utils/planning'; import {EVENTS, MAIN, SPIKED_STATE} from '../constants'; function getCurrentListViewType(state?: IPlanningAppState) { @@ -123,19 +132,58 @@ export const getRelatedPlanningsForModalEvent = createSelector( (itemId, events, plannings, agendas) => getRelatedPlanningsForEvent(itemId, events, plannings, agendas) ); -export const planningWithEventDetails = createSelector( +export const planningWithEventDetails = createSelector< + IPlanningAppState, + IPlanningItem | null, + {[eventId: string]: IEventItem}, + IEventItem | null +>( [currentPlanning, storedEvents], - (item, events) => item && events[item.event_item] + (item, events) => { + if (item == null) { + return null; + } + + const relatedEventIds = getRelatedEventIdsForPlanning(item, 'primary'); + + return relatedEventIds.length > 0 ? events[relatedEventIds[0]] : null; + } ); -export const planningEditAssociatedEvent = createSelector( +export const planningEditAssociatedEvent = createSelector< + IPlanningAppState, + IEventOrPlanningItem | null, + {[eventId: string]: IEventItem}, + IEventItem | null +>( [currentItem, storedEvents], - (item, events) => item && events[item.event_item] + (item, events) => { + if (item == null || item.type === 'event') { + return null; + } + + const relatedEventIds = getRelatedEventIdsForPlanning(item, 'primary'); + + return relatedEventIds.length > 0 ? events[relatedEventIds[0]] : null; + } ); -export const planningEditAssociatedEventModal = createSelector( +export const planningEditAssociatedEventModal = createSelector< + IPlanningAppState, + IEventOrPlanningItem | null, + {[eventId: string]: IEventItem}, + IEventItem | null +>( [currentItemModal, storedEvents], - (item, events) => item && events[item.event_item] + (item, events) => { + if (item == null || item.type === 'event') { + return null; + } + + const relatedEventIds = getRelatedEventIdsForPlanning(item, 'primary'); + + return relatedEventIds.length > 0 ? events[relatedEventIds[0]] : null; + } ); export const currentCalendarId = (state) => get(state, 'events.currentCalendarId'); 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/selectors/multiSelect.ts b/client/selectors/multiSelect.ts index 76927935e..be0aa717d 100644 --- a/client/selectors/multiSelect.ts +++ b/client/selectors/multiSelect.ts @@ -1,7 +1,10 @@ import {createSelector} from 'reselect'; -import {get} from 'lodash'; +import {get, cloneDeep} from 'lodash'; + +import {IEventItem, IPlanningAppState, IPlanningItem} from '../interfaces'; import {storedEvents} from './events'; import {storedPlannings} from './planning'; +import {getRelatedEventIdsForPlanning} from '../utils/planning'; export const selectedEventIds = (state) => get(state, 'multiSelect.selectedEventIds'); export const selectedEvents = createSelector( @@ -11,11 +14,33 @@ export const selectedEvents = createSelector( export const lastSelectedEventDate = (state) => get(state, 'multiSelect.lastSelectedEventDate'); export const selectedPlanningIds = (state) => get(state, 'multiSelect.selectedPlanningIds'); -export const selectedPlannings = createSelector( + + +export const selectedPlannings = createSelector< + IPlanningAppState, + {[planningId: string]: IPlanningItem}, + {[eventId: string]: IEventItem}, + Array, + Array +>( [storedPlannings, storedEvents, selectedPlanningIds], - (plannings, events, planningIds) => planningIds.map((planningId) => ({ - ...plannings[planningId], - event: get(events, get(plannings[planningId], 'event_item')), - })) + (plannings, events, planningIds) => ( + planningIds.map((planningId) => { + if (plannings[planningId] == null) { + return null; + } + + const planningItem: IPlanningItem & {event?: IEventItem} = cloneDeep(plannings[planningId]); + const relatedEventIds = getRelatedEventIdsForPlanning(planningItem, 'primary'); + + if (relatedEventIds.length > 0 && events[relatedEventIds[0]] != null) { + planningItem.event = events[relatedEventIds[0]]; + } + + return planningItem; + }) + .filter((planningItem) => planningItem != null) + ) ); + export const lastSelectedPlanningDate = (state) => get(state, 'multiSelect.lastSelectedPlanningDate'); diff --git a/client/selectors/tests/assignments_test.ts b/client/selectors/tests/assignments_test.ts index 8a5cf3d72..f47b6ef93 100644 --- a/client/selectors/tests/assignments_test.ts +++ b/client/selectors/tests/assignments_test.ts @@ -65,7 +65,10 @@ describe('selectors', () => { plannings: { a: { name: 'name a', - event_item: 'event1', + related_events: [{ + _id: 'event1', + link_type: 'primary', + }], agendas: ['1', '2'], }, b: { diff --git a/client/selectors/tests/general_test.ts b/client/selectors/tests/general_test.ts index c86063228..e3dd4972f 100644 --- a/client/selectors/tests/general_test.ts +++ b/client/selectors/tests/general_test.ts @@ -63,7 +63,10 @@ describe('selectors', () => { plannings: { a: { name: 'name a', - event_item: 'event1', + related_events: [{ + _id: 'event1', + link_type: 'primary', + }], agendas: ['1', '2'], }, b: { diff --git a/client/selectors/tests/planning_test.ts b/client/selectors/tests/planning_test.ts index aea134066..101734fa5 100644 --- a/client/selectors/tests/planning_test.ts +++ b/client/selectors/tests/planning_test.ts @@ -65,7 +65,10 @@ describe('selectors', () => { plannings: { a: { name: 'name a', - event_item: 'event1', + related_events: [{ + _id: 'event1', + link_type: 'primary', + }], agendas: ['1', '2'], }, b: { diff --git a/client/utils/events.ts b/client/utils/events.ts index 43b52c09f..8736a9fd3 100644 --- a/client/utils/events.ts +++ b/client/utils/events.ts @@ -56,8 +56,7 @@ import { sortBasedOnTBC, sanitizeItemFields, } from './index'; -import {getUsersDefaultLanguage} from './users'; -import {toUIFrameworkInterface} from './planning'; +import {toUIFrameworkInterface, getRelatedEventIdsForPlanning} from './planning'; /** @@ -166,11 +165,17 @@ function getRelatedEventsForRecurringEvent( } if (plannings.length > 0) { - const eventIds = map(events, '_id'); + const eventIds = events.map((event) => event._id); plannings = plannings.filter( - (p) => ((eventIds.indexOf(p.event_item) > -1 || p.event_item === recurringEvent._id) && - (!postedPlanningOnly || p.pubstatus === POST_STATE.USABLE)) + (plan) => { + const primaryEventIds = getRelatedEventIdsForPlanning(plan, 'primary'); + + return ( + (eventIds.includes(primaryEventIds[0]) || primaryEventIds[0] === recurringEvent._id) && + (!postedPlanningOnly || plan.pubstatus === POST_STATE.USABLE) + ); + } ); } @@ -1357,11 +1362,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, diff --git a/client/utils/locks.ts b/client/utils/locks.ts index 046b44949..543d56fe3 100644 --- a/client/utils/locks.ts +++ b/client/utils/locks.ts @@ -57,8 +57,8 @@ function getPlanningLock(item: IPlanningItem | null, lockedItems: ILockedItems): return lockedItems.planning[item._id]; } else if (item.recurrence_id != null && lockedItems.recurring[item.recurrence_id] != null) { return lockedItems.recurring[item.recurrence_id]; - } else if (item.event_item != null && lockedItems.event[item.event_item] != null) { - return lockedItems.event[item.event_item]; + } else if ((item.related_events?.length ?? 0) > 0 && lockedItems.event[item.related_events[0]._id] != null) { + return lockedItems.event[item.related_events[0]._id]; } return null; diff --git a/client/utils/planning.ts b/client/utils/planning.ts index 081e76a34..ddec0c141 100644 --- a/client/utils/planning.ts +++ b/client/utils/planning.ts @@ -20,7 +20,7 @@ import { IFeaturedPlanningItem, ICoverageScheduledUpdate, IDateTime, - IItemAction, + IItemAction, IPlanningRelatedEventLink, IPlanningRelatedEventLinkType, } from '../interfaces'; const appConfig = config as IPlanningConfig; @@ -378,7 +378,7 @@ export function mapCoverageByDate(coverages: Array = []): // ad hoc plan created directly from planning list and not from an event function isPlanAdHoc(plan: IPlanningItem): boolean { - return plan.event_item == null; + return getRelatedEventLinksForPlanning(plan, 'primary').length === 0; } function isPlanMultiDay(plan: IPlanningItem): boolean { @@ -1070,8 +1070,11 @@ function getPlanningByDate( dates[groupDate.format('YYYY-MM-DD')] = groupDate; } }; + const primaryEventIds = getRelatedEventIdsForPlanning(plan, 'primary'); - plan.event = get(events, get(plan, 'event_item')); + plan.event = primaryEventIds.length > 0 ? + events[primaryEventIds[0]] : + undefined; plan.coverages.forEach((coverage) => { setCoverageToDate(coverage); @@ -1394,8 +1397,9 @@ function getDefaultCoverageDueDate( eventItem?: IEventItem, ): moment.Moment | null { let coverageTime: moment.Moment = null; + const primaryEventIds = getRelatedEventIdsForPlanning(planningItem, 'primary'); - if (planningItem?.event_item == null) { + if (primaryEventIds.length === 0) { coverageTime = moment(planningItem?.planning_date || moment()); } else if (eventItem) { coverageTime = moment(eventItem?.dates?.end || moment()); @@ -1641,6 +1645,20 @@ function duplicateCoverage( return diffCoverages; } +export function getRelatedEventLinksForPlanning( + plan: Partial, + linkType: IPlanningRelatedEventLinkType +): Array { + return (plan.related_events || []).filter((link) => link.link_type === linkType); +} + +export function getRelatedEventIdsForPlanning( + plan: Partial, + linkType: IPlanningRelatedEventLinkType +): Array { + return getRelatedEventLinksForPlanning(plan, linkType).map((event) => event._id); +} + // eslint-disable-next-line consistent-this const self = { canSpikePlanning, diff --git a/client/utils/testData.ts b/client/utils/testData.ts index dc1c42807..6dfbfaa47 100644 --- a/client/utils/testData.ts +++ b/client/utils/testData.ts @@ -695,7 +695,10 @@ export const plannings = [ slugline: 'Planning2', planning_date: '2016-10-15T13:01:11', headline: 'Some Plan 2', - event_item: 'e1', + related_events: [{ + _id: 'e1', + link_type: 'primary', + }], coverages: [ { coverage_id: 'c4', diff --git a/client/utils/tests/events_test.ts b/client/utils/tests/events_test.ts index 09c929a82..eb448584f 100644 --- a/client/utils/tests/events_test.ts +++ b/client/utils/tests/events_test.ts @@ -113,7 +113,10 @@ describe('EventUtils', () => { standalone: { _id: 'p1', type: 'planning', - event_item: 'e9', + related_events: [{ + _id: 'e9', + link_type: 'primary', + }], lock_user: 'ident1', lock_session: 'session1', lock_action: 'edit', @@ -123,7 +126,10 @@ describe('EventUtils', () => { direct: { _id: 'p2', type: 'planning', - event_item: 'e10', + related_events: [{ + _id: 'e10', + link_type: 'primary', + }], recurrence_id: 'r5', lock_user: 'ident1', lock_session: 'session1', @@ -133,7 +139,10 @@ describe('EventUtils', () => { indirect: { _id: 'p3', type: 'planning', - event_item: 'e12', + related_events: [{ + _id: 'e12', + link_type: 'primary', + }], recurrence_id: 'r6', lock_user: 'ident1', lock_session: 'session1', @@ -157,7 +166,7 @@ describe('EventUtils', () => { [locks.events.standalone.otherUser._id]: lockUtils.getLockFromItem( locks.events.standalone.otherUser ), - [locks.plans.standalone.event_item]: lockUtils.getLockFromItem(locks.plans.standalone), + [locks.plans.standalone.related_events[0]._id]: lockUtils.getLockFromItem(locks.plans.standalone), }, recurring: { [locks.events.recurring.currentUser.currentSession.recurrence_id]: lockUtils.getLockFromItem( diff --git a/client/utils/tests/planning_test.ts b/client/utils/tests/planning_test.ts index fce751214..02b925f34 100644 --- a/client/utils/tests/planning_test.ts +++ b/client/utils/tests/planning_test.ts @@ -53,7 +53,10 @@ describe('PlanningUtils', () => { currentSession: { _id: 'p5', type: 'planning', - event_item: 'e1', + related_events: [{ + _id: 'e1', + link_type: 'primary', + }], lock_user: 'ident1', lock_session: 'session1', lock_action: 'edit', @@ -62,7 +65,10 @@ describe('PlanningUtils', () => { otherSession: { _id: 'p6', type: 'planning', - event_item: 'e2', + related_events: [{ + _id: 'e2', + link_type: 'primary', + }], lock_user: 'ident1', lock_session: 'session2', lock_action: 'edit', @@ -72,7 +78,10 @@ describe('PlanningUtils', () => { otherUser: { _id: 'p7', type: 'planning', - event_item: 'e3', + related_events: [{ + _id: 'e3', + link_type: 'primary', + }], lock_user: 'ident2', lock_session: 'session3', lock_action: 'edit', @@ -81,7 +90,10 @@ describe('PlanningUtils', () => { notLocked: { _id: 'p8', type: 'planning', - event_item: 'e4', + related_events: [{ + _id: 'e4', + link_type: 'primary', + }], }, }, recurring: { @@ -89,7 +101,10 @@ describe('PlanningUtils', () => { currentSession: { _id: 'p9', type: 'planning', - event_item: 'e5', + related_events: [{ + _id: 'e5', + link_type: 'primary', + }], recurrence_id: 'r1', lock_user: 'ident1', lock_session: 'session1', @@ -99,7 +114,10 @@ describe('PlanningUtils', () => { otherSession: { _id: 'p10', type: 'planning', - event_item: 'e6', + related_events: [{ + _id: 'e6', + link_type: 'primary', + }], recurrence_id: 'r2', lock_user: 'ident1', lock_session: 'session2', @@ -110,7 +128,10 @@ describe('PlanningUtils', () => { otherUser: { _id: 'p11', type: 'planning', - event_item: 'e7', + related_events: [{ + _id: 'e7', + link_type: 'primary', + }], recurrence_id: 'r3', lock_user: 'ident2', lock_session: 'session3', @@ -120,7 +141,10 @@ describe('PlanningUtils', () => { notLocked: { _id: 'p12', type: 'planning', - event_item: 'e8', + related_events: [{ + _id: 'e8', + link_type: 'primary', + }], recurrence_id: 'r4', }, }, @@ -128,19 +152,28 @@ describe('PlanningUtils', () => { standalone: { _id: 'p13', type: 'planning', - event_item: 'e9', + related_events: [{ + _id: 'e9', + link_type: 'primary', + }], }, recurring: { direct: { _id: 'p14', type: 'planning', - event_item: 'e10', + related_events: [{ + _id: 'e10', + link_type: 'primary', + }], recurrence_id: 'r5', }, indirect: { _id: 'p15', type: 'planning', - event_item: 'e11', + related_events: [{ + _id: 'e11', + link_type: 'primary', + }], recurrence_id: 'r5', }, }, @@ -175,13 +208,13 @@ describe('PlanningUtils', () => { [locks.events.standalone._id]: lockUtils.getLockFromItem( locks.events.standalone ), - [locks.plans.event.currentUser.currentSession.event_item]: lockUtils.getLockFromItem( + [locks.plans.event.currentUser.currentSession.related_events[0]._id]: lockUtils.getLockFromItem( locks.plans.event.currentUser.currentSession ), - [locks.plans.event.currentUser.otherSession.event_item]: lockUtils.getLockFromItem( + [locks.plans.event.currentUser.otherSession.related_events[0]._id]: lockUtils.getLockFromItem( locks.plans.event.currentUser.otherSession ), - [locks.plans.event.otherUser.event_item]: lockUtils.getLockFromItem( + [locks.plans.event.otherUser.related_events[0]._id]: lockUtils.getLockFromItem( locks.plans.event.otherUser ), }, @@ -622,7 +655,10 @@ describe('PlanningUtils', () => { 'Edit', ]); - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'draft', planning_ids: ['1'], @@ -654,7 +690,10 @@ describe('PlanningUtils', () => { 'Edit', ]); - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'postponed', planning_ids: ['1'], @@ -679,7 +718,10 @@ describe('PlanningUtils', () => { expectActions(itemActions, ['Duplicate', 'Edit']); - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'cancelled', planning_ids: ['1'], @@ -701,7 +743,10 @@ describe('PlanningUtils', () => { 'Duplicate', ]); - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'rescheduled', planning_ids: ['1'], @@ -717,7 +762,10 @@ describe('PlanningUtils', () => { it('unposted event and unposted planning', () => { planning.state = 'killed'; - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'killed', planning_ids: ['1'], @@ -739,7 +787,10 @@ describe('PlanningUtils', () => { it('posted event and unposted planning', () => { planning.state = 'killed'; - planning.event_item = '1'; + planning.related_events = [{ + _id: '1', + link_type: 'primary', + }]; event = { state: 'scheduled', planning_ids: ['1'], @@ -967,11 +1018,16 @@ describe('PlanningUtils', () => { const newsCoverageStatus = [{qcode: 'ncostat:int'}]; const planned = moment('2119-03-15T09:00:00+11:00'); let eventEnd = moment('2119-03-17T09:00:00+11:00'); - const plan = {slugline: 'Test', + const plan = { + slugline: 'Test', internal_note: 'Internal Note', ednote: 'Ed note', planning_date: planned, - event_item: 'xxx'}; + related_events: [{ + _id: 'xxx', + link_type: 'primary', + }], + }; const event = {dates: {end: eventEnd}}; let coverage = planningUtils.defaultCoverageValues(newsCoverageStatus, plan, event); @@ -995,11 +1051,16 @@ describe('PlanningUtils', () => { const planned = moment('2119-03-15T09:00:00+11:00'); const eventStart = moment('2119-03-17T09:00:00+11:00'); const eventEnd = moment('2119-03-17T19:00:00+11:00'); - const plan = {slugline: 'Test', + const plan = { + slugline: 'Test', internal_note: 'Internal Note', ednote: 'Ed note', planning_date: planned, - event_item: 'xxx'}; + related_events: [{ + _id: 'xxx', + link_type: 'primary', + }], + }; const event = {dates: {end: eventEnd, start: eventStart}}; appConfig.long_event_duration_threshold = 4; diff --git a/package-lock.json b/package-lock.json index 1f21f52e9..98fe562e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "superdesk-planning", - "version": "2.7.0-dev", + "version": "2.7.0-rc8", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -15,33 +15,33 @@ } }, "@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/highlight": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", - "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.2.tgz", + "integrity": "sha512-Yac1ao4flkTxTteCDZLEvdxg2fZfz1v8M4QpaGypq/WPDqg3ijHYbDfs+LG5hvzSoqaSZ9/Z9lKSP3CjZjv+pA==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.24.5", + "@babel/helper-validator-identifier": "^7.22.20", "chalk": "^2.4.2", "js-tokens": "^4.0.0", "picocolors": "^1.0.0" } }, "@babel/parser": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", - "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz", + "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==", "dev": true }, "@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz", + "integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==", "requires": { "regenerator-runtime": "^0.14.0" }, @@ -239,9 +239,9 @@ }, "dependencies": { "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", "dev": true } } @@ -336,9 +336,9 @@ "dev": true }, "@types/node": { - "version": "20.12.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.11.tgz", - "integrity": "sha512-vDg9PZ/zi+Nqp6boSOT7plNuthRugEKixDv5sFTIpkE89MmNtEArAShI4mxuX2+UrLEe9pxC1vm2cjm9YlWbJw==", + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -481,10 +481,13 @@ "dev": true }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } } } }, @@ -1179,9 +1182,9 @@ "dev": true }, "aws4": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", - "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.0.tgz", + "integrity": "sha512-3AungXC4I8kKsS9PuS4JH2nc+0bVY/mjgrephHTIi8fpEeGsTHBUJeosp0Wc1myYMElmD0B3Oc4XL/HVJ4PV2g==", "dev": true }, "axios": { @@ -1305,12 +1308,6 @@ "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", "dev": true }, - "base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", - "dev": true - }, "batch": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", @@ -1343,6 +1340,16 @@ "integrity": "sha512-nbE1WxOTTrUWIfsfZ4aHGYu5DOuNkbxGokjV6Z2kxfJK3uaAb8zNK1muzOeipoLHZjInT4Br88BHpzevc681xA==", "dev": true }, + "bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "optional": true, + "requires": { + "file-uri-to-path": "1.0.0" + } + }, "block-stream": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/block-stream/-/block-stream-0.0.9.tgz", @@ -1383,6 +1390,12 @@ "unpipe": "1.0.0" }, "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -1391,6 +1404,15 @@ "requires": { "safer-buffer": ">= 2.1.2 < 3" } + }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } } } }, @@ -1613,9 +1635,9 @@ "dev": true }, "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", "dev": true }, "cache-base": { @@ -1709,9 +1731,9 @@ } }, "caniuse-db": { - "version": "1.0.30001617", - "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001617.tgz", - "integrity": "sha512-JEfZYiroeTkXqNyFv8JJR0aN1tpTgBhst4UawTTKQ7ZVlAsMPWq1U8H9F/THtIXcV6u3CZCFEnkX+Fhgve8Z/w==", + "version": "1.0.30001633", + "resolved": "https://registry.npmjs.org/caniuse-db/-/caniuse-db-1.0.30001633.tgz", + "integrity": "sha512-Ba2XtmlCeKAixOIoWRcoXY0aKjKvdckV/yDVg5sVm4SIjyeFZgtCI4smJ9pLCC57bHUtw5VWhpjWGqES4k9h6A==", "dev": true }, "caseless": { @@ -1854,18 +1876,18 @@ }, "dependencies": { "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -1904,12 +1926,6 @@ "safe-buffer": "^5.0.1" } }, - "circular-json": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", - "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", - "dev": true - }, "clap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", @@ -2211,14 +2227,6 @@ "on-headers": "~1.0.2", "safe-buffer": "5.1.2", "vary": "~1.1.2" - }, - "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true - } } }, "concat-map": { @@ -2249,6 +2257,38 @@ "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" + }, + "dependencies": { + "finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + } + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true + } } }, "connect-history-api-fallback": { @@ -2676,12 +2716,6 @@ "@babel/runtime": "^7.21.0" } }, - "date-format": { - "version": "4.0.14", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", - "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", - "dev": true - }, "dateformat": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-1.0.12.tgz", @@ -3154,9 +3188,9 @@ "dev": true }, "electron-to-chromium": { - "version": "1.4.762", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.762.tgz", - "integrity": "sha512-rrFvGweLxPwwSwJOjIopy3Vr+J3cIPtZzuc74bmlvmBIgQO3VYJDvVrlj94iKZ3ukXUH64Ex31hSfRTLqvjYJQ==", + "version": "1.4.802", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.802.tgz", + "integrity": "sha512-TnTMUATbgNdPXVSHsxvNVSG0uEd6cSZsANjm8c9HbvflZVVn1yTRcmVXYT1Ma95/ssB/Dcd30AHweH2TE+dNpA==", "dev": true }, "elliptic": { @@ -3226,6 +3260,12 @@ "ws": "~8.11.0" }, "dependencies": { + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3861,9 +3901,9 @@ "dev": true }, "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", + "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", "dev": true }, "events": { @@ -3916,6 +3956,22 @@ "shebang-command": "^1.2.0", "which": "^1.2.9" } + }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true } } }, @@ -4059,27 +4115,21 @@ "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", "dev": true }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - } - }, "path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", "dev": true }, + "qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "requires": { + "side-channel": "^1.0.4" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -4396,6 +4446,13 @@ "integrity": "sha512-0k45oWBokCqh2MOexeYKpyqmGKG+8mQ2Wd8iawx+uWd/weWJQAZ6SoPybagdCI4xFisag8iAR77WPm4h3pTfxA==", "dev": true }, + "file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "optional": true + }, "filename-regex": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz", @@ -4424,35 +4481,18 @@ } }, "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", "dev": true, "requires": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", - "on-finished": "~2.3.0", + "on-finished": "2.4.1", "parseurl": "~1.3.3", - "statuses": "~1.5.0", + "statuses": "2.0.1", "unpipe": "~1.0.0" - }, - "dependencies": { - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true - } } }, "find-up": { @@ -5624,14 +5664,6 @@ "requires": { "eventemitter3": "3.1.0", "url-toolkit": "^2.1.6" - }, - "dependencies": { - "eventemitter3": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz", - "integrity": "sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA==", - "dev": true - } } }, "hmac-drbg": { @@ -5852,6 +5884,14 @@ "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + } } }, "http-proxy-middleware": { @@ -6900,19 +6940,34 @@ "yargs": "^16.1.1" }, "dependencies": { + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true + }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" + } + }, + "debug": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", + "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", + "dev": true, + "requires": { + "ms": "2.1.2" } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" @@ -6924,6 +6979,12 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, "rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", @@ -6933,6 +6994,31 @@ "glob": "^7.1.3" } }, + "socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "dev": true, + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + } + }, + "socket.io-adapter": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", + "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", + "dev": true, + "requires": { + "debug": "~4.3.4", + "ws": "~8.11.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -7355,6 +7441,12 @@ "streamroller": "^3.1.5" }, "dependencies": { + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7430,13 +7522,12 @@ } }, "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" + "yallist": "^4.0.0" } }, "make-error": { @@ -7774,9 +7865,9 @@ "dev": true }, "nan": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", - "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==", + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", "dev": true }, "nanomatch": { @@ -8040,6 +8131,16 @@ "which": "^1.2.9" } }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, "strip-ansi": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", @@ -8054,6 +8155,12 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "dev": true + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true } } }, @@ -9545,13 +9652,10 @@ "dev": true }, "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true }, "query-string": { "version": "4.3.4", @@ -9680,6 +9784,12 @@ "unpipe": "1.0.0" }, "dependencies": { + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -10192,12 +10302,6 @@ "uuid": "^3.3.2" }, "dependencies": { - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true - }, "uuid": { "version": "3.4.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", @@ -10593,16 +10697,6 @@ "y18n": "^3.2.1", "yargs-parser": "^5.0.1" } - }, - "yargs-parser": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", - "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", - "dev": true, - "requires": { - "camelcase": "^3.0.0", - "object.assign": "^4.1.0" - } } } }, @@ -11118,65 +11212,6 @@ } } }, - "socket.io": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", - "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, - "socket.io-adapter": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.4.tgz", - "integrity": "sha512-wDNHGXGewWAjQPt3pyeYBtpWSq9cLE5UW1ZUPL/2eGK9jtse/FpXib7epSTsz0Q0m+6sg6Y4KtcFTlah1bdOVg==", - "dev": true, - "requires": { - "debug": "~4.3.4", - "ws": "~8.11.0" - }, - "dependencies": { - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } - }, "socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -11187,6 +11222,11 @@ "debug": "~4.3.1" }, "dependencies": { + "component-emitter": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.2.1.tgz", + "integrity": "sha512-jPatnhd33viNplKjqXKRkGU345p263OIWzDL2wH3LGIGp5Kojo+uXizHmOADRvhGFFTnJqX3jBAKP6vvmSDKcA==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11196,6 +11236,11 @@ "ms": "2.1.2" } }, + "isarray": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", + "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -11329,9 +11374,9 @@ } }, "spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "spdy": { @@ -11462,6 +11507,12 @@ "fs-extra": "^8.1.0" }, "dependencies": { + "date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -11712,6 +11763,12 @@ "integrity": "sha512-j/Toj7f1z98Hh2cYo2BVr85EpIRWqUi7rtRSGxh/cqUjqrnJe9l9UE7IUGd2vQ2p+kSHLkSzObQPZPLUC6TQwg==", "dev": true }, + "circular-json": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", + "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", + "dev": true + }, "cli-cursor": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", @@ -11925,6 +11982,16 @@ "integrity": "sha512-4JD/Ivzg7PoW8NzdrBSr3UFwC9mHgvI7Z6z3QGBsSHgKaRTUDmyZAAKJo2UbG1kUVfS9WS8bi36N49U1xw43DA==", "dev": true }, + "lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "requires": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" + } + }, "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", @@ -12024,11 +12091,17 @@ "requires": { "mkdirp": "^0.5.1" } + }, + "yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true } } }, "superdesk-core": { - "version": "github:superdesk/superdesk-client-core#9ae1cec83428198cf0ef1552fef9af6098da2b4d", + "version": "github:superdesk/superdesk-client-core#61d411e5074fd6736693e67dcdf5b503d91198a8", "from": "github:superdesk/superdesk-client-core#develop", "dev": true, "requires": { @@ -12126,7 +12199,7 @@ "sass-loader": "6.0.6", "shortid": "2.2.8", "style-loader": "0.20.2", - "superdesk-ui-framework": "^3.1.3", + "superdesk-ui-framework": "^3.1.9", "ts-loader": "3.5.0", "typescript": "4.9.5", "uuid": "8.3.1", @@ -12212,9 +12285,9 @@ } }, "superdesk-ui-framework": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.1.5.tgz", - "integrity": "sha512-b+lJJGrf2vCyMabpUCngsbnINZQ6tGRZVjKfqZbPPge5jZVufOTDCM2aBCqj41AEEfVGM6r1BvZNQgClafRQ9g==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.1.9.tgz", + "integrity": "sha512-mgBkdsv/mvG02WUNt+7szbw+V0xDK0BtG5nuZag/GuvHjJmTGW0s0OhuC/Q+/SGkwg/iR+C7DLYqUY4l+7TQXQ==", "dev": true, "requires": { "@popperjs/core": "^2.4.0", @@ -12626,9 +12699,9 @@ "dev": true }, "type": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz", - "integrity": "sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw==", + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==", "dev": true }, "type-check": { @@ -13245,6 +13318,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -13743,6 +13817,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -14082,9 +14157,9 @@ "dev": true }, "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, "yargs": { @@ -14100,13 +14175,33 @@ "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" + }, + "dependencies": { + "yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true + } } }, "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.1.tgz", + "integrity": "sha512-wpav5XYiddjXxirPoCTUPbqM0PXvJ9hiBMvuJgInvo4/lAOTZzUprArw17q2O1P2+GHhbBr18/iQwjL5Z9BqfA==", + "dev": true, + "requires": { + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" + }, + "dependencies": { + "camelcase": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", + "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", + "dev": true + } + } }, "yn": { "version": "2.0.0", diff --git a/package.json b/package.json index a3f7ee398..813ffe0b5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superdesk-planning", - "version": "2.7.0-dev", + "version": "2.8.0-dev", "license": "AGPL-3.0", "description": "", "repository": { @@ -61,11 +61,14 @@ "karma-verbose-reporter": "0.0.6", "karma-webpack": "^2.0.13", "react-test-renderer": "16.2.0", + "redux": "^4.2.1", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.4.2", "simulant": "^0.2.2", "sinon": "^4.5.0", "superdesk-code-style": "1.5.0", "superdesk-core": "github:superdesk/superdesk-client-core#develop", - "superdesk-ui-framework": "^3.0.76", + "superdesk-ui-framework": "^3.1.9", "ts-node": "~7.0.1", "tslint": "5.11.0", "typescript-eslint-parser": "^18.0.0" @@ -76,9 +79,9 @@ "moment": "^2.29.4", "moment-timezone": "^0.5.41", "react": "^16.9.0", - "redux": "^4.0.5", + "redux": "^4.2.1", "redux-logger": "^3.0.6", "redux-thunk": "^2.3.0", - "superdesk-ui-framework": "^3.0.71" + "superdesk-ui-framework": "*" } } diff --git a/server/planning/__init__.py b/server/planning/__init__.py index 45eda101f..58f960ad9 100644 --- a/server/planning/__init__.py +++ b/server/planning/__init__.py @@ -78,7 +78,7 @@ from planning.planning_locks import init_app as init_planning_locks_app from planning.search.planning_autocomplete import init_app as init_planning_autocomplete_app -__version__ = "2.7.0-dev" +__version__ = "2.8.0-dev" _SERVER_PATH = os.path.dirname(os.path.realpath(__file__)) diff --git a/server/planning/events/events.py b/server/planning/events/events.py index 4a007240f..264820725 100644 --- a/server/planning/events/events.py +++ b/server/planning/events/events.py @@ -110,6 +110,16 @@ def get_coverage_id(coverage: EmbeddedCoverageItem) -> str: ] +def is_event_updated(new_item: Event, old_item: Event) -> bool: + if new_item.get("name") != old_item.get("name"): + return True + new_subject = set([subject.get("qcode") for subject in new_item.get("subject", [])]) + old_subject = set([subject.get("qcode") for subject in old_item.get("subject", [])]) + if new_subject != old_subject: + return True + return False + + class EventsService(superdesk.Service): """Service class for the events model.""" @@ -137,7 +147,7 @@ def patch_in_mongo(self, id, document, original) -> Optional[Dict[str, Any]]: return response def is_new_version(self, new_item, old_item): - return is_new_version(new_item, old_item) + return is_new_version(new_item, old_item) or is_event_updated(new_item, old_item) def ingest_cancel(self, item, feeding_service): """Ignore cancelling on ingest, this will happen in ``update_post_item``""" diff --git a/server/planning/feed_parsers/onclusive.py b/server/planning/feed_parsers/onclusive.py index 31b38e482..a04fea917 100644 --- a/server/planning/feed_parsers/onclusive.py +++ b/server/planning/feed_parsers/onclusive.py @@ -75,6 +75,7 @@ def parse(self, content, provider=None): self.parse_event_details(event, item) self.parse_category(event, item) self.parse_contact_info(event, item) + self.set_expiry(item, provider) all_events.append(item) except EmbargoedException: logger.info("Ignoring embargoed event %s", event["itemId"]) @@ -262,14 +263,20 @@ def parse_contact_info(self, event, item): for contact_info in event.get("pressContacts"): item.setdefault("event_contact_info", []) contact_uri = "onclusive:{}".format(contact_info["pressContactID"]) - data = {"uri": contact_uri} + data = { + "uri": contact_uri, + "contact_email": [], + "contact_phone": [], + "organisation": "", + "first_name": "", + "last_name": "", + } + if contact_info.get("pressContactEmail"): - data.setdefault("contact_email", []).append(contact_info["pressContactEmail"]) + data["contact_email"].append(contact_info["pressContactEmail"]) if contact_info.get("pressContactTelephone"): - data.setdefault("contact_phone", []).append( - {"number": contact_info["pressContactTelephone"], "public": True} - ) + data["contact_phone"].append({"number": contact_info["pressContactTelephone"], "public": True}) if contact_info.get("pressContactOffice"): data["organisation"] = contact_info["pressContactOffice"] @@ -285,7 +292,6 @@ def parse_contact_info(self, event, item): existing_contact = get_resource_service("contacts").find_one(req=None, uri=contact_uri) if existing_contact is None: - logger.debug("New contact %s %s", contact_uri, data.get("organisation")) data.update( { "is_active": True, @@ -295,7 +301,14 @@ def parse_contact_info(self, event, item): get_resource_service("contacts").post([data]) item["event_contact_info"].append(bson.ObjectId(data["_id"])) else: - logger.debug("Existing contact %s %s", contact_uri, data.get("organisation")) existing_contact_id = bson.ObjectId(existing_contact["_id"]) get_resource_service("contacts").patch(existing_contact_id, data) item["event_contact_info"].append(existing_contact_id) + + def set_expiry(self, event, provider) -> None: + expiry_minutes = ( + int(provider.get("content_expiry") if provider else 0) + or int(app.config.get("INGEST_EXPIRY_MINUTES", 0)) + or (60 * 24) + ) + event["expiry"] = event["dates"]["end"] + datetime.timedelta(minutes=(expiry_minutes)) diff --git a/server/planning/feed_parsers/onclusive_tests.py b/server/planning/feed_parsers/onclusive_tests.py index 2d1cc5dae..79918b778 100644 --- a/server/planning/feed_parsers/onclusive_tests.py +++ b/server/planning/feed_parsers/onclusive_tests.py @@ -97,17 +97,23 @@ def test_content(self): data = deepcopy(self.data) data["pressContacts"][0]["pressContactEmail"] = "foo@example.com" + data["pressContacts"][0].pop("pressContactTelephone") + data["pressContacts"][0]["pressContactName"] = "Foo Bar" item = OnclusiveFeedParser().parse([data])[0] self.assertIsInstance(item["event_contact_info"][0], bson.ObjectId) contact = superdesk.get_resource_service("contacts").find_one(req=None, _id=item["event_contact_info"][0]) self.assertEqual(1, superdesk.get_resource_service("contacts").find({}).count()) self.assertEqual(["foo@example.com"], contact["contact_email"]) + self.assertEqual([], contact["contact_phone"]) + self.assertEqual("Foo", contact["first_name"]) self.assertEqual(item["occur_status"]["qcode"], "eocstat:eos5") - [data][0]["isProvisional"] = True + data["isProvisional"] = True item = OnclusiveFeedParser().parse([data])[0] self.assertEqual(item["occur_status"]["qcode"], "eocstat:eos3") + self.assertGreater(item["expiry"], item["dates"]["end"]) + def test_content_no_time(self): data = self.data.copy() data["time"] = "" diff --git a/server/planning/feeding_services/onclusive_api_service.py b/server/planning/feeding_services/onclusive_api_service.py index 81e300cc6..21131408f 100644 --- a/server/planning/feeding_services/onclusive_api_service.py +++ b/server/planning/feeding_services/onclusive_api_service.py @@ -109,8 +109,6 @@ def _update(self, provider, update): update["tokens"]["import_finished"] = None update["tokens"]["date"] = "" - reingesting = update["tokens"].get("reingesting") - if update["tokens"].get("import_finished"): # populate it for cases when import was done before we introduced the field update["tokens"].setdefault("next_start", update["tokens"]["import_finished"] - timedelta(hours=5)) @@ -157,15 +155,12 @@ def _update(self, provider, update): logger.info("Onclusive returned %d items", len(items)) for item in items: item.setdefault("language", self.language) - if reingesting: - item["versioncreated"] += timedelta(seconds=1) # bump versioncreated to trigger an update if items: yield items update["tokens"][iterations_param] = i else: # there was no break so we are done update["tokens"]["import_finished"] = utcnow() - update["tokens"]["reingesting"] = False except SoftTimeLimitExceeded: logger.warning("stopped due to time limit, tokens=%s", update["tokens"]) diff --git a/server/planning/feeding_services/onclusive_api_service_tests.py b/server/planning/feeding_services/onclusive_api_service_tests.py index 22a1a03f0..c2281cf61 100644 --- a/server/planning/feeding_services/onclusive_api_service_tests.py +++ b/server/planning/feeding_services/onclusive_api_service_tests.py @@ -105,6 +105,4 @@ def test_reingest(self, lock_touch): items = list(self.service._update(self.provider, updates)) assert 10 == len(items) assert 1 == len(items[0]) - assert items[0][0]["versioncreated"].isoformat() > self.event["versioncreated"] assert updates["tokens"]["import_finished"] - assert not updates["tokens"]["reingesting"] diff --git a/server/planning/planning/planning.py b/server/planning/planning/planning.py index 9ecbd2826..e497d9aa2 100644 --- a/server/planning/planning/planning.py +++ b/server/planning/planning/planning.py @@ -207,7 +207,7 @@ def on_created(self, docs): added_agendas=doc.get("agendas") or [], removed_agendas=[], session=session_id, - event_ids=get_related_event_ids_for_planning(doc), # Event IDs for both primary and secondary events + event_ids=get_related_event_ids_for_planning(doc, "primary"), # Event IDs for primary events ) self._update_event_history(doc) planning_created.send(self, item=doc) @@ -458,6 +458,9 @@ def on_updated(self, updates, original, from_ingest=False): item_id = str(original[config.ID_FIELD]) session_id = get_auth().get(config.ID_FIELD) user_id = str(updates.get("version_creator", "")) + doc = deepcopy(original) + doc.update(updates) + push_notification( "planning:updated", item=item_id, @@ -465,10 +468,9 @@ def on_updated(self, updates, original, from_ingest=False): added_agendas=added, removed_agendas=removed, session=session_id, + event_ids=get_related_event_ids_for_planning(doc, "primary"), ) - doc = deepcopy(original) - doc.update(updates) self.generate_related_assignments([doc]) updates["coverages"] = doc.get("coverages") or [] @@ -480,7 +482,7 @@ def on_updated(self, updates, original, from_ingest=False): user=user_id, lock_session=session_id, etag=updates["_etag"], - event_ids=get_related_event_ids_for_planning(doc), # Event IDs for both primary and secondary events, + event_ids=get_related_event_ids_for_planning(doc, "primary"), # Event IDs for primary events, recurrence_id=original.get("recurrence_id") or None, from_ingest=from_ingest, ) diff --git a/server/planning/tests/events_service_test.py b/server/planning/tests/events_service_test.py new file mode 100644 index 000000000..daa72b041 --- /dev/null +++ b/server/planning/tests/events_service_test.py @@ -0,0 +1,31 @@ +from datetime import datetime, timedelta + +from planning.events.events import EventsService +from planning.types import Event + + +def test_is_new_version(): + service = EventsService() + + new_event: Event = {"versioncreated": datetime.now()} + old_event: Event = {"versioncreated": datetime.now() - timedelta(days=1)} + + assert service.is_new_version(new_event, old_event) + + new_event = {"versioncreated": datetime.now()} + old_event = new_event.copy() + + assert not service.is_new_version(new_event, old_event) + + new_event["subject"] = [{"qcode": "foo"}] + old_event["subject"] = [{"qcode": "bar"}] + + assert service.is_new_version(new_event, old_event) + + +def test_should_update(): + service = EventsService() + new_event: Event = {"versioncreated": datetime.now()} + old_event: Event = new_event.copy() + + assert service.should_update(old_event, new_event, provider={}) diff --git a/server/requirements.txt b/server/requirements.txt index 6dd981d6a..085a5941c 100644 --- a/server/requirements.txt +++ b/server/requirements.txt @@ -8,7 +8,7 @@ wooper==0.4.4 requests requests-mock==1.12.1 icalendar>=4.0.3,<5.1 -coverage==7.5.1 +coverage==7.5.3 deepdiff coveralls mock diff --git a/setup.py b/setup.py index a1c6da75d..eb27bb628 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ setup( name="superdesk-planning", - version="2.7.0-dev", + version="2.8.0-dev0", description=DESCRIPTION, long_description=DESCRIPTION, package_dir={"": "server"},