Updates FluentUI9 demo and react-group-membership-manager to SPFX 1.16.1 and FluentUI 9.10.1 (#3423)
Merging conflicts
This commit is contained in:
parent
c87ed9d054
commit
f3af010335
|
@ -9,7 +9,7 @@ This demos the use of the new [Fluent UI version 9](https://github.com/microsoft
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
![SPFx 1.16.0](https://img.shields.io/badge/SPFx-1.16-green.svg)
|
![SPFx 1.16.1](https://img.shields.io/badge/SPFx-1.16.1-green.svg)
|
||||||
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
||||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||||
|
@ -46,7 +46,7 @@ Version|Date|Comments
|
||||||
-------|----|--------
|
-------|----|--------
|
||||||
1.0|April 20, 2022|Initial release
|
1.0|April 20, 2022|Initial release
|
||||||
1.0.1|November 14, 2022|Updated to SPFx 15, latest Fluent UI 9, shim based theme mapping
|
1.0.1|November 14, 2022|Updated to SPFx 15, latest Fluent UI 9, shim based theme mapping
|
||||||
1.0.2|November 16, 2022|
|
1.0.2|January 18, 2023|Updated SPFx 16.1
|
||||||
|
|
||||||
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-fluentui-9) then unzip it)
|
- Clone this repository (or [download this solution as a .ZIP file](https://pnp.github.io/download-partial/?url=https://github.com/pnp/sp-dev-fx-webparts/tree/main/samples/react-fluentui-9) then unzip it)
|
||||||
- From your command-line, change your current directory to the directory containing this sample (`react-fluentui-9`, located under `samples`)
|
- From your command-line, change your current directory to the directory containing this sample (`react-fluentui-9`, located under `samples`)
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"description"
|
"description"
|
||||||
],
|
],
|
||||||
"creationDateTime": "2022-04-20",
|
"creationDateTime": "2022-04-20",
|
||||||
"updateDateTime": "2022-11-16",
|
"updateDateTime": "2023-01-18",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.16"
|
"value": "1.16.1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnails": [
|
"thumbnails": [
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -12,22 +12,22 @@
|
||||||
"test": "gulp test"
|
"test": "gulp test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.7.0",
|
"@fluentui/react-components": "^9.10.1",
|
||||||
"@microsoft/sp-core-library": "1.16.0",
|
"@microsoft/sp-core-library": "1.16.1",
|
||||||
"@microsoft/sp-lodash-subset": "1.16.0",
|
"@microsoft/sp-lodash-subset": "1.16.1",
|
||||||
"@microsoft/sp-office-ui-fabric-core": "1.16.0",
|
"@microsoft/sp-office-ui-fabric-core": "1.16.1",
|
||||||
"@microsoft/sp-property-pane": "1.16.0",
|
"@microsoft/sp-property-pane": "1.16.1",
|
||||||
"@microsoft/sp-webpart-base": "1.16.0",
|
"@microsoft/sp-webpart-base": "1.16.1",
|
||||||
"react": "17.0.1",
|
"react": "17.0.1",
|
||||||
"react-dom": "17.0.1",
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/eslint-config-spfx": "1.16.0",
|
"@microsoft/eslint-config-spfx": "1.16.1",
|
||||||
"@microsoft/eslint-plugin-spfx": "1.16.0",
|
"@microsoft/eslint-plugin-spfx": "1.16.1",
|
||||||
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
||||||
"@microsoft/sp-build-web": "1.16.0",
|
"@microsoft/sp-build-web": "1.16.1",
|
||||||
"@microsoft/sp-module-interfaces": "1.16.0",
|
"@microsoft/sp-module-interfaces": "1.16.1",
|
||||||
"@rushstack/eslint-config": "2.5.1",
|
"@rushstack/eslint-config": "2.5.1",
|
||||||
"@types/react": "17.0.45",
|
"@types/react": "17.0.45",
|
||||||
"@types/react-dom": "17.0.17",
|
"@types/react-dom": "17.0.17",
|
||||||
|
|
|
@ -154,6 +154,8 @@ const mapAliasColors = (palette: Partial<IPalette>, inverted: boolean): ColorTok
|
||||||
colorNeutralShadowKeyDarker: 'rgba(0,0,0,0.24)',
|
colorNeutralShadowKeyDarker: 'rgba(0,0,0,0.24)',
|
||||||
colorBrandShadowAmbient: 'rgba(0,0,0,0.30)',
|
colorBrandShadowAmbient: 'rgba(0,0,0,0.30)',
|
||||||
colorBrandShadowKey: 'rgba(0,0,0,0.25)',
|
colorBrandShadowKey: 'rgba(0,0,0,0.25)',
|
||||||
|
colorNeutralStencil1Alpha: webLightTheme.colorNeutralStencil1Alpha,
|
||||||
|
colorNeutralStencil2Alpha: webLightTheme.colorNeutralStencil2Alpha,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"@microsoft/generator-sharepoint": {
|
"@microsoft/generator-sharepoint": {
|
||||||
"plusBeta": false,
|
"plusBeta": false,
|
||||||
"isCreatingSolution": true,
|
"isCreatingSolution": true,
|
||||||
"version": "1.15.2",
|
"version": "1.16.1",
|
||||||
"libraryName": "group-membership-manager",
|
"libraryName": "group-membership-manager",
|
||||||
"libraryId": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
"libraryId": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
||||||
"environment": "spo",
|
"environment": "spo",
|
||||||
|
@ -11,6 +11,9 @@
|
||||||
"solutionShortDescription": "Group Membership Manager description",
|
"solutionShortDescription": "Group Membership Manager description",
|
||||||
"skipFeatureDeployment": true,
|
"skipFeatureDeployment": true,
|
||||||
"isDomainIsolated": false,
|
"isDomainIsolated": false,
|
||||||
"componentType": "webpart"
|
"componentType": "webpart",
|
||||||
|
"sdkVersions": {
|
||||||
|
"@microsoft/teams-js": "2.4.1"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,7 @@ This app is an example of managing the membership of a group you own including t
|
||||||
|
|
||||||
## Compatibility
|
## Compatibility
|
||||||
|
|
||||||
![SPFx 1.15.2](https://img.shields.io/badge/SPFx-1.15.2-green.svg)
|
![SPFx 1.16.1](https://img.shields.io/badge/SPFx-1.16.1-green.svg)
|
||||||
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
![Node.js v16](https://img.shields.io/badge/Node.js-v16-green.svg)
|
||||||
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
![Compatible with SharePoint Online](https://img.shields.io/badge/SharePoint%20Online-Compatible-green.svg)
|
||||||
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
![Does not work with SharePoint 2019](https://img.shields.io/badge/SharePoint%20Server%202019-Incompatible-red.svg "SharePoint Server 2019 requires SPFx 1.4.1 or lower")
|
||||||
|
@ -35,6 +35,7 @@ This app is an example of managing the membership of a group you own including t
|
||||||
| Version | Date | Comments |
|
| Version | Date | Comments |
|
||||||
| ------- | ---------------- | --------------- |
|
| ------- | ---------------- | --------------- |
|
||||||
| 1.0 | August 25, 2022 | Initial release |
|
| 1.0 | August 25, 2022 | Initial release |
|
||||||
|
| 1.1| | Jan 18, 2022 | Updated to SPFx 1.16.1 |
|
||||||
|
|
||||||
|
|
||||||
## Minimal Path to Awesome
|
## Minimal Path to Awesome
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
"This app is an example of managing the membership of a group you own including the owners of the group as well as using FluentUI v9"
|
"This app is an example of managing the membership of a group you own including the owners of the group as well as using FluentUI v9"
|
||||||
],
|
],
|
||||||
"creationDateTime": "2022-08-25",
|
"creationDateTime": "2022-08-25",
|
||||||
"updateDateTime": "2022-08-25",
|
"updateDateTime": "2023-01-18",
|
||||||
"products": [
|
"products": [
|
||||||
"SharePoint"
|
"SharePoint"
|
||||||
],
|
],
|
||||||
|
@ -21,7 +21,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "SPFX-VERSION",
|
"key": "SPFX-VERSION",
|
||||||
"value": "1.14"
|
"value": "1.16.1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"thumbnails": [
|
"thumbnails": [
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
"solution": {
|
"solution": {
|
||||||
"name": "group-membership-manager-client-side-solution",
|
"name": "group-membership-manager-client-side-solution",
|
||||||
"id": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
"id": "2a487e9a-b62a-484a-9bc0-e78b65550b61",
|
||||||
"version": "1.0.0.0",
|
"version": "1.1.0.0",
|
||||||
"includeClientSideAssets": true,
|
"includeClientSideAssets": true,
|
||||||
"title": "Group Membership Manager",
|
"title": "Group Membership Manager",
|
||||||
"skipFeatureDeployment": true,
|
"skipFeatureDeployment": true,
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
"websiteUrl": "https://nbdev.uk",
|
"websiteUrl": "https://nbdev.uk",
|
||||||
"privacyUrl": "",
|
"privacyUrl": "",
|
||||||
"termsOfUseUrl": "",
|
"termsOfUseUrl": "",
|
||||||
"mpnId": "Undefined-1.15.2"
|
"mpnId": "Undefined-1.16.1"
|
||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"shortDescription": {
|
"shortDescription": {
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,32 +3,36 @@
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16.13.0 <17.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "gulp bundle",
|
"build": "gulp bundle",
|
||||||
"clean": "gulp clean",
|
"clean": "gulp clean",
|
||||||
"test": "gulp test"
|
"test": "gulp test"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluentui/react-components": "^9.3.1",
|
"@fluentui/react-components": "^9.10.1",
|
||||||
"@microsoft/sp-core-library": "1.15.2",
|
"@microsoft/sp-component-base": "^1.16.1",
|
||||||
"@microsoft/sp-lodash-subset": "1.15.2",
|
"@microsoft/sp-core-library": "1.16.1",
|
||||||
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
|
"@microsoft/sp-lodash-subset": "1.16.1",
|
||||||
"@microsoft/sp-property-pane": "1.15.2",
|
"@microsoft/sp-office-ui-fabric-core": "1.16.1",
|
||||||
"@microsoft/sp-webpart-base": "1.15.2",
|
"@microsoft/sp-property-pane": "1.16.1",
|
||||||
"react": "16.13.1",
|
"@microsoft/sp-webpart-base": "1.16.1",
|
||||||
"react-dom": "16.13.1",
|
"react": "17.0.1",
|
||||||
|
"react-dom": "17.0.1",
|
||||||
"tslib": "2.3.1"
|
"tslib": "2.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@microsoft/eslint-config-spfx": "1.15.2",
|
"@microsoft/eslint-config-spfx": "1.16.1",
|
||||||
"@microsoft/eslint-plugin-spfx": "1.15.2",
|
"@microsoft/eslint-plugin-spfx": "1.16.1",
|
||||||
"@microsoft/microsoft-graph-types": "^2.24.0",
|
"@microsoft/microsoft-graph-types": "^2.25.0",
|
||||||
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
"@microsoft/rush-stack-compiler-4.5": "0.2.2",
|
||||||
"@microsoft/sp-build-web": "1.15.2",
|
"@microsoft/sp-build-web": "1.16.1",
|
||||||
"@microsoft/sp-module-interfaces": "1.15.2",
|
"@microsoft/sp-module-interfaces": "1.16.1",
|
||||||
"@rushstack/eslint-config": "2.5.1",
|
"@rushstack/eslint-config": "2.5.1",
|
||||||
"@types/react": "16.9.51",
|
"@types/react": "17.0.45",
|
||||||
"@types/react-dom": "16.9.8",
|
"@types/react-dom": "17.0.17",
|
||||||
"@types/webpack-env": "~1.15.2",
|
"@types/webpack-env": "~1.15.2",
|
||||||
"ajv": "^6.12.5",
|
"ajv": "^6.12.5",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,173 @@
|
||||||
|
import { IPalette, ITheme as ThemeV8 } from '@microsoft/sp-component-base';
|
||||||
|
|
||||||
|
import { ColorTokens, Theme as ThemeV9, webLightTheme } from '@fluentui/react-components';
|
||||||
|
import { blackAlpha, whiteAlpha, grey } from './themeDuplicates';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates v9 color tokens from a v8 palette.
|
||||||
|
* https://github.com/microsoft/fluentui/blob/master/apps/public-docsite-v9/src/shims/ThemeShim/v9ThemeShim.ts
|
||||||
|
*/
|
||||||
|
const mapAliasColors = (palette: Partial<IPalette>, inverted: boolean): ColorTokens => {
|
||||||
|
return {
|
||||||
|
colorNeutralForeground1: palette.neutralPrimary || webLightTheme.colorNeutralForeground1,
|
||||||
|
colorNeutralForeground1Hover: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Hover,
|
||||||
|
colorNeutralForeground1Pressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Pressed,
|
||||||
|
colorNeutralForeground1Selected: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Selected,
|
||||||
|
colorNeutralForeground2: palette.neutralSecondary || webLightTheme.colorNeutralForeground2,
|
||||||
|
colorNeutralForeground2Hover: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Hover,
|
||||||
|
colorNeutralForeground2Pressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Pressed,
|
||||||
|
colorNeutralForeground2Selected: palette.neutralPrimary || webLightTheme.colorNeutralForeground2Selected,
|
||||||
|
colorNeutralForeground2BrandHover: palette.themePrimary || webLightTheme.colorNeutralForeground2BrandHover,
|
||||||
|
colorNeutralForeground2BrandPressed: palette.themeDarkAlt || webLightTheme.colorNeutralForeground2BrandPressed,
|
||||||
|
colorNeutralForeground2BrandSelected: palette.themePrimary || webLightTheme.colorNeutralForeground2BrandSelected,
|
||||||
|
colorNeutralForeground3: palette.neutralTertiary || webLightTheme.colorNeutralForeground3,
|
||||||
|
colorNeutralForeground3Hover: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Hover,
|
||||||
|
colorNeutralForeground3Pressed: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Pressed,
|
||||||
|
colorNeutralForeground3Selected: palette.neutralSecondary || webLightTheme.colorNeutralForeground3Selected,
|
||||||
|
colorNeutralForeground3BrandHover: palette.themePrimary || webLightTheme.colorNeutralForeground3BrandHover,
|
||||||
|
colorNeutralForeground3BrandPressed: palette.themeDarkAlt || webLightTheme.colorNeutralForeground3BrandPressed,
|
||||||
|
colorNeutralForeground3BrandSelected: palette.themePrimary || webLightTheme.colorNeutralForeground3BrandSelected,
|
||||||
|
colorNeutralForeground4: palette.neutralQuaternary || webLightTheme.colorNeutralForeground4,
|
||||||
|
colorNeutralForegroundDisabled: palette.neutralTertiaryAlt || webLightTheme.colorNeutralForegroundDisabled,
|
||||||
|
colorNeutralForegroundInvertedDisabled: whiteAlpha[40],
|
||||||
|
colorBrandForegroundLink: palette.themeDarkAlt || webLightTheme.colorBrandForegroundLink,
|
||||||
|
colorBrandForegroundLinkHover: palette.themeDark || webLightTheme.colorBrandForegroundLinkHover,
|
||||||
|
colorBrandForegroundLinkPressed: palette.themeDarker || webLightTheme.colorBrandForegroundLinkPressed,
|
||||||
|
colorBrandForegroundLinkSelected: palette.themeDarkAlt || webLightTheme.colorBrandForegroundLinkSelected,
|
||||||
|
colorNeutralForeground2Link: palette.neutralSecondary || webLightTheme.colorNeutralForeground2Link,
|
||||||
|
colorNeutralForeground2LinkHover: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkHover,
|
||||||
|
colorNeutralForeground2LinkPressed: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkPressed,
|
||||||
|
colorNeutralForeground2LinkSelected: palette.neutralPrimary || webLightTheme.colorNeutralForeground2LinkSelected,
|
||||||
|
colorCompoundBrandForeground1: palette.themePrimary || webLightTheme.colorCompoundBrandForeground1,
|
||||||
|
colorCompoundBrandForeground1Hover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandForeground1Hover,
|
||||||
|
colorCompoundBrandForeground1Pressed: palette.themeDark || webLightTheme.colorCompoundBrandForeground1Pressed,
|
||||||
|
colorBrandForeground1: palette.themePrimary || webLightTheme.colorBrandForeground1,
|
||||||
|
colorBrandForeground2: palette.themeDarkAlt || webLightTheme.colorBrandForeground2,
|
||||||
|
colorNeutralForeground1Static: palette.neutralPrimary || webLightTheme.colorNeutralForeground1Static,
|
||||||
|
colorNeutralForegroundInverted: palette.white || webLightTheme.colorNeutralForegroundInverted,
|
||||||
|
colorNeutralForegroundInvertedHover: palette.white || webLightTheme.colorNeutralForegroundInvertedHover,
|
||||||
|
colorNeutralForegroundInvertedPressed: palette.white || webLightTheme.colorNeutralForegroundInvertedPressed,
|
||||||
|
colorNeutralForegroundInvertedSelected: palette.white || webLightTheme.colorNeutralForegroundInvertedSelected,
|
||||||
|
colorNeutralForegroundOnBrand: palette.white || webLightTheme.colorNeutralForegroundOnBrand,
|
||||||
|
colorNeutralForegroundStaticInverted: palette.white || webLightTheme.colorNeutralForegroundStaticInverted,
|
||||||
|
colorNeutralForegroundInvertedLink: palette.white || webLightTheme.colorNeutralForegroundInvertedLink,
|
||||||
|
colorNeutralForegroundInvertedLinkHover: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkHover,
|
||||||
|
colorNeutralForegroundInvertedLinkPressed: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkPressed,
|
||||||
|
colorNeutralForegroundInvertedLinkSelected: palette.white || webLightTheme.colorNeutralForegroundInvertedLinkSelected,
|
||||||
|
colorNeutralForegroundInverted2: palette.white || webLightTheme.colorNeutralForegroundInverted2,
|
||||||
|
colorBrandForegroundInverted: palette.themeSecondary || webLightTheme.colorBrandForegroundInverted,
|
||||||
|
colorBrandForegroundInvertedHover: palette.themeTertiary || webLightTheme.colorBrandForegroundInvertedHover,
|
||||||
|
colorBrandForegroundInvertedPressed: palette.themeSecondary || webLightTheme.colorBrandForegroundInvertedPressed,
|
||||||
|
colorBrandForegroundOnLight: palette.themePrimary || webLightTheme.colorBrandForegroundOnLight,
|
||||||
|
colorBrandForegroundOnLightHover: palette.themeDarkAlt || webLightTheme.colorBrandForegroundOnLightHover,
|
||||||
|
colorBrandForegroundOnLightPressed: palette.themeDark || webLightTheme.colorBrandForegroundOnLightPressed,
|
||||||
|
colorBrandForegroundOnLightSelected: palette.themeDark || webLightTheme.colorBrandForegroundOnLightSelected,
|
||||||
|
colorNeutralBackground1: palette.white || webLightTheme.colorNeutralBackground1,
|
||||||
|
colorNeutralBackground1Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground1Hover,
|
||||||
|
colorNeutralBackground1Pressed: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground1Pressed,
|
||||||
|
colorNeutralBackground1Selected: palette.neutralLight || webLightTheme.colorNeutralBackground1Selected,
|
||||||
|
colorNeutralBackground2: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground2,
|
||||||
|
colorNeutralBackground2Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground2Hover,
|
||||||
|
colorNeutralBackground2Pressed: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground2Pressed,
|
||||||
|
colorNeutralBackground2Selected: palette.neutralLight || webLightTheme.colorNeutralBackground2Selected,
|
||||||
|
colorNeutralBackground3: palette.neutralLighter || webLightTheme.colorNeutralBackground3,
|
||||||
|
colorNeutralBackground3Hover: palette.neutralLight || webLightTheme.colorNeutralBackground3Hover,
|
||||||
|
colorNeutralBackground3Pressed: palette.neutralQuaternary || webLightTheme.colorNeutralBackground3Pressed,
|
||||||
|
colorNeutralBackground3Selected: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralBackground3Selected,
|
||||||
|
colorNeutralBackground4: palette.neutralLighter || webLightTheme.colorNeutralBackground4,
|
||||||
|
colorNeutralBackground4Hover: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground4Hover,
|
||||||
|
colorNeutralBackground4Pressed: palette.neutralLighter || webLightTheme.colorNeutralBackground4Pressed,
|
||||||
|
colorNeutralBackground4Selected: palette.white || webLightTheme.colorNeutralBackground4Selected,
|
||||||
|
colorNeutralBackground5: palette.neutralLight || webLightTheme.colorNeutralBackground5,
|
||||||
|
colorNeutralBackground5Hover: palette.neutralLighter || webLightTheme.colorNeutralBackground5Hover,
|
||||||
|
colorNeutralBackground5Pressed: palette.neutralLighter || webLightTheme.colorNeutralBackground5Pressed,
|
||||||
|
colorNeutralBackground5Selected: palette.neutralLighterAlt || webLightTheme.colorNeutralBackground5Selected,
|
||||||
|
colorNeutralBackground6: palette.neutralLight || webLightTheme.colorNeutralBackground6,
|
||||||
|
colorNeutralBackgroundStatic: grey[20],
|
||||||
|
colorNeutralBackgroundInverted: palette.neutralSecondary || webLightTheme.colorNeutralBackgroundInverted,
|
||||||
|
colorSubtleBackground: 'transparent',
|
||||||
|
colorSubtleBackgroundHover: palette.neutralLighter || webLightTheme.colorSubtleBackgroundHover,
|
||||||
|
colorSubtleBackgroundPressed: palette.neutralQuaternaryAlt || webLightTheme.colorSubtleBackgroundPressed,
|
||||||
|
colorSubtleBackgroundSelected: palette.neutralLight || webLightTheme.colorSubtleBackgroundSelected,
|
||||||
|
colorSubtleBackgroundLightAlphaHover: inverted ? whiteAlpha[10] : whiteAlpha[80],
|
||||||
|
colorSubtleBackgroundLightAlphaPressed: inverted ? whiteAlpha[5] : whiteAlpha[50],
|
||||||
|
colorSubtleBackgroundLightAlphaSelected: 'transparent',
|
||||||
|
colorSubtleBackgroundInverted: 'transparent',
|
||||||
|
colorSubtleBackgroundInvertedHover: blackAlpha[10],
|
||||||
|
colorSubtleBackgroundInvertedPressed: blackAlpha[30],
|
||||||
|
colorSubtleBackgroundInvertedSelected: blackAlpha[20],
|
||||||
|
colorTransparentBackground: 'transparent',
|
||||||
|
colorTransparentBackgroundHover: 'transparent',
|
||||||
|
colorTransparentBackgroundPressed: 'transparent',
|
||||||
|
colorTransparentBackgroundSelected: 'transparent',
|
||||||
|
colorNeutralBackgroundDisabled: palette.neutralLighter || webLightTheme.colorNeutralBackgroundDisabled,
|
||||||
|
colorNeutralBackgroundInvertedDisabled: whiteAlpha[10],
|
||||||
|
colorNeutralStencil1: palette.neutralLight || webLightTheme.colorNeutralStencil1,
|
||||||
|
colorNeutralStencil2: palette.neutralLighterAlt || webLightTheme.colorNeutralStencil2,
|
||||||
|
colorBackgroundOverlay: blackAlpha[10],
|
||||||
|
colorScrollbarOverlay: blackAlpha[50],
|
||||||
|
colorBrandBackground: palette.themePrimary || webLightTheme.colorBrandBackground,
|
||||||
|
colorBrandBackgroundHover: palette.themeDarkAlt || webLightTheme.colorBrandBackgroundHover,
|
||||||
|
colorBrandBackgroundPressed: palette.themeDarker || webLightTheme.colorBrandBackgroundPressed,
|
||||||
|
colorBrandBackgroundSelected: palette.themeDark || webLightTheme.colorBrandBackgroundSelected,
|
||||||
|
colorCompoundBrandBackground: palette.themePrimary || webLightTheme.colorCompoundBrandBackground,
|
||||||
|
colorCompoundBrandBackgroundHover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandBackgroundHover,
|
||||||
|
colorCompoundBrandBackgroundPressed: palette.themeDark || webLightTheme.colorCompoundBrandBackgroundPressed,
|
||||||
|
colorBrandBackgroundStatic: palette.themePrimary || webLightTheme.colorBrandBackgroundStatic,
|
||||||
|
colorBrandBackground2: palette.themeLighterAlt || webLightTheme.colorBrandBackground2,
|
||||||
|
colorBrandBackgroundInverted: palette.white || webLightTheme.colorBrandBackgroundInverted,
|
||||||
|
colorBrandBackgroundInvertedHover: palette.themeLighterAlt || webLightTheme.colorBrandBackgroundInvertedHover,
|
||||||
|
colorBrandBackgroundInvertedPressed: palette.themeLight || webLightTheme.colorBrandBackgroundInvertedPressed,
|
||||||
|
colorBrandBackgroundInvertedSelected: palette.themeLighter || webLightTheme.colorBrandBackgroundInvertedSelected,
|
||||||
|
colorNeutralStrokeAccessible: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessible,
|
||||||
|
colorNeutralStrokeAccessibleHover: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessibleHover,
|
||||||
|
colorNeutralStrokeAccessiblePressed: palette.neutralSecondary || webLightTheme.colorNeutralStrokeAccessiblePressed,
|
||||||
|
colorNeutralStrokeAccessibleSelected: palette.themePrimary || webLightTheme.colorNeutralStrokeAccessibleSelected,
|
||||||
|
colorNeutralStroke1: palette.neutralQuaternary || webLightTheme.colorNeutralStroke1,
|
||||||
|
colorNeutralStroke1Hover: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Hover,
|
||||||
|
colorNeutralStroke1Pressed: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Pressed,
|
||||||
|
colorNeutralStroke1Selected: palette.neutralTertiaryAlt || webLightTheme.colorNeutralStroke1Selected,
|
||||||
|
colorNeutralStroke2: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralStroke2,
|
||||||
|
colorNeutralStroke3: palette.neutralLighter || webLightTheme.colorNeutralStroke3,
|
||||||
|
colorNeutralStrokeOnBrand: palette.white || webLightTheme.colorNeutralStrokeOnBrand,
|
||||||
|
colorNeutralStrokeOnBrand2: palette.white || webLightTheme.colorNeutralStrokeOnBrand2,
|
||||||
|
colorNeutralStrokeOnBrand2Hover: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Hover,
|
||||||
|
colorNeutralStrokeOnBrand2Pressed: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Pressed,
|
||||||
|
colorNeutralStrokeOnBrand2Selected: palette.white || webLightTheme.colorNeutralStrokeOnBrand2Selected,
|
||||||
|
colorBrandStroke1: palette.themePrimary || webLightTheme.colorBrandStroke1,
|
||||||
|
colorBrandStroke2: palette.themeLight || webLightTheme.colorBrandStroke2,
|
||||||
|
colorCompoundBrandStroke: palette.themePrimary || webLightTheme.colorCompoundBrandStroke,
|
||||||
|
colorCompoundBrandStrokeHover: palette.themeDarkAlt || webLightTheme.colorCompoundBrandStrokeHover,
|
||||||
|
colorCompoundBrandStrokePressed: palette.themeDark || webLightTheme.colorCompoundBrandStrokePressed,
|
||||||
|
colorNeutralStrokeDisabled: palette.neutralQuaternaryAlt || webLightTheme.colorNeutralStrokeDisabled,
|
||||||
|
colorNeutralStrokeInvertedDisabled: whiteAlpha[40],
|
||||||
|
colorTransparentStroke: 'transparent',
|
||||||
|
colorTransparentStrokeInteractive: 'transparent',
|
||||||
|
colorTransparentStrokeDisabled: 'transparent',
|
||||||
|
colorStrokeFocus1: palette.white || webLightTheme.colorStrokeFocus1,
|
||||||
|
colorStrokeFocus2: palette.black || webLightTheme.colorStrokeFocus2,
|
||||||
|
colorNeutralShadowAmbient: 'rgba(0,0,0,0.12)',
|
||||||
|
colorNeutralShadowKey: 'rgba(0,0,0,0.14)',
|
||||||
|
colorNeutralShadowAmbientLighter: 'rgba(0,0,0,0.06)',
|
||||||
|
colorNeutralShadowKeyLighter: 'rgba(0,0,0,0.07)',
|
||||||
|
colorNeutralShadowAmbientDarker: 'rgba(0,0,0,0.20)',
|
||||||
|
colorNeutralShadowKeyDarker: 'rgba(0,0,0,0.24)',
|
||||||
|
colorBrandShadowAmbient: 'rgba(0,0,0,0.30)',
|
||||||
|
colorBrandShadowKey: 'rgba(0,0,0,0.25)',
|
||||||
|
colorNeutralStencil1Alpha: webLightTheme.colorNeutralStencil1Alpha,
|
||||||
|
colorNeutralStencil2Alpha: webLightTheme.colorNeutralStencil2Alpha,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a v9 theme from a v8 theme.
|
||||||
|
* You can optional pass a base v9 theme; otherwise webLightTheme is used.
|
||||||
|
*/
|
||||||
|
export const createv9Theme = (themeV8: ThemeV8, baseThemeV9?: ThemeV9): ThemeV9 => {
|
||||||
|
const baseTheme = baseThemeV9 ?? webLightTheme;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...baseTheme,
|
||||||
|
...mapAliasColors(themeV8.palette, themeV8.isInverted)
|
||||||
|
};
|
||||||
|
};
|
|
@ -1,15 +1,15 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import * as ReactDom from 'react-dom';
|
import * as ReactDom from 'react-dom';
|
||||||
import { Version } from '@microsoft/sp-core-library';
|
import { Version } from '@microsoft/sp-core-library';
|
||||||
import { IPropertyPaneConfiguration } from '@microsoft/sp-property-pane';
|
|
||||||
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
|
||||||
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
import { IReadonlyTheme } from '@microsoft/sp-component-base';
|
||||||
import GroupMembershipManager from './components/GroupMembershipManager';
|
import GroupMembershipManager from './components/GroupMembershipManager';
|
||||||
import { IGroupMembershipManagerProps } from './components/IGroupMembershipManagerProps';
|
import { IGroupMembershipManagerProps } from './components/IGroupMembershipManagerProps';
|
||||||
import { FluentProvider, FluentProviderProps, teamsDarkTheme, teamsLightTheme, webLightTheme, webDarkTheme, Theme } from '@fluentui/react-components';
|
import { FluentProvider, FluentProviderProps, teamsDarkTheme, teamsLightTheme, webLightTheme, webDarkTheme, Theme } from '@fluentui/react-components';
|
||||||
|
import { createv9Theme } from '../../shims/v9ThemeShim';
|
||||||
|
|
||||||
export enum AppMode {
|
export enum AppMode {
|
||||||
SharePoint, SharePointLocal, Teams, TeamsLocal
|
SharePoint, Teams, Office, Outlook
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart<{}> {
|
export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart<{}> {
|
||||||
|
@ -17,9 +17,16 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
||||||
private _appMode: AppMode = AppMode.SharePoint;
|
private _appMode: AppMode = AppMode.SharePoint;
|
||||||
private _theme: Theme = webLightTheme;
|
private _theme: Theme = webLightTheme;
|
||||||
|
|
||||||
protected onInit(): Promise<void> {
|
protected async onInit(): Promise<void> {
|
||||||
//on initalizational set the App Mode
|
if (!!this.context.sdks.microsoftTeams) {
|
||||||
this._appMode = !!this.context.sdks.microsoftTeams ? (this.context.isServedFromLocalhost ? AppMode.TeamsLocal : AppMode.Teams) : (this.context.isServedFromLocalhost) ? AppMode.SharePointLocal : AppMode.SharePoint;
|
const teamsContext = await this.context.sdks.microsoftTeams.teamsJs.app.getContext();
|
||||||
|
switch (teamsContext.app.host.name.toLowerCase()) {
|
||||||
|
case 'teams': this._appMode = AppMode.Teams; break;
|
||||||
|
case 'office': this._appMode = AppMode.Office; break;
|
||||||
|
case 'outlook': this._appMode = AppMode.Outlook; break;
|
||||||
|
default: throw new Error('Unknown host');
|
||||||
|
}
|
||||||
|
} else this._appMode = AppMode.SharePoint;
|
||||||
return super.onInit();
|
return super.onInit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,9 +44,11 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
||||||
const fluentElement: React.ReactElement<FluentProviderProps> = React.createElement(
|
const fluentElement: React.ReactElement<FluentProviderProps> = React.createElement(
|
||||||
FluentProvider,
|
FluentProvider,
|
||||||
{
|
{
|
||||||
theme : this._appMode === AppMode.Teams || this._appMode === AppMode.TeamsLocal ?
|
theme: this._appMode === AppMode.Teams ?
|
||||||
this._isDarkTheme ? teamsDarkTheme : teamsLightTheme :
|
this._isDarkTheme ? teamsDarkTheme : teamsLightTheme :
|
||||||
this._isDarkTheme ? webDarkTheme : this._theme
|
this._appMode === AppMode.SharePoint ?
|
||||||
|
this._isDarkTheme ? webDarkTheme : this._theme :
|
||||||
|
this._isDarkTheme ? webDarkTheme : webLightTheme
|
||||||
},
|
},
|
||||||
element
|
element
|
||||||
);
|
);
|
||||||
|
@ -50,30 +59,9 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
||||||
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
protected onThemeChanged(currentTheme: IReadonlyTheme | undefined): void {
|
||||||
if (!currentTheme) return;
|
if (!currentTheme) return;
|
||||||
this._isDarkTheme = !!currentTheme.isInverted;
|
this._isDarkTheme = !!currentTheme.isInverted;
|
||||||
|
|
||||||
//if the app mode is sharepoint, adjust the fluent ui 9 web light theme to use the sharepoint theme color, teams/dark mode should be fine on default
|
//if the app mode is sharepoint, adjust the fluent ui 9 web light theme to use the sharepoint theme color, teams/dark mode should be fine on default
|
||||||
if (this._appMode === AppMode.SharePoint || this._appMode === AppMode.SharePointLocal) {
|
if (this._appMode === AppMode.SharePoint) {
|
||||||
this._theme = {...webLightTheme,
|
this._theme = createv9Theme(currentTheme, webLightTheme);
|
||||||
colorBrandBackground: currentTheme.palette.themePrimary,
|
|
||||||
colorBrandBackgroundHover: currentTheme.palette.themeDark,
|
|
||||||
colorBrandBackgroundPressed: currentTheme.palette.themeDarker,
|
|
||||||
colorCompoundBrandForeground1: currentTheme.palette.themePrimary,
|
|
||||||
colorCompoundBrandForeground1Hover: currentTheme.palette.themeDark,
|
|
||||||
colorCompoundBrandForeground1Pressed: currentTheme.palette.themeDarker,
|
|
||||||
colorNeutralForeground2BrandHover: currentTheme.palette.themeSecondary,
|
|
||||||
colorNeutralForeground2BrandPressed: currentTheme.palette.themeDarkAlt,
|
|
||||||
colorNeutralForeground2BrandSelected: currentTheme.palette.themeDarkAlt,
|
|
||||||
colorBrandForeground1: currentTheme.palette.themeSecondary,
|
|
||||||
colorBrandStroke1: currentTheme.palette.themePrimary,
|
|
||||||
colorBrandStroke2: currentTheme.palette.themeSecondary,
|
|
||||||
colorCompoundBrandStroke: currentTheme.palette.themePrimary,
|
|
||||||
colorCompoundBrandStrokeHover: currentTheme.palette.themeSecondary,
|
|
||||||
colorCompoundBrandStrokePressed: currentTheme.palette.themeDarkAlt,
|
|
||||||
colorCompoundBrandBackground: currentTheme.palette.themePrimary,
|
|
||||||
colorCompoundBrandBackgroundHover: currentTheme.palette.themeDark,
|
|
||||||
colorCompoundBrandBackgroundPressed: currentTheme.palette.themeDarker,
|
|
||||||
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,11 +72,4 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
|
||||||
protected get dataVersion(): Version {
|
protected get dataVersion(): Version {
|
||||||
return Version.parse('1.0');
|
return Version.parse('1.0');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
|
|
||||||
return {
|
|
||||||
pages: [
|
|
||||||
]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,14 @@ import * as React from 'react';
|
||||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||||
import styles from './GroupMembershipManager.module.scss';
|
import styles from './GroupMembershipManager.module.scss';
|
||||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
import { Alert } from "@fluentui/react-components/unstable";
|
||||||
import { Button, Checkbox, Divider, Input, Label, Spinner, useId } from "@fluentui/react-components";
|
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Button, Checkbox, Divider, Input, Label, Spinner, useId, DialogContent } from "@fluentui/react-components";
|
||||||
import { PersonAddRegular } from '@fluentui/react-icons';
|
import { PersonAddRegular } from '@fluentui/react-icons';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
import { GraphError } from '@microsoft/microsoft-graph-clientv1';
|
||||||
|
|
||||||
|
|
||||||
export enum AddUserMode { Member, Owner }
|
export enum AddUserMode { Member, Owner }
|
||||||
enum rState { Idle, Running, Error, Completed }
|
enum rState { Idle, Running, Error, Completed }
|
||||||
|
@ -44,14 +43,13 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
||||||
"members@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
"members@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
||||||
} : {
|
} : {
|
||||||
"owners@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
"owners@odata.bind": users.map(m => `https://graph.microsoft.com/v1.0/directoryObjects/${m.id}`)
|
||||||
}, (e: GraphError, response ) => {
|
}, (e: GraphError) => {
|
||||||
if (e) throw e.message;
|
if (e) throw e.message;
|
||||||
else setRunning(rState.Completed)
|
else setRunning(rState.Completed)
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
} catch (error) {
|
} catch (error: unknown) {
|
||||||
handleError(error);
|
handleError(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
@ -59,60 +57,60 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setRunning(rState.Idle);
|
setRunning(rState.Idle);
|
||||||
if (onCompleted) onCompleted();
|
if (onCompleted) onCompleted();
|
||||||
} , 5000);
|
}, 5000);
|
||||||
}, [running]);
|
}, [running]);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (searchTerm && searchTerm !== "") {
|
if (searchTerm && searchTerm !== "") {
|
||||||
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
||||||
client.api('/me/people').search(searchTerm).filter("personType/subclass eq 'OrganizationUser'").get((error: GraphError, response: { value: MicrosoftGraph.Person[] }) => {
|
client.api('/me/people').search(searchTerm).filter("personType/subclass eq 'OrganizationUser'").get((error: GraphError, response: { value: MicrosoftGraph.Person[] }) => {
|
||||||
if (error) throw error.message;
|
if (error) throw error.message;
|
||||||
else setSearchResults(response.value);
|
else setSearchResults(response.value);
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
}).catch(handleError);
|
}).catch(handleError);
|
||||||
} else setSearchResults(null)
|
} else setSearchResults(null)
|
||||||
}, [searchTerm]);
|
}, [searchTerm]);
|
||||||
|
|
||||||
|
|
||||||
const inputId = useId('input-with-placeholder');
|
const inputId = useId('input-with-placeholder');
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
|
<Button appearance='primary' icon={<PersonAddRegular />} onClick={() => setOpen(true)}>{strings.Add}</Button>
|
||||||
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
||||||
<DialogTrigger>
|
<DialogSurface>
|
||||||
<Button appearance='primary' icon={<PersonAddRegular />}>{strings.Add}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogSurface aria-label="label">
|
|
||||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.AddDialogTitleOwner : strings.AddDialogTitle}{Group.displayName}</DialogTitle>
|
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className={styles.stack}>
|
<DialogTitle>{Mode === AddUserMode.Owner ? strings.AddDialogTitleOwner : strings.AddDialogTitle}{Group.displayName}</DialogTitle>
|
||||||
{running !== rState.Error && _error && <Alert intent="error">{_error}</Alert>}
|
<DialogContent>
|
||||||
{running === rState.Error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error}</Alert>}
|
<div className={styles.stack}>
|
||||||
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Added} {Group.displayName}</Alert>}
|
{running !== rState.Error && _error && <Alert intent="error">{_error}</Alert>}
|
||||||
{users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
{running === rState.Error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error}</Alert>}
|
||||||
{users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Added} {Group.displayName}</Alert>}
|
||||||
{running !== rState.Completed && <Checkbox disabled={running === rState.Running} defaultChecked onChange={(e, d?) => setUsers(d?.checked ? users.concat([u]) : users.filter(_u => _u.id !== u.id)) } />}
|
{users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
||||||
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.scoredEmailAddresses[0].address} />
|
{users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
||||||
</div>)}
|
{running !== rState.Completed && <Checkbox disabled={running === rState.Running} defaultChecked onChange={(e, d?) => setUsers(d?.checked ? users.concat([u]) : users.filter(_u => _u.id !== u.id))} />}
|
||||||
</div>}
|
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.scoredEmailAddresses[0].address} />
|
||||||
{(running === rState.Idle || running === rState.Error) && <>
|
</div>)}
|
||||||
<Divider />
|
</div>}
|
||||||
<Label htmlFor={inputId}>{strings.Search}</Label>
|
{(running === rState.Idle || running === rState.Error) && <>
|
||||||
<Input placeholder={strings.SearchPlaceholder} onChange={(e, d) => setSearchTerm(d.value)} id={inputId} />
|
<Divider />
|
||||||
{searchResults && searchResults.map(u => <div key={u.id} className={styles.stackHoz}>
|
<Label htmlFor={inputId}>{strings.Search}</Label>
|
||||||
<Checkbox onChange={(e, d?) => setUsers(d?.checked ? users.concat([u]) : users.filter(_u => _u.id !== u.id)) } />
|
<Input placeholder={strings.SearchPlaceholder} onChange={(e, d) => setSearchTerm(d.value)} id={inputId} />
|
||||||
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.scoredEmailAddresses[0].address} />
|
{searchResults && searchResults.map(u => <div key={u.id} className={styles.stackHoz}>
|
||||||
</div>)}
|
<Checkbox onChange={(e, d?) => setUsers(d?.checked ? users.concat([u]) : users.filter(_u => _u.id !== u.id))} />
|
||||||
</>}
|
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.scoredEmailAddresses[0].address} />
|
||||||
{running === rState.Running && <Spinner labelPosition='below' label={strings.Adding} /> }
|
</div>)}
|
||||||
</div>
|
</>}
|
||||||
|
{running === rState.Running && <Spinner labelPosition='below' label={strings.Adding} />}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
{users.length > 0 && <Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={add}>{strings.Add}</Button>}
|
||||||
|
</DialogActions>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogActions>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
{users.length > 0 && <Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={add}>{strings.Add}</Button>}
|
|
||||||
</DialogActions>
|
|
||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
</>)
|
||||||
}
|
}
|
|
@ -2,15 +2,15 @@ import * as React from 'react';
|
||||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||||
import styles from './GroupMembershipManager.module.scss';
|
import styles from './GroupMembershipManager.module.scss';
|
||||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
import { Alert } from "@fluentui/react-components/unstable";
|
||||||
import { Button, Spinner } from "@fluentui/react-components";
|
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Button, Spinner, DialogContent } from "@fluentui/react-components";
|
||||||
import { AddUserMode } from './AddUser';
|
import { AddUserMode } from './AddUser';
|
||||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||||
import { PersonDeleteRegular } from '@fluentui/react-icons';
|
import { PersonDeleteRegular } from '@fluentui/react-icons';
|
||||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||||
import { BatchRequestStep, BatchRequestContent, BatchRequestBody, BatchResponseContent } from '@microsoft/microsoft-graph-client';
|
import { BatchRequestStep, BatchRequestContent, BatchRequestBody, BatchResponseContent } from '@microsoft/microsoft-graph-clientv1';
|
||||||
|
|
||||||
enum rState { Idle, Running, Error, Completed }
|
enum rState { Idle, Running, Error, Completed }
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ export default function RemoveUser({ Group, Users, Mode, context, onCompleted }:
|
||||||
const userRequestSteps: BatchRequestStep[] = Users.map((v, i) => ({
|
const userRequestSteps: BatchRequestStep[] = Users.map((v, i) => ({
|
||||||
id: i.toString(),
|
id: i.toString(),
|
||||||
request: new Request(`/groups/${Group.id}/${Mode === AddUserMode.Member ? 'members' : 'owners'}/${v.id}/$ref`, {
|
request: new Request(`/groups/${Group.id}/${Mode === AddUserMode.Member ? 'members' : 'owners'}/${v.id}/$ref`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
})
|
})
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@ -74,33 +74,34 @@ export default function RemoveUser({ Group, Users, Mode, context, onCompleted }:
|
||||||
}, [running]);
|
}, [running]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
|
<Button appearance='primary' icon={<PersonDeleteRegular />} disabled={Users.length === 0} onClick={() => setOpen(true)}>{strings.Remove}</Button>
|
||||||
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
||||||
<DialogTrigger>
|
|
||||||
<Button appearance='primary' icon={<PersonDeleteRegular />} disabled={Users.length === 0}>{strings.Remove}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogSurface aria-label="label">
|
<DialogSurface aria-label="label">
|
||||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.RemoveDialogTitleOwner : strings.RemoveDialogTitle}{Group.displayName}</DialogTitle>
|
|
||||||
<DialogBody>
|
<DialogBody>
|
||||||
<div className={styles.stack}>
|
<DialogTitle>{Mode === AddUserMode.Owner ? strings.RemoveDialogTitleOwner : strings.RemoveDialogTitle}{Group.displayName}</DialogTitle>
|
||||||
{running !== rState.Error && _error && <Alert intent="error">{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
|
||||||
{running === rState.Error && _error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
<DialogContent>
|
||||||
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Removed} {Group.displayName}</Alert>}
|
<div className={styles.stack}>
|
||||||
{Users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
{running !== rState.Error && _error && <Alert intent="error">{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
||||||
{Users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
{running === rState.Error && _error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error.split('||').map((v, i) => (<div key={i}>{v}</div>))}</Alert>}
|
||||||
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.mail} />
|
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Removed} {Group.displayName}</Alert>}
|
||||||
</div>)}
|
{Users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
||||||
</div>}
|
{Users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
||||||
{running === rState.Running && <Spinner labelPosition='below' label={strings.Removing} />}
|
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.mail} />
|
||||||
</div>
|
</div>)}
|
||||||
|
</div>}
|
||||||
|
{running === rState.Running && <Spinner labelPosition='below' label={strings.Removing} />}
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<DialogTrigger>
|
||||||
|
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={remove}>{strings.Remove}</Button>
|
||||||
|
</DialogActions>
|
||||||
</DialogBody>
|
</DialogBody>
|
||||||
<DialogActions>
|
|
||||||
<DialogTrigger>
|
|
||||||
<Button appearance="secondary" disabled={running === rState.Running}>{strings.Close}</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<Button appearance="primary" disabled={running === rState.Completed || running === rState.Running} onClick={remove}>{strings.Remove}</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</DialogSurface>
|
</DialogSurface>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
</>)
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
@import '~office-ui-fabric-react/dist/sass/References.scss';
|
@import '~@fluentui/react/dist/sass/References.scss';
|
||||||
|
|
||||||
.groupMembershipManager {
|
.groupMembershipManager {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
|
|
@ -4,15 +4,13 @@ import { IGroupMembershipManagerProps } from './IGroupMembershipManagerProps';
|
||||||
import * as strings from 'GroupMembershipManagerWebPartStrings';
|
import * as strings from 'GroupMembershipManagerWebPartStrings';
|
||||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||||
import { Checkbox, CheckboxOnChangeData, Spinner, Subtitle1, Textarea, Tooltip } from '@fluentui/react-components';
|
import { Spinner, Subtitle1, Textarea, Tooltip } from '@fluentui/react-components';
|
||||||
import { Dropdown, Option, Toolbar } from "@fluentui/react-components/unstable";
|
import { Dropdown, Option, Toolbar } from "@fluentui/react-components/unstable";
|
||||||
import { TableBody, TableCell, TableRow, Table } from '@fluentui/react-table';
|
|
||||||
import AddUser, { AddUserMode } from './AddUser';
|
import AddUser, { AddUserMode } from './AddUser';
|
||||||
import RemoveUser from './DeleteUser';
|
import RemoveUser from './DeleteUser';
|
||||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
import { GraphError } from '@microsoft/microsoft-graph-clientv1';
|
||||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
import { PeopleTeam24Regular, MailLink24Regular, ShieldLock24Regular, Eye24Regular, EyeOff24Regular } from '@fluentui/react-icons'
|
||||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
import TableGrid from './Table';
|
||||||
import { PeopleTeam24Regular, MailLink24Regular,ShieldLock24Regular, Eye24Regular, EyeOff24Regular } from '@fluentui/react-icons'
|
|
||||||
|
|
||||||
type OnSelectData = {
|
type OnSelectData = {
|
||||||
optionValue: string;
|
optionValue: string;
|
||||||
|
@ -34,7 +32,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
||||||
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,mailEnabled,resourceProvisioningOptions,securityEnabled,description").get((error, response: { value: MicrosoftGraph.Group[] }) => {
|
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,mailEnabled,resourceProvisioningOptions,securityEnabled,description").get((error: undefined, response: { value: MicrosoftGraph.Group[] }) => {
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
setGroups(response.value.filter(g => g.groupTypes).sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
setGroups(response.value.filter(g => g.groupTypes).sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
||||||
}).catch(console.error);
|
}).catch(console.error);
|
||||||
|
@ -61,25 +59,17 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
||||||
if (group) loadGroup().catch(console.error);
|
if (group) loadGroup().catch(console.error);
|
||||||
}, [group]);
|
}, [group]);
|
||||||
|
|
||||||
const checkUser = (data: CheckboxOnChangeData, user: MicrosoftGraph.User): void => {
|
|
||||||
setToRemove(data.checked ? toRemove.concat([user]) : toRemove.filter(u => u.id !== user.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const checkOwner = (data: CheckboxOnChangeData, user: MicrosoftGraph.User): void => {
|
|
||||||
setRemoveOwner(data.checked ? removeOwner.concat([user]) : removeOwner.filter(u => u.id !== user.id));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className={`${styles.groupMembershipManager} ${hasTeamsContext ? styles.teams : ''}`}>
|
<section className={`${styles.groupMembershipManager} ${hasTeamsContext ? styles.teams : ''}`}>
|
||||||
{!groups && <Spinner labelPosition='below' label={strings.LoadingGroups} />}
|
{!groups && <Spinner labelPosition='below' label={strings.LoadingGroups} />}
|
||||||
{groups && <section>
|
{groups && <section>
|
||||||
<Subtitle1 block>{strings.PickGroup}</Subtitle1>
|
<Subtitle1 block>{strings.PickGroup}</Subtitle1>
|
||||||
<div className={styles.stackHoz} style={{ alignItems: 'center'}}>
|
<div className={styles.stackHoz} style={{ alignItems: 'center' }}>
|
||||||
<Dropdown placeholder={strings.PickGroup} size="large" onSelect={(e, d?: OnSelectData) => setGroup(d ? d.optionValue : null)}>
|
<Dropdown placeholder={strings.PickGroup} size="large" onOptionSelect={(e, d?: OnSelectData) => { setGroup(d?.optionValue); }}>
|
||||||
{groups.map(group => <Option id={group.id} key={group.id}>{group.displayName}</Option>)}
|
{groups.map(group => <Option key={group.mailNickname}>{group.displayName}</Option>)}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{group && groups.filter(g => g.displayName === group)[0].resourceProvisioningOptions?.filter(g => g === "Team").length === 1 &&
|
{group && groups.filter(g => g.displayName === group)[0].resourceProvisioningOptions?.filter(g => g === "Team").length === 1 &&
|
||||||
<Tooltip relationship='label' content="Team"><span><PeopleTeam24Regular /></span></Tooltip>
|
<Tooltip relationship='label' content="Team"><span><PeopleTeam24Regular /></span></Tooltip>
|
||||||
}
|
}
|
||||||
{group && groups.filter(g => g.displayName === group)[0].mailEnabled &&
|
{group && groups.filter(g => g.displayName === group)[0].mailEnabled &&
|
||||||
<Tooltip relationship='label' content="Mail Enabled"><span><MailLink24Regular /></span></Tooltip>
|
<Tooltip relationship='label' content="Mail Enabled"><span><MailLink24Regular /></span></Tooltip>
|
||||||
|
@ -96,34 +86,22 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
||||||
</div>
|
</div>
|
||||||
</section>}
|
</section>}
|
||||||
{(!members || !owners) && groups && group && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
{(!members || !owners) && groups && group && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
||||||
{groups && group && <div className={styles.stack} style={{marginTop: 10 }}>
|
{groups && group && <div className={styles.stack} style={{ marginTop: 10 }}>
|
||||||
<Textarea value={groups.filter(g => g.displayName === group)[0].description} resize='vertical' readOnly />
|
<Textarea value={groups.filter(g => g.displayName === group)[0].description} resize='vertical' readOnly />
|
||||||
<div className={`${styles.stackHoz} ${styles.spaceBetween}`} style={{marginTop: 10 }}>
|
<div className={`${styles.stackHoz} ${styles.spaceBetween}`} style={{ marginTop: 10 }}>
|
||||||
<div style={{width: '49%'}}>
|
<div style={{ width: '49%' }}>
|
||||||
<Subtitle1>{strings.Members}</Subtitle1>
|
<Subtitle1>{strings.Members}</Subtitle1>
|
||||||
{!members && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
{!members && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
||||||
{members && <>
|
{members && <>
|
||||||
{//don't display the toolbar if the group is dynamic
|
{//don't display the toolbar if the group is dynamic
|
||||||
!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) && <Toolbar>
|
!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) && <Toolbar>
|
||||||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
||||||
<RemoveUser Users={toRemove} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
<RemoveUser Users={toRemove} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
||||||
</Toolbar>}
|
</Toolbar>}
|
||||||
<Table>
|
<TableGrid Users={members} ServiceScope={context.serviceScope} onSelectionChange={setToRemove} />
|
||||||
<TableBody>
|
|
||||||
{members.map(user => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell>
|
|
||||||
{!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) &&
|
|
||||||
<Checkbox onChange={(ev, data) => checkUser(data, user)} />}
|
|
||||||
<SPFxPeopleCard primaryText={user.displayName} serviceScope={context.serviceScope} email={user.userPrincipalName} size={PersonaSize.size24} secondaryText={user.mail} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<div style={{width: '49%'}}>
|
<div style={{ width: '49%' }}>
|
||||||
<Subtitle1>{strings.Owners}</Subtitle1>
|
<Subtitle1>{strings.Owners}</Subtitle1>
|
||||||
{!owners && <Spinner labelPosition='below' label={strings.LoadingOwners} />}
|
{!owners && <Spinner labelPosition='below' label={strings.LoadingOwners} />}
|
||||||
{owners && <>
|
{owners && <>
|
||||||
|
@ -131,19 +109,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
||||||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
||||||
<RemoveUser Users={removeOwner} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
<RemoveUser Users={removeOwner} context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
<Table>
|
<TableGrid Users={owners} ServiceScope={context.serviceScope} onSelectionChange={setRemoveOwner} ReadOnly={groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0} />
|
||||||
<TableBody>
|
|
||||||
{owners.map(user => (
|
|
||||||
<TableRow key={user.id}>
|
|
||||||
<TableCell>
|
|
||||||
{!(groups.filter(g => g.displayName === group)[0].groupTypes?.filter(g => g === "DynamicMembership").length > 0) &&
|
|
||||||
<Checkbox onChange={(ev, data) => checkOwner(data, user)} />}
|
|
||||||
<SPFxPeopleCard primaryText={user.displayName} serviceScope={context.serviceScope} email={user.userPrincipalName} size={PersonaSize.size24} secondaryText={user.mail} />
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { TableColumnDefinition, createTableColumn, TableRowId, useTableFeatures, useTableSelection, TableHeader, TableRow, Table, TableSelectionCell, TableHeaderCell, TableBody, TableCell, TableCellLayout } from "@fluentui/react-components/unstable";
|
||||||
|
import * as React from 'react';
|
||||||
|
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||||
|
import SPFxPeopleCard from "./SPFxPeopleCard";
|
||||||
|
import { ServiceScope } from "@microsoft/sp-core-library";
|
||||||
|
import { PersonaSize } from "office-ui-fabric-react/lib/Persona";
|
||||||
|
|
||||||
|
export interface TableGridProps {
|
||||||
|
Users: MicrosoftGraph.User[];
|
||||||
|
ServiceScope: ServiceScope;
|
||||||
|
ReadOnly?: boolean;
|
||||||
|
onSelectionChange?: (SelectedUsers: MicrosoftGraph.User[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TableGrid(props: TableGridProps): React.ReactElement<TableGridProps> {
|
||||||
|
const columns: TableColumnDefinition<MicrosoftGraph.User>[] = React.useMemo(
|
||||||
|
() => [
|
||||||
|
createTableColumn<MicrosoftGraph.User>({
|
||||||
|
columnId: 'username',
|
||||||
|
})
|
||||||
|
],
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [selectedRows, setSelectedRows] = React.useState(() => new Set<TableRowId>([]));
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRows,
|
||||||
|
selection: { allRowsSelected, someRowsSelected, toggleAllRows, toggleRow, isRowSelected },
|
||||||
|
} = useTableFeatures(
|
||||||
|
{
|
||||||
|
columns,
|
||||||
|
items: props.Users,
|
||||||
|
},
|
||||||
|
[
|
||||||
|
useTableSelection({
|
||||||
|
selectionMode: 'multiselect',
|
||||||
|
selectedItems: selectedRows,
|
||||||
|
onSelectionChange: (e, data) => {
|
||||||
|
if (props.ReadOnly) return;
|
||||||
|
if (props.onSelectionChange) props.onSelectionChange(props.Users.filter((v, i) => data.selectedItems.has(i)));
|
||||||
|
setSelectedRows(data.selectedItems);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const rows = getRows(row => {
|
||||||
|
const selected = isRowSelected(row.rowId);
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
onClick: (e: React.MouseEvent) => { if (!props.ReadOnly) toggleRow(e, row.rowId) },
|
||||||
|
onKeyDown: (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!props.ReadOnly) toggleRow(e, row.rowId);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selected,
|
||||||
|
appearance: selected ? ('brand' as const) : ('none' as const),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleAllKeydown = React.useCallback(
|
||||||
|
(e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||||
|
if (e.key === ' ') {
|
||||||
|
toggleAllRows(e);
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[toggleAllRows],
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return (<Table aria-label="Table with multiselect">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
{!props.ReadOnly && <TableSelectionCell
|
||||||
|
checked={allRowsSelected ? true : someRowsSelected ? 'mixed' : false}
|
||||||
|
onClick={toggleAllRows}
|
||||||
|
onKeyDown={toggleAllKeydown}
|
||||||
|
checkboxIndicator={{ 'aria-label': 'Select all rows ' }}
|
||||||
|
/>}
|
||||||
|
<TableHeaderCell>User</TableHeaderCell>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{rows.map(({ item, selected, onClick, onKeyDown, appearance }) => (
|
||||||
|
<TableRow
|
||||||
|
key={item.id}
|
||||||
|
onClick={onClick}
|
||||||
|
onKeyDown={onKeyDown}
|
||||||
|
aria-selected={selected}
|
||||||
|
appearance={appearance}
|
||||||
|
>
|
||||||
|
{!props.ReadOnly && <TableSelectionCell checked={selected} checkboxIndicator={{ 'aria-label': 'Select row' }} />}
|
||||||
|
<TableCell>
|
||||||
|
<TableCellLayout>
|
||||||
|
<SPFxPeopleCard primaryText={item.displayName} serviceScope={props.ServiceScope} email={item.userPrincipalName} size={PersonaSize.size24} secondaryText={item.mail} />
|
||||||
|
</TableCellLayout>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>);
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://developer.microsoft.com/json-schemas/teams/v1.14/MicrosoftTeams.schema.json",
|
||||||
|
"manifestVersion": "1.14",
|
||||||
|
"packageName": "react-group-membership-manager-client-side-solution",
|
||||||
|
"id": "698a4f5b-686f-475d-81fb-9bcf98850b96",
|
||||||
|
"version": "1.1.0",
|
||||||
|
"developer": {
|
||||||
|
"name": "Nick Brown",
|
||||||
|
"websiteUrl": "https://nbdev.uk",
|
||||||
|
"privacyUrl": "https://nbdev.uk",
|
||||||
|
"termsOfUseUrl": "https://nbdev.uk"
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"short": "Group Membership Manager",
|
||||||
|
"full": "React Group Membership Manager"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"short": "Group Membership Manager",
|
||||||
|
"full": "React Group Membership Manager using Fluent UI 9"
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"outline": "698a4f5b-686f-475d-81fb-9bcf98850b96_outline.png",
|
||||||
|
"color": "698a4f5b-686f-475d-81fb-9bcf98850b96_color.png"
|
||||||
|
},
|
||||||
|
"accentColor": "#004578",
|
||||||
|
"staticTabs": [
|
||||||
|
{
|
||||||
|
"contentUrl": "https://{teamSiteDomain}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest=/_layouts/15/teamshostedapp.aspx%3Fteams%26personal%26componentId=698a4f5b-686f-475d-81fb-9bcf98850b96%26forceLocale={locale}",
|
||||||
|
"name": "Group Membership Manager",
|
||||||
|
"scopes": ["personal"],
|
||||||
|
"entityId": "100001"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configurableTabs": [
|
||||||
|
{
|
||||||
|
"configurationUrl": "https://{teamSiteDomain}{teamSitePath}/_layouts/15/TeamsLogon.aspx?SPFX=true&dest={teamSitePath}/_layouts/15/teamshostedapp.aspx%3FopenPropertyPane=true%26teams%26componentId=698a4f5b-686f-475d-81fb-9bcf98850b96%26forceLocale={locale}",
|
||||||
|
"canUpdateConfiguration": false,
|
||||||
|
"scopes": [
|
||||||
|
"team",
|
||||||
|
"groupchat"
|
||||||
|
],
|
||||||
|
"context": [
|
||||||
|
"channelTab",
|
||||||
|
"privateChatTab",
|
||||||
|
"meetingSidePanel",
|
||||||
|
"meetingDetailsTab",
|
||||||
|
"meetingChatTab"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"identity"
|
||||||
|
],
|
||||||
|
"validDomains": [
|
||||||
|
"*.login.microsoftonline.com",
|
||||||
|
"*.sharepoint.com",
|
||||||
|
"resourceseng.blob.core.windows.net"
|
||||||
|
],
|
||||||
|
"webApplicationInfo": {
|
||||||
|
"resource": "https://{teamSiteDomain}",
|
||||||
|
"id": "00000003-0000-0ff1-ce00-000000000000"
|
||||||
|
}
|
||||||
|
}
|
|
@ -13,9 +13,7 @@
|
||||||
"outDir": "lib",
|
"outDir": "lib",
|
||||||
"inlineSources": false,
|
"inlineSources": false,
|
||||||
"strictNullChecks": false,
|
"strictNullChecks": false,
|
||||||
"noUnusedLocals": false,
|
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
|
|
||||||
"typeRoots": [
|
"typeRoots": [
|
||||||
"./node_modules/@types",
|
"./node_modules/@types",
|
||||||
"./node_modules/@microsoft"
|
"./node_modules/@microsoft"
|
||||||
|
|
Loading…
Reference in New Issue