From 38baeabd2c26ac79341b1db78cd6f087f78a3e4d Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Fri, 23 Aug 2024 15:15:38 +0300 Subject: [PATCH 1/9] Store location translations, render location name for selected language (#2067) --- .../GeoLookupInput/AddGeoLookupInput.tsx | 2 ++ .../AddGeoLookupResultsPopUp.tsx | 36 +++++++++---------- .../GeoLookupInput/LocationItem.tsx | 9 +++-- client/components/UI/List/ActionMenu.tsx | 22 ++++++------ client/components/fields/editor/Location.tsx | 2 +- client/interfaces.ts | 1 + 6 files changed, 39 insertions(+), 33 deletions(-) diff --git a/client/components/GeoLookupInput/AddGeoLookupInput.tsx b/client/components/GeoLookupInput/AddGeoLookupInput.tsx index e8f46906b..1ae491ad1 100644 --- a/client/components/GeoLookupInput/AddGeoLookupInput.tsx +++ b/client/components/GeoLookupInput/AddGeoLookupInput.tsx @@ -230,6 +230,7 @@ export class AddGeoLookupInput extends React.Component { qcode: location.guid, address: location.address, details: location.details, + translations: location.translations, }; // external address might not be there. @@ -274,6 +275,7 @@ export class AddGeoLookupInput extends React.Component { {initialValue?.name == null ? null : ( { } handleEnterKey() { + const {localSuggests, suggests} = this.props; + const localSuggestLen = localSuggests?.length ?? 0; + if (this.state.activeOptionIndex > -1) { - if (this.state.activeOptionIndex < get(this.props.localSuggests, 'length', -1)) { - this.props.onChange(this.props.localSuggests[this.state.activeOptionIndex]); + if (this.state.activeOptionIndex < (localSuggests.length ?? -1)) { + this.props.onChange(localSuggests[this.state.activeOptionIndex]); return; } - if (this.state.activeOptionIndex === get(this.props.localSuggests, 'length', 0)) { + if (this.state.activeOptionIndex === localSuggestLen) { this.onSearch(); return; } - if (this.state.activeOptionIndex >= get(this.props.localSuggests, 'length', 0) + 1) { - this.props.onChange(this.props.suggests[ - this.state.activeOptionIndex - (get(this.props.localSuggests, 'length', 0) + 1)]); + if (this.state.activeOptionIndex >= localSuggestLen + 1) { + this.props.onChange(suggests[this.state.activeOptionIndex - localSuggestLen + 1]); } } } @@ -100,8 +102,8 @@ export class AddGeoLookupResultsPopUp extends React.Component { handleDownArrowKey() { if (this.state.activeOptionIndex < (1 + // External search button - get(this.props.localSuggests, 'length', 0) + - get(this.props.suggests, 'length', 0)) - 1 + (this.props.localSuggests?.length ?? 0) + + (this.props.suggests?.length ?? 0)) - 1 ) { this.setState({activeOptionIndex: this.state.activeOptionIndex + 1}); @@ -141,10 +143,8 @@ export class AddGeoLookupResultsPopUp extends React.Component { render() { const {gettext} = superdeskApi.localization; - const localSuggests = get(this.props.localSuggests, 'length') > 0 ? - this.props.localSuggests : []; - const suggests = get(this.props.suggests, 'length') > 0 ? - this.props.suggests : []; + const localSuggests = this.props.localSuggests ?? []; + const suggests = this.props.suggests ?? []; const tabLabels = [( { active={index === this.state.activeOptionIndex} /> ))} - {get(localSuggests, 'length') === 0 && ( + {localSuggests.length === 0 && (
  • {gettext('No results found')}
  • @@ -220,14 +220,12 @@ export class AddGeoLookupResultsPopUp extends React.Component { key={index} location={suggest} onClick={this.props.onChange.bind(null, suggest)} - active={( - index + - get(this.props.localSuggests, 'length', 0) + - 1 - ) === this.state.activeOptionIndex} + active={(index + localSuggests.length + 1) + === this.state.activeOptionIndex + } /> ))} - {get(suggests, 'length') === 0 && ( + {suggests.length === 0 && (
  • {gettext('No results found')}
  • diff --git a/client/components/GeoLookupInput/LocationItem.tsx b/client/components/GeoLookupInput/LocationItem.tsx index b1dda096b..01165d1f4 100644 --- a/client/components/GeoLookupInput/LocationItem.tsx +++ b/client/components/GeoLookupInput/LocationItem.tsx @@ -16,6 +16,7 @@ interface IProps { active?: boolean; readOnly?: boolean; onRemoveLocation?(): void; + languageCode?: string; } export class LocationItem extends React.PureComponent { @@ -23,6 +24,10 @@ export class LocationItem extends React.PureComponent { const {gettext} = superdeskApi.localization; const location = this.props.location; + const locationNameComputed = this.props.languageCode + ? location.translations?.name[`name:${this.props.languageCode}`] ?? location.name + : location.name; + return ( { {(this.props.readOnly || this.props.onRemoveLocation == null) ? null : ( diff --git a/client/components/UI/List/ActionMenu.tsx b/client/components/UI/List/ActionMenu.tsx index 0a171d6bf..e337c6c2b 100644 --- a/client/components/UI/List/ActionMenu.tsx +++ b/client/components/UI/List/ActionMenu.tsx @@ -1,24 +1,24 @@ import React from 'react'; -import PropTypes from 'prop-types'; import classNames from 'classnames'; +interface IProps { + children: React.ReactNode; + row?: boolean; + className?: string; +} /** * @ngdoc react * @name ActionMenu * @description Component to encapsulate three-dot action menu in list a item */ -export const ActionMenu = ({children, row}) => ( +export const ActionMenu = ({children, className, row = true}: IProps) => (
    {children}
    ); - -ActionMenu.propTypes = { - children: PropTypes.node.isRequired, - row: PropTypes.bool, -}; - -ActionMenu.defaultProps = {row: true}; diff --git a/client/components/fields/editor/Location.tsx b/client/components/fields/editor/Location.tsx index 4a6ca1b8a..6761bde71 100644 --- a/client/components/fields/editor/Location.tsx +++ b/client/components/fields/editor/Location.tsx @@ -22,7 +22,7 @@ export class EditorFieldLocation extends React.PureComponent { {...this.props} field={field} label={this.props.label ?? gettext('Location')} - value={get(this.props.item, field, this.props.defaultValue)} + value={this.props.item[field] ?? this.props.defaultValue} disableSearch={!this.props.enableExternalSearch} disableAddLocation={this.props.disableAddLocation} readOnly={this.props.disabled} diff --git a/client/interfaces.ts b/client/interfaces.ts index 07a5e666a..0b1188fd2 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -389,6 +389,7 @@ export interface IEventLocation { lat: number; lon: number; }; + translations?: ILocation['translations']; } export interface IItemAction { From a45af311adcc76f6a9fcd2bf45854d4cfaec6a05 Mon Sep 17 00:00:00 2001 From: Ketan <73937490+devketanpro@users.noreply.github.com> Date: Tue, 27 Aug 2024 09:32:38 +0530 Subject: [PATCH 2/9] Assignment Mails : use event_start time in ICS event based on config [SDBELGA-862] (#2069) * Assignment Mail : use event_start time in ICS event based on config [SDBELGA-862] * refactore logic --- server/planning/assignments/assignments.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/server/planning/assignments/assignments.py b/server/planning/assignments/assignments.py index 9a8889704..309d28f1e 100644 --- a/server/planning/assignments/assignments.py +++ b/server/planning/assignments/assignments.py @@ -430,8 +430,16 @@ def send_assignment_notification(self, updates, original=None, force=False): event = Event() event["UID"] = UID event["CLASS"] = "PUBLIC" - event["DTSTART"] = scheduled_time.strftime("%Y%m%dT%H%M%SZ") - event["DTEND"] = scheduled_time.strftime("%Y%m%dT%H%M%SZ") + + # Use Event start and End time based on Config + if app.config.get("ASSIGNMENT_MAIL_ICAL_USE_EVENT_DATES") and event_item: + event_dates = event_item["dates"] + event["DTSTART"] = event_dates["start"].strftime("%Y%m%dT%H%M%SZ") + event["DTEND"] = event_dates["end"].strftime("%Y%m%dT%H%M%SZ") + else: + event["DTSTART"] = scheduled_time.strftime("%Y%m%dT%H%M%SZ") + event["DTEND"] = scheduled_time.strftime("%Y%m%dT%H%M%SZ") + event[f"SUMMARY;LANGUAGE={language}"] = summary event["DESCRIPTION"] = assignment.get("description_text", "") event["PRIORITY"] = priority From e82f5812b49035959e2356167f60bec4788505a5 Mon Sep 17 00:00:00 2001 From: Ketan <73937490+devketanpro@users.noreply.github.com> Date: Wed, 28 Aug 2024 11:42:44 +0530 Subject: [PATCH 3/9] Assignment mails - Language specific mail based on the language of the coverage [SDBELGA-866] (#2071) * Assignment mails - Language specific mail based on the language of the coverage [SDBELGA-866] * minor change --- server/planning/assignments/assignments.py | 8 +++++++- server/planning/utils.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/server/planning/assignments/assignments.py b/server/planning/assignments/assignments.py index 309d28f1e..76790564a 100644 --- a/server/planning/assignments/assignments.py +++ b/server/planning/assignments/assignments.py @@ -67,7 +67,7 @@ from planning.common import format_address, get_assginment_name from apps.content import push_content_notification from .assignments_history import ASSIGNMENT_HISTORY_ACTIONS -from planning.utils import get_event_formatted_dates, get_formatted_contacts +from planning.utils import get_event_formatted_dates, get_formatted_contacts, update_event_item_with_translations_value from superdesk.preferences import get_user_notification_preferences logger = logging.getLogger(__name__) @@ -472,6 +472,12 @@ def send_assignment_notification(self, updates, original=None, force=False): formatted_contacts = get_formatted_contacts(event_item) if event_item else [] fomatted_event_date = get_event_formatted_dates(event_item) if event_item else "" + event_item = ( + update_event_item_with_translations_value(event_item, assignment.get("planning", {}).get("language")) + if event_item + else None + ) + # The assignment is to an external contact or a user if assigned_to.get("contact") or assigned_to.get("user"): # If it is a reassignment diff --git a/server/planning/utils.py b/server/planning/utils.py index 07679f0f2..0d24ac182 100644 --- a/server/planning/utils.py +++ b/server/planning/utils.py @@ -100,3 +100,14 @@ def get_event_formatted_dates(event: Dict[str, Any]) -> str: return "{} {}".format(time_short(start, tz), date_short(start, tz)) return "{} - {}, {}".format(time_short(start, tz), time_short(end, tz), date_short(start, tz)) + + +def update_event_item_with_translations_value(event_item: Dict[str, Any], language: str) -> Dict[str, Any]: + if not event_item.get("translations") or not language: + return event_item + updated_event_item = event_item.copy() + for translation in event_item["translations"]: + if translation["language"] == language: + updated_event_item[translation["field"]] = translation["value"] + + return updated_event_item From 5788ebc8b2e7e183b7c4db48030ee64e554584f7 Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Wed, 28 Aug 2024 13:49:48 +0300 Subject: [PATCH 4/9] Adjust max height, vertical position (#2072) --- client/components/Coverages/CoverageIcons.tsx | 2 +- client/components/Coverages/coverage-icons.scss | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/components/Coverages/CoverageIcons.tsx b/client/components/Coverages/CoverageIcons.tsx index a24bb7582..08386ef01 100644 --- a/client/components/Coverages/CoverageIcons.tsx +++ b/client/components/Coverages/CoverageIcons.tsx @@ -94,7 +94,7 @@ export class CoverageIcons extends React.PureComponent { return ( (
    diff --git a/client/components/Coverages/coverage-icons.scss b/client/components/Coverages/coverage-icons.scss index 02ba64789..2f6791cb9 100644 --- a/client/components/Coverages/coverage-icons.scss +++ b/client/components/Coverages/coverage-icons.scss @@ -3,7 +3,7 @@ border-radius: var(--b-radius--medium); padding: 1.5rem; box-shadow: var(--sd-shadow__dropdown); - max-height: 100%; + max-height: 400px; overflow: auto; } From ffd3ce25ba6a79a30263e904a8cd470eb7aa1355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Ja=C5=A1ek?= Date: Mon, 2 Sep 2024 09:29:22 +0200 Subject: [PATCH 5/9] fix planning parser error (#2066) https://sentry.sourcefabric.org/share/issue/4e6405ba79c04368a63c46c8fd154b09/ --- server/planning/feed_parsers/superdesk_planning_xml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/planning/feed_parsers/superdesk_planning_xml.py b/server/planning/feed_parsers/superdesk_planning_xml.py index 54041f797..f3dbed37d 100644 --- a/server/planning/feed_parsers/superdesk_planning_xml.py +++ b/server/planning/feed_parsers/superdesk_planning_xml.py @@ -225,7 +225,7 @@ def parse_coverage_planning(self, news_coverage_elt, item): if planning_elt is not None: headline_elt = planning_elt.find(self.qname("headline")) content = planning_elt.find(self.qname("itemClass")).get("qcode") - planning = {"slugline": headline_elt.text.strip(), "g2_content_type": content.split(":")[1]} + planning = {"slugline": (headline_elt.text or "").strip(), "g2_content_type": content.split(":")[1]} description_elt = planning_elt.find(self.qname("description")) if description_elt is not None and description_elt.text: From a36c67369948a95a3470dffb6a9c04d13debd188 Mon Sep 17 00:00:00 2001 From: Ketan <73937490+devketanpro@users.noreply.github.com> Date: Tue, 10 Sep 2024 15:06:44 +0530 Subject: [PATCH 6/9] Tooltip not displaying on coverage icon if an event/planning item was created using 'duplicate' option [CPCN-899] (#2079) * add coverage assignee details and coverage status on duplicate planning * added a new config and make changes according config * fix tests --- README.md | 3 +++ server/features/planning_duplicate.feature | 18 +++++++++--------- server/planning/common.py | 4 ++++ server/planning/planning/planning_duplicate.py | 15 ++++++++++++--- server/settings.py | 2 ++ 5 files changed, 30 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 8aa912cc7..0c12b9e8e 100644 --- a/README.md +++ b/README.md @@ -187,6 +187,9 @@ Below sections include the config options that can be defined in settings.py. * language (includes `languages` if multilingual is enabled) * definition_short (copies to Planning item's `Description Text`) * priority +* PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS + * Default: False (the current behavior where assignee details are removed) + * If true, the `assigned_to` field (assignee details) is retained when duplicating planning items with coverages. ### Assignments Config * SLACK_BOT_TOKEN diff --git a/server/features/planning_duplicate.feature b/server/features/planning_duplicate.feature index 66f36e792..0dd0f6a71 100644 --- a/server/features/planning_duplicate.feature +++ b/server/features/planning_duplicate.feature @@ -1,6 +1,6 @@ Feature: Duplicate Planning - @auth @notification + @auth @notification @vocabulary Scenario: Duplicate a Planning item When we post to "planning" with success """ @@ -149,7 +149,7 @@ Feature: Duplicate Planning ] """ - @auth + @auth @vocabulary Scenario: Planning can only be duplicated by user having privileges When we post to "planning" with success """ @@ -184,7 +184,7 @@ Feature: Duplicate Planning Then we get OK response - @auth @notification + @auth @notification @vocabulary Scenario: Coverage workflow_status defaults to draft on duplication item When we post to "planning" with success """ @@ -279,7 +279,7 @@ Feature: Duplicate Planning } """ - @auth @notification + @auth @notification @vocabulary Scenario: Duplicating a posted Planning item won't repost it When we post to "planning" with success """ @@ -333,7 +333,7 @@ Feature: Duplicate Planning ]} """ - @auth + @auth @vocabulary Scenario: Duplicate a past planning item will have current date Given "planning" """ @@ -377,7 +377,7 @@ Feature: Duplicate Planning {"expired": "__no_value__"} """ - @auth + @auth @vocabulary Scenario: Duplicating a Planning item will link to the same Event Given "events" """ @@ -423,7 +423,7 @@ Feature: Duplicate Planning } """ - @auth + @auth @vocabulary Scenario: Duplicating an expired Planning item will remove the link to the Event Given "events" """ @@ -471,7 +471,7 @@ Feature: Duplicate Planning } """ - @auth + @auth @vocabulary Scenario: Duplicating a canceled Planning item will clear the state_reason Given "events" """ @@ -542,7 +542,7 @@ Feature: Duplicate Planning } """ - @auth + @auth @vocabulary Scenario: Duplicating a rescheduled Planning item will clear the state_reason Given "events" """ diff --git a/server/planning/common.py b/server/planning/common.py index d53f8f5ad..717dcbdc0 100644 --- a/server/planning/common.py +++ b/server/planning/common.py @@ -267,6 +267,10 @@ def get_default_coverage_status_qcode_on_ingest(current_app=None): return (current_app or app).config.get("PLANNING_DEFAULT_COVERAGE_STATUS_ON_INGEST", "ncostat:int") +def get_config_planning_duplicate_retain_assignee_details(current_app=None): + return (current_app or app).config.get("PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS", False) + + def get_coverage_status_from_cv(qcode: str): coverage_states = get_resource_service("vocabularies").find_one(req=None, _id="newscoveragestatus") diff --git a/server/planning/planning/planning_duplicate.py b/server/planning/planning/planning_duplicate.py index 2de22c6ba..1d6c55b63 100644 --- a/server/planning/planning/planning_duplicate.py +++ b/server/planning/planning/planning_duplicate.py @@ -17,7 +17,13 @@ from superdesk.metadata.item import GUID_NEWSML from superdesk.utc import utcnow, utc_to_local from flask import request -from planning.common import ITEM_STATE, WORKFLOW_STATE, TEMP_ID_PREFIX +from planning.common import ( + ITEM_STATE, + WORKFLOW_STATE, + TEMP_ID_PREFIX, + get_coverage_status_from_cv, + get_config_planning_duplicate_retain_assignee_details, +) from copy import deepcopy @@ -99,12 +105,15 @@ def _duplicate_planning(self, original): new_plan["planning_date"] = new_plan["planning_date"] + (local_datetime.date() - planning_datetime.date()) for cov in new_plan.get("coverages") or []: - cov.pop("assigned_to", None) cov.get("planning", {}).pop("workflow_status_reason", None) cov.pop("scheduled_updates", None) cov.get("planning", {})["scheduled"] = new_plan.get("planning_date") cov["coverage_id"] = TEMP_ID_PREFIX + "duplicate" cov["workflow_status"] = WORKFLOW_STATE.DRAFT - cov["news_coverage_status"] = {"qcode": "ncostat:int"} + cov["news_coverage_status"] = get_coverage_status_from_cv("ncostat:int") + cov["news_coverage_status"].pop("is_active", None) + + if not get_config_planning_duplicate_retain_assignee_details(): + cov.pop("assigned_to", None) return new_plan diff --git a/server/settings.py b/server/settings.py index 3518d27a4..4c1064807 100644 --- a/server/settings.py +++ b/server/settings.py @@ -189,3 +189,5 @@ def env(variable, fallback_value=None): ELASTICSEARCH_AUTO_AGGREGATIONS = False PLANNING_DEFAULT_COVERAGE_STATUS_ON_INGEST = "ncostat:int" + +PLANNING_DUPLICATE_RETAIN_ASSIGNEE_DETAILS = False From c8719c40d86f5653e2782ab147f07ab394654ba8 Mon Sep 17 00:00:00 2001 From: Nikola Stojanovic <68916411+dzonidoo@users.noreply.github.com> Date: Tue, 10 Sep 2024 16:37:54 +0200 Subject: [PATCH 7/9] Event and Planning profile editor doesn't display Groups in Fields (#2074) * add variant prop on toggleBox component * scrolable list style --- .../components/ContentProfiles/FieldTab/FieldList.tsx | 1 + .../ContentProfiles/GroupTab/GroupEditor.tsx | 1 + client/components/SortItems/style.scss | 10 ++++++++++ 3 files changed, 12 insertions(+) diff --git a/client/components/ContentProfiles/FieldTab/FieldList.tsx b/client/components/ContentProfiles/FieldTab/FieldList.tsx index 137a8fae6..5f3c635fc 100644 --- a/client/components/ContentProfiles/FieldTab/FieldList.tsx +++ b/client/components/ContentProfiles/FieldTab/FieldList.tsx @@ -194,6 +194,7 @@ export class FieldList extends React.PureComponent { this.renderList() ) : ( {
    {!this.props.languages?.length ? null : ( Date: Thu, 12 Sep 2024 12:01:22 +0300 Subject: [PATCH 8/9] Update to v4 (#2082) --- .github/workflows/ci-e2e.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-e2e.yml b/.github/workflows/ci-e2e.yml index bfbb17250..6bcb6b1b6 100644 --- a/.github/workflows/ci-e2e.yml +++ b/.github/workflows/ci-e2e.yml @@ -47,7 +47,7 @@ jobs: CYPRESS_SCREENSHOTS_FOLDER: /tmp/cypress - name: Upload screenshots if: ${{ failure() }} - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: screenshots-e2e-${{ matrix.e2e }} path: /tmp/cypress/**/*.png From 55f1293b0bd9b1ea96724715f36bc9235df1129a Mon Sep 17 00:00:00 2001 From: Konstantin Markov Date: Thu, 12 Sep 2024 12:11:38 +0300 Subject: [PATCH 9/9] 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: