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:
Nick Brown 2023-01-31 16:07:54 +00:00 committed by GitHub
parent c87ed9d054
commit f3af010335
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 11283 additions and 9902 deletions

View File

@ -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`)

View File

@ -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

View File

@ -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",

View File

@ -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,
}; };
}; };

View File

@ -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"
}
} }
} }

View File

@ -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

View File

@ -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": [

View File

@ -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

View File

@ -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

View File

@ -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)
};
};

View File

@ -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();
} }
@ -32,14 +39,16 @@ export default class GroupMembershipManagerWebPart extends BaseClientSideWebPart
context: this.context context: this.context
} }
); );
//wrap the component with the Fluent UI 9 Provider. //wrap the component with the Fluent UI 9 Provider.
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: [
]
};
}
} }

View File

@ -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,75 +43,74 @@ 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(() => {
if (running === rState.Completed) setTimeout(() => { if (running === rState.Completed) setTimeout(() => {
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>
) </>)
} }

View File

@ -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>
) </>)
} }

View File

@ -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;

View File

@ -4,16 +4,14 @@ 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;
selectedOptions: string[]; selectedOptions: 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,33 +59,25 @@ 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>
} }
{group && groups.filter(g => g.displayName === group)[0].securityEnabled && {group && groups.filter(g => g.displayName === group)[0].securityEnabled &&
<Tooltip relationship='label' content="Security Enabled"><span><ShieldLock24Regular /></span></Tooltip> <Tooltip relationship='label' content="Security Enabled"><span><ShieldLock24Regular /></span></Tooltip>
} }
{group && groups.filter(g => g.displayName === group)[0].visibility && {group && groups.filter(g => g.displayName === group)[0].visibility &&
<Tooltip relationship='label' content={groups.filter(g => g.displayName === group)[0].visibility}><span> <Tooltip relationship='label' content={groups.filter(g => g.displayName === group)[0].visibility}><span>
{groups.filter(g => g.displayName === group)[0].visibility === 'Public' && <Eye24Regular />} {groups.filter(g => g.displayName === group)[0].visibility === 'Public' && <Eye24Regular />}
{groups.filter(g => g.displayName === group)[0].visibility === 'Private' && <EyeOff24Regular />} {groups.filter(g => g.displayName === group)[0].visibility === 'Private' && <EyeOff24Regular />}
@ -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>

View File

@ -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>);
}

View File

@ -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"
}
}

View File

@ -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"