Merge branch 'dev'
|
@ -1,7 +1,7 @@
|
||||||
# React Graph Calendar Web Part
|
# React Graph Calendar Web Part
|
||||||
|
|
||||||
## Summary
|
## 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
|
### Web Part in SharePoint Online
|
||||||
![The web part in action](./assets/react-graph-calendar-spo.gif)
|
![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
|
### Web Part in Microsoft Teams
|
||||||
![The web part in action](./assets/react-graph-calendar-teams.gif)
|
![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
|
* React Framework
|
||||||
* Full Calendar (fullcalendar.io)
|
* Full Calendar (fullcalendar.io)
|
||||||
* Microsoft Teams API
|
* Microsoft Teams API
|
||||||
* Office UI Fabric
|
* Office UI Fabric
|
||||||
|
|
||||||
## Used SharePoint Framework Version
|
## 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
|
## Applies to
|
||||||
|
|
||||||
|
@ -38,6 +38,7 @@ react-graph-calendar | [Sébastien Levert](https://www.linkedin.com/in/sebastien
|
||||||
Version|Date|Comments
|
Version|Date|Comments
|
||||||
-------|----|--------
|
-------|----|--------
|
||||||
1.0 |December 29, 2019 | Initial Release
|
1.0 |December 29, 2019 | Initial Release
|
||||||
|
1.1 |January 08, 2020 | Bumped to SPFx 1.10 and added the Personal Tab support
|
||||||
|
|
||||||
## Disclaimer
|
## 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.**
|
**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",
|
"resource": "Microsoft Graph",
|
||||||
"scope": "Group.Read.All"
|
"scope": "Group.Read.All"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resource": "Microsoft Graph",
|
||||||
|
"scope": "Calendars.Read"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
@ -17,10 +17,10 @@
|
||||||
"@fullcalendar/moment": "^4.3.0",
|
"@fullcalendar/moment": "^4.3.0",
|
||||||
"@fullcalendar/moment-timezone": "^4.3.0",
|
"@fullcalendar/moment-timezone": "^4.3.0",
|
||||||
"@fullcalendar/react": "^4.3.0",
|
"@fullcalendar/react": "^4.3.0",
|
||||||
"@microsoft/sp-core-library": "1.9.1",
|
"@microsoft/sp-core-library": "1.10.0",
|
||||||
"@microsoft/sp-lodash-subset": "1.9.1",
|
"@microsoft/sp-lodash-subset": "1.10.0",
|
||||||
"@microsoft/sp-office-ui-fabric-core": "1.9.1",
|
"@microsoft/sp-office-ui-fabric-core": "1.10.0",
|
||||||
"@microsoft/sp-webpart-base": "1.9.1",
|
"@microsoft/sp-webpart-base": "1.10.0",
|
||||||
"@types/es6-promise": "0.0.33",
|
"@types/es6-promise": "0.0.33",
|
||||||
"@types/react": "16.8.8",
|
"@types/react": "16.8.8",
|
||||||
"@types/react-dom": "16.8.3",
|
"@types/react-dom": "16.8.3",
|
||||||
|
@ -35,10 +35,10 @@
|
||||||
"@types/react": "16.8.8"
|
"@types/react": "16.8.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/sp-build-web": "1.9.1",
|
"@microsoft/sp-build-web": "1.10.0",
|
||||||
"@microsoft/sp-tslint-rules": "1.9.1",
|
"@microsoft/sp-tslint-rules": "1.10.0",
|
||||||
"@microsoft/sp-module-interfaces": "1.9.1",
|
"@microsoft/sp-module-interfaces": "1.10.0",
|
||||||
"@microsoft/sp-webpart-workbench": "1.9.1",
|
"@microsoft/sp-webpart-workbench": "1.10.0",
|
||||||
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
"@microsoft/rush-stack-compiler-2.9": "0.7.16",
|
||||||
"gulp": "~3.9.1",
|
"gulp": "~3.9.1",
|
||||||
"@types/chai": "3.4.34",
|
"@types/chai": "3.4.34",
|
||||||
|
|
|
@ -12,15 +12,15 @@
|
||||||
// Components that allow authors to embed arbitrary script code should set this to true.
|
// 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
|
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
|
||||||
"requiresCustomScript": false,
|
"requiresCustomScript": false,
|
||||||
"supportedHosts": ["SharePointWebPart", "TeamsTab"],
|
"supportedHosts": ["SharePointWebPart", "TeamsTab", "TeamsPersonalApp"],
|
||||||
"preconfiguredEntries": [{
|
"preconfiguredEntries": [{
|
||||||
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
|
||||||
"group": { "default": "Other" },
|
"group": { "default": "Other" },
|
||||||
"title": { "default": "GraphCalendar" },
|
"title": { "default": "Graph Calendar" },
|
||||||
"description": { "default": "GraphCalendar description" },
|
"description": { "default": "Graph Calendar" },
|
||||||
"officeFabricIconFontName": "Page",
|
"officeFabricIconFontName": "Calendar",
|
||||||
"properties": {
|
"properties": {
|
||||||
"description": "GraphCalendar"
|
"description": "Graph Calendar"
|
||||||
}
|
}
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,15 +44,14 @@ export default class GraphCalendarWebPart extends BaseClientSideWebPart<IGraphCa
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sets the Teams context if in Teams
|
// Sets the Teams context if in Teams
|
||||||
if (this.context.microsoftTeams) {
|
if (this.context.sdks.microsoftTeams) {
|
||||||
this.context.microsoftTeams.getContext(context => {
|
this._teamsContext = this.context.sdks.microsoftTeams.context;
|
||||||
this._teamsContext = context;
|
|
||||||
// resolve the promise
|
|
||||||
resolve(undefined);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize the OUIF icons if in Teams
|
// Initialize the OUIF icons if in Teams
|
||||||
initializeIcons();
|
initializeIcons();
|
||||||
|
|
||||||
|
// resolve the promise
|
||||||
|
resolve(undefined);
|
||||||
} else {
|
} else {
|
||||||
// resolve the promise
|
// resolve the promise
|
||||||
resolve(undefined);
|
resolve(undefined);
|
||||||
|
|
|
@ -17,6 +17,12 @@ interface IGraphCalendarState {
|
||||||
isEventDetailsOpen: boolean;
|
isEventDetailsOpen: boolean;
|
||||||
currentSelectedEvent: EventInput;
|
currentSelectedEvent: EventInput;
|
||||||
groupId: string;
|
groupId: string;
|
||||||
|
tabType: TabType;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TabType {
|
||||||
|
TeamsTab,
|
||||||
|
PersonalTab
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GraphCalendar extends React.Component<IGraphCalendarProps, IGraphCalendarState> {
|
export default class GraphCalendar extends React.Component<IGraphCalendarProps, IGraphCalendarState> {
|
||||||
|
@ -45,7 +51,8 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
||||||
currentActiveEndDate: null,
|
currentActiveEndDate: null,
|
||||||
isEventDetailsOpen: false,
|
isEventDetailsOpen: false,
|
||||||
currentSelectedEvent: null,
|
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 ]}
|
plugins={[ dayGridPlugin ]}
|
||||||
windowResize={this._handleResize.bind(this)}
|
windowResize={this._handleResize.bind(this)}
|
||||||
datesRender={this._datesRender.bind(this)}
|
datesRender={this._datesRender.bind(this)}
|
||||||
eventClick={this._handleEventClick.bind(this)}
|
eventClick={this._openEventPanel.bind(this)}
|
||||||
height={this.state.height}
|
height={this.state.height}
|
||||||
events={this.state.events} />
|
events={this.state.events} />
|
||||||
{this.state.currentSelectedEvent &&
|
{this.state.currentSelectedEvent &&
|
||||||
|
@ -81,6 +88,8 @@ export default class GraphCalendar extends React.Component<IGraphCalendarProps,
|
||||||
isOpen={this.state.isEventDetailsOpen}
|
isOpen={this.state.isEventDetailsOpen}
|
||||||
type={ PanelType.smallFixedFar }
|
type={ PanelType.smallFixedFar }
|
||||||
headerText={this.state.currentSelectedEvent ? this.state.currentSelectedEvent.title : ""}
|
headerText={this.state.currentSelectedEvent ? this.state.currentSelectedEvent.title : ""}
|
||||||
|
onDismiss={this._closeEventPanel.bind(this)}
|
||||||
|
isLightDismiss={true}
|
||||||
closeButtonAriaLabel='Close'>
|
closeButtonAriaLabel='Close'>
|
||||||
<h3>Start Time</h3>
|
<h3>Start Time</h3>
|
||||||
<span>{moment(this.state.currentSelectedEvent.start).format('MMMM Do YYYY [at] h:mm:ss a')}</span>
|
<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;
|
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
|
* Handles the click event and opens the OUIF Panel
|
||||||
* @param eventClickInfo The information about the selected event
|
* @param eventClickInfo The information about the selected event
|
||||||
*/
|
*/
|
||||||
private _handleEventClick(eventClickInfo: any) {
|
private _openEventPanel(eventClickInfo: any) {
|
||||||
this.setState({
|
this.setState({
|
||||||
isEventDetailsOpen: true,
|
isEventDetailsOpen: true,
|
||||||
currentSelectedEvent: eventClickInfo.event
|
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
|
* If the view changed, reload the events based on the active view
|
||||||
* @param info Information about the current 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 {
|
private _loadEvents(startDate: Date, endDate: Date): void {
|
||||||
|
|
||||||
// If a Group was found, execute the query. If not, do nothing.
|
// 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) {
|
if(this.state.groupId || this.state.tabType == TabType.PersonalTab) {
|
||||||
|
|
||||||
this.props.context.msGraphClientFactory
|
this.props.context.msGraphClientFactory
|
||||||
.getClient()
|
.getClient()
|
||||||
.then((client: MSGraphClient): void => {
|
.then((client: MSGraphClient): void => {
|
||||||
|
|
||||||
|
let apiUrl: string = `/groups/${this.state.groupId}/events`;
|
||||||
|
if(this._isPersonalTab()) {
|
||||||
|
apiUrl = '/me/events';
|
||||||
|
}
|
||||||
|
|
||||||
client
|
client
|
||||||
.api(`/groups/${this.state.groupId}/events`)
|
.api(apiUrl)
|
||||||
.version("v1.0")
|
.version("v1.0")
|
||||||
.select('subject,start,end,location,bodyPreview,isAllDay')
|
.select('subject,start,end,location,bodyPreview,isAllDay')
|
||||||
.filter(`start/dateTime ge '${startDate.toISOString()}' and end/dateTime le '${endDate.toISOString()}'`)
|
.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
|
## 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" />
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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": [
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 * 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: '',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|