React graph feedback form update (#1086)

Updated sample react-graph-feedback-form
This commit is contained in:
Sergei Zheleznov 2020-01-11 16:55:35 +01:00 committed by Vesa Juvonen
parent 11da7ad89d
commit 844ab1e4ce
11 changed files with 3714 additions and 1331 deletions

View File

@ -8,7 +8,7 @@ Sample SPFx React web part which allows sending emails using Microsoft Graph.
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-1.8.2-green.svg) ![drop](https://img.shields.io/badge/version-1.9.1-green.svg)
## Applies to ## Applies to
@ -25,7 +25,8 @@ react-graph-feedback-form|Sergei Zheleznov (CollabStack)
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|August 12, 2019|Initial release 1.0.0|August 12, 2019|Initial release
1.0.3|Dec 15, 2019|Added Logger (@pnp/logging), Added max message length property (PropertyFieldNumber control from spfx-controls-react), Code refactoring, SPFx updated to 1.9.1
## Disclaimer ## Disclaimer
@ -54,5 +55,7 @@ This sample illustrates the following concepts:
* sending e-mails using Microsoft Graph * sending e-mails using Microsoft Graph
* using MSGraphClient in a SharePoint Framework web part * using MSGraphClient in a SharePoint Framework web part
* using @microsoft/microsoft-graph-types * using @microsoft/microsoft-graph-types
* using @pnp/logging
* using @pnp/spfx-property-controls
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-graph-feedback-form" /> <img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-graph-feedback-form" />

View File

@ -13,6 +13,7 @@
}, },
"externals": {}, "externals": {},
"localizedResources": { "localizedResources": {
"FeedbackFormWebPartStrings": "lib/webparts/feedbackForm/loc/{locale}.js" "FeedbackFormWebPartStrings": "lib/webparts/feedbackForm/loc/{locale}.js",
"PropertyControlStrings": "node_modules/@pnp/spfx-property-controls/lib/loc/{locale}.js"
} }
} }

View File

@ -3,7 +3,7 @@
"solution": { "solution": {
"name": "spfx-feedback-form-client-side-solution", "name": "spfx-feedback-form-client-side-solution",
"id": "8cf91ad7-be8e-4f6c-a1eb-a790f3ef5a32", "id": "8cf91ad7-be8e-4f6c-a1eb-a790f3ef5a32",
"version": "1.0.0.4", "version": "1.0.0.5",
"includeClientSideAssets": true, "includeClientSideAssets": true,
"isDomainIsolated": false, "isDomainIsolated": false,
"webApiPermissionRequests": [ "webApiPermissionRequests": [

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "react-graph-feedback-form", "name": "react-graph-feedback-form",
"version": "1.0.0", "version": "1.0.3",
"author": { "author": {
"name": "Sergei Zheleznov", "name": "Sergei Zheleznov",
"url": "https://collabstack.de" "url": "https://collabstack.de"
@ -16,31 +16,33 @@
"test": "gulp test" "test": "gulp test"
}, },
"dependencies": { "dependencies": {
"react": "16.7.0", "@microsoft/sp-core-library": "1.9.1",
"react-dom": "16.7.0", "@microsoft/sp-lodash-subset": "1.9.1",
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
"@microsoft/sp-property-pane": "1.9.1",
"@microsoft/sp-webpart-base": "1.9.1",
"@pnp/logging": "^1.3.6",
"@pnp/spfx-property-controls": "1.16.0",
"@types/es6-promise": "0.0.33",
"@types/react": "16.7.22", "@types/react": "16.7.22",
"@types/react-dom": "16.8.0", "@types/react-dom": "16.8.0",
"office-ui-fabric-react": "6.143.0",
"@microsoft/sp-core-library": "1.8.2",
"@microsoft/sp-property-pane": "1.8.2",
"@microsoft/sp-webpart-base": "1.8.2",
"@microsoft/sp-lodash-subset": "1.8.2",
"@microsoft/sp-office-ui-fabric-core": "1.8.2",
"@types/webpack-env": "1.13.1", "@types/webpack-env": "1.13.1",
"@types/es6-promise": "0.0.33" "office-ui-fabric-react": "^6.143.0",
"react": "16.7.0",
"react-dom": "16.7.0"
}, },
"resolutions": { "resolutions": {
"@types/react": "16.7.22" "@types/react": "16.7.22"
}, },
"devDependencies": { "devDependencies": {
"@microsoft/microsoft-graph-types": "^1.10.0", "@microsoft/microsoft-graph-types": "^1.10.0",
"@microsoft/rush-stack-compiler-2.9": "0.7.7", "@microsoft/rush-stack-compiler-2.9": "0.9.10",
"@microsoft/sp-build-web": "1.8.2", "@microsoft/sp-build-web": "1.9.1",
"@microsoft/sp-module-interfaces": "1.8.2", "@microsoft/sp-module-interfaces": "1.9.1",
"@microsoft/sp-tslint-rules": "1.8.2", "@microsoft/sp-tslint-rules": "1.9.1",
"@microsoft/sp-webpart-workbench": "1.8.2", "@microsoft/sp-webpart-workbench": "1.9.1",
"@types/chai": "3.4.34", "@types/chai": "4.2.7",
"@types/mocha": "2.2.38", "@types/mocha": "5.2.7",
"ajv": "~5.2.2", "ajv": "~5.2.2",
"gulp": "~3.9.1" "gulp": "~3.9.1"
} }

View File

@ -6,38 +6,52 @@ import {
IPropertyPaneConfiguration, IPropertyPaneConfiguration,
PropertyPaneTextField PropertyPaneTextField
} from '@microsoft/sp-property-pane'; } from '@microsoft/sp-property-pane';
import * as strings from 'FeedbackFormWebPartStrings'; import * as strings from 'FeedbackFormWebPartStrings';
import FeedbackForm from './components/FeedbackForm'; import FeedbackForm from './components/FeedbackForm';
import { IFeedbackFormProps } from './components/IFeedbackFormProps'; import { IFeedbackFormProps } from './components/IFeedbackFormProps';
import { MSGraphClient } from '@microsoft/sp-http'; import { MSGraphClient } from '@microsoft/sp-http';
// https://sharepoint.github.io/sp-dev-fx-property-controls/
import { PropertyFieldNumber } from '@pnp/spfx-property-controls/lib/PropertyFieldNumber';
import {
Logger,
ConsoleListener,
LogLevel
} from "@pnp/logging";
// https://pnp.github.io/pnpjs/logging/docs/
// https://blog.mastykarz.nl/logging-sharepoint-framework/
const LOG_SOURCE: string = 'FeedbackFormWebPart';
Logger.subscribe(new ConsoleListener());
Logger.activeLogLevel = LogLevel.Info;
export interface IFeedbackFormWebPartProps { export interface IFeedbackFormWebPartProps {
targetEmail: string; targetEmail: string;
subject: string; subject: string;
maxMessageLength: number;
} }
export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedbackFormWebPartProps> { export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedbackFormWebPartProps> {
private graphClient: MSGraphClient;
private _graphClient: MSGraphClient; public async onInit(): Promise<void> {
Logger.write(`[${LOG_SOURCE}] onInit()`);
public onInit(): Promise<void> { try {
return new Promise<void>((resolve: () => void, reject: (error: any) => void ): void => { Logger.write(`[${LOG_SOURCE}] trying to retrieve graphClient`);
this.context.msGraphClientFactory this.graphClient = await this.context.msGraphClientFactory.getClient();
.getClient() } catch (error) {
.then((cli: MSGraphClient): void => { Logger.writeJSON(error, LogLevel.Error);
this._graphClient = cli; }
resolve();
}, err => reject(err));
});
} }
public render(): void { public render(): void {
Logger.write(`[${LOG_SOURCE}] render()`);
const element: React.ReactElement<IFeedbackFormProps> = React.createElement( const element: React.ReactElement<IFeedbackFormProps> = React.createElement(
FeedbackForm, FeedbackForm,
{ {
graphClient: this._graphClient, graphClient: this.graphClient,
targetEmail: this.properties.targetEmail, targetEmail: this.properties.targetEmail,
maxMessageLength: this.properties.maxMessageLength,
subject: this.properties.subject subject: this.properties.subject
} }
); );
@ -45,6 +59,7 @@ export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedback
} }
protected onDispose(): void { protected onDispose(): void {
Logger.write(`[${LOG_SOURCE}] onDispose()`);
ReactDom.unmountComponentAtNode(this.domElement); ReactDom.unmountComponentAtNode(this.domElement);
} }
@ -68,6 +83,14 @@ export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedback
}), }),
PropertyPaneTextField('subject', { PropertyPaneTextField('subject', {
label: strings.SubjectFieldLabel label: strings.SubjectFieldLabel
}),
PropertyFieldNumber("maxMessageLength", {
key: "maxMessageLength",
label: "Maximum length of a message",
value: this.properties.maxMessageLength,
maxValue: 250,
minValue: 3,
disabled: false
}) })
] ]
} }

View File

@ -1,9 +0,0 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.feedbackForm {
max-width: 700px;
margin: 0px auto;
.formActions {
margin: 10px 0;
}
}

View File

@ -1,20 +1,28 @@
import * as React from 'react'; import * as React from 'react';
import styles from './FeedbackForm.module.scss';
import { IFeedbackFormProps } from './IFeedbackFormProps'; import { IFeedbackFormProps } from './IFeedbackFormProps';
import { escape } from '@microsoft/sp-lodash-subset'; import { escape } from '@microsoft/sp-lodash-subset';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
// https://developer.microsoft.com/en-us/fabric#/controls/web
import { import {
TextField, TextField,
DefaultButton, DefaultButton,
MessageBar, MessageBar,
MessageBarType, MessageBarType,
MessageBarButton MessageBarButton,
Stack
} from 'office-ui-fabric-react'; } from 'office-ui-fabric-react';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import {
Logger,
LogLevel
} from "@pnp/logging";
const LOG_SOURCE: string = 'FeedbackForm';
export interface IFeedbackFormState { export interface IFeedbackFormState {
isBusy: boolean; isBusy: boolean;
message: string; messageWasSended: boolean;
messageSended: boolean; messageText: string;
} }
export default class FeedbackForm extends React.Component<IFeedbackFormProps, IFeedbackFormState> { export default class FeedbackForm extends React.Component<IFeedbackFormProps, IFeedbackFormState> {
@ -24,80 +32,127 @@ export default class FeedbackForm extends React.Component<IFeedbackFormProps, IF
this.state = { this.state = {
isBusy: false, isBusy: false,
message: '', messageWasSended: false,
messageSended: false messageText: '',
}; };
} }
public render(): React.ReactElement<IFeedbackFormProps> { public render(): React.ReactElement<IFeedbackFormProps> {
return ( return (
<div className={ styles.feedbackForm }> <div>
{this.props.targetEmail ? '' : ( {this.props.targetEmail ? '' : this.notConfiguredAlert}
<MessageBar messageBarType={MessageBarType.warning}>Target email is empty! Please configure this web part first.</MessageBar> {this.state.messageWasSended ? this.messageBar : this.feedbackForm}
)}
{this.state.messageSended ? (
<MessageBar
actions={
<div>
<MessageBarButton onClick={()=>{
this.setState({
messageSended:false
});
}}>I want to send more!</MessageBarButton>
</div>
}
messageBarType={MessageBarType.success}
isMultiline={false}
>
Message was sent!
</MessageBar>
) :
(
<>
<TextField disabled={this.state.isBusy} label="Feedback" multiline rows={3} name="text" value={this.state.message} onChange={this._onChange} />
<div className={ styles.formActions }>
<DefaultButton disabled={this.state.isBusy || !this.props.targetEmail} onClick={this._sendMessage}>Send</DefaultButton>
</div>
</>
)}
</div> </div>
); );
} }
private _onChange = (event: React.ChangeEvent<HTMLInputElement>) : void => { private get feedbackForm(): JSX.Element {
this.setState({message:event.target.value});
const { messageText, isBusy } = this.state;
const { targetEmail, maxMessageLength } = this.props;
return(
<Stack gap={5} styles={{ root: { width: 650, margin: "0 auto" } }}>
<TextField
disabled={isBusy}
label="Feedback"
maxLength={maxMessageLength}
multiline
rows={3}
value={messageText}
onChange={this.onTextFieldChangeHandler}
/>
<p>{messageText.length} of {maxMessageLength}</p>
<div>
<DefaultButton
disabled={isBusy || !targetEmail}
onClick={this.sendMessageHandler}
>Send Message</DefaultButton>
</div>
</Stack>
);
} }
private _sendMessage = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) : Promise<void> => { private get messageBar(): JSX.Element {
this.setState({isBusy:true}); Logger.write(`[${LOG_SOURCE}] renderMessageBar()`);
return(
const msg = { <MessageBar
subject: escape(this.props.subject), actions = {
importance:"low", <div>
body:{ <MessageBarButton onClick={this.messageBarButtonOnClickHandler}>I want to send more!</MessageBarButton>
contentType:"html", </div>
content: escape(this.state.message)
},
toRecipients:[
{
emailAddress:{
address: this.props.targetEmail
}
} }
] messageBarType={MessageBarType.success}
} as MicrosoftGraph.Message; isMultiline={false}
>
Message was sent!
</MessageBar>
);
}
private get notConfiguredAlert(): JSX.Element {
Logger.write(`[${LOG_SOURCE}] renderNotConfiguredAlert()`);
return(
<MessageBar messageBarType={MessageBarType.warning}>Target email is empty! Please configure this web part first.</MessageBar>
);
}
private onTextFieldChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>): void => {
const messageText = event.target.value;
this.setState({messageText});
}
private sendMessageHandler = async(): Promise<void> => {
Logger.write(`[${LOG_SOURCE}] sendMessageHandler()`);
const { graphClient, targetEmail, subject } = this.props;
const { messageText } = this.state;
this.setState({ isBusy:true });
Logger.write(`[${LOG_SOURCE}] composing message`);
const message: MicrosoftGraph.Message = {
subject: escape(subject),
importance:"low",
body: {
contentType:"html",
content: escape(messageText)
},
toRecipients: [
{
emailAddress: {
address: targetEmail
}
}
]
};
let messageWasSended = false;
try {
Logger.write(`[${LOG_SOURCE}] trying send email to ${this.props.targetEmail}`);
await graphClient.api('/me/sendMail').post({message});
messageWasSended = true;
} catch (error) {
Logger.writeJSON(error, LogLevel.Error);
} finally {
await this.props.graphClient.api('/me/sendMail')
.post({
message : msg
}).then(() => {
this.setState({ this.setState({
isBusy:false, isBusy:false,
message: '', messageWasSended
messageSended: true
}); });
},(error: any) => { }
console.log(error); }
});
private messageBarButtonOnClickHandler = (): void => {
this.setState({
messageWasSended:false,
messageText: '',
});
} }
} }

View File

@ -3,5 +3,6 @@ import { MSGraphClient } from '@microsoft/sp-http';
export interface IFeedbackFormProps { export interface IFeedbackFormProps {
graphClient: MSGraphClient; graphClient: MSGraphClient;
targetEmail: string; targetEmail: string;
maxMessageLength: number;
subject: string; subject: string;
} }

View File

@ -27,4 +27,4 @@
"variable-name": false, "variable-name": false,
"whitespace": false "whitespace": false
} }
} }