Skip to content

Commit a05863b

Browse files
authored
Merge pull request #52 from microsoftgraph/create-event
Tutorial update
2 parents 4e6b40b + 134e573 commit a05863b

21 files changed

+1083
-680
lines changed

demo/README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,21 +26,17 @@ If you don't have a Microsoft account, there are a couple of options to get a fr
2626

2727
- Set **Name** to `React Graph Tutorial`.
2828
- Set **Supported account types** to **Accounts in any organizational directory and personal Microsoft accounts**.
29-
- Under **Redirect URI**, set the first drop-down to `Web` and set the value to `http://localhost:3000`.
29+
- Under **Redirect URI**, set the first drop-down to `Single-page application (SPA)` and set the value to `http://localhost:3000`.
3030

3131
![A screenshot of the Register an application page](/tutorial/images/aad-register-an-app.png)
3232

3333
1. Choose **Register**. On the **Angular Graph Tutorial** page, copy the value of the **Application (client) ID** and save it, you will need it in the next step.
3434

3535
![A screenshot of the application ID of the new app registration](/tutorial/images/aad-application-id.png)
3636

37-
1. Select **Authentication** under **Manage**. Locate the **Implicit grant** section and enable **Access tokens** and **ID tokens**. Choose **Save**.
38-
39-
![A screenshot of the Implicit grant section](/tutorial/images/aad-implicit-grant.png)
40-
4137
## Configure the sample
4238

43-
1. Rename the `./graph-tutorial/src/Config.ts.example` file to `./graph-tutorial/src/Config.ts`.
39+
1. Rename the `./graph-tutorial/src/Config.example.ts` file to `./graph-tutorial/src/Config.ts`.
4440
1. Edit the `./graph-tutorial/src/Config.ts` file and make the following changes.
4541
1. Replace `YOUR_APP_ID_HERE` with the **Application Id** you got from the App Registration Portal.
4642
1. In your command-line interface (CLI), navigate to the `graph-tutorial` directory and run the following command to install requirements.

demo/graph-tutorial/package-lock.json

Lines changed: 471 additions & 553 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

demo/graph-tutorial/package.json

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,27 +3,29 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@fortawesome/fontawesome-free": "^5.13.1",
6+
"@azure/msal-browser": "^2.1.0",
7+
"@fortawesome/fontawesome-free": "^5.14.0",
78
"@microsoft/microsoft-graph-client": "^2.0.0",
89
"@testing-library/jest-dom": "^4.2.4",
910
"@testing-library/react": "^9.3.2",
1011
"@testing-library/user-event": "^7.1.2",
1112
"@types/jest": "^24.0.0",
12-
"@types/microsoft-graph": "^1.13.0",
13-
"@types/node": "^12.0.0",
14-
"@types/react": "^16.9.0",
13+
"@types/microsoft-graph": "^1.18.0",
14+
"@types/node": "^12.12.55",
15+
"@types/react": "^16.9.49",
1516
"@types/react-dom": "^16.9.0",
1617
"@types/react-router-dom": "^5.1.5",
17-
"@types/reactstrap": "^8.5.0",
18-
"bootstrap": "^4.5.0",
18+
"@types/reactstrap": "^8.5.1",
19+
"bootstrap": "^4.5.2",
1920
"moment": "^2.27.0",
20-
"msal": "^1.3.2",
21+
"moment-timezone": "^0.5.31",
2122
"react": "^16.13.1",
2223
"react-dom": "^16.13.1",
2324
"react-router-dom": "^5.2.0",
24-
"react-scripts": "3.4.1",
25+
"react-scripts": "^3.4.3",
2526
"reactstrap": "^8.5.1",
26-
"typescript": "~3.7.2"
27+
"typescript": "~3.7.2",
28+
"windows-iana": "^4.2.1"
2729
},
2830
"scripts": {
2931
"start": "react-scripts start",

demo/graph-tutorial/src/App.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import NavBar from './NavBar';
88
import ErrorMessage from './ErrorMessage';
99
import Welcome from './Welcome';
1010
import Calendar from './Calendar';
11+
import NewEvent from './NewEvent';
1112
import 'bootstrap/dist/css/bootstrap.css';
1213

1314
class App extends Component<AuthComponentProps> {
@@ -42,6 +43,12 @@ class App extends Component<AuthComponentProps> {
4243
<Calendar {...props} /> :
4344
<Redirect to="/" />
4445
} />
46+
<Route exact path="/newevent"
47+
render={(props) =>
48+
this.props.isAuthenticated ?
49+
<NewEvent {...props} /> :
50+
<Redirect to="/" />
51+
} />
4552
</Container>
4653
</div>
4754
</Router>

demo/graph-tutorial/src/AuthProvider.tsx

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
import React from 'react';
4-
import { UserAgentApplication } from 'msal';
4+
import { PublicClientApplication } from '@azure/msal-browser';
55

66
import { config } from './Config';
77
import { getUserDetails } from './GraphService';
@@ -25,7 +25,7 @@ interface AuthProviderState {
2525
export default function withAuthProvider<T extends React.Component<AuthComponentProps>>
2626
(WrappedComponent: new (props: AuthComponentProps, context?: any) => T): React.ComponentClass {
2727
return class extends React.Component<any, AuthProviderState> {
28-
private userAgentApplication: UserAgentApplication;
28+
private publicClientApplication: PublicClientApplication;
2929

3030
constructor(props: any) {
3131
super(props);
@@ -36,7 +36,7 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
3636
};
3737

3838
// Initialize the MSAL application object
39-
this.userAgentApplication = new UserAgentApplication({
39+
this.publicClientApplication = new PublicClientApplication({
4040
auth: {
4141
clientId: config.appId,
4242
redirectUri: config.redirectUri
@@ -51,9 +51,9 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
5151
componentDidMount() {
5252
// If MSAL already has an account, the user
5353
// is already logged in
54-
var account = this.userAgentApplication.getAccount();
54+
const accounts = this.publicClientApplication.getAllAccounts();
5555

56-
if (account) {
56+
if (accounts && accounts.length > 0) {
5757
// Enhance user object with data from Graph
5858
this.getUserProfile();
5959
}
@@ -74,11 +74,12 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
7474
async login() {
7575
try {
7676
// Login via popup
77-
await this.userAgentApplication.loginPopup(
77+
await this.publicClientApplication.loginPopup(
7878
{
7979
scopes: config.scopes,
8080
prompt: "select_account"
8181
});
82+
8283
// After login, get the user's profile
8384
await this.getUserProfile();
8485
}
@@ -92,27 +93,34 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
9293
}
9394

9495
logout() {
95-
this.userAgentApplication.logout();
96+
this.publicClientApplication.logout();
9697
}
9798

9899
async getAccessToken(scopes: string[]): Promise<string> {
99100
try {
101+
const accounts = this.publicClientApplication
102+
.getAllAccounts();
103+
104+
if (accounts.length <= 0) throw new Error('login_required');
100105
// Get the access token silently
101106
// If the cache contains a non-expired token, this function
102107
// will just return the cached token. Otherwise, it will
103108
// make a request to the Azure OAuth endpoint to get a token
104-
var silentResult = await this.userAgentApplication.acquireTokenSilent({
105-
scopes: scopes
106-
});
109+
var silentResult = await this.publicClientApplication
110+
.acquireTokenSilent({
111+
scopes: scopes,
112+
account: accounts[0]
113+
});
107114

108115
return silentResult.accessToken;
109116
} catch (err) {
110117
// If a silent request fails, it may be because the user needs
111118
// to login or grant consent to one or more of the requested scopes
112119
if (this.isInteractionRequired(err)) {
113-
var interactiveResult = await this.userAgentApplication.acquireTokenPopup({
114-
scopes: scopes
115-
});
120+
var interactiveResult = await this.publicClientApplication
121+
.acquireTokenPopup({
122+
scopes: scopes
123+
});
116124

117125
return interactiveResult.accessToken;
118126
} else {
@@ -133,7 +141,9 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
133141
isAuthenticated: true,
134142
user: {
135143
displayName: user.displayName,
136-
email: user.mail || user.userPrincipalName
144+
email: user.mail || user.userPrincipalName,
145+
timeZone: user.mailboxSettings.timeZone,
146+
timeFormat: user.mailboxSettings.timeFormat
137147
},
138148
error: null
139149
});
@@ -179,7 +189,8 @@ export default function withAuthProvider<T extends React.Component<AuthComponent
179189
return (
180190
error.message.indexOf('consent_required') > -1 ||
181191
error.message.indexOf('interaction_required') > -1 ||
182-
error.message.indexOf('login_required') > -1
192+
error.message.indexOf('login_required') > -1 ||
193+
error.message.indexOf('no_account_in_silent_request') > -1
183194
);
184195
}
185196
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
.calendar-view-date-cell {
2+
width: 150px;
3+
}
4+
5+
.calendar-view-date {
6+
width: 40px;
7+
font-size: 36px;
8+
line-height: 36px;
9+
margin-right: 10px;
10+
}
11+
12+
.calendar-view-month {
13+
font-size: 0.75em;
14+
}
15+
16+
.calendar-view-timespan {
17+
width: 200px;
18+
}
19+
20+
.calendar-view-subject {
21+
font-size: 1.25em;
22+
}
23+
24+
.calendar-view-organizer {
25+
font-size: .75em;
26+
}

demo/graph-tutorial/src/Calendar.tsx

Lines changed: 97 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,126 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33
import React from 'react';
4+
import { NavLink as RouterNavLink } from 'react-router-dom';
45
import { Table } from 'reactstrap';
5-
import moment from 'moment';
6+
import moment, { Moment } from 'moment-timezone';
7+
import { findOneIana } from "windows-iana";
68
import { Event } from 'microsoft-graph';
79
import { config } from './Config';
8-
import { getEvents } from './GraphService';
10+
import { getUserWeekCalendar } from './GraphService';
911
import withAuthProvider, { AuthComponentProps } from './AuthProvider';
12+
import CalendarDayRow from './CalendarDayRow';
13+
import './Calendar.css';
1014

1115
interface CalendarState {
16+
eventsLoaded: boolean;
1217
events: Event[];
13-
}
14-
15-
// Helper function to format Graph date/time
16-
function formatDateTime(dateTime: string | undefined) {
17-
if (dateTime !== undefined) {
18-
return moment.utc(dateTime).local().format('M/D/YY h:mm A');
19-
}
18+
startOfWeek: Moment | undefined;
2019
}
2120

2221
class Calendar extends React.Component<AuthComponentProps, CalendarState> {
2322
constructor(props: any) {
2423
super(props);
25-
2624
this.state = {
27-
events: []
25+
eventsLoaded: false,
26+
events: [],
27+
startOfWeek: undefined
2828
};
2929
}
3030

31-
async componentDidMount() {
32-
try {
33-
// Get the user's access token
34-
var accessToken = await this.props.getAccessToken(config.scopes);
35-
// Get the user's events
36-
var events = await getEvents(accessToken);
37-
// Update the array of events in state
38-
this.setState({ events: events.value });
39-
}
40-
catch (err) {
41-
this.props.setError('ERROR', JSON.stringify(err));
31+
async componentDidUpdate()
32+
{
33+
if (this.props.user && !this.state.eventsLoaded)
34+
{
35+
try {
36+
// Get the user's access token
37+
var accessToken = await this.props.getAccessToken(config.scopes);
38+
39+
// Convert user's Windows time zone ("Pacific Standard Time")
40+
// to IANA format ("America/Los_Angeles")
41+
// Moment needs IANA format
42+
var ianaTimeZone = findOneIana(this.props.user.timeZone);
43+
44+
// Get midnight on the start of the current week in the user's timezone,
45+
// but in UTC. For example, for Pacific Standard Time, the time value would be
46+
// 07:00:00Z
47+
var startOfWeek = moment.tz(ianaTimeZone!.valueOf()).startOf('week').utc();
48+
49+
// Get the user's events
50+
var events = await getUserWeekCalendar(accessToken, this.props.user.timeZone, startOfWeek);
51+
52+
// Update the array of events in state
53+
this.setState({
54+
eventsLoaded: true,
55+
events: events,
56+
startOfWeek: startOfWeek
57+
});
58+
}
59+
catch (err) {
60+
this.props.setError('ERROR', JSON.stringify(err));
61+
}
4262
}
4363
}
4464

4565
// <renderSnippet>
4666
render() {
67+
var sunday = moment(this.state.startOfWeek);
68+
var monday = moment(sunday).add(1, 'day');
69+
var tuesday = moment(monday).add(1, 'day');
70+
var wednesday = moment(tuesday).add(1, 'day');
71+
var thursday = moment(wednesday).add(1, 'day');
72+
var friday = moment(thursday).add(1, 'day');
73+
var saturday = moment(friday).add(1, 'day');
74+
4775
return (
4876
<div>
49-
<h1>Calendar</h1>
50-
<Table>
51-
<thead>
52-
<tr>
53-
<th scope="col">Organizer</th>
54-
<th scope="col">Subject</th>
55-
<th scope="col">Start</th>
56-
<th scope="col">End</th>
57-
</tr>
58-
</thead>
59-
<tbody>
60-
{this.state.events.map(
61-
function(event: Event){
62-
return(
63-
<tr key={event.id}>
64-
<td>{event.organizer?.emailAddress?.name}</td>
65-
<td>{event.subject}</td>
66-
<td>{formatDateTime(event.start?.dateTime)}</td>
67-
<td>{formatDateTime(event.end?.dateTime)}</td>
68-
</tr>
69-
);
70-
})}
71-
</tbody>
72-
</Table>
77+
<div className="mb-3">
78+
<h1 className="mb-3">{sunday.format('MMMM D, YYYY')} - {saturday.format('MMMM D, YYYY')}</h1>
79+
<RouterNavLink to="/newevent" className="btn btn-light btn-sm" exact>New event</RouterNavLink>
80+
</div>
81+
<div className="calendar-week">
82+
<div className="table-responsive">
83+
<Table size="sm">
84+
<thead>
85+
<tr>
86+
<th>Date</th>
87+
<th>Time</th>
88+
<th>Event</th>
89+
</tr>
90+
</thead>
91+
<tbody>
92+
<CalendarDayRow
93+
date={sunday}
94+
timeFormat={this.props.user.timeFormat}
95+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === sunday.day()) } />
96+
<CalendarDayRow
97+
date={monday}
98+
timeFormat={this.props.user.timeFormat}
99+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === monday.day()) } />
100+
<CalendarDayRow
101+
date={tuesday}
102+
timeFormat={this.props.user.timeFormat}
103+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === tuesday.day()) } />
104+
<CalendarDayRow
105+
date={wednesday}
106+
timeFormat={this.props.user.timeFormat}
107+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === wednesday.day()) } />
108+
<CalendarDayRow
109+
date={thursday}
110+
timeFormat={this.props.user.timeFormat}
111+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === thursday.day()) } />
112+
<CalendarDayRow
113+
date={friday}
114+
timeFormat={this.props.user.timeFormat}
115+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === friday.day()) } />
116+
<CalendarDayRow
117+
date={saturday}
118+
timeFormat={this.props.user.timeFormat}
119+
events={this.state.events.filter(event => moment(event.start?.dateTime).day() === saturday.day()) } />
120+
</tbody>
121+
</Table>
122+
</div>
123+
</div>
73124
</div>
74125
);
75126
}

0 commit comments

Comments
 (0)