From 6905c95423e4c222cf8aabcd30b95da88be888fe Mon Sep 17 00:00:00 2001 From: Ketan <73937490+devketanpro@users.noreply.github.com> Date: Mon, 12 Feb 2024 09:34:11 +0530 Subject: [PATCH] As a planning user I want to see up to 5 templates recently used by me in the Event creation pull down menu [SDBELGA-763] (#1898) * register default user prefrence recentevent templates * add functionality to saved and fetch recent events templates and display instead of all templates * refactore code * refactor component for template handling * fix syntax and add function calls * Refactore code * add recent event template selector and refactor code --- client/actions/events/api.ts | 30 ++++++++++++++-- client/actions/events/notifications.ts | 20 ++++++++--- .../Main/CreateNewSubnavDropdown.tsx | 35 ++++++++++++++++--- client/constants/events.ts | 1 + client/controllers/PlanningController.tsx | 1 + client/interfaces.ts | 1 + client/reducers/events.ts | 4 +++ client/selectors/events.ts | 23 ++++++++++-- server/planning/events/__init__.py | 2 ++ 9 files changed, 104 insertions(+), 13 deletions(-) diff --git a/client/actions/events/api.ts b/client/actions/events/api.ts index 24cf37a19..f0efd79ed 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 ee7380cc6..81563f28a 100644 --- a/client/components/Main/CreateNewSubnavDropdown.tsx +++ b/client/components/Main/CreateNewSubnavDropdown.tsx @@ -6,7 +6,7 @@ import {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'; interface IProps { @@ -16,12 +16,21 @@ interface IProps { privileges: {[key: string]: number}; createEventFromTemplate(template: IEventTemplate): void; eventTemplates: Array; + recentTemplates?: Array; } 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]) { @@ -43,11 +52,22 @@ class CreateNewSubnavDropdownFn extends React.PureComponent { id: 'create_event', }); - this.props.eventTemplates.forEach((template) => { + if (recentTemplates.length !== 0) { + recentTemplates.forEach((template) => { + items.push({ + label: template.template_name, + icon: 'icon-event icon--blue', + group: gettext('Recent Templates'), + action: () => createEventFromTemplate(template), + id: template._id, + }); + }); + } + eventTemplates.forEach((template) => { items.push({ label: template.template_name, icon: 'icon-event icon--blue', - group: gettext('From template'), + group: gettext('ALL Templates'), action: () => createEventFromTemplate(template), id: template._id, }); @@ -80,11 +100,16 @@ class CreateNewSubnavDropdownFn extends React.PureComponent { function mapStateToProps(state) { return { 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/constants/events.ts b/client/constants/events.ts index bced64b10..c81763355 100644 --- a/client/constants/events.ts +++ b/client/constants/events.ts @@ -22,6 +22,7 @@ export const EVENTS = { RECEIVE_CALENDARS: 'RECEIVE_CALENDARS', RECEIVE_EVENT_TEMPLATES: 'RECEIVE_EVENT_TEMPLATES', EXPIRE_EVENTS: 'EXPIRE_EVENTS', + EVENT_RECENT_TEMPLATES: 'EVENT_RECENT_TEMPLATES', }, // Number of ids to look for by single request // because url length must stay short diff --git a/client/controllers/PlanningController.tsx b/client/controllers/PlanningController.tsx index bfc12b3c7..4c80c095f 100644 --- a/client/controllers/PlanningController.tsx +++ b/client/controllers/PlanningController.tsx @@ -89,6 +89,7 @@ export class PlanningController { this.store.dispatch(actions.autosave.fetchAll()), this.store.dispatch(actions.eventsPlanning.ui.fetchFilters()), this.store.dispatch(eventsApi.fetchEventTemplates()), + this.store.dispatch(eventsApi.getEventsRecentTemplates()), ]) .then(() => ( // 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"])