From 55f1293b0bd9b1ea96724715f36bc9235df1129a Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Thu, 12 Sep 2024 12:11:38 +0300 Subject: [PATCH] Fix closing items from the bottom bar in planning module (#2070) --- client/actions/agenda.ts | 3 +- client/actions/assignments/notifications.ts | 4 +- client/actions/main.ts | 41 +++++++++++-------- client/actions/planning/api.ts | 2 +- client/actions/planning/notifications.ts | 3 +- client/actions/planning/tests/api_test.ts | 4 +- .../planning/tests/notifications_test.ts | 5 +-- client/api/combined.ts | 17 +++++++- client/api/events.ts | 6 +-- client/api/planning.ts | 10 ++--- client/api/search.ts | 28 +++++++++++-- .../GeoLookupInput/AddGeoLookupInput.tsx | 2 +- client/components/IgnoreCancelSaveModal.tsx | 2 +- .../forms/updateRecurringEventsForm.tsx | 2 +- client/components/UI/SearchBar/index.tsx | 2 +- client/components/UI/SearchField/index.tsx | 2 +- client/interfaces.ts | 1 + server/planning/planning_locks.py | 4 +- 18 files changed, 89 insertions(+), 49 deletions(-) diff --git a/client/actions/agenda.ts b/client/actions/agenda.ts index e2157511b..c646475c7 100644 --- a/client/actions/agenda.ts +++ b/client/actions/agenda.ts @@ -9,6 +9,7 @@ import {AGENDA, MODALS, EVENTS} from '../constants'; import {getErrorMessage, gettext, planningUtils} from '../utils'; import {planning, showModal, main} from './index'; import {convertStringFields} from '../utils/strings'; +import planningApis from '../actions/planning/api'; const openAgenda = () => ( (dispatch) => ( @@ -302,7 +303,7 @@ const createPlanningFromEvent = ( newPlanningItem.agendas = newPlanningItem.agendas.concat(agendas); return (dispatch) => ( - dispatch(planning.api.save({}, newPlanningItem)) + dispatch(planningApis.save({}, newPlanningItem)) ); }; diff --git a/client/actions/assignments/notifications.ts b/client/actions/assignments/notifications.ts index ce0bb89f0..d19d81077 100644 --- a/client/actions/assignments/notifications.ts +++ b/client/actions/assignments/notifications.ts @@ -9,9 +9,9 @@ import {lockUtils, assignmentUtils, gettext, isExistingItem} from '../../utils'; import * as selectors from '../../selectors'; import assignments from './index'; import main from '../main'; -import planning from '../planning'; import {hideModal, showModal} from '../index'; import * as actions from '../../actions'; +import planningApis from '../planning/api'; const _notifyAssignmentEdited = (assignmentId) => ( (dispatch, getState, {notify}) => { @@ -191,7 +191,7 @@ const _updatePlannigRelatedToAssignment = (data) => ( return Promise.resolve(); } - dispatch(planning.api.loadPlanningByIds([data.planning])); + dispatch(planningApis.loadPlanningByIds([data.planning])); dispatch(main.fetchItemHistory(planningItem)); } ); diff --git a/client/actions/main.ts b/client/actions/main.ts index 95718be86..0c0e0d96c 100644 --- a/client/actions/main.ts +++ b/client/actions/main.ts @@ -1,7 +1,10 @@ import {get, isEmpty, isEqual, isNil, omit} from 'lodash'; import moment from 'moment'; -import {appConfig} from 'appConfig'; +import {appConfig as config} from 'appConfig'; + +const appConfig = config as IPlanningConfig; + import {IUser} from 'superdesk-api'; import {planningApi, superdeskApi} from '../superdeskApi'; import { @@ -18,6 +21,7 @@ import { ITEM_TYPE, IEventTemplate, IEventItem, + IPlanningConfig, } from '../interfaces'; import { @@ -68,6 +72,7 @@ import eventsPlanningUi from './eventsPlanning/ui'; import * as selectors from '../selectors'; import {validateItem} from '../validators'; import {searchParamsToOld} from '../utils/search'; +import {searchAndStore} from '../api/combined'; function openForEdit(item: IEventOrPlanningItem, updateUrl: boolean = true, modal: boolean = false) { return (dispatch, getState) => { @@ -716,7 +721,7 @@ const openIgnoreCancelSaveModal = ({ const storedItems = itemType === ITEM_TYPE.EVENT ? selectors.events.storedEvents(getState()) : selectors.planning.storedPlannings(getState()); - const item = get(storedItems, itemId) || {}; + const item = storedItems[itemId] ?? {}; if (!isExistingItem(item)) { delete item._id; @@ -725,19 +730,17 @@ const openIgnoreCancelSaveModal = ({ let promise = Promise.resolve(item); if (itemType === ITEM_TYPE.EVENT && eventUtils.isEventRecurring(item)) { - const originalEvent = get(storedItems, itemId, {}); - - promise = dispatch(eventsApi.query({ - recurrenceId: originalEvent.recurrence_id, - maxResults: appConfig.max_recurrent_events, - onlyFuture: false, - })) - .then((relatedEvents) => ({ - ...item, - _recurring: relatedEvents || [item], - _events: [], - _originalEvent: originalEvent, - })); + promise = searchAndStore({ + recurrence_id: item.recurrence_id, + max_results: appConfig.max_recurrent_events, + only_future: false, + include_associated_planning: true, + }).then((relatedEvents) => ({ + ...item, + _recurring: relatedEvents.filter((item) => item.type === 'event') ?? [item], + _events: [], + _originalEvent: item, + })); } return promise.then((itemWithAssociatedData) => ( @@ -1200,8 +1203,10 @@ const openFromLockActions = () => ( if (action) { /* get the item we're operating on */ dispatch(self.fetchById(sessionLastLock.item_id, sessionLastLock.item_type)).then((item) => { - actionUtils.getActionDispatches({dispatch: dispatch, eventOnly: false, - planningOnly: false})[action[0].actionName](item, false, false); + actionUtils.getActionDispatches({ + dispatch: dispatch, eventOnly: false, + planningOnly: false + })[action[0].actionName](item, false, false); }); } } @@ -1460,7 +1465,7 @@ function onItemUnlocked( })); if (getItemType(item) === ITEM_TYPE.PLANNING && selectors.general.currentWorkspace(state) - === WORKSPACE.AUTHORING) { + === WORKSPACE.AUTHORING) { dispatch(self.closePreviewAndEditorForItems([item])); } } diff --git a/client/actions/planning/api.ts b/client/actions/planning/api.ts index 3fa242e1c..4cf20cd8c 100644 --- a/client/actions/planning/api.ts +++ b/client/actions/planning/api.ts @@ -486,7 +486,7 @@ const unpost = (original, updates) => ( * Also loads all the associated contacts (if any) * @param {array, object} plannings - An array of planning item objects */ -const receivePlannings = (plannings) => ( +const receivePlannings = (plannings): any => ( (dispatch) => { dispatch(actions.contacts.fetchContactsFromPlanning(plannings)); dispatch({ diff --git a/client/actions/planning/notifications.ts b/client/actions/planning/notifications.ts index cfae83f1b..3522f9e6b 100644 --- a/client/actions/planning/notifications.ts +++ b/client/actions/planning/notifications.ts @@ -14,6 +14,7 @@ import {events, fetchAgendas} from '../index'; import main from '../main'; import {showModal, hideModal} from '../index'; import eventsPlanning from '../eventsPlanning'; +import planningApis from '../planning/api'; /** * WS Action when a new Planning item is created @@ -104,7 +105,7 @@ const onPlanningLocked = (e, data) => ( const sessionId = selectors.general.session(getState()).sessionId; - return dispatch(planning.api.getPlanning(data.item, false)) + return dispatch(planningApis.getPlanning(data.item, false)) .then((planInStore) => { let plan = { ...planInStore, diff --git a/client/actions/planning/tests/api_test.ts b/client/actions/planning/tests/api_test.ts index a3d48acfa..2da3826e2 100644 --- a/client/actions/planning/tests/api_test.ts +++ b/client/actions/planning/tests/api_test.ts @@ -6,10 +6,8 @@ import { restoreSinonStub, convertEventDatesToMoment, } from '../../../utils/testUtils'; -import {createTestStore} from '../../../utils'; -import {PLANNING, SPIKED_STATE, WORKFLOW_STATE} from '../../../constants'; +import {PLANNING, SPIKED_STATE} from '../../../constants'; import {MAIN} from '../../../constants'; -import * as selectors from '../../../selectors'; import contactsApi from '../../contacts'; import {planningApis} from '../../../api'; diff --git a/client/actions/planning/tests/notifications_test.ts b/client/actions/planning/tests/notifications_test.ts index 79fcfbff2..27b2553a2 100644 --- a/client/actions/planning/tests/notifications_test.ts +++ b/client/actions/planning/tests/notifications_test.ts @@ -1,5 +1,5 @@ import {planningApi} from '../../../superdeskApi'; -import planningApis from '../api'; +import planningApis from '../../planning/api'; import planningUi from '../ui'; import featuredPlanning from '../featuredPlanning'; import eventsPlanningUi from '../../eventsPlanning/ui'; @@ -197,7 +197,7 @@ describe('actions.planning.notifications', () => { describe('onPlanningLocked', () => { beforeEach(() => { sinon.stub(planningApi.locks, 'setItemAsLocked').returns(undefined); - sinon.stub(planningApis, 'getPlanning').returns(Promise.resolve(data.plannings[0])); + sinon.stub(planningApis, 'getPlanning').returns(() => Promise.resolve(data.plannings[0])); }); afterEach(() => { @@ -219,7 +219,6 @@ describe('actions.planning.notifications', () => { )) .then(() => { expect(planningApi.locks.setItemAsLocked.callCount).toBe(1); - expect(planningApis.getPlanning.callCount).toBe(1); expect(planningApis.getPlanning.args[0]).toEqual([ 'p1', false, diff --git a/client/api/combined.ts b/client/api/combined.ts index cca8ece5f..0ba285da8 100644 --- a/client/api/combined.ts +++ b/client/api/combined.ts @@ -8,11 +8,11 @@ import { IEventOrPlanningItem, } from '../interfaces'; import {IRestApiResponse} from 'superdesk-api'; -import {searchRaw, searchRawGetAll, convertCommonParams, cvsToString, arrayToString} from './search'; +import {searchRaw, searchRawGetAll, convertCommonParams, cvsToString, arrayToString, searchRawAndStore} from './search'; import {eventUtils, planningUtils} from '../utils'; import {planningApi} from '../superdeskApi'; import {combinedSearchProfile} from '../selectors/forms'; -import {searchPlanningGetAll} from './planning'; +import {searchPlanningGetAll, convertPlanningParams} from './planning'; import {searchEventsGetAll} from './events'; type IResponse = IRestApiResponse; @@ -67,6 +67,18 @@ export function searchCombinedGetAll(params: ISearchParams): Promise({ + ...convertCommonParams(params), + ...convertPlanningParams(params), + repo: FILTER_TYPE.COMBINED, + }).then((res) => { + res._items.forEach(modifyItemForClient); + + return res._items; + }); +} + export function getEventsAndPlanning(params: ISearchParams): Promise<{ events: Array; plannings: Array; @@ -145,5 +157,6 @@ export const combined: IPlanningAPI['combined'] = { getRecurringEventsAndPlanningItems: getRecurringEventsAndPlanningItems, getEventsAndPlanning: getEventsAndPlanning, getSearchProfile: getCombinedSearchProfile, + searchAndStore: searchAndStore, }; diff --git a/client/api/events.ts b/client/api/events.ts index fb029428c..398f0dac1 100644 --- a/client/api/events.ts +++ b/client/api/events.ts @@ -16,7 +16,7 @@ import {EVENTS, TEMP_ID_PREFIX} from '../constants'; import {arrayToString, convertCommonParams, cvsToString, searchRaw, searchRawGetAll} from './search'; import {eventUtils} from '../utils'; import {eventProfile, eventSearchProfile} from '../selectors/forms'; -import * as actions from '../actions'; +import planningApis from '../actions/planning/api'; const appConfig = config as IPlanningConfig; @@ -161,7 +161,7 @@ function create(updates: Partial): Promise> { }).then((planningItems) => { // Make sure to update the Redux Store with the latest Planning items // So that the Editor can set the state with these latest items - planningApi.redux.store.dispatch(actions.planning.api.receivePlannings(planningItems)); + planningApi.redux.store.dispatch(planningApis.receivePlannings(planningItems)); return events; }); @@ -207,7 +207,7 @@ function update(original: IEventItem, updates: Partial): Promise { // Make sure to update the Redux Store with the latest Planning items // So that the Editor can set the state with these latest items - planningApi.redux.store.dispatch(actions.planning.api.receivePlannings(planningItems)); + planningApi.redux.store.dispatch(planningApis.receivePlannings(planningItems)); return events; }); diff --git a/client/api/planning.ts b/client/api/planning.ts index 56f44e960..16a4ddc1a 100644 --- a/client/api/planning.ts +++ b/client/api/planning.ts @@ -21,11 +21,11 @@ import {planningProfile, planningSearchProfile} from '../selectors/forms'; import {featured} from './featured'; import {PLANNING} from '../constants'; import * as selectors from '../selectors'; -import * as actions from '../actions'; +import planningApis from '../actions/planning/api'; const appConfig = config as IPlanningConfig; -function convertPlanningParams(params: ISearchParams): Partial { +export function convertPlanningParams(params: ISearchParams): Partial { return { agendas: arrayToString(params.agendas), no_agenda_assigned: params.no_agenda_assigned, @@ -90,7 +90,7 @@ export function getPlanningById( .then(modifyItemForClient) .then((item) => { if (saveToStore) { - dispatch(actions.planning.api.receivePlannings([item])); + dispatch(planningApis.receivePlannings([item])); } return item; @@ -215,7 +215,7 @@ function bulkAddCoverageToWorkflow(planningItems: Array): Promise return planning.update(plan, updates) .then((updatedPlan) => { - dispatch(actions.planning.api.receivePlannings([updatedPlan])); + dispatch(planningApis.receivePlannings([updatedPlan])); return updatedPlan; }); @@ -254,7 +254,7 @@ function addCoverageToWorkflow( return planning.update(plan, updates) .then((updatedPlan) => { notify.success(gettext('Coverage added to workflow.')); - dispatch(actions.planning.api.receivePlannings([updatedPlan])); + dispatch(planningApis.receivePlannings([updatedPlan])); return updatedPlan; }) diff --git a/client/api/search.ts b/client/api/search.ts index 7892f84ee..d04716eae 100644 --- a/client/api/search.ts +++ b/client/api/search.ts @@ -1,10 +1,12 @@ -import {ISearchAPIParams, ISearchParams} from '../interfaces'; -import {superdeskApi} from '../superdeskApi'; +import {IEventOrPlanningItem, ISearchAPIParams, ISearchParams} from '../interfaces'; +import {superdeskApi, planningApi as sdPlanningApi} from '../superdeskApi'; import {IRestApiResponse} from 'superdesk-api'; import {getDateTimeElasticFormat, getTimeZoneOffset} from '../utils'; import {default as timeUtils} from '../utils/time'; import {appConfig} from 'appConfig'; - +import planningApi from '../actions/planning/api'; +import eventsApi from '../actions/events/api'; +import {partition} from 'lodash'; export function cvsToString(items?: Array<{[key: string]: any}>, field: string = 'qcode'): string { return arrayToString( @@ -51,6 +53,7 @@ export function convertCommonParams(params: ISearchParams): Partial(args: ISearchAPIParams): Promise(args: ISearchAPIParams) => { + const {dispatch} = sdPlanningApi.redux.store; + + return superdeskApi.dataApi.queryRawJson>( + 'events_planning_search', + excludeNullParams(args) + ).then((res) => { + const [relatedPlans, events] = partition(res._items, (item: IEventOrPlanningItem) => item.type === 'planning'); + + if (args.include_associated_planning) { + dispatch(planningApi.receivePlannings(relatedPlans)); + } + + dispatch(eventsApi.receiveEvents(events)); + + return res; + }); +}; + export function searchRawGetAll(args: ISearchAPIParams): Promise> { const params = excludeNullParams(args); let items: Array = []; diff --git a/client/components/GeoLookupInput/AddGeoLookupInput.tsx b/client/components/GeoLookupInput/AddGeoLookupInput.tsx index 1ae491ad1..c173c6abd 100644 --- a/client/components/GeoLookupInput/AddGeoLookupInput.tsx +++ b/client/components/GeoLookupInput/AddGeoLookupInput.tsx @@ -1,6 +1,6 @@ import React from 'react'; import Geolookup from 'react-geolookup'; -import DebounceInput from 'react-debounce-input'; +import {DebounceInput} from 'react-debounce-input'; import {appConfig} from 'appConfig'; import {IRestApiResponse} from 'superdesk-api'; diff --git a/client/components/IgnoreCancelSaveModal.tsx b/client/components/IgnoreCancelSaveModal.tsx index fe4eb1f45..c03a857f4 100644 --- a/client/components/IgnoreCancelSaveModal.tsx +++ b/client/components/IgnoreCancelSaveModal.tsx @@ -86,7 +86,7 @@ export class IgnoreCancelSaveModalComponent extends React.Component} onEventUpdateMethodChange={this.onEventUpdateMethodChange} onPlanningUpdateMethodChange={this.onPlanningUpdateMethodChange} modalProps={{ diff --git a/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx b/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx index 9284b4193..6591166d2 100644 --- a/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx +++ b/client/components/ItemActionConfirmation/forms/updateRecurringEventsForm.tsx @@ -75,7 +75,7 @@ function getRecurringPlanningToUpdate( updates: Partial, plannings: {[planningId: string]: IPlanningItem} ): Array { - const originalCoverages: IPlanningEmbeddedCoverageMap = (original.planning_ids || []) + const originalCoverages: IPlanningEmbeddedCoverageMap = (original.planning_ids ?? []) .map((planningId) => plannings[planningId]) .reduce((planningItems, planningItem) => { planningItems[planningItem._id] = (planningItem.coverages ?? []).reduce( diff --git a/client/components/UI/SearchBar/index.tsx b/client/components/UI/SearchBar/index.tsx index 29a4549de..2fcb42785 100644 --- a/client/components/UI/SearchBar/index.tsx +++ b/client/components/UI/SearchBar/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import DebounceInput from 'react-debounce-input'; +import {DebounceInput} from 'react-debounce-input'; import {isNil, uniqueId} from 'lodash'; import {gettext} from '../../../utils/gettext'; import './style.scss'; diff --git a/client/components/UI/SearchField/index.tsx b/client/components/UI/SearchField/index.tsx index 0983e0662..a224f19d0 100644 --- a/client/components/UI/SearchField/index.tsx +++ b/client/components/UI/SearchField/index.tsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import DebounceInput from 'react-debounce-input'; +import {DebounceInput} from 'react-debounce-input'; import {uniqueId} from 'lodash'; import {KEYCODES} from '../constants'; import {onEventCapture} from '../utils'; diff --git a/client/interfaces.ts b/client/interfaces.ts index 0b1188fd2..cd07641ce 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -2207,6 +2207,7 @@ export interface IPlanningAPI { getEditorProfile(): ICoverageFormProfile; }; combined: { + searchAndStore(params: ISearchParams): Promise>; search(params: ISearchParams): Promise>; searchGetAll(params: ISearchParams): Promise>; getRecurringEventsAndPlanningItems( diff --git a/server/planning/planning_locks.py b/server/planning/planning_locks.py index 2a0652d17..59fbcd35e 100644 --- a/server/planning/planning_locks.py +++ b/server/planning/planning_locks.py @@ -91,7 +91,7 @@ def _get_planning_module_locks(): continue lock = { - "item_id": item.get("_id") if not item.get("recurrence_id") else item["recurrence_id"], + "item_id": item.get("_id"), "item_type": item.get("type"), "user": item.get("lock_user"), "session": item.get("lock_session"), @@ -99,7 +99,7 @@ def _get_planning_module_locks(): "time": item.get("lock_time"), } if item.get("recurrence_id"): - locks["recurring"][lock["item_id"]] = lock + locks["recurring"][item["recurrence_id"]] = lock elif item.get("event_item"): locks["event"][item["event_item"]] = lock else: