diff --git a/client/actions/events/api.ts b/client/actions/events/api.ts index f0efd79ed..5aca012ec 100644 --- a/client/actions/events/api.ts +++ b/client/actions/events/api.ts @@ -731,7 +731,7 @@ const addEventRecentTemplate = (field: string, templateId: IEventTemplate['_id'] const getEventsRecentTemplates = () => ( (dispatch, getState, {preferencesService}) => preferencesService.get() .then((result) => { - const templates = take(result[RECENT_EVENTS_TEMPLATES_KEY]['templates'], 5); + const templates = take(result?.[RECENT_EVENTS_TEMPLATES_KEY]?.['templates'], 5); dispatch({type: EVENTS.ACTIONS.EVENT_RECENT_TEMPLATES, payload: templates}); }) diff --git a/client/components/Main/CreateNewSubnavDropdown.tsx b/client/components/Main/CreateNewSubnavDropdown.tsx index 81563f28a..28b05e443 100644 --- a/client/components/Main/CreateNewSubnavDropdown.tsx +++ b/client/components/Main/CreateNewSubnavDropdown.tsx @@ -2,12 +2,14 @@ import React from 'react'; import {connect} from 'react-redux'; import {superdeskApi} from '../../superdeskApi'; -import {IEventTemplate} from '../../interfaces'; +import {ICalendar, IEventTemplate} from '../../interfaces'; import {PRIVILEGES} from '../../constants'; import * as actions from '../../actions'; import {eventTemplates, getRecentTemplatesSelector} from '../../selectors/events'; import {Dropdown, IDropdownItem} from '../UI/SubNav'; +import {showModal} from '@superdesk/common'; +import PlanningTemplatesModal from '../PlanningTemplatesModal/PlanningTemplatesModal'; interface IProps { addEvent(): void; @@ -16,9 +18,12 @@ interface IProps { privileges: {[key: string]: number}; createEventFromTemplate(template: IEventTemplate): void; eventTemplates: Array; + calendars: Array; recentTemplates?: Array; } +const MORE_TEMPLATES_THRESHOLD = 5; + class CreateNewSubnavDropdownFn extends React.PureComponent { render() { const {gettext} = superdeskApi.localization; @@ -52,26 +57,41 @@ class CreateNewSubnavDropdownFn extends React.PureComponent { id: 'create_event', }); - if (recentTemplates.length !== 0) { - recentTemplates.forEach((template) => { + /** + * Sort the templates by their name. + */ + const sortedTemplates = eventTemplates + .sort((templ1, templ2) => templ1.template_name.localeCompare(templ2.template_name)); + + recentTemplates + .forEach((template) => { items.push({ label: template.template_name, icon: 'icon-event icon--blue', - group: gettext('Recent Templates'), + group: gettext('From template'), action: () => createEventFromTemplate(template), id: template._id, }); }); - } - eventTemplates.forEach((template) => { + + if (sortedTemplates.length > MORE_TEMPLATES_THRESHOLD) { items.push({ - label: template.template_name, + label: gettext('More templates...'), icon: 'icon-event icon--blue', - group: gettext('ALL Templates'), - action: () => createEventFromTemplate(template), - id: template._id, + group: gettext('From template'), + action: () => { + showModal(({closeModal}) => ( + + )); + }, + id: 'more_templates', }); - }); + } } return items.length === 0 ? null : ( @@ -99,6 +119,7 @@ class CreateNewSubnavDropdownFn extends React.PureComponent { function mapStateToProps(state) { return { + calendars: state.events.calendars, eventTemplates: eventTemplates(state), recentTemplates: getRecentTemplatesSelector(state) }; diff --git a/client/components/PlanningTemplatesModal/PlanningTemplatesModal.tsx b/client/components/PlanningTemplatesModal/PlanningTemplatesModal.tsx new file mode 100644 index 000000000..1a9563016 --- /dev/null +++ b/client/components/PlanningTemplatesModal/PlanningTemplatesModal.tsx @@ -0,0 +1,105 @@ +import {ICalendar, IEventTemplate} from '../../interfaces'; +import React from 'react'; +import {SearchBar, Modal, Dropdown} from 'superdesk-ui-framework/react'; +import {superdeskApi} from '../../superdeskApi'; +import {TemplatesListView} from './TemplatesListView'; + +interface IProps { + closeModal: () => void; + calendars: Array; + eventTemplates: Array; + createEventFromTemplate(template: IEventTemplate): void; +} + +interface IState { + activeCalendarFilter?: string; + searchQuery: string; +} + +export default class PlanningTemplatesModal extends React.Component { + constructor(props) { + super(props); + + this.state = { + activeCalendarFilter: null, + searchQuery: '', + }; + } + + render(): React.ReactNode { + const {gettext} = superdeskApi.localization; + const {closeModal, createEventFromTemplate, calendars, eventTemplates} = this.props; + const allCalendarsLabel = gettext('All Calendars'); + const calendarDropdownItems = []; + const activeCalendarName = this.props.calendars + .find((cal) => cal.qcode === this.state.activeCalendarFilter)?.name; + const dropdownLabel = this.state.activeCalendarFilter + ? gettext('Calendar: {{activeCalendarName}}', {activeCalendarName}) + : allCalendarsLabel; + + if (this.state.activeCalendarFilter) { + calendarDropdownItems.push({ + label: allCalendarsLabel, + onSelect: () => { + this.setState({ + activeCalendarFilter: null, + }); + } + }); + } + + if ((calendars?.length ?? 0) > 0) { + calendarDropdownItems.push( + ...calendars.map((calendar) => ({ + label: calendar.name, + onSelect: () => { + this.setState({ + activeCalendarFilter: calendar.qcode, + }); + } + })) + ); + } + + return ( + +
+ { + this.setState({ + searchQuery: value, + }); + }} + placeholder={gettext('Search templates')} + boxed + > + + {dropdownLabel} + + + +
+
+ ); + } +} diff --git a/client/components/PlanningTemplatesModal/TemplatesListView.tsx b/client/components/PlanningTemplatesModal/TemplatesListView.tsx new file mode 100644 index 000000000..b3092de71 --- /dev/null +++ b/client/components/PlanningTemplatesModal/TemplatesListView.tsx @@ -0,0 +1,79 @@ +import {ICalendar, IEventTemplate} from 'interfaces'; +import React from 'react'; +import {gettext} from 'superdesk-core/scripts/core/utils'; +import {Heading, BoxedList, BoxedListItem} from 'superdesk-ui-framework/react'; + +type ITemplatesListViewProps = { + closeModal: () => void; + eventTemplates: Array; + calendars: Array; + activeCalendarFilter?: string; + searchQuery: string; + createEventFromTemplate: (template: IEventTemplate) => void; +} + +export const TemplatesListView: React.FC = ({ + eventTemplates, + calendars, + closeModal, + activeCalendarFilter, + searchQuery, + createEventFromTemplate, +}: ITemplatesListViewProps) => { + const searchQueryTemplateMatches = eventTemplates + .filter((template) => template.template_name.includes(searchQuery)); + const calendarsFiltered = activeCalendarFilter + ? [calendars.find(({qcode}) => activeCalendarFilter === qcode)] + : calendars; + + const filteredTemplates = calendarsFiltered + .map((_calendar) => ({ + calendar: _calendar, + templates: searchQueryTemplateMatches + .filter((template) => template.data.calendars.find(({qcode}) => qcode === _calendar.qcode)), + })) + .filter((group) => activeCalendarFilter + ? group.calendar.qcode === activeCalendarFilter + : group.templates.length > 0 + ); + + return ( + <> + {filteredTemplates.map(({calendar, templates}) => ( + + {calendar.name} + { + templates?.length > 0 ? ( + + {templates.map((template) => ( + { + createEventFromTemplate(template); + closeModal(); + }} + > + {template.template_name} + + ))} + + ) : ( + + {gettext('No templates available in this calendar group.')} + + ) + } + + + ))} + {activeCalendarFilter == null && searchQuery && filteredTemplates.length === 0 && ( +
+ + {gettext('No templates found.')} + +
+ )} + + ); +}; diff --git a/package-lock.json b/package-lock.json index cae43446e..af6e1d254 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14265,7 +14265,7 @@ "reflect.ownkeys": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha512-qOLsBKHCpSOFKK1NUOCGC5VyeufB6lEsFe92AL2bhIJsacZS1qdoOZSbPk3MYKuT2cFlRDnulKXuuElIrMjGUg==", + "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=", "dev": true }, "regenerator-runtime": { @@ -16550,7 +16550,7 @@ } }, "superdesk-core": { - "version": "github:superdesk/superdesk-client-core#761cffad6d4bbd0fb51f60547c8159b27d12f0fb", + "version": "github:superdesk/superdesk-client-core#67cee3cbf58134a6e21a7693b5ab536b0a3a2217", "from": "github:superdesk/superdesk-client-core#develop", "dev": true, "requires": { @@ -16764,9 +16764,9 @@ } }, "superdesk-ui-framework": { - "version": "3.0.71", - "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.0.71.tgz", - "integrity": "sha512-JF3pnmLWUnWXougST9fuwAQaQjMD2VzdvnNVOBm3wnytOmUUJC1cUaWUIouVabuHS3J74yZNLugO3TU9FhruPg==", + "version": "3.0.72", + "resolved": "https://registry.npmjs.org/superdesk-ui-framework/-/superdesk-ui-framework-3.0.72.tgz", + "integrity": "sha512-uIMC0nCGldfUirlu9FXBkOfJHtMScEL6AJgy5tJLZrOzYaBLWxb2ZLtEpCMboTnpUuoPwBtYpNkt3FMNjQpwug==", "dev": true, "requires": { "@material-ui/lab": "^4.0.0-alpha.56", diff --git a/package.json b/package.json index 9b64a76bc..a8b463e08 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "sinon": "^4.5.0", "superdesk-code-style": "1.5.0", "superdesk-core": "github:superdesk/superdesk-client-core#develop", - "superdesk-ui-framework": "^3.0.71", + "superdesk-ui-framework": "^3.0.72", "ts-node": "~7.0.1", "tslint": "5.11.0", "typescript-eslint-parser": "^18.0.0"