add show recurring events property

This commit is contained in:
Abderahman88 2020-10-27 19:17:21 +01:00
parent f2f0679c43
commit 13be45e4cd
6 changed files with 235 additions and 77 deletions

View File

@ -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,

View File

@ -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",

View File

@ -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",

View File

@ -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<IGraphCalendarWebPartProps> {
@ -26,6 +26,7 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart<IGraphCa
GraphCalendar,
{
limit: this.properties.limit,
showRecurrence: this.properties.showRecurrence,
context: this.context,
teamsContext: this._teamsContext
}
@ -43,6 +44,10 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart<IGraphCa
this.properties.limit = 100;
}
if (this.properties.showRecurrence === undefined) {
this.properties.showRecurrence = true;
}
// Sets the Teams context if in Teams
if (this.context.sdks.microsoftTeams) {
this._teamsContext = this.context.sdks.microsoftTeams.context;
@ -82,6 +87,10 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart<IGraphCa
label: "Events to load per active view",
max: 500,
min: 50
}),
PropertyPaneCheckbox('showRecurrence', {
text: "Show recurring events",
checked: true
})
]
}

View File

@ -8,6 +8,9 @@ import { EventInput } from '@fullcalendar/core';
import dayGridPlugin from '@fullcalendar/daygrid';
import * as moment from 'moment-timezone';
import { Panel, PanelType } from 'office-ui-fabric-react/lib/Panel';
import { extendMoment } from 'moment-range';
const { range } = extendMoment(moment);
interface IGraphCalendarState {
events: EventInput[];
@ -36,10 +39,10 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
super(props);
// If this is running in Teams, embed the specific Teams styling
if(this._isRunningInTeams()) {
if (this._isRunningInTeams()) {
import("./GraphCalendar.Teams.module.scss");
if(this.props.teamsContext.theme == "dark") {
if (this.props.teamsContext.theme == "dark") {
import("./GraphCalendar.Teams.Dark.module.scss");
}
}
@ -73,11 +76,11 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
*/
public render(): React.ReactElement<IGraphCalendarProps> {
return (
<div className={ styles.graphCalendar }>
<FullCalendar
<div className={styles.graphCalendar}>
<FullCalendar
ref={this.calendar}
defaultView="dayGridMonth"
plugins={[ dayGridPlugin ]}
defaultView="dayGridMonth"
plugins={[dayGridPlugin]}
windowResize={this._handleResize.bind(this)}
datesRender={this._datesRender.bind(this)}
eventClick={this._openEventPanel.bind(this)}
@ -86,7 +89,7 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
{this.state.currentSelectedEvent &&
<Panel
isOpen={this.state.isEventDetailsOpen}
type={ PanelType.smallFixedFar }
type={PanelType.smallFixedFar}
headerText={this.state.currentSelectedEvent ? this.state.currentSelectedEvent.title : ""}
onDismiss={this._closeEventPanel.bind(this)}
isLightDismiss={true}
@ -95,14 +98,14 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
<span>{moment(this.state.currentSelectedEvent.start).format('MMMM Do YYYY [at] h:mm:ss a')}</span>
<h3>Start Time</h3>
<span>{moment(this.state.currentSelectedEvent.end).format('MMMM Do YYYY [at] h:mm:ss a')}</span>
{this.state.currentSelectedEvent.extendedProps["location"] &&
{this.state.currentSelectedEvent.extendedProps["location"] &&
<div>
<h3>Location</h3>
<span>{this.state.currentSelectedEvent.extendedProps["location"]}</span>
</div>
}
{this.state.currentSelectedEvent.extendedProps["body"] &&
<div>
{this.state.currentSelectedEvent.extendedProps["body"] &&
<div>
<h3>Body</h3>
<span>{this.state.currentSelectedEvent.extendedProps["body"]}</span>
</div>
@ -118,7 +121,7 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
* Mainly used for Teams validation so it renders "full-screen" in Teams
*/
private _calculateHeight(): number {
if(this._isRunningInTeams()) {
if (this._isRunningInTeams()) {
return window.innerHeight - 30;
} else {
return 600;
@ -138,7 +141,7 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
private _isPersonalTab() {
let _isPersonalTab: Boolean = false;
if(this._isRunningInTeams() && !this.props.teamsContext.teamId) {
if (this._isRunningInTeams() && !this.props.teamsContext.teamId) {
_isPersonalTab = true;
}
@ -165,16 +168,16 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
currentSelectedEvent: null
});
}
/**
* If the view changed, reload the events based on the active view
* @param info Information about the current active view
*/
private _datesRender(info: any) {
if(this.calendar.value) {
if (this.calendar.value) {
// If the active view has changed
if((this.state.currentActiveStartDate && this.state.currentActiveEndDate) && this.state.currentActiveStartDate.toString() != info.view.activeStart.toString() && this.state.currentActiveEndDate.toString() != info.view.activeEnd.toString()) {
if ((this.state.currentActiveStartDate && this.state.currentActiveEndDate) && this.state.currentActiveStartDate.toString() != info.view.activeStart.toString() && this.state.currentActiveEndDate.toString() != info.view.activeEnd.toString()) {
this._loadEvents(info.view.activeStart, info.view.activeEnd);
}
}
@ -184,13 +187,89 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
* Handles the resize event when in Microsoft Teams to ensure a proper responsive behaviour
*/
private _handleResize() {
if(this._isRunningInTeams()) {
if (this._isRunningInTeams()) {
this.setState({
height: window.innerHeight - 30
});
}
}
/**
* Convert data to Array<EventInput>
* @param data Events from API
*/
private _transformEvents(data: any): Array<EventInput> {
let events: Array<EventInput> = new Array<EventInput>();
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<EventInput> {
let events: Array<EventInput> = new Array<EventInput>();
//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<IGraphCalendarProps,
private _loadEvents(startDate: Date, endDate: Date): void {
// If a Group was found or running in the context of a Personal tab, execute the query. If not, do nothing.
if(this.state.groupId || this.state.tabType == TabType.PersonalTab) {
if (this.state.groupId || this.state.tabType == TabType.PersonalTab) {
var events: Array<EventInput> = new Array<EventInput>();
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<EventInput> = new Array<EventInput>();
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<EventInput>
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<EventInput>) => {
this._getRecurrentEvents(recData, startDate, endDate).then((recEvents: Array<EventInput>) => {
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<EventInput>, startDate: Date, endDate: Date): Promise<Array<EventInput>> {
return new Promise<Array<EventInput>>((resolve, reject) => {
this.props.context.msGraphClientFactory
.getClient()
.then((client: MSGraphClient): void => {
var recEvents: Array<EventInput> = new Array<EventInput>();
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<Array<EventInput>> {
return new Promise<Array<EventInput>>((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<EventInput> = new Array<EventInput>();
recEvents = this._filterRecEvents(res, startDate, endDate);
resolve(recEvents);
}
});
});
});
}
}

View File

@ -3,6 +3,7 @@ import * as microsoftTeams from '@microsoft/teams-js';
export interface IGraphCalendarProps {
limit: number;
showRecurrence: boolean;
context: WebPartContext;
teamsContext: microsoftTeams.Context;
}