diff --git a/client/actions/events/api.ts b/client/actions/events/api.ts index 24cf37a19..5aca012ec 100644 --- a/client/actions/events/api.ts +++ b/client/actions/events/api.ts @@ -1,7 +1,7 @@ -import {get, isEqual, cloneDeep, pickBy, has, find, every} from 'lodash'; +import {get, isEqual, cloneDeep, pickBy, has, find, every, take} from 'lodash'; import {planningApi} from '../../superdeskApi'; -import {ISearchSpikeState, IEventSearchParams, IEventItem, IPlanningItem} from '../../interfaces'; +import {ISearchSpikeState, IEventSearchParams, IEventItem, IPlanningItem, IEventTemplate} from '../../interfaces'; import {appConfig} from 'appConfig'; import { @@ -689,6 +689,7 @@ const createEventTemplate = (itemId) => (dispatch, getState, {api, modal, notify }) .then(() => { dispatch(fetchEventTemplates()); + dispatch(getEventsRecentTemplates()); }, (error) => { notify.error( getErrorMessage(error, gettext('Failed to save the event template')) @@ -713,6 +714,29 @@ const createEventTemplate = (itemId) => (dispatch, getState, {api, modal, notify }); }; +const RECENT_EVENTS_TEMPLATES_KEY = 'events_templates:recent'; + +const addEventRecentTemplate = (field: string, templateId: IEventTemplate['_id']) => ( + (dispatch, getState, {preferencesService}) => preferencesService.get() + .then((result = {}) => { + result[RECENT_EVENTS_TEMPLATES_KEY] = result[RECENT_EVENTS_TEMPLATES_KEY] || {}; + result[RECENT_EVENTS_TEMPLATES_KEY][field] = result[RECENT_EVENTS_TEMPLATES_KEY][field] || []; + result[RECENT_EVENTS_TEMPLATES_KEY][field] = result[RECENT_EVENTS_TEMPLATES_KEY][field].filter( + (i) => i !== templateId); + result[RECENT_EVENTS_TEMPLATES_KEY][field].unshift(templateId); + return preferencesService.update(result); + }) +); + +const getEventsRecentTemplates = () => ( + (dispatch, getState, {preferencesService}) => preferencesService.get() + .then((result) => { + const templates = take(result?.[RECENT_EVENTS_TEMPLATES_KEY]?.['templates'], 5); + + dispatch({type: EVENTS.ACTIONS.EVENT_RECENT_TEMPLATES, payload: templates}); + }) +); + // eslint-disable-next-line consistent-this const self = { loadEventsByRecurrenceId, @@ -745,6 +769,8 @@ const self = { removeFile, fetchEventTemplates, createEventTemplate, + addEventRecentTemplate, + getEventsRecentTemplates, }; export default self; diff --git a/client/actions/events/notifications.ts b/client/actions/events/notifications.ts index b23bdb77e..0da3a75cd 100644 --- a/client/actions/events/notifications.ts +++ b/client/actions/events/notifications.ts @@ -366,10 +366,22 @@ const self = { }; export const planningEventTemplateEvents = { - 'events-template:created': () => eventsApi.fetchEventTemplates, - 'events-template:updated': () => eventsApi.fetchEventTemplates, - 'events-template:replaced': () => eventsApi.fetchEventTemplates, - 'events-template:deleted': () => eventsApi.fetchEventTemplates, + 'events-template:created': () => { + eventsApi.fetchEventTemplates(); + eventsApi.getEventsRecentTemplates(); + }, + 'events-template:updated': () => { + eventsApi.fetchEventTemplates(); + eventsApi.getEventsRecentTemplates(); + }, + 'events-template:replaced': () => { + eventsApi.fetchEventTemplates(); + eventsApi.getEventsRecentTemplates(); + }, + 'events-template:deleted': () => { + eventsApi.fetchEventTemplates(); + eventsApi.getEventsRecentTemplates(); + }, }; // Map of notification name and Action Event to execute diff --git a/client/components/Main/CreateNewSubnavDropdown.tsx b/client/components/Main/CreateNewSubnavDropdown.tsx index 3d605a8d6..28b05e443 100644 --- a/client/components/Main/CreateNewSubnavDropdown.tsx +++ b/client/components/Main/CreateNewSubnavDropdown.tsx @@ -6,7 +6,7 @@ import {ICalendar, IEventTemplate} from '../../interfaces'; import {PRIVILEGES} from '../../constants'; import * as actions from '../../actions'; -import {eventTemplates} from '../../selectors/events'; +import {eventTemplates, getRecentTemplatesSelector} from '../../selectors/events'; import {Dropdown, IDropdownItem} from '../UI/SubNav'; import {showModal} from '@superdesk/common'; import PlanningTemplatesModal from '../PlanningTemplatesModal/PlanningTemplatesModal'; @@ -19,6 +19,7 @@ interface IProps { createEventFromTemplate(template: IEventTemplate): void; eventTemplates: Array; calendars: Array; + recentTemplates?: Array; } const MORE_TEMPLATES_THRESHOLD = 5; @@ -26,7 +27,15 @@ const MORE_TEMPLATES_THRESHOLD = 5; class CreateNewSubnavDropdownFn extends React.PureComponent { render() { const {gettext} = superdeskApi.localization; - const {addEvent, addPlanning, createPlanningOnly, privileges, createEventFromTemplate} = this.props; + const { + addEvent, + addPlanning, + createPlanningOnly, + privileges, + createEventFromTemplate, + recentTemplates, + eventTemplates + } = this.props; const items: Array = []; if (privileges[PRIVILEGES.PLANNING_MANAGEMENT]) { @@ -51,11 +60,10 @@ class CreateNewSubnavDropdownFn extends React.PureComponent { /** * Sort the templates by their name. */ - const sortedTemplates = this.props.eventTemplates + const sortedTemplates = eventTemplates .sort((templ1, templ2) => templ1.template_name.localeCompare(templ2.template_name)); - sortedTemplates - .slice(0, MORE_TEMPLATES_THRESHOLD) + recentTemplates .forEach((template) => { items.push({ label: template.template_name, @@ -113,11 +121,16 @@ function mapStateToProps(state) { return { calendars: state.events.calendars, eventTemplates: eventTemplates(state), + recentTemplates: getRecentTemplatesSelector(state) }; } const mapDispatchToProps = (dispatch) => ({ - createEventFromTemplate: (template: IEventTemplate) => dispatch(actions.main.createEventFromTemplate(template)), + createEventFromTemplate: (template: IEventTemplate) => { + dispatch(actions.main.createEventFromTemplate(template)); + dispatch(actions.events.api.addEventRecentTemplate('templates', template._id)); + dispatch(actions.events.api.getEventsRecentTemplates()); + }, }); export const CreateNewSubnavDropdown = connect( diff --git a/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx b/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx index 9a33e0a33..8a8fd19d4 100644 --- a/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx +++ b/client/components/RelatedPlannings/PlanningMetaData/RelatedPlanningListItem.tsx @@ -30,6 +30,7 @@ interface IProps { desks: Array; contentTypes: Array; lockedItems: ILockedItems; + isAgendaEnabled: boolean; } const mapStateToProps = (state) => ({ @@ -75,14 +76,19 @@ class RelatedPlanningListItemComponent extends React.PureComponent { {this.props.item.slugline} - - - {gettext('Agenda:')} - - + {this.props.isAgendaEnabled && ( + + + {gettext('Agenda:')} + + + - - + + )} diff --git a/client/components/fields/editor/EventRelatedPlannings/EventRelatedPlannings.tsx b/client/components/fields/editor/EventRelatedPlannings/EventRelatedPlannings.tsx index 51aa728e8..e6a6a5174 100644 --- a/client/components/fields/editor/EventRelatedPlannings/EventRelatedPlannings.tsx +++ b/client/components/fields/editor/EventRelatedPlannings/EventRelatedPlannings.tsx @@ -8,7 +8,7 @@ import { IProfileSchemaTypeList, ISearchProfile } from '../../../../interfaces'; -import {superdeskApi} from '../../../../superdeskApi'; +import {planningApi, superdeskApi} from '../../../../superdeskApi'; import {ButtonGroup, Button} from 'superdesk-ui-framework/react'; import {Row} from '../../../UI/Form'; @@ -32,6 +32,7 @@ interface IProps extends IEditorFieldProps { export class EditorFieldEventRelatedPlannings extends React.PureComponent { render() { const {gettext} = superdeskApi.localization; + const isAgendaEnabled = planningApi.planning.getEditorProfile().editor.agendas.enabled; const disabled = this.props.disabled || this.props.schema?.read_only; return ( @@ -88,6 +89,7 @@ export class EditorFieldEventRelatedPlannings extends React.PureComponent )) )} diff --git a/client/components/fields/editor/EventRelatedPlannings/RelatedPlanningItem.tsx b/client/components/fields/editor/EventRelatedPlannings/RelatedPlanningItem.tsx index cff0adbf4..15f83a21d 100644 --- a/client/components/fields/editor/EventRelatedPlannings/RelatedPlanningItem.tsx +++ b/client/components/fields/editor/EventRelatedPlannings/RelatedPlanningItem.tsx @@ -36,6 +36,7 @@ interface IProps { scrollOnChange: boolean ): void; addCoverageToWorkflow(original: IPlanningItem, coverage: IPlanningCoverageItem, index: number): void; + isAgendaEnabled: boolean; } export class RelatedPlanningItem extends React.PureComponent { @@ -104,7 +105,7 @@ export class RelatedPlanningItem extends React.PureComponent { render() { const {gettext} = superdeskApi.localization; - const {item} = this.props; + const {item, isAgendaEnabled} = this.props; const hideRemoveIcon = !this.props.item._id.startsWith(TEMP_ID_PREFIX) || this.props.disabled; return ( @@ -118,6 +119,7 @@ export class RelatedPlanningItem extends React.PureComponent { ( // Load the current items that are currently open for Preview/Editing diff --git a/client/interfaces.ts b/client/interfaces.ts index 4d63d5824..027ec59e1 100644 --- a/client/interfaces.ts +++ b/client/interfaces.ts @@ -1654,6 +1654,7 @@ export interface IEventState { currentCalendarId?: ICalendar['qcode']; currentFilterId?: ISearchFilter['_id']; eventTemplates: Array; + recentEventTemplates?: Array; } export interface IEditorFormState { diff --git a/client/reducers/events.ts b/client/reducers/events.ts index 01eac34e5..055aa81b8 100644 --- a/client/reducers/events.ts +++ b/client/reducers/events.ts @@ -316,6 +316,10 @@ const eventsReducer = createReducer(initialState, { ...state, eventTemplates: payload, }), + [EVENTS.ACTIONS.EVENT_RECENT_TEMPLATES]: (state, payload) => ({ + ...state, + recentEventTemplates: payload, + }) }); const onEventPostChanged = (state, payload) => { diff --git a/client/selectors/events.ts b/client/selectors/events.ts index 0caa1b614..0b848a8d5 100644 --- a/client/selectors/events.ts +++ b/client/selectors/events.ts @@ -2,7 +2,7 @@ import {createSelector} from 'reselect'; import {get, sortBy} from 'lodash'; import {appConfig} from 'appConfig'; -import {IEventItem, IPlanningAppState, LIST_VIEW_TYPE} from '../interfaces'; +import {IEventItem, IEventState, IEventTemplate, IPlanningAppState, LIST_VIEW_TYPE} from '../interfaces'; import {currentPlanning, storedPlannings} from './planning'; import {agendas, userPreferences} from './general'; @@ -18,7 +18,8 @@ export const eventIdsInList = (state) => get(state, 'events.eventsInList', []); export const eventHistory = (state) => get(state, 'events.eventHistoryItems'); export const currentSearch = (state) => get(state, 'main.search.EVENTS.currentSearch'); export const fullText = (state) => get(state, 'main.search.EVENTS.fulltext', ''); -export const eventTemplates = (state) => state.events.eventTemplates; +export const eventTemplates = (state:IEventState) => state.events.eventTemplates; +export const recentTemplates = (state:IEventState) => state.events.recentEventTemplates; export const currentEventFilterId = (state: IPlanningAppState) => state?.events?.currentFilterId; const isEventsView = (state) => get(state, 'main.filter', '') === MAIN.FILTERS.EVENTS; @@ -222,3 +223,21 @@ export const defaultCalendarFilter = createSelector( [usersDefaultCalendar], (calendar) => calendar || {qcode: EVENTS.FILTER.DEFAULT} ); + + +export const getRecentTemplatesSelector = createSelector< + IEventState, + Array, + IEventState, + Array>([recentTemplates, eventTemplates], + (recentTemplatesId, eventTemplates) => { + if (recentTemplatesId && recentTemplatesId.length !== 0) { + return eventTemplates.filter((template) => + recentTemplatesId.includes(template._id) + ).sort( + (a, b) => recentTemplatesId.indexOf(a._id) - recentTemplatesId.indexOf(b._id) + ); + } + return []; + } + ); diff --git a/server/planning/events/__init__.py b/server/planning/events/__init__.py index 856a2305b..cdbb39d75 100644 --- a/server/planning/events/__init__.py +++ b/server/planning/events/__init__.py @@ -180,4 +180,6 @@ def init_app(app): description=lazy_gettext("Ability to create and manage Event templates"), ) + superdesk.register_default_user_preference("events_templates:recent", {}) + superdesk.intrinsic_privilege(EventsUnlockResource.endpoint_name, method=["POST"])