diff --git a/samples/react-graph-calendar/config/package-solution.json b/samples/react-graph-calendar/config/package-solution.json index 9e1eb392c..cf3a60688 100644 --- a/samples/react-graph-calendar/config/package-solution.json +++ b/samples/react-graph-calendar/config/package-solution.json @@ -3,7 +3,7 @@ "solution": { "name": "react-graph-calendar-client-side-solution", "id": "42fe0a0f-c4d9-4b05-806c-3857decb3d71", - "version": "1.0.0.0", + "version": "1.0.1.0", "includeClientSideAssets": true, "skipFeatureDeployment": true, "isDomainIsolated": false, diff --git a/samples/react-graph-calendar/package-lock.json b/samples/react-graph-calendar/package-lock.json index 6e1d3c31a..a051683d5 100644 --- a/samples/react-graph-calendar/package-lock.json +++ b/samples/react-graph-calendar/package-lock.json @@ -7093,7 +7093,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", - "dev": true, "requires": { "es5-ext": "^0.10.50", "type": "^1.0.1" @@ -7697,7 +7696,6 @@ "version": "0.10.53", "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", - "dev": true, "requires": { "es6-iterator": "~2.0.3", "es6-symbol": "~3.1.3", @@ -7713,7 +7711,6 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", - "dev": true, "requires": { "d": "1", "es5-ext": "^0.10.35", @@ -7777,7 +7774,6 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", - "dev": true, "requires": { "d": "^1.0.1", "ext": "^1.1.2" @@ -8171,7 +8167,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", - "dev": true, "requires": { "type": "^2.0.0" }, @@ -8179,8 +8174,7 @@ "type": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/type/-/type-2.0.0.tgz", - "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==", - "dev": true + "integrity": "sha512-KBt58xCHry4Cejnc2ISQAF7QY+ORngsWfxezO68+12hKV6lQY8P/psIkcbjeHWn7MqcgciWJyCCevFMJdIXpow==" } } }, @@ -13579,6 +13573,14 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz", "integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==" }, + "moment-range": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/moment-range/-/moment-range-4.0.2.tgz", + "integrity": "sha512-n8sceWwSTjmz++nFHzeNEUsYtDqjgXgcOBzsHi+BoXQU2FW+eU92LUaK8gqOiSu5PG57Q9sYj1Fz4LRDj4FtKA==", + "requires": { + "es6-symbol": "^3.1.0" + } + }, "moment-timezone": { "version": "0.5.27", "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.27.tgz", @@ -13711,8 +13713,7 @@ "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", - "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=", - "dev": true + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" }, "nice-try": { "version": "1.0.5", @@ -18013,8 +18014,7 @@ "type": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", - "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", - "dev": true + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" }, "type-check": { "version": "0.3.2", diff --git a/samples/react-graph-calendar/package.json b/samples/react-graph-calendar/package.json index 9862d33d0..ffdabae85 100644 --- a/samples/react-graph-calendar/package.json +++ b/samples/react-graph-calendar/package.json @@ -26,6 +26,7 @@ "@types/react-dom": "16.8.3", "@types/webpack-env": "1.13.1", "moment": "^2.24.0", + "moment-range": "^4.0.2", "moment-timezone": "^0.5.27", "office-ui-fabric-react": "6.189.2", "react": "16.8.5", diff --git a/samples/react-graph-calendar/src/webparts/graphCalendar/GraphCalendarWebPart.ts b/samples/react-graph-calendar/src/webparts/graphCalendar/GraphCalendarWebPart.ts index 7884f1df4..ca314be9d 100644 --- a/samples/react-graph-calendar/src/webparts/graphCalendar/GraphCalendarWebPart.ts +++ b/samples/react-graph-calendar/src/webparts/graphCalendar/GraphCalendarWebPart.ts @@ -3,7 +3,6 @@ import * as ReactDom from 'react-dom'; import { Version } from '@microsoft/sp-core-library'; import { BaseClientSideWebPart, - IPropertyPaneConfiguration, PropertyPaneTextField } from '@microsoft/sp-webpart-base'; @@ -12,10 +11,11 @@ import GraphCalendar from './components/GraphCalendar'; import { IGraphCalendarProps } from './components/IGraphCalendarProps'; import * as microsoftTeams from '@microsoft/teams-js'; import { initializeIcons } from 'office-ui-fabric-react'; -import { PropertyPaneSlider } from '@microsoft/sp-property-pane'; +import { PropertyPaneSlider, PropertyPaneCheckbox, IPropertyPaneConfiguration } from '@microsoft/sp-property-pane'; export interface IGraphCalendarWebPartProps { limit: number; + showRecurrence: boolean; } export default class GraphCalendarWebPart extends BaseClientSideWebPart { @@ -26,6 +26,7 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart { return ( -
- + {moment(this.state.currentSelectedEvent.start).format('MMMM Do YYYY [at] h:mm:ss a')}

Start Time

{moment(this.state.currentSelectedEvent.end).format('MMMM Do YYYY [at] h:mm:ss a')} - {this.state.currentSelectedEvent.extendedProps["location"] && + {this.state.currentSelectedEvent.extendedProps["location"] &&

Location

{this.state.currentSelectedEvent.extendedProps["location"]}
} - {this.state.currentSelectedEvent.extendedProps["body"] && -
+ {this.state.currentSelectedEvent.extendedProps["body"] && +

Body

{this.state.currentSelectedEvent.extendedProps["body"]}
@@ -118,7 +121,7 @@ export default class GraphCalendar extends React.Component + * @param data Events from API + */ + private _transformEvents(data: any): Array { + let events: Array = new Array(); + data.value.map((item: any) => { + // Build a Timezone enabled Date + let currentStartDate = moment.tz(item.start.dateTime, item.start.timeZone); + let currentEndDate = moment.tz(item.end.dateTime, item.end.timeZone); + + // Adding all retrieved events to the result array + events.push({ + id: item.id, + title: item.subject, + + // If the event is an All Day event, add 1 day without Timezone to the start date + start: !item.isAllDay ? currentStartDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentStartDate).add(1, 'd').toISOString(), + + // If the event is an All Day event, add 1 day without Timezone to the end date + end: !item.isAllDay ? currentEndDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentEndDate).add(1, 'd').toISOString(), + allDay: item.isAllDay, + location: item.location.displayName, + body: item.bodyPreview, + type: item.type + }); + }); + return events; + } + + /** + * Check if the recurring events need to be shown on the current state of the Calendar + * If the range of the recurring event overlaps the range of the Calendar, then the event needs to be shown. + * @param data All the recurrent (base) events ever made + * @param startDate The first visible date on the calendar + * @param endDate The last visible date on the calendar + */ + private _filterRecEvents(data: any, startDate: Date, endDate: Date): Array { + let events: Array = new Array(); + //Range of the Calendar + var r1 = range(startDate, endDate); + + data.value.map((item: any) => { + // Build a Timezone enabled Date + let currentStartDate = moment.tz(item.start.dateTime, item.start.timeZone); + let currentEndDate = moment.tz(item.end.dateTime, item.end.timeZone); + + var d1 = item.recurrence.range.startDate; + var d2 = item.recurrence.range.endDate; + var recStartDate = moment(d1).toDate(); + var recEndDate = moment(d2).toDate(); + + //Range of the recurring event item + var r2 = range(recStartDate, recEndDate); + + + //Check if both ranges overlap + if (!!r1.overlaps(r2)) { + events.push({ + id: item.id, + title: item.subject, + // If the event is an All Day event, add 1 day without Timezone to the start date + start: !item.isAllDay ? currentStartDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentStartDate).add(1, 'd').toISOString(), + // If the event is an All Day event, add 1 day without Timezone to the end date + end: !item.isAllDay ? currentEndDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentEndDate).add(1, 'd').toISOString(), + allDay: item.isAllDay, + location: item.location.displayName, + body: item.bodyPreview, + type: item.type + }); + } + + }); + return events; + } + /** * Loads the Events based on the current state of the Calendar * @param startDate The first visible date on the calendar @@ -199,61 +278,129 @@ export default class GraphCalendar extends React.Component = new Array(); this.props.context.msGraphClientFactory - .getClient() - .then((client: MSGraphClient): void => { + .getClient() + .then((client: MSGraphClient): void => { + let apiUrl: string = `/groups/${this.state.groupId}/events`; + if (this._isPersonalTab()) { + apiUrl = '/me/events'; + } - let apiUrl: string = `/groups/${this.state.groupId}/events`; - if(this._isPersonalTab()) { - apiUrl = '/me/events'; - } + client + .api(apiUrl) + .version("v1.0") + .select('subject,start,end,location,bodyPreview,isAllDay,type') + .filter(`start/dateTime ge '${startDate.toISOString()}' and end/dateTime le '${endDate.toISOString()}' and type eq 'singleInstance'`) + .top(this.props.limit) + .get((err, res) => { - client - .api(apiUrl) - .version("v1.0") - .select('subject,start,end,location,bodyPreview,isAllDay') - .filter(`start/dateTime ge '${startDate.toISOString()}' and end/dateTime le '${endDate.toISOString()}'`) - .top(this.props.limit) - .get((err, res) => { - - if (err) { - console.error(err); - return; - } - - var events: Array = new Array(); + if (err) { + console.error(err); + return; + } - res.value.map((item: any) => { - // Build a Timezone enabled Date - let currentStartDate = moment.tz(item.start.dateTime, item.start.timeZone); - let currentEndDate = moment.tz(item.end.dateTime, item.end.timeZone); + //Transform API data to Array + events = this._transformEvents(res); - // Adding all retrieved events to the result array - events.push({ - title: item.subject, - - // If the event is an All Day event, add 1 day without Timezone to the start date - start: !item.isAllDay ? currentStartDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentStartDate).add(1, 'd').toISOString(), - - // If the event is an All Day event, add 1 day without Timezone to the end date - end: !item.isAllDay ? currentEndDate.clone().tz(Intl.DateTimeFormat().resolvedOptions().timeZone).format() : moment(currentEndDate).add(1, 'd').toISOString(), - allDay: item.isAllDay, - location: item.location.displayName, - body: item.bodyPreview + if (this.props.showRecurrence) { + //Get recurring events and merge with the other (standard) events + this._getRecurringMaster(startDate, endDate).then((recData: Array) => { + this._getRecurrentEvents(recData, startDate, endDate).then((recEvents: Array) => { + this.setState({ + events: [...recEvents, ...events].slice(0, this.props.limit), + }); + }); + }); + } else { + //Show only (standard) events + this.setState({ + events: events, + }); + } + + // Sets the state with current active calendar dates + this.setState({ + currentActiveStartDate: startDate, + currentActiveEndDate: endDate, + currentSelectedEvent: null }); }); - - // Sets the state with the retrieved events and current active calendar dates - this.setState({ - events: events, - currentActiveStartDate: startDate, - currentActiveEndDate: endDate, - currentSelectedEvent: null - }); - }); - }); + }); } } + + /** + * Get all recurrent events based on the current state of the Calendar + * @param events All the recurrent base events + * @param startDate The first visible date on the calendar + * @param endDate The last visible date on the calendar + */ + private _getRecurrentEvents(events: Array, startDate: Date, endDate: Date): Promise> { + return new Promise>((resolve, reject) => { + this.props.context.msGraphClientFactory + .getClient() + .then((client: MSGraphClient): void => { + var recEvents: Array = new Array(); + var count = 0; + events.map((item: any) => { + let apiUrl: string = `/groups/${this.state.groupId}/events/${item.id}/instances?startDateTime=${startDate.toISOString()}&endDateTime=${endDate.toISOString()}`; + if(this._isPersonalTab()) { + apiUrl = `/me/events/${item.id}/instances?startDateTime=${startDate.toISOString()}&endDateTime=${endDate.toISOString()}`; + } + client + .api(apiUrl) + .version("v1.0") + .select('subject,start,end,location,bodyPreview,isAllDay,type') + .get((err, res) => { + if (err) { + reject(err); + } + recEvents = recEvents.concat(this._transformEvents(res)); + count += 1; + if (count == events.length) { + resolve(recEvents); + } + }); + }); + + }); + }); + } + + /** + * Get all recurrent (base) events ever created + * Filter the base events based on the current state of the Calendar + * @param startDate The first visible date on the calendar + * @param endDate The last visible date on the calendar + */ + private _getRecurringMaster(startDate: Date, endDate: Date): Promise> { + return new Promise>((resolve, reject) => { + this.props.context.msGraphClientFactory + .getClient() + .then((client: MSGraphClient): void => { + let apiUrl: string = `/groups/${this.state.groupId}/events`; + if(this._isPersonalTab()) { + apiUrl = '/me/events'; + } + client + .api(apiUrl) + .version("v1.0") + .select('subject,start,end,location,bodyPreview,isAllDay,type,recurrence') + .filter(`type eq 'seriesMaster'`) //recurrening event is type 'seriesMaster' + .get((err, res) => { + if (err) { + reject(err); + } + else { + var recEvents: Array = new Array(); + recEvents = this._filterRecEvents(res, startDate, endDate); + resolve(recEvents); + } + }); + }); + }); + } } diff --git a/samples/react-graph-calendar/src/webparts/graphCalendar/components/IGraphCalendarProps.ts b/samples/react-graph-calendar/src/webparts/graphCalendar/components/IGraphCalendarProps.ts index e8f46931c..1be074a1b 100644 --- a/samples/react-graph-calendar/src/webparts/graphCalendar/components/IGraphCalendarProps.ts +++ b/samples/react-graph-calendar/src/webparts/graphCalendar/components/IGraphCalendarProps.ts @@ -3,6 +3,7 @@ import * as microsoftTeams from '@microsoft/teams-js'; export interface IGraphCalendarProps { limit: number; + showRecurrence: boolean; context: WebPartContext; teamsContext: microsoftTeams.Context; }