Merge branch 'dev'
|
@ -1,7 +1,7 @@
|
|||
# React Graph Calendar Web Part
|
||||
|
||||
## Summary
|
||||
This is a sample web part developed using React Framework to gather events from the underlying group calendar of a Team site. This sample also demonstrates the utilization of web parts as Teams tabs and offering a visualization context to change behaviors based on the platform used (Getting the proper information from the team vs. SharePoint site, understanding the context of the theme on Teams, etc.).
|
||||
This is a sample web part developed using React Framework to gather events from the underlying group calendar of a Team site. This sample also demonstrates the utilization of web parts as Teams tabs and Personal tab and offering a visualization context to change behaviors based on the platform used (Getting the proper information from the team vs. SharePoint site, understanding the context of the theme on Teams, etc.).
|
||||
|
||||
### Web Part in SharePoint Online
|
||||
![The web part in action](./assets/react-graph-calendar-spo.gif)
|
||||
|
@ -9,14 +9,14 @@ This is a sample web part developed using React Framework to gather events from
|
|||
### Web Part in Microsoft Teams
|
||||
![The web part in action](./assets/react-graph-calendar-teams.gif)
|
||||
|
||||
Webpart is developed using below technologies
|
||||
Web part is developed using below technologies
|
||||
* React Framework
|
||||
* Full Calendar (fullcalendar.io)
|
||||
* Microsoft Teams API
|
||||
* Office UI Fabric
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/version-1.9.1-green.svg)
|
||||
![drop](https://img.shields.io/badge/version-1.10-green.svg)
|
||||
|
||||
## Applies to
|
||||
|
||||
|
@ -38,6 +38,7 @@ react-graph-calendar | [Sébastien Levert](https://www.linkedin.com/in/sebastien
|
|||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0 |December 29, 2019 | Initial Release
|
||||
1.1 |January 08, 2020 | Bumped to SPFx 1.10 and added the Personal Tab support
|
||||
|
||||
## Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
|
Before Width: | Height: | Size: 9.1 MiB After Width: | Height: | Size: 5.2 MiB |
Before Width: | Height: | Size: 9.3 MiB After Width: | Height: | Size: 8.7 MiB |
|
@ -11,6 +11,10 @@
|
|||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Calendars.Read"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
@ -17,10 +17,10 @@
|
|||
"@fullcalendar/moment": "^4.3.0",
|
||||
"@fullcalendar/moment-timezone": "^4.3.0",
|
||||
"@fullcalendar/react": "^4.3.0",
|
||||
"@microsoft/sp-core-library": "1.9.1",
|
||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
||||
"@microsoft/sp-webpart-base": "1.9.1",
|
||||
"@microsoft/sp-core-library": "1.10.0",
|
||||
"@microsoft/sp-lodash-subset": "1.10.0",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
|
||||
"@microsoft/sp-webpart-base": "1.10.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
|
@ -35,10 +35,10 @@
|
|||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/sp-build-web": "1.9.1",
|
||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||
"@microsoft/sp-build-web": "1.10.0",
|
||||
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||
"gulp": "~3.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
|
|
|
@ -12,15 +12,15 @@
|
|||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart", "TeamsTab"],
|
||||
"supportedHosts": ["SharePointWebPart", "TeamsTab", "TeamsPersonalApp"],
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "GraphCalendar" },
|
||||
"description": { "default": "GraphCalendar description" },
|
||||
"officeFabricIconFontName": "Page",
|
||||
"title": { "default": "Graph Calendar" },
|
||||
"description": { "default": "Graph Calendar" },
|
||||
"officeFabricIconFontName": "Calendar",
|
||||
"properties": {
|
||||
"description": "GraphCalendar"
|
||||
"description": "Graph Calendar"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
|
@ -44,15 +44,14 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart<IGraphCa
|
|||
}
|
||||
|
||||
// Sets the Teams context if in Teams
|
||||
if (this.context.microsoftTeams) {
|
||||
this.context.microsoftTeams.getContext(context => {
|
||||
this._teamsContext = context;
|
||||
// resolve the promise
|
||||
resolve(undefined);
|
||||
});
|
||||
if (this.context.sdks.microsoftTeams) {
|
||||
this._teamsContext = this.context.sdks.microsoftTeams.context;
|
||||
|
||||
// Initialize the OUIF icons if in Teams
|
||||
initializeIcons();
|
||||
|
||||
// resolve the promise
|
||||
resolve(undefined);
|
||||
} else {
|
||||
// resolve the promise
|
||||
resolve(undefined);
|
||||
|
|
|
@ -17,6 +17,12 @@ interface IGraphCalendarState {
|
|||
isEventDetailsOpen: boolean;
|
||||
currentSelectedEvent: EventInput;
|
||||
groupId: string;
|
||||
tabType: TabType;
|
||||
}
|
||||
|
||||
enum TabType {
|
||||
TeamsTab,
|
||||
PersonalTab
|
||||
}
|
||||
|
||||
export default class GraphCalendar extends React.Component<IGraphCalendarProps, IGraphCalendarState> {
|
||||
|
@ -45,7 +51,8 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
|||
currentActiveEndDate: null,
|
||||
isEventDetailsOpen: false,
|
||||
currentSelectedEvent: null,
|
||||
groupId: this._isRunningInTeams() ? this.props.teamsContext.groupId : this.props.context.pageContext.site.group ? this.props.context.pageContext.site.group.id : ""
|
||||
groupId: this._isRunningInTeams() ? this.props.teamsContext.groupId : this.props.context.pageContext.site.group ? this.props.context.pageContext.site.group.id : "",
|
||||
tabType: this._isRunningInTeams() ? (this._isPersonalTab() ? TabType.PersonalTab : TabType.TeamsTab) : TabType.TeamsTab
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -73,7 +80,7 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
|||
plugins={[ dayGridPlugin ]}
|
||||
windowResize={this._handleResize.bind(this)}
|
||||
datesRender={this._datesRender.bind(this)}
|
||||
eventClick={this._handleEventClick.bind(this)}
|
||||
eventClick={this._openEventPanel.bind(this)}
|
||||
height={this.state.height}
|
||||
events={this.state.events} />
|
||||
{this.state.currentSelectedEvent &&
|
||||
|
@ -81,6 +88,8 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
|||
isOpen={this.state.isEventDetailsOpen}
|
||||
type={ PanelType.smallFixedFar }
|
||||
headerText={this.state.currentSelectedEvent ? this.state.currentSelectedEvent.title : ""}
|
||||
onDismiss={this._closeEventPanel.bind(this)}
|
||||
isLightDismiss={true}
|
||||
closeButtonAriaLabel='Close'>
|
||||
<h3>Start Time</h3>
|
||||
<span>{moment(this.state.currentSelectedEvent.start).format('MMMM Do YYYY [at] h:mm:ss a')}</span>
|
||||
|
@ -123,17 +132,40 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
|||
return this.props.teamsContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates if the current web part is running in a Personal Tab
|
||||
*/
|
||||
private _isPersonalTab() {
|
||||
let _isPersonalTab: Boolean = false;
|
||||
|
||||
if(this._isRunningInTeams() && !this.props.teamsContext.teamId) {
|
||||
_isPersonalTab = true;
|
||||
}
|
||||
|
||||
return _isPersonalTab;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event and opens the OUIF Panel
|
||||
* @param eventClickInfo The information about the selected event
|
||||
*/
|
||||
private _handleEventClick(eventClickInfo: any) {
|
||||
private _openEventPanel(eventClickInfo: any) {
|
||||
this.setState({
|
||||
isEventDetailsOpen: true,
|
||||
currentSelectedEvent: eventClickInfo.event
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event on the dismiss from the Panel and closes the OUIF Panel
|
||||
*/
|
||||
private _closeEventPanel() {
|
||||
this.setState({
|
||||
isEventDetailsOpen: true,
|
||||
currentSelectedEvent: null
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* If the view changed, reload the events based on the active view
|
||||
* @param info Information about the current active view
|
||||
|
@ -166,15 +198,20 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
|||
*/
|
||||
private _loadEvents(startDate: Date, endDate: Date): void {
|
||||
|
||||
// If a Group was found, execute the query. If not, do nothing.
|
||||
if(this.state.groupId) {
|
||||
// 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) {
|
||||
|
||||
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(`/groups/${this.state.groupId}/events`)
|
||||
.api(apiUrl)
|
||||
.version("v1.0")
|
||||
.select('subject,start,end,location,bodyPreview,isAllDay')
|
||||
.filter(`start/dateTime ge '${startDate.toISOString()}' and end/dateTime le '${endDate.toISOString()}'`)
|
||||
|
|
|
@ -8,7 +8,7 @@ Sample SPFx React web part which allows sending emails using Microsoft Graph.
|
|||
|
||||
## 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
|
||||
|
||||
|
@ -25,7 +25,8 @@ react-graph-feedback-form|Sergei Zheleznov (CollabStack)
|
|||
|
||||
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
|
||||
|
@ -54,5 +55,7 @@ This sample illustrates the following concepts:
|
|||
* sending e-mails using Microsoft Graph
|
||||
* using MSGraphClient in a SharePoint Framework web part
|
||||
* 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" />
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
},
|
||||
"externals": {},
|
||||
"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"
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
"solution": {
|
||||
"name": "spfx-feedback-form-client-side-solution",
|
||||
"id": "8cf91ad7-be8e-4f6c-a1eb-a790f3ef5a32",
|
||||
"version": "1.0.0.4",
|
||||
"version": "1.0.0.5",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "react-graph-feedback-form",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.3",
|
||||
"author": {
|
||||
"name": "Sergei Zheleznov",
|
||||
"url": "https://collabstack.de"
|
||||
|
@ -16,31 +16,33 @@
|
|||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "16.7.0",
|
||||
"react-dom": "16.7.0",
|
||||
"@microsoft/sp-core-library": "1.9.1",
|
||||
"@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-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/es6-promise": "0.0.33"
|
||||
"office-ui-fabric-react": "^6.143.0",
|
||||
"react": "16.7.0",
|
||||
"react-dom": "16.7.0"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.7.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^1.10.0",
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.7",
|
||||
"@microsoft/sp-build-web": "1.8.2",
|
||||
"@microsoft/sp-module-interfaces": "1.8.2",
|
||||
"@microsoft/sp-tslint-rules": "1.8.2",
|
||||
"@microsoft/sp-webpart-workbench": "1.8.2",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.9.10",
|
||||
"@microsoft/sp-build-web": "1.9.1",
|
||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||
"@types/chai": "4.2.7",
|
||||
"@types/mocha": "5.2.7",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
|
|
|
@ -6,38 +6,52 @@ import {
|
|||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-property-pane';
|
||||
|
||||
import * as strings from 'FeedbackFormWebPartStrings';
|
||||
import FeedbackForm from './components/FeedbackForm';
|
||||
import { IFeedbackFormProps } from './components/IFeedbackFormProps';
|
||||
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 {
|
||||
targetEmail: string;
|
||||
subject: string;
|
||||
maxMessageLength: number;
|
||||
}
|
||||
|
||||
export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedbackFormWebPartProps> {
|
||||
|
||||
private _graphClient: MSGraphClient;
|
||||
|
||||
public onInit(): Promise<void> {
|
||||
return new Promise<void>((resolve: () => void, reject: (error: any) => void ): void => {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((cli: MSGraphClient): void => {
|
||||
this._graphClient = cli;
|
||||
resolve();
|
||||
}, err => reject(err));
|
||||
});
|
||||
private graphClient: MSGraphClient;
|
||||
public async onInit(): Promise<void> {
|
||||
Logger.write(`[${LOG_SOURCE}] onInit()`);
|
||||
try {
|
||||
Logger.write(`[${LOG_SOURCE}] trying to retrieve graphClient`);
|
||||
this.graphClient = await this.context.msGraphClientFactory.getClient();
|
||||
} catch (error) {
|
||||
Logger.writeJSON(error, LogLevel.Error);
|
||||
}
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
Logger.write(`[${LOG_SOURCE}] render()`);
|
||||
const element: React.ReactElement<IFeedbackFormProps> = React.createElement(
|
||||
FeedbackForm,
|
||||
{
|
||||
graphClient: this._graphClient,
|
||||
graphClient: this.graphClient,
|
||||
targetEmail: this.properties.targetEmail,
|
||||
maxMessageLength: this.properties.maxMessageLength,
|
||||
subject: this.properties.subject
|
||||
}
|
||||
);
|
||||
|
@ -45,6 +59,7 @@ export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedback
|
|||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
Logger.write(`[${LOG_SOURCE}] onDispose()`);
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
|
@ -68,6 +83,14 @@ export default class FeedbackFormWebPart extends BaseClientSideWebPart<IFeedback
|
|||
}),
|
||||
PropertyPaneTextField('subject', {
|
||||
label: strings.SubjectFieldLabel
|
||||
}),
|
||||
PropertyFieldNumber("maxMessageLength", {
|
||||
key: "maxMessageLength",
|
||||
label: "Maximum length of a message",
|
||||
value: this.properties.maxMessageLength,
|
||||
maxValue: 250,
|
||||
minValue: 3,
|
||||
disabled: false
|
||||
})
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1,20 +1,28 @@
|
|||
import * as React from 'react';
|
||||
import styles from './FeedbackForm.module.scss';
|
||||
import { IFeedbackFormProps } from './IFeedbackFormProps';
|
||||
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 {
|
||||
TextField,
|
||||
DefaultButton,
|
||||
MessageBar,
|
||||
MessageBarType,
|
||||
MessageBarButton
|
||||
MessageBarButton,
|
||||
Stack
|
||||
} 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 {
|
||||
isBusy: boolean;
|
||||
message: string;
|
||||
messageSended: boolean;
|
||||
messageWasSended: boolean;
|
||||
messageText: string;
|
||||
}
|
||||
|
||||
export default class FeedbackForm extends React.Component<IFeedbackFormProps, IFeedbackFormState> {
|
||||
|
@ -24,26 +32,55 @@ export default class FeedbackForm extends React.Component<IFeedbackFormProps, IF
|
|||
|
||||
this.state = {
|
||||
isBusy: false,
|
||||
message: '',
|
||||
messageSended: false
|
||||
messageWasSended: false,
|
||||
messageText: '',
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IFeedbackFormProps> {
|
||||
|
||||
return (
|
||||
<div className={ styles.feedbackForm }>
|
||||
{this.props.targetEmail ? '' : (
|
||||
<MessageBar messageBarType={MessageBarType.warning}>Target email is empty! Please configure this web part first.</MessageBar>
|
||||
)}
|
||||
{this.state.messageSended ? (
|
||||
<MessageBar
|
||||
actions={
|
||||
<div>
|
||||
<MessageBarButton onClick={()=>{
|
||||
this.setState({
|
||||
messageSended:false
|
||||
});
|
||||
}}>I want to send more!</MessageBarButton>
|
||||
{this.props.targetEmail ? '' : this.notConfiguredAlert}
|
||||
{this.state.messageWasSended ? this.messageBar : this.feedbackForm}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private get feedbackForm(): JSX.Element {
|
||||
|
||||
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 get messageBar(): JSX.Element {
|
||||
Logger.write(`[${LOG_SOURCE}] renderMessageBar()`);
|
||||
return(
|
||||
<MessageBar
|
||||
actions = {
|
||||
<div>
|
||||
<MessageBarButton onClick={this.messageBarButtonOnClickHandler}>I want to send more!</MessageBarButton>
|
||||
</div>
|
||||
}
|
||||
messageBarType={MessageBarType.success}
|
||||
|
@ -51,53 +88,71 @@ export default class FeedbackForm extends React.Component<IFeedbackFormProps, IF
|
|||
>
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
private _onChange = (event: React.ChangeEvent<HTMLInputElement>) : void => {
|
||||
this.setState({message:event.target.value});
|
||||
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 _sendMessage = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) : Promise<void> => {
|
||||
this.setState({isBusy:true});
|
||||
private onTextFieldChangeHandler = (event: React.ChangeEvent<HTMLTextAreaElement>): void => {
|
||||
const messageText = event.target.value;
|
||||
this.setState({messageText});
|
||||
}
|
||||
|
||||
const msg = {
|
||||
subject: escape(this.props.subject),
|
||||
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:{
|
||||
body: {
|
||||
contentType:"html",
|
||||
content: escape(this.state.message)
|
||||
content: escape(messageText)
|
||||
},
|
||||
toRecipients:[
|
||||
toRecipients: [
|
||||
{
|
||||
emailAddress:{
|
||||
address: this.props.targetEmail
|
||||
emailAddress: {
|
||||
address: targetEmail
|
||||
}
|
||||
}
|
||||
]
|
||||
} as MicrosoftGraph.Message;
|
||||
};
|
||||
|
||||
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({
|
||||
isBusy:false,
|
||||
message: '',
|
||||
messageSended: true
|
||||
messageWasSended
|
||||
});
|
||||
},(error: any) => {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
private messageBarButtonOnClickHandler = (): void => {
|
||||
|
||||
this.setState({
|
||||
messageWasSended:false,
|
||||
messageText: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ import { MSGraphClient } from '@microsoft/sp-http';
|
|||
export interface IFeedbackFormProps {
|
||||
graphClient: MSGraphClient;
|
||||
targetEmail: string;
|
||||
maxMessageLength: number;
|
||||
subject: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
|
||||
[*]
|
||||
|
||||
# change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
|
||||
# we recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[{package,bower}.json]
|
||||
indent_style = space
|
||||
indent_size = 2
|
|
@ -0,0 +1,32 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
|
||||
# Dependency directories
|
||||
node_modules
|
||||
|
||||
# Build generated files
|
||||
dist
|
||||
lib
|
||||
solution
|
||||
temp
|
||||
*.sppkg
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Visual Studio files
|
||||
.ntvs_analysis.dat
|
||||
.vs
|
||||
bin
|
||||
obj
|
||||
|
||||
# Resx Generated Code
|
||||
*.resx.ts
|
||||
|
||||
# Styles Generated Code
|
||||
*.scss.ts
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"@microsoft/generator-sharepoint": {
|
||||
"isCreatingSolution": true,
|
||||
"environment": "spo",
|
||||
"version": "1.9.1",
|
||||
"libraryName": "react-manage-o-365-groups",
|
||||
"libraryId": "0db7eade-b32d-477c-8a16-b7646837c370",
|
||||
"packageManager": "npm",
|
||||
"isDomainIsolated": false,
|
||||
"componentType": "webpart"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
## Manage Office 365 Groups with SPFx
|
||||
|
||||
### Summary
|
||||
Office 365 Groups is the foundational membership service, that drives all teamwork across Microsoft 365. Once in a group, we can get the benefits of the group-connected services like shared Outlook inbox, shared calendar, SharePoint site, Planner, Power BI, Yammer, and Teams.
|
||||
|
||||
Is there a way to see all Office 365 Groups in a tenant?
|
||||
*Yes, partially. You can view and manage your own groups in Outlook on the web.*
|
||||
|
||||
This SPFx solution goes beyond this limitation and provides below functionalities using MS Graph APIs:
|
||||
1. List all public and private groups with basic information (name, description, privacy, logo, etc.)
|
||||
2. Search the group
|
||||
3. Join and leave public groups
|
||||
4. Join a private group with approval of group administrator (using Power Automate flow) and leave the private group
|
||||
5. Manage group link for administrators
|
||||
6. Browse MS Teams associated with group
|
||||
7. Set up new group
|
||||
|
||||
![WebPart Preview](./assets/web-part-preview.gif)
|
||||
|
||||
### NPM Packages Used
|
||||
Below NPM package(s) are used to develop this sample:
|
||||
1. PnP/PnPjs (https://pnp.github.io/pnpjs/)
|
||||
2. @pnp/spfx-controls-react (https://sharepoint.github.io/sp-dev-fx-controls-react/)
|
||||
|
||||
### Project setup and important files
|
||||
```txt
|
||||
react-manage-o365-groups
|
||||
├── Power Automate Flow <-- Power Automate Flow Templates
|
||||
│ └── Logic Apps Template
|
||||
│ └── Join Private Group.json
|
||||
│ └── Package
|
||||
│ └── JoinPrivateGroup_20191230151251.zip
|
||||
└── src
|
||||
└── models
|
||||
├── IGroup.ts
|
||||
├── ITeamChannel.ts
|
||||
└── services
|
||||
├── O365GroupService.ts <-- Extensible Service
|
||||
└── webparts
|
||||
└── o365GroupsManager
|
||||
├── O365GroupsManagerWebPart.manifest.json
|
||||
├── O365GroupsManagerWebPart.ts
|
||||
├── components
|
||||
│ └── O365GroupsManager
|
||||
│ │ ├── O365GroupsManager.tsx <-- Group Management Component
|
||||
│ │ ├── O365GroupsManager.module.scss
|
||||
│ │ ├── IO365GroupsManagerProps.ts
|
||||
│ │ ├── IO365GroupsManagerState.ts
|
||||
│ └── GroupList <-- Group Listing Component
|
||||
| │ ├── GroupList.tsx
|
||||
| │ ├── IGroupListProps.ts
|
||||
| │ ├── IGroupListState.ts
|
||||
│ └── NewGroup <-- New Group Creation Component
|
||||
| ├── NewGroup.tsx
|
||||
| ├── INewGroupProps.ts
|
||||
| ├── INewGroupState.ts
|
||||
└── loc
|
||||
├── en-us.js
|
||||
└── mystrings.d.ts
|
||||
```
|
||||
|
||||
## Used SharePoint Framework Version
|
||||
![drop](https://img.shields.io/badge/drop-1.9.1-green.svg)
|
||||
|
||||
# Demo
|
||||
The SPFx solution provides below functionalities:
|
||||
|
||||
## O365 Group Listing and Search
|
||||
The web part lists all public and private O365 groups and helps to search the group by name.
|
||||
|
||||
![Group Listing and Search](./assets/group-listing-search.gif)
|
||||
|
||||
|
||||
Below are the list of options available for user based on the role.
|
||||
|
||||
Group Visibility|Role|Action
|
||||
----------------|----|------
|
||||
Public|Owner|Manage Group
|
||||
Public|Member|Leave Group
|
||||
Public|-|Join Group
|
||||
Private|Owner|Manage Group
|
||||
Private|Member|Leave Group
|
||||
Private|-|Request to Join Group
|
||||
|
||||
## Join the Public Group
|
||||
Clicking "Join Group" icon against the O365 group helps to join the public group.
|
||||
|
||||
![Join Public Group](./assets/join-group.gif)
|
||||
|
||||
## Join the Private Group
|
||||
Clicking "Request to Join Group" icon against the O365 group helps to send the approval request to the owners of the private group. The approval takes place using Power Automate.
|
||||
|
||||
![Join Private Group](./assets/join-private-group.gif)
|
||||
|
||||
## Leave the Group
|
||||
Clicking "Leave Group" icon against the O365 group helps to leave the public and private group.
|
||||
|
||||
![Leave Group](./assets/leave-group.gif)
|
||||
|
||||
## Create New Group
|
||||
The web part helps to create new O365 group.
|
||||
|
||||
![New Group](./assets/new-group.gif)
|
||||
|
||||
## Applies to
|
||||
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
|
||||
* [SharePoint Online](https://docs.microsoft.com/en-us/sharepoint/sharepoint-online)
|
||||
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
|
||||
|
||||
# WebPart Properties
|
||||
Property|Type|Required|Comments
|
||||
--------|----|--------|--------
|
||||
Flow URL|Text|No|URL of MS Flow (Power Automate) to join the private group
|
||||
|
||||
# Solution
|
||||
|
||||
Solution|Author(s)
|
||||
--------|---------
|
||||
react-manage-o365-groups|[Nanddeep Nachan](https://www.linkedin.com/in/nanddeepnachan/) (SharePoint Consultant, [@NanddeepNachan](https://twitter.com/NanddeepNachan) )
|
||||
|[Smita Nachan](https://www.linkedin.com/in/smitanachan/) (SharePoint Consultant, [@SmitaNachan](https://twitter.com/SmitaNachan) )
|
||||
|
||||
## Version history
|
||||
|
||||
Version|Date|Comments
|
||||
-------|----|--------
|
||||
1.0.0|January 01, 2020|Initial release
|
||||
|
||||
|
||||
# Disclaimer
|
||||
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
|
||||
|
||||
---
|
||||
|
||||
# Prerequisites
|
||||
|
||||
- Administrative access to Azure AD of Office 365 tenant
|
||||
- Permissions to create a flow in Power Automate (prior MS Flow)
|
||||
- Set up Power Automate flow for approval to join private group. Please refer [Calling Graph API from Power Automate Flow](https://www.c-sharpcorner.com/article/calling-graph-api-from-power-automate-flow/)
|
||||
- SharePoint Online tenant
|
||||
- Site Collection created under the **/sites/** or **/**-
|
||||
- You need following set of permissions in order to manage Office 365 groups. Find out more about consuming the [Microsoft Graph API in the SharePoint Framework](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aad-tutorial)
|
||||
```
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.AccessAsUser.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.ReadWrite.All"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
# Minimal Path to Awesome
|
||||
|
||||
- Clone this repo
|
||||
- Navigate to the folder with current sample
|
||||
- Restore dependencies: `$ npm i`
|
||||
- Bundle the solution: `$ gulp bundle --ship`
|
||||
- Package the solution: `$ gulp package-solution --ship`
|
||||
- Locate the solution at `./sharepoint/solution/react-manage-o365-groups.sppkg` and upload it to SharePoint tenant app catalog
|
||||
- You will see a message saying that solution has pending permissions which need to be approved:
|
||||
![Pending permission requests](./assets/pending-permission-requests.png)
|
||||
- Approve the permission requests. There are [different options available](https://docs.microsoft.com/en-us/sharepoint/dev/spfx/use-aadhttpclient#manage-permission-requests) - new SharePoint Admin UI, PowerShell, [`$o365`](https://pnp.github.io/office365-cli/) cli.
|
||||
- For the matter of this sample, the fastest way to do it is through new SharePoint Admin UI.
|
||||
- Open Web API permission management page by navigating to the url `https://<tenant>-admin.sharepoint.com/_layouts/15/online/AdminHome.aspx#/webApiPermissionManagement` (change the `<tenant>` to your O365 tenant name) or by going to the new Admin UI directly from old SharePoint Admin Center.
|
||||
- Select pending requests and approve it:
|
||||
![Approve request from new Admin UI](./assets/approve-request.gif)
|
||||
- Run `$ gulp serve --nobrowser`
|
||||
- Open hosted workbench, i.e. `https://<tenant>.sharepoint.com/sites/<your site>/_layouts/15/workbench.aspx`
|
||||
- Search and add `O365 Groups Manager` web part to see it in action
|
||||
|
||||
# Features
|
||||
This project contains sample client-side web part built on the SharePoint Framework illustrating possibilities to manage Office 365 Groups using React and MS Graph.
|
||||
|
||||
This sample illustrates the following concepts on top of the SharePoint Framework:
|
||||
- Using PnP/PnPjs
|
||||
- Creating extensible services
|
||||
- Explore MS Graph APIs for Office 365 Group
|
||||
- Using the MSGraphClient in a SharePoint Framework web part
|
||||
- Requesting API permissions in a SharePoint Framework package
|
||||
- Communicating with the Microsoft Graph using its REST API
|
||||
- Using Office UI Fabric controls for building SharePoint Framework client-side web parts
|
||||
- Passing web part properties to React components
|
||||
- Call MS Flow (Power Automate) flow from SharePoint Framework web part
|
After Width: | Height: | Size: 770 KiB |
After Width: | Height: | Size: 1.7 MiB |
After Width: | Height: | Size: 238 KiB |
After Width: | Height: | Size: 3.6 MiB |
After Width: | Height: | Size: 256 KiB |
After Width: | Height: | Size: 706 KiB |
After Width: | Height: | Size: 34 KiB |
After Width: | Height: | Size: 152 KiB |
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
|
||||
"version": "2.0",
|
||||
"bundles": {
|
||||
"o-365-groups-manager-web-part": {
|
||||
"components": [
|
||||
{
|
||||
"entrypoint": "./lib/webparts/o365GroupsManager/O365GroupsManagerWebPart.js",
|
||||
"manifest": "./src/webparts/o365GroupsManager/O365GroupsManagerWebPart.manifest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"O365GroupsManagerWebPartStrings": "lib/webparts/o365GroupsManager/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
|
||||
"deployCdnPath": "temp/deploy"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
|
||||
"workingDir": "./temp/deploy/",
|
||||
"account": "<!-- STORAGE ACCOUNT NAME -->",
|
||||
"container": "react-manage-o-365-groups",
|
||||
"accessKey": "<!-- ACCESS KEY -->"
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
|
||||
"solution": {
|
||||
"name": "react-manage-o365-groups-client-side-solution",
|
||||
"id": "0db7eade-b32d-477c-8a16-b7646837c370",
|
||||
"version": "1.0.0.0",
|
||||
"includeClientSideAssets": true,
|
||||
"isDomainIsolated": false,
|
||||
"webApiPermissionRequests": [
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Directory.AccessAsUser.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.Read.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "User.ReadWrite.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
"paths": {
|
||||
"zippedPackage": "solution/react-manage-o365-groups.sppkg"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
|
||||
"port": 4321,
|
||||
"https": true,
|
||||
"initialPage": "https://localhost:5432/workbench",
|
||||
"api": {
|
||||
"port": 5432,
|
||||
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
|
||||
"cdnBasePath": "<!-- PATH TO CDN -->"
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
const gulp = require('gulp');
|
||||
const build = require('@microsoft/sp-build-web');
|
||||
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
|
||||
|
||||
build.initialize(gulp);
|
|
@ -0,0 +1,47 @@
|
|||
{
|
||||
"name": "react-manage-o-365-groups",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"main": "lib/index.js",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp bundle",
|
||||
"clean": "gulp clean",
|
||||
"test": "gulp test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/sp-core-library": "1.9.1",
|
||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
||||
"@microsoft/sp-webpart-base": "1.9.1",
|
||||
"@pnp/common": "^1.3.8",
|
||||
"@pnp/logging": "^1.3.8",
|
||||
"@pnp/odata": "^1.3.8",
|
||||
"@pnp/sp": "^1.3.8",
|
||||
"@pnp/spfx-controls-react": "^1.15.0",
|
||||
"@types/es6-promise": "0.0.33",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/webpack-env": "1.13.1",
|
||||
"office-ui-fabric-react": "6.189.2",
|
||||
"react": "16.8.5",
|
||||
"react-dom": "16.8.5"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "16.8.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^1.12.0",
|
||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||
"@microsoft/sp-build-web": "1.9.1",
|
||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
||||
"@types/chai": "3.4.34",
|
||||
"@types/mocha": "2.2.38",
|
||||
"ajv": "~5.2.2",
|
||||
"gulp": "~3.9.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
// A file is required to be in the root of the /src directory by the TypeScript compiler
|
|
@ -0,0 +1,15 @@
|
|||
// Represents attributes of an O365 Group
|
||||
export interface IGroup {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description?: string;
|
||||
visibility?: string;
|
||||
url?: string;
|
||||
thumbnail?: string;
|
||||
userRole?: string;
|
||||
teamsConnected?: boolean;
|
||||
}
|
||||
|
||||
export interface IGroupCollection {
|
||||
value: IGroup[];
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
// Represents attributes of MS Teams channel
|
||||
export interface ITeamChannel {
|
||||
id: string;
|
||||
displayName: string;
|
||||
description: string;
|
||||
webUrl: string;
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
export * from './IGroup';
|
||||
export * from './ITeamChannel';
|
|
@ -0,0 +1,306 @@
|
|||
import { MSGraphClient, SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions, HttpClientResponse, HttpClient, IHttpClientOptions } from "@microsoft/sp-http";
|
||||
import * as MicrosoftGraph from "@microsoft/microsoft-graph-types";
|
||||
import { WebPartContext } from "@microsoft/sp-webpart-base";
|
||||
import { IGroup, IGroupCollection, ITeamChannel } from "../models";
|
||||
import { GraphRequest } from "@microsoft/microsoft-graph-client";
|
||||
import { Group } from "@microsoft/microsoft-graph-types";
|
||||
|
||||
export class O365GroupService {
|
||||
public context: WebPartContext;
|
||||
|
||||
public setup(context: WebPartContext): void {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public getGroups(): Promise<IGroup[]> {
|
||||
return new Promise<IGroup[]>((resolve, reject) => {
|
||||
try {
|
||||
// Prepare the output array
|
||||
var o365groups: Array<IGroup> = new Array<IGroup>();
|
||||
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api("/groups?$filter=groupTypes/any(c:c eq 'Unified')")
|
||||
.get((error: any, groups: IGroupCollection, rawResponse: any) => {
|
||||
// Map the response to the output array
|
||||
groups.value.map((item: any) => {
|
||||
o365groups.push({
|
||||
id: item.id,
|
||||
displayName: item.displayName,
|
||||
description: item.description,
|
||||
visibility: item.visibility,
|
||||
teamsConnected: item.resourceProvisioningOptions.indexOf("Team") > -1 ? true : false
|
||||
});
|
||||
});
|
||||
|
||||
resolve(o365groups);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getMyMemberGroups(): Promise<IGroup[]> {
|
||||
return new Promise<IGroup[]>((resolve, reject) => {
|
||||
try {
|
||||
// Prepare the output array
|
||||
var o365groups: Array<IGroup> = new Array<IGroup>();
|
||||
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api("/me/memberOf/$/microsoft.graph.group?$filter=groupTypes/any(a:a eq 'unified')")
|
||||
.get((error: any, groups: IGroupCollection, rawResponse: any) => {
|
||||
// Map the response to the output array
|
||||
groups.value.map((item: any) => {
|
||||
o365groups.push({
|
||||
id: item.id,
|
||||
displayName: item.displayName,
|
||||
description: item.description,
|
||||
visibility: item.visibility
|
||||
});
|
||||
});
|
||||
|
||||
resolve(o365groups);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getMyOwnerGroups(): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
try {
|
||||
// Prepare the output array
|
||||
var o365groups: Array<IGroup> = new Array<IGroup>();
|
||||
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api("/me/ownedObjects/$/microsoft.graph.group")
|
||||
.get((error: any, groups: any, rawResponse: any) => {
|
||||
// Map the response to the output array
|
||||
groups.value.map((item: any) => {
|
||||
o365groups.push({
|
||||
id: item.id,
|
||||
displayName: item.displayName,
|
||||
description: item.description,
|
||||
visibility: item.visibility
|
||||
});
|
||||
});
|
||||
|
||||
resolve(o365groups);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public addMember(groupId: string): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`/groups/${groupId}/members/$ref`)
|
||||
.post(`{ "@odata.id": "https://graph.microsoft.com/v1.0/users/${this.context.pageContext.user.loginName}" }`)
|
||||
.then((addMemberResponse) => {
|
||||
if (addMemberResponse == undefined) {
|
||||
resolve();
|
||||
}
|
||||
else {
|
||||
throw new Error(`Error occured while joining the Group`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getUserId(): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
try {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`/me/id`)
|
||||
.get((error: any, userId: any, rawResponse: any) => {
|
||||
resolve(userId.value);
|
||||
});
|
||||
});
|
||||
}
|
||||
catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public removeMember(groupId: string): Promise<any> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
this.getUserId().then(userId => {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`/groups/${groupId}/members/${userId}/$ref`)
|
||||
.delete((error: any, response: any, rawResponse: any) => {
|
||||
if (rawResponse.status === 204) {
|
||||
resolve(response);
|
||||
}
|
||||
else {
|
||||
throw new Error(`Error occured while leaving the Group`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public requestToJoinPrivateGroup(flowUrl: string, groupId: string, groupName: string, groupUrl: string): Promise<any> {
|
||||
|
||||
const body: string = JSON.stringify({
|
||||
'groupId': groupId,
|
||||
'groupName': groupName,
|
||||
'groupUrl': groupUrl,
|
||||
'requestorName': this.context.pageContext.user.displayName,
|
||||
'requestorEmail': this.context.pageContext.user.email
|
||||
});
|
||||
|
||||
const requestHeaders: Headers = new Headers();
|
||||
requestHeaders.append('Content-type', 'application/json');
|
||||
|
||||
const httpClientOptions: IHttpClientOptions = {
|
||||
body: body,
|
||||
headers: requestHeaders
|
||||
};
|
||||
|
||||
return this.context.httpClient.post(
|
||||
flowUrl,
|
||||
HttpClient.configurations.v1,
|
||||
httpClientOptions)
|
||||
.then((response: HttpClientResponse): Promise<HttpClientResponse> => {
|
||||
return response.json();
|
||||
});
|
||||
}
|
||||
|
||||
public getGroupLink(groups: IGroup): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
try {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`/groups/${groups.id}/sites/root/weburl`)
|
||||
.get((error: any, group: any, rawResponse: any) => {
|
||||
resolve(group);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public getGroupThumbnail(groups: IGroup): Promise<any> {
|
||||
return new Promise<any>((resolve, reject) => {
|
||||
try {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`/groups/${groups.id}/photos/48x48/$value`)
|
||||
.responseType('blob')
|
||||
.get((error: any, group: any, rawResponse: any) => {
|
||||
resolve(window.URL.createObjectURL(group));
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public createGroup(groupName: string, groupDescription: string, groupVisibility: string, groupOwners: string[], groupMembers: string[]): Promise<void> {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const groupRequest: Group = {
|
||||
displayName: groupName,
|
||||
description: groupDescription,
|
||||
groupTypes: [
|
||||
"Unified"
|
||||
],
|
||||
mailEnabled: true,
|
||||
mailNickname: groupName.replace(/\s/g, ""),
|
||||
securityEnabled: false,
|
||||
visibility: groupVisibility
|
||||
};
|
||||
|
||||
if (groupOwners && groupOwners.length) {
|
||||
groupRequest['owners@odata.bind'] = groupOwners.map(owner => {
|
||||
return `https://graph.microsoft.com/v1.0/users/${owner}`;
|
||||
});
|
||||
}
|
||||
|
||||
if (groupMembers && groupMembers.length) {
|
||||
groupRequest['members@odata.bind'] = groupMembers.map(member => {
|
||||
return `https://graph.microsoft.com/v1.0/users/${member}`;
|
||||
});
|
||||
}
|
||||
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api("/groups")
|
||||
.post(groupRequest)
|
||||
.then((groupResponse) => {
|
||||
console.log(groupResponse);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getTeamChannels = async (teamId): Promise<ITeamChannel[]> => {
|
||||
return new Promise<ITeamChannel[]>((resolve, reject) => {
|
||||
try {
|
||||
this.context.msGraphClientFactory
|
||||
.getClient()
|
||||
.then((client: MSGraphClient) => {
|
||||
client
|
||||
.api(`teams/${teamId}/channels`)
|
||||
.get((error: any, channelsResponse: any, rawResponse: any) => {
|
||||
// // Prepare the output array
|
||||
// var teamChannels: Array<ITeamChannel> = new Array<ITeamChannel>();
|
||||
|
||||
// // Map the response to the output array
|
||||
// channelsResponse.value.map((item: any) => {
|
||||
// teamChannels.push({
|
||||
// id: item.id,
|
||||
// displayName: item.displayName,
|
||||
// description: item.description,
|
||||
// webUrl: item.webUrl
|
||||
// });
|
||||
// });
|
||||
|
||||
resolve(channelsResponse.value);
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Error getting channels for team ' + teamId, error);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const GroupService = new O365GroupService();
|
||||
export default GroupService;
|
|
@ -0,0 +1 @@
|
|||
export * from './O365GroupService';
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
|
||||
"id": "71856ff0-277e-4373-b2dd-cf1291a10f58",
|
||||
"alias": "O365GroupsManagerWebPart",
|
||||
"componentType": "WebPart",
|
||||
|
||||
// The "*" signifies that the version should be taken from the package.json
|
||||
"version": "*",
|
||||
"manifestVersion": 2,
|
||||
|
||||
// If true, the component can only be installed on sites where Custom Script is allowed.
|
||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
||||
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||
"requiresCustomScript": false,
|
||||
"supportedHosts": ["SharePointWebPart"],
|
||||
|
||||
"preconfiguredEntries": [{
|
||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||
"group": { "default": "Other" },
|
||||
"title": { "default": "O365 Groups Manager" },
|
||||
"description": { "default": "Manage O365 Groups" },
|
||||
"officeFabricIconFontName": "Group",
|
||||
"properties": {
|
||||
"description": "O365GroupsManager"
|
||||
}
|
||||
}]
|
||||
}
|
|
@ -0,0 +1,72 @@
|
|||
import * as React from 'react';
|
||||
import * as ReactDom from 'react-dom';
|
||||
import { Version } from '@microsoft/sp-core-library';
|
||||
import {
|
||||
BaseClientSideWebPart,
|
||||
IPropertyPaneConfiguration,
|
||||
PropertyPaneTextField
|
||||
} from '@microsoft/sp-webpart-base';
|
||||
|
||||
import * as strings from 'O365GroupsManagerWebPartStrings';
|
||||
import O365GroupsManager from './components/O365GroupsManager/O365GroupsManager';
|
||||
import { IO365GroupsManagerProps } from './components/O365GroupsManager/IO365GroupsManagerProps';
|
||||
import O365GroupService from '../../services/O365GroupService';
|
||||
|
||||
export interface IO365GroupsManagerWebPartProps {
|
||||
flowUrl: string;
|
||||
}
|
||||
|
||||
export default class O365GroupsManagerWebPart extends BaseClientSideWebPart<IO365GroupsManagerWebPartProps> {
|
||||
|
||||
public render(): void {
|
||||
const element: React.ReactElement<IO365GroupsManagerProps> = React.createElement(
|
||||
O365GroupsManager,
|
||||
{
|
||||
flowUrl: this.properties.flowUrl,
|
||||
context: this.context
|
||||
}
|
||||
);
|
||||
|
||||
ReactDom.render(element, this.domElement);
|
||||
}
|
||||
|
||||
protected onInit(): Promise<void> {
|
||||
return super.onInit().then(() => {
|
||||
O365GroupService.setup(this.context);
|
||||
});
|
||||
}
|
||||
|
||||
protected onDispose(): void {
|
||||
ReactDom.unmountComponentAtNode(this.domElement);
|
||||
}
|
||||
|
||||
protected get disableReactivePropertyChanges(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected get dataVersion(): Version {
|
||||
return Version.parse('1.0');
|
||||
}
|
||||
|
||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
||||
return {
|
||||
pages: [
|
||||
{
|
||||
header: {
|
||||
description: strings.PropertyPaneDescription
|
||||
},
|
||||
groups: [
|
||||
{
|
||||
groupName: strings.BasicGroupName,
|
||||
groupFields: [
|
||||
PropertyPaneTextField('flowUrl', {
|
||||
label: strings.FlowUrlLabel
|
||||
})
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,263 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../O365GroupsManager/O365GroupsManager.module.scss';
|
||||
import { IGroupListProps } from './IGroupListProps';
|
||||
import { IGroupListState } from './IGroupListState';
|
||||
import { FocusZone, FocusZoneDirection } from 'office-ui-fabric-react/lib/FocusZone';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
import { Image, ImageFit } from 'office-ui-fabric-react/lib/Image';
|
||||
import { IconButton, IIconProps, Stack } from 'office-ui-fabric-react';
|
||||
import { TeachingBubble } from 'office-ui-fabric-react/lib/TeachingBubble';
|
||||
import { DirectionalHint } from 'office-ui-fabric-react/lib/Callout';
|
||||
import { List } from 'office-ui-fabric-react/lib/List';
|
||||
import { ITheme, mergeStyleSets, getTheme, getFocusStyle } from 'office-ui-fabric-react/lib/Styling';
|
||||
import { IGroup, IGroupCollection } from "../../../../models/IGroup";
|
||||
import O365GroupService from '../../../../services/O365GroupService';
|
||||
|
||||
interface IGroupListClassObject {
|
||||
itemCell: string;
|
||||
itemImage: string;
|
||||
itemContent: string;
|
||||
itemName: string;
|
||||
itemIndex: string;
|
||||
chevron: string;
|
||||
}
|
||||
|
||||
// Icons
|
||||
const teamsIcon: IIconProps = { iconName: 'TeamsLogo' };
|
||||
const joinIcon: IIconProps = { iconName: 'Subscribe' };
|
||||
const leaveIcon: IIconProps = { iconName: 'Unsubscribe' };
|
||||
const manageIcon: IIconProps = { iconName: 'AccountManagement' };
|
||||
const requestToJoinIcon: IIconProps = { iconName: 'SecurityGroup' };
|
||||
|
||||
// List style
|
||||
const theme: ITheme = getTheme();
|
||||
const { palette, semanticColors, fonts } = theme;
|
||||
|
||||
const classNames: IGroupListClassObject = mergeStyleSets({
|
||||
itemCell: [
|
||||
getFocusStyle(theme, { inset: -1 }),
|
||||
{
|
||||
minHeight: 54,
|
||||
padding: 10,
|
||||
boxSizing: 'border-box',
|
||||
borderBottom: `1px solid ${semanticColors.bodyDivider}`,
|
||||
display: 'flex',
|
||||
selectors: {
|
||||
'&:hover': { background: palette.neutralLight }
|
||||
}
|
||||
}
|
||||
],
|
||||
itemImage: {
|
||||
flexShrink: 0
|
||||
},
|
||||
itemContent: {
|
||||
marginLeft: 10,
|
||||
overflow: 'hidden',
|
||||
flexGrow: 1
|
||||
},
|
||||
itemName: [
|
||||
fonts.xLarge,
|
||||
{
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis'
|
||||
}
|
||||
],
|
||||
itemIndex: {
|
||||
fontSize: fonts.small.fontSize,
|
||||
color: palette.neutralTertiary,
|
||||
marginBottom: 10
|
||||
},
|
||||
chevron: {
|
||||
alignSelf: 'center',
|
||||
marginLeft: 10,
|
||||
color: palette.neutralTertiary,
|
||||
fontSize: fonts.large.fontSize,
|
||||
flexShrink: 0
|
||||
}
|
||||
});
|
||||
|
||||
export default class GroupList extends React.Component<IGroupListProps, IGroupListState> {
|
||||
private _originalItems: IGroup[];
|
||||
private _menuButtonElement: HTMLElement;
|
||||
|
||||
constructor(props: IGroupListProps) {
|
||||
super(props);
|
||||
|
||||
props.items.map(group => {
|
||||
let myUserRole: string = "";
|
||||
|
||||
if (props.ownerGroups.indexOf(group.id) > -1) {
|
||||
myUserRole = "Owner";
|
||||
}
|
||||
else if (props.memberGroups.indexOf(group.id) > -1) {
|
||||
myUserRole = "Member";
|
||||
}
|
||||
|
||||
group.userRole = myUserRole;
|
||||
});
|
||||
|
||||
this._originalItems = props.items;
|
||||
|
||||
this.state = {
|
||||
filterText: '',
|
||||
isTeachingBubbleVisible: false,
|
||||
groups: this._originalItems
|
||||
};
|
||||
|
||||
this._onRenderCell = this._onRenderCell.bind(this);
|
||||
this._onDismiss = this._onDismiss.bind(this);
|
||||
this._getGroupLinks(this._originalItems);
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IGroupListProps> {
|
||||
const { groups = [] } = this.state;
|
||||
const resultCountText = groups.length === this._originalItems.length ? '' : ` (${groups.length} of ${this._originalItems.length} shown)`;
|
||||
|
||||
return (
|
||||
<div className={styles.groupContainer}>
|
||||
<FocusZone direction={FocusZoneDirection.vertical}>
|
||||
<TextField label={'Filter by name' + resultCountText} onChange={this._onFilterChanged} />
|
||||
<List items={groups} onRenderCell={this._onRenderCell} />
|
||||
{this.state.isTeachingBubbleVisible ? (
|
||||
<div>
|
||||
<TeachingBubble
|
||||
calloutProps={{ directionalHint: DirectionalHint.bottomLeftEdge }}
|
||||
isWide={true}
|
||||
hasCloseIcon={true}
|
||||
closeButtonAriaLabel="Close"
|
||||
target={this._menuButtonElement}
|
||||
onDismiss={this._onDismiss}
|
||||
headline="Manage O365 Groups"
|
||||
>
|
||||
{this.state.techingBubbleMessage}
|
||||
</TeachingBubble>
|
||||
</div>
|
||||
) : null}
|
||||
</FocusZone>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
public _getGroupLinks = (groups: any): void => {
|
||||
groups.map(groupItem => (
|
||||
O365GroupService.getGroupLink(groupItem).then(groupUrl => {
|
||||
if (groupUrl !== null) {
|
||||
this.setState(prevState => ({
|
||||
groups: prevState.groups.map(group => group.id === groupItem.id ? { ...group, url: groupUrl.value } : group)
|
||||
}));
|
||||
}
|
||||
})
|
||||
));
|
||||
|
||||
this._getGroupThumbnails(groups);
|
||||
}
|
||||
|
||||
public _getGroupThumbnails = (groups: any): void => {
|
||||
groups.map(groupItem => (
|
||||
O365GroupService.getGroupThumbnail(groupItem).then(grouptb => {
|
||||
console.log(grouptb);
|
||||
this.setState(prevState => ({
|
||||
groups: prevState.groups.map(group => group.id === groupItem.id ? { ...group, thumbnail: grouptb } : group)
|
||||
}));
|
||||
})
|
||||
));
|
||||
}
|
||||
|
||||
private _onFilterChanged = (_: any, text: string): void => {
|
||||
this.setState({
|
||||
filterText: text,
|
||||
groups: text ? this._originalItems.filter(item => item.displayName.toLowerCase().indexOf(text.toLowerCase()) >= 0) : this._originalItems
|
||||
});
|
||||
|
||||
this._getGroupLinks(this.state.groups);
|
||||
}
|
||||
|
||||
private _onRenderCell(group: IGroup, index: number | undefined): JSX.Element {
|
||||
return (
|
||||
<div className={classNames.itemCell} data-is-focusable={true}>
|
||||
<Image className={classNames.itemImage} src={group.thumbnail} width={50} height={50} imageFit={ImageFit.cover} />
|
||||
<div className={classNames.itemContent}>
|
||||
<div className={classNames.itemName}>
|
||||
<a href={group.url} target="_blank">{group.displayName}</a>
|
||||
{
|
||||
group.teamsConnected &&
|
||||
<IconButton iconProps={teamsIcon} title="Open MS Teams" ariaLabel="Open MS Teams" onClick={(event) => { this._openMSTeamsClicked(group.id); }} />
|
||||
}
|
||||
</div>
|
||||
<div className={classNames.itemIndex}>{group.visibility}</div>
|
||||
<div>{group.description}</div>
|
||||
</div>
|
||||
{
|
||||
group.userRole === "Owner" &&
|
||||
<IconButton iconProps={manageIcon} title="Manage Group" ariaLabel="Manage Group" onClick={(event) => { this._manageGroupClicked(group.id); }} />
|
||||
}
|
||||
{
|
||||
group.userRole === "Member" &&
|
||||
<span className="ms-TeachingBubbleBasicExample-buttonArea" ref={menuButton => (this._menuButtonElement = menuButton!)}>
|
||||
<IconButton iconProps={leaveIcon} title="Leave Group" ariaLabel="Leave Group" onClick={(event) => { this._leaveGroupClicked(group.id, group.displayName); }} />
|
||||
</span>
|
||||
}
|
||||
{
|
||||
group.visibility === "Public" && group.userRole === "" &&
|
||||
<span className="ms-TeachingBubbleBasicExample-buttonArea" ref={menuButton => (this._menuButtonElement = menuButton!)}>
|
||||
<IconButton iconProps={joinIcon} title="Join Group" ariaLabel="Join Group" onClick={(event) => { this._joinGroupClicked(group.id, group.displayName); }} />
|
||||
</span>
|
||||
}
|
||||
{
|
||||
group.visibility === "Private" && group.userRole === "" && this.props.flowUrl != undefined &&
|
||||
<span className="ms-TeachingBubbleBasicExample-buttonArea" ref={menuButton => (this._menuButtonElement = menuButton!)}>
|
||||
<IconButton iconProps={requestToJoinIcon} title="Request to Join Group" ariaLabel="Request to Join Group" onClick={(event) => { this._requestJoinGroupClicked(group.id, group.displayName, group.url); }} />
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _onDismiss(ev: any): void {
|
||||
this.setState({
|
||||
isTeachingBubbleVisible: false
|
||||
});
|
||||
}
|
||||
|
||||
private _manageGroupClicked = (groupId: string) => {
|
||||
window.open("https://admin.microsoft.com/Adminportal/Home?source=applauncher#/groups");
|
||||
}
|
||||
|
||||
private _openMSTeamsClicked = (groupId: string) => {
|
||||
O365GroupService.getTeamChannels(groupId).then(response => {
|
||||
window.open(response[0].webUrl, '_blank');
|
||||
});
|
||||
}
|
||||
|
||||
private _leaveGroupClicked = (groupId: string, groupName: string) => {
|
||||
O365GroupService.removeMember(groupId).then(response => {
|
||||
this.setState(prevState => ({
|
||||
groups: prevState.groups.map(group => group.id === groupId ? { ...group, userRole: "" } : group),
|
||||
isTeachingBubbleVisible: true,
|
||||
techingBubbleMessage: 'You have left the group: ' + groupName
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private _joinGroupClicked = (groupId: string, groupName: string) => {
|
||||
O365GroupService.addMember(groupId).then(response => {
|
||||
this.setState(prevState => ({
|
||||
groups: prevState.groups.map(group => group.id === groupId ? { ...group, userRole: "Member" } : group),
|
||||
isTeachingBubbleVisible: true,
|
||||
techingBubbleMessage: 'You have joined the group: ' + groupName
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
private _requestJoinGroupClicked = (groupId: string, groupName: string, groupUrl: string) => {
|
||||
this.setState({
|
||||
isTeachingBubbleVisible: true,
|
||||
techingBubbleMessage: 'Request sent to join the private group: ' + groupName
|
||||
});
|
||||
|
||||
O365GroupService.requestToJoinPrivateGroup(this.props.flowUrl, groupId, groupName, groupUrl).then(response => {
|
||||
console.log(response);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import { IGroup } from "../../../../models/IGroup";
|
||||
|
||||
export interface IGroupListProps {
|
||||
flowUrl?: string;
|
||||
items?: IGroup[];
|
||||
ownerGroups?: string[];
|
||||
memberGroups?: string[];
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { IGroup } from "../../../../models/IGroup";
|
||||
|
||||
export interface IGroupListState {
|
||||
filterText?: string;
|
||||
isTeachingBubbleVisible?: boolean;
|
||||
techingBubbleMessage?: string;
|
||||
groups?: IGroup[];
|
||||
ownerGroups?: string[];
|
||||
memberGroups?: string[];
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
export interface INewGroupProps {
|
||||
returnToMainPage: () => void;
|
||||
context: WebPartContext;
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import { MessageBarType } from 'office-ui-fabric-react';
|
||||
|
||||
export interface INewGroupState {
|
||||
name: string;
|
||||
description: string;
|
||||
visibility: string;
|
||||
owners: string[];
|
||||
members: string[];
|
||||
showMessageBar: boolean;
|
||||
messageType?: MessageBarType;
|
||||
message?: string;
|
||||
}
|
|
@ -0,0 +1,173 @@
|
|||
import * as React from 'react';
|
||||
import styles from '../O365GroupsManager/O365GroupsManager.module.scss';
|
||||
import { INewGroupProps } from './INewGroupProps';
|
||||
import { INewGroupState } from './INewGroupState';
|
||||
import { TextField } from 'office-ui-fabric-react/lib/TextField';
|
||||
import { Label } from 'office-ui-fabric-react/lib/Label';
|
||||
import { ChoiceGroup, IChoiceGroupOption } from 'office-ui-fabric-react/lib/ChoiceGroup';
|
||||
import { PeoplePicker, PrincipalType, IPeoplePickerUserItem } from "@pnp/spfx-controls-react/lib/PeoplePicker";
|
||||
import { MessageBar, MessageBarType, IStackProps, Stack, ActionButton, IIconProps, DefaultButton } from 'office-ui-fabric-react';
|
||||
import { autobind } from 'office-ui-fabric-react';
|
||||
import O365GroupService from '../../../../services/O365GroupService';
|
||||
|
||||
const backIcon: IIconProps = { iconName: 'NavigateBack' };
|
||||
|
||||
const verticalStackProps: IStackProps = {
|
||||
styles: { root: { overflow: 'hidden', width: '100%' } },
|
||||
tokens: { childrenGap: 20 }
|
||||
};
|
||||
|
||||
export default class NewGroup extends React.Component<INewGroupProps, INewGroupState> {
|
||||
|
||||
constructor(props: INewGroupProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
description: '',
|
||||
visibility: 'Public',
|
||||
owners: [],
|
||||
members: [],
|
||||
showMessageBar: false
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<INewGroupProps> {
|
||||
return (
|
||||
<div className={styles.o365GroupsManager}>
|
||||
<div className={styles.container}>
|
||||
|
||||
<div className={styles.row}>
|
||||
<div className={styles.headerStyle}>
|
||||
<h1 className={styles.headerMsgStyle}>
|
||||
<span>Add New Group</span>
|
||||
<ActionButton className={styles.newHeaderLinkStyle} iconProps={backIcon} allowDisabledFocus onClick={this.props.returnToMainPage}>
|
||||
Back to listing
|
||||
</ActionButton>
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{
|
||||
this.state.showMessageBar
|
||||
?
|
||||
<div className="form-group">
|
||||
<Stack {...verticalStackProps}>
|
||||
<MessageBar messageBarType={this.state.messageType}>{this.state.message}</MessageBar>
|
||||
</Stack>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
|
||||
<div className="form-group">
|
||||
<TextField label="Name" required onChanged={this.onchangedName} />
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<TextField label="Description" required multiline rows={3} onChanged={this.onchangedDescription} />
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<Label required={true}>Visibility</Label>
|
||||
<ChoiceGroup
|
||||
defaultSelectedKey="Public"
|
||||
options={[
|
||||
{
|
||||
key: 'Public',
|
||||
text: 'Public - Anyone can see group content'
|
||||
},
|
||||
{
|
||||
key: 'Private',
|
||||
text: 'Private - Only members can see group content'
|
||||
}
|
||||
]}
|
||||
onChange={this.onChangeVisibility}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<PeoplePicker
|
||||
context={this.props.context}
|
||||
titleText="Owners"
|
||||
personSelectionLimit={3}
|
||||
showHiddenInUI={false}
|
||||
principalTypes={[PrincipalType.User]}
|
||||
selectedItems={this._getPeoplePickerOwners.bind(this)}
|
||||
isRequired={true} />
|
||||
</div>
|
||||
|
||||
<div className="form-group">
|
||||
<PeoplePicker
|
||||
context={this.props.context}
|
||||
titleText="Members"
|
||||
personSelectionLimit={3}
|
||||
showHiddenInUI={false}
|
||||
principalTypes={[PrincipalType.User]}
|
||||
selectedItems={this._getPeoplePickerMembers}
|
||||
isRequired={true} />
|
||||
</div>
|
||||
|
||||
<div className={`${styles.buttonRow} form-group`}>
|
||||
<DefaultButton text="Submit" onClick={this.createNewGroup} />
|
||||
|
||||
<DefaultButton text="Cancel" onClick={this.props.returnToMainPage} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private _getPeoplePickerOwners(items: IPeoplePickerUserItem[]) {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
owners: items.map(x => x.id.replace('i:0#.f|membership|', ''))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private _getPeoplePickerMembers(items: IPeoplePickerUserItem[]) {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
members: items.map(x => x.id.replace('i:0#.f|membership|', ''))
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onchangedName(groupName: any): void {
|
||||
this.setState({ name: groupName });
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onchangedDescription(groupDescription: any): void {
|
||||
this.setState({ description: groupDescription });
|
||||
}
|
||||
|
||||
@autobind
|
||||
private onChangeVisibility(ev: React.FormEvent<HTMLInputElement>, option: IChoiceGroupOption): void {
|
||||
this.setState({ visibility: option.key });
|
||||
}
|
||||
|
||||
@autobind
|
||||
private createNewGroup(): void {
|
||||
try {
|
||||
O365GroupService.createGroup(this.state.name, this.state.description, this.state.visibility, this.state.owners, this.state.members);
|
||||
|
||||
this.setState({
|
||||
message: "Group " + this.state.name + " is created successfully!",
|
||||
showMessageBar: true,
|
||||
messageType: MessageBarType.success
|
||||
});
|
||||
} catch (error) {
|
||||
this.setState({
|
||||
message: "Group " + this.state.name + " creation failed with error: " + error,
|
||||
showMessageBar: true,
|
||||
messageType: MessageBarType.error
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
|
||||
export interface IO365GroupsManagerProps {
|
||||
flowUrl: string;
|
||||
context: WebPartContext;
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import { IGroup, IGroupCollection } from "../../../../models/IGroup";
|
||||
|
||||
export interface IO365GroupsManagerState {
|
||||
isLoading: boolean;
|
||||
groups: IGroup[];
|
||||
ownerGroups: string[];
|
||||
memberGroups: string[];
|
||||
showNewGroupScreen: boolean;
|
||||
loadCount: number;
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
||||
|
||||
.o365GroupsManager {
|
||||
.container {
|
||||
max-width: 700px;
|
||||
margin: 0px auto;
|
||||
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.groupContainer {
|
||||
max-height: 60vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-top: 5px;
|
||||
padding-right: 10px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 10px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.row {
|
||||
@include ms-Grid-row;
|
||||
// @include ms-fontColor-white;
|
||||
background-color: transparent;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.buttonRow {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.column {
|
||||
@include ms-Grid-col;
|
||||
@include ms-lg10;
|
||||
@include ms-xl8;
|
||||
@include ms-xlPush2;
|
||||
@include ms-lgPush1;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include ms-font-xl;
|
||||
// @include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.subTitle {
|
||||
@include ms-font-l;
|
||||
// @include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.description {
|
||||
@include ms-font-l;
|
||||
// @include ms-fontColor-white;
|
||||
}
|
||||
|
||||
.headerStyle {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.headerMsgStyle {
|
||||
display: inline-block;
|
||||
width:100%;
|
||||
}
|
||||
|
||||
.newHeaderLinkStyle {
|
||||
display: inline;
|
||||
float: right;
|
||||
font-size:14px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.requiredField:after {
|
||||
content:"*";
|
||||
color:red;
|
||||
margin-left:2px;
|
||||
}
|
||||
|
||||
.button {
|
||||
// Our button
|
||||
text-decoration: none;
|
||||
height: 32px;
|
||||
|
||||
// Primary Button
|
||||
min-width: 80px;
|
||||
background-color: $ms-color-themePrimary;
|
||||
border-color: $ms-color-themePrimary;
|
||||
color: $ms-color-white;
|
||||
|
||||
// Basic Button
|
||||
outline: transparent;
|
||||
position: relative;
|
||||
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: $ms-font-size-m;
|
||||
font-weight: $ms-font-weight-regular;
|
||||
border-width: 0;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
padding: 0 16px;
|
||||
|
||||
.label {
|
||||
font-weight: $ms-font-weight-semibold;
|
||||
font-size: $ms-font-size-m;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
margin: 0 4px;
|
||||
vertical-align: top;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
import * as React from 'react';
|
||||
import styles from './O365GroupsManager.module.scss';
|
||||
import { IO365GroupsManagerProps } from './IO365GroupsManagerProps';
|
||||
import { IO365GroupsManagerState } from './IO365GroupsManagerState';
|
||||
import O365GroupService from '../../../../services/O365GroupService';
|
||||
import GroupList from '../GroupList/GroupList';
|
||||
import NewGroup from "../NewGroup/NewGroup";
|
||||
import { autobind, ActionButton, IIconProps } from 'office-ui-fabric-react';
|
||||
import { Spinner } from 'office-ui-fabric-react/lib/Spinner';
|
||||
|
||||
const addGroupIcon: IIconProps = { iconName: 'AddGroup' };
|
||||
|
||||
export default class O365GroupsManager extends React.Component<IO365GroupsManagerProps, IO365GroupsManagerState> {
|
||||
constructor(props: IO365GroupsManagerProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
isLoading: true,
|
||||
groups: [],
|
||||
ownerGroups: [],
|
||||
memberGroups: [],
|
||||
showNewGroupScreen: false,
|
||||
loadCount: 0
|
||||
};
|
||||
}
|
||||
|
||||
public render(): React.ReactElement<IO365GroupsManagerProps> {
|
||||
return (
|
||||
<div className={styles.o365GroupsManager}>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.row}>
|
||||
{
|
||||
this.state.loadCount === 3 && !this.state.showNewGroupScreen
|
||||
?
|
||||
<p>
|
||||
<h1 className={styles.headerMsgStyle}>O365 Groups Manager</h1>
|
||||
<GroupList flowUrl={this.props.flowUrl} items={this.state.groups} ownerGroups={this.state.ownerGroups} memberGroups={this.state.memberGroups} ></GroupList>
|
||||
<br/>
|
||||
<ActionButton className={styles.newHeaderLinkStyle} iconProps={addGroupIcon} allowDisabledFocus onClick={this.showNewGroupScreen}>
|
||||
New Group
|
||||
</ActionButton>
|
||||
</p>
|
||||
:
|
||||
!this.state.showNewGroupScreen &&
|
||||
<Spinner label="Loading Groups..." />
|
||||
}
|
||||
{
|
||||
this.state.showNewGroupScreen &&
|
||||
<div>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.headerStyle}>
|
||||
<NewGroup returnToMainPage={this.showMainScreen} context={this.props.context} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@autobind
|
||||
public showNewGroupScreen() {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
showNewGroupScreen: true
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@autobind
|
||||
public showMainScreen() {
|
||||
this.setState(() => {
|
||||
return {
|
||||
...this.state,
|
||||
showNewGroupScreen: false
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
public componentDidMount(): void {
|
||||
this._getGroups();
|
||||
}
|
||||
|
||||
public _getGroups = (): void => {
|
||||
O365GroupService.getMyOwnerGroups().then(groups => {
|
||||
console.log(groups);
|
||||
this.setState({
|
||||
ownerGroups: groups.map(item => item.id),
|
||||
loadCount: this.state.loadCount + 1
|
||||
});
|
||||
});
|
||||
|
||||
O365GroupService.getMyMemberGroups().then(groups => {
|
||||
console.log(groups);
|
||||
this.setState({
|
||||
memberGroups: groups.map(item => item.id),
|
||||
loadCount: this.state.loadCount + 1
|
||||
});
|
||||
});
|
||||
|
||||
O365GroupService.getGroups().then(groups => {
|
||||
console.log(groups);
|
||||
|
||||
this.setState({
|
||||
isLoading: false,
|
||||
groups: groups,
|
||||
loadCount: this.state.loadCount + 1
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
define([], function() {
|
||||
return {
|
||||
"PropertyPaneDescription": "Description",
|
||||
"BasicGroupName": "Group Name",
|
||||
"FlowUrlLabel": "Flow URL"
|
||||
}
|
||||
});
|
10
samples/react-manage-o365-groups/src/webparts/o365GroupsManager/loc/mystrings.d.ts
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
declare interface IO365GroupsManagerWebPartStrings {
|
||||
PropertyPaneDescription: string;
|
||||
BasicGroupName: string;
|
||||
FlowUrlLabel: string;
|
||||
}
|
||||
|
||||
declare module 'O365GroupsManagerWebPartStrings' {
|
||||
const strings: IO365GroupsManagerWebPartStrings;
|
||||
export = strings;
|
||||
}
|
After Width: | Height: | Size: 3.0 KiB |
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"extends": "./node_modules/@microsoft/rush-stack-compiler-2.9/includes/tsconfig-web.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"jsx": "react",
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"experimentalDecorators": true,
|
||||
"skipLibCheck": true,
|
||||
"outDir": "lib",
|
||||
"inlineSources": false,
|
||||
"strictNullChecks": false,
|
||||
"noUnusedLocals": false,
|
||||
"typeRoots": [
|
||||
"./node_modules/@types",
|
||||
"./node_modules/@microsoft"
|
||||
],
|
||||
"types": [
|
||||
"es6-promise",
|
||||
"webpack-env"
|
||||
],
|
||||
"lib": [
|
||||
"es5",
|
||||
"dom",
|
||||
"es2015.collection"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"lib"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"extends": "@microsoft/sp-tslint-rules/base-tslint.json",
|
||||
"rules": {
|
||||
"class-name": false,
|
||||
"export-name": false,
|
||||
"forin": false,
|
||||
"label-position": false,
|
||||
"member-access": true,
|
||||
"no-arg": false,
|
||||
"no-console": false,
|
||||
"no-construct": false,
|
||||
"no-duplicate-variable": true,
|
||||
"no-eval": false,
|
||||
"no-function-expression": true,
|
||||
"no-internal-module": true,
|
||||
"no-shadowed-variable": true,
|
||||
"no-switch-case-fall-through": true,
|
||||
"no-unnecessary-semicolons": true,
|
||||
"no-unused-expression": true,
|
||||
"no-use-before-declare": true,
|
||||
"no-with-statement": true,
|
||||
"semicolon": true,
|
||||
"trailing-comma": false,
|
||||
"typedef": false,
|
||||
"typedef-whitespace": false,
|
||||
"use-named-parameter": true,
|
||||
"variable-name": false,
|
||||
"whitespace": false
|
||||
}
|
||||
}
|