From 2e2ccc919ec6a8e1a0a0b5ffb75462c304d1dad6 Mon Sep 17 00:00:00 2001 From: Nick Brown Date: Mon, 16 Aug 2021 17:41:54 +0100 Subject: [PATCH] Added support for private channel population --- .../package-lock.json | 36 ++-- .../package.json | 6 +- .../components/TeamsMembershipUpdater.tsx | 191 +++++++++++++----- .../teamsMembershipUpdater/loc/en-us.js | 4 +- .../teamsMembershipUpdater/loc/mystrings.d.ts | 2 + 5 files changed, 167 insertions(+), 72 deletions(-) diff --git a/samples/react-teams-membership-updater/package-lock.json b/samples/react-teams-membership-updater/package-lock.json index 622717817..1ef04233c 100644 --- a/samples/react-teams-membership-updater/package-lock.json +++ b/samples/react-teams-membership-updater/package-lock.json @@ -4803,9 +4803,9 @@ } }, "@types/react": { - "version": "17.0.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz", - "integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==", + "version": "17.0.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz", + "integrity": "sha512-YTLgu7oS5zvSqq49X5Iue5oAbVGhgPc5Au29SJC4VeE17V6gASoOxVkUDy9pXFMRFxCWCD9fLeweNFizo3UzOg==", "dev": true, "requires": { "@types/prop-types": "*", @@ -7888,9 +7888,9 @@ } }, "tar": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", - "integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", + "version": "6.1.8", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz", + "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==", "dev": true, "requires": { "chownr": "^2.0.0", @@ -16541,9 +16541,9 @@ "integrity": "sha512-gcBs5HHr7tjkvk/+Ls10ttb3jEllRn7SvJitX/kx/gQq8BiFMSMKr1w+oNqXlh4EgkBHWUlJVPrYUu1KW/jVaQ==" }, "office-ui-fabric-react": { - "version": "7.173.0", - "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.173.0.tgz", - "integrity": "sha512-CXiIneX2lGL1l9vQa2t0nHnngKGUeCnOb0xoK58aGUeWKZuDpiVmxguokkvmrVr8F4hNb8thLfbyqFWpa7VJHQ==", + "version": "7.174.0", + "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.174.0.tgz", + "integrity": "sha512-hnuISSifwA7nSihuxpdNlh5plAmaPJqcDZUdhswak964Kb/8/ckMz/7BRQf+u9pGNs6LR14iDfRF/4RjLLzs6g==", "requires": { "@fluentui/date-time-utilities": "^7.9.1", "@fluentui/react-focus": "^7.17.6", @@ -16947,9 +16947,9 @@ "dev": true }, "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-root": { "version": "0.1.1", @@ -18324,9 +18324,9 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-papaparse": { - "version": "3.16.1", - "resolved": "https://registry.npmjs.org/react-papaparse/-/react-papaparse-3.16.1.tgz", - "integrity": "sha512-xwijSNf6vXYK9RwGGYYLOe6PNop5nRfmD1oprUjO6qUPK7OHR28GTWLDX6+UKeIS8Tq4Pa8DSYfZYPtFVLMIYg==", + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/react-papaparse/-/react-papaparse-3.17.1.tgz", + "integrity": "sha512-Pe/eheSNMDJzJJWVtryp4A3GjzMzkop1G+a3xNPsBCkPN3YzgX3ed4Vfb4kbMjbYNqS3k2gevXjszHtyFZIWtg==", "requires": { "@types/papaparse": "^5.0.4", "papaparse": "^5.2.0" @@ -21406,9 +21406,9 @@ } }, "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz", + "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==", "dev": true, "requires": { "querystringify": "^2.1.1", diff --git a/samples/react-teams-membership-updater/package.json b/samples/react-teams-membership-updater/package.json index dc96fddb3..1e0a69c42 100644 --- a/samples/react-teams-membership-updater/package.json +++ b/samples/react-teams-membership-updater/package.json @@ -17,10 +17,10 @@ "@microsoft/sp-webpart-base": "1.12.1", "@pnp/spfx-controls-react": "^3.2.1", "@pnp/spfx-property-controls": "^3.2.0", - "office-ui-fabric-react": "7.173.0", + "office-ui-fabric-react": "^7.174.0", "react": "16.9.0", "react-dom": "16.9.0", - "react-papaparse": "^3.16.1" + "react-papaparse": "^3.17.1" }, "resolutions": { "@types/react": "16.8.8" @@ -31,7 +31,7 @@ "@microsoft/sp-module-interfaces": "1.12.1", "@microsoft/sp-tslint-rules": "1.12.1", "@microsoft/sp-webpart-workbench": "1.12.1", - "@types/react": "^17.0.13", + "@types/react": "^17.0.18", "@types/react-dom": "^17.0.8", "@types/webpack-env": "^1.16.2", "ajv": "~5.2.2", diff --git a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/components/TeamsMembershipUpdater.tsx b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/components/TeamsMembershipUpdater.tsx index 5c55406d8..85c048b00 100644 --- a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/components/TeamsMembershipUpdater.tsx +++ b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/components/TeamsMembershipUpdater.tsx @@ -25,7 +25,9 @@ export enum Stage { export interface ITeamsMembershipUpdaterState { items: IDropdownOption[]; + privateChannels: IDropdownOption[]; selectionDetails: IDropdownOption; + selectionChannel: IDropdownOption; csvdata: any[]; csvcolumns: IColumn[]; csvSelected: IDropdownOption; @@ -59,7 +61,9 @@ export default class TeamsMembershipUpdater extends React.Component, item: IDropdownOption): void => { - this.setState({ ...this.state, stage: Stage.CheckingOwnership, logs: [], errors: [], logurl: null }); + this.setState({ ...this.state, stage: Stage.CheckingOwnership, selectionChannel: null, privateChannels: [], logs: [], errors: [], logurl: null }); this.props.context.msGraphClientFactory.getClient().then((client: MSGraphClient): void => { client.api(`groups/${item.key}/owners`).version("v1.0").get((err, res) => { if (err) { @@ -138,12 +142,25 @@ export default class TeamsMembershipUpdater extends React.Component { + if (err2) { + this.addError(err2.message, err2); + return; + } + this.setState({...this.state, selectionDetails: item, groupOwners: _owners, stage: Stage.Ready, privateChannels: res2.value.map(r => ({ key: r.id, text: r.displayName })) }); + }); + } else this.setState({ ...this.state, stage: Stage.ErrorOwnership }); }); }); } + public onChannelChange = (event: React.FormEvent, item: IDropdownOption): void => { + this.setState({ ...this.state, stage: Stage.Ready, selectionChannel: item, logs: [], errors: [], logurl: null }); + } + public onEmailChange = (event: React.FormEvent, item: IDropdownOption): void => { this.setState({ ...this.state, csvSelected: item }); } @@ -159,7 +176,20 @@ export default class TeamsMembershipUpdater extends React.Component { _m = _m.concat(members ); }); + if (res['@odata.nextLink']) this.loadMembers(res['@odata.nextLink'], client).then((members) => { _m = _m.concat(members); }); + result(_m); + }); + }); + } + + private loadChannelMembers = async (client: MSGraphClient, url: string): Promise> => { + return new Promise>(result => { + client.api(url).get((err, res: { value: MicrosoftGraph.AadUserConversationMember[], '@odata.nextLink'?: string }) =>{ + if (err) { + this.addError(err.message, err); + } + let _m = res.value; + if (res['@odata.nextLink']) this.loadChannelMembers(client, res['@odata.nextLink']).then((members) => { _m = _m.concat(members); }); result(_m); }); }); @@ -168,60 +198,118 @@ export default class TeamsMembershipUpdater extends React.Component { this.setState({ ...this.state, stage: Stage.LoadingCurrentMembers }); this.props.context.msGraphClientFactory.getClient().then((client: MSGraphClient): void => { - this.loadMembers(`groups/${this.state.selectionDetails.key}/members`, client).then((_members) => { - console.debug(_members); - this.setState({ ...this.state, groupMembers: _members, stage: Stage.ComparingMembers }); + if (this.state.selectionChannel === null) { + this.loadMembers(`groups/${this.state.selectionDetails.key}/members`, client).then((_members) => { + console.debug(_members); + this.setState({ ...this.state, groupMembers: _members, stage: Stage.ComparingMembers }); - this.addLog(`Found ${_members.length} members existing in the group`); + this.addLog(`Found ${_members.length} members existing in the group`); - let _delete: Array = new Array(); + let _delete: Array = new Array(); - //filter the members lists to find out if they no longer exist in the csv file and add those to the delete queue, ignore group owners - _members = _members.filter(m => { - if (this._data.some(value => value[this.state.csvSelected.text].toLowerCase() === m.mail.toLowerCase()) || this.state.groupOwners.some(value => value.toLowerCase() === m.userPrincipalName.toLowerCase())) return m; - else { if (this.state.delete == true) { _delete.push(m); this.addLog(`Will delete ${m.mail}`); } } - }); + //filter the members lists to find out if they no longer exist in the csv file and add those to the delete queue, ignore group owners + _members = _members.filter(m => { + if (this._data.some(value => value[this.state.csvSelected.text].toLowerCase() === m.mail.toLowerCase()) || this.state.groupOwners.some(value => value.toLowerCase() === m.userPrincipalName.toLowerCase())) return m; + else { if (this.state.delete == true) { _delete.push(m); this.addLog(`Will delete ${m.mail}`); } } + }); - let reqs: IRequest[] = []; - if (this.state.delete == true) { - this.setState({ ...this.state, stage: Stage.RemovingOrphendMembers }); - let _i, _j, _k, temparray, chunk = 20; - for (_i = 0, _j = _delete.length, _k = 0; _i < _j; _i += chunk) { - temparray = _delete.slice(_i, _i + chunk); - reqs.push({ requests: temparray.map(e1 => { _k++; return { id: `${_k}`, method: "DELETE", url: `groups/${this.state.selectionDetails.key}/members/${e1.id}/$ref` }; }) }); + let reqs: IRequest[] = []; + if (this.state.delete == true) { + this.setState({ ...this.state, stage: Stage.RemovingOrphendMembers }); + let _i, _j, _k, temparray, chunk = 20; + for (_i = 0, _j = _delete.length, _k = 0; _i < _j; _i += chunk) { + temparray = _delete.slice(_i, _i + chunk); + reqs.push({ requests: temparray.map(e1 => { _k++; return { id: `${_k}`, method: "DELETE", url: `groups/${this.state.selectionDetails.key}/members/${e1.id}/$ref` }; }) }); + } } - } - let newMembers: string[] = []; + let newMembers: string[] = []; - //filter the csv to look for users that do not exist the members list and add those to the add queue - this._data.forEach(async e2 => { - if (_members.some(m => m.mail.toLowerCase() === e2[this.state.csvSelected.text].toLowerCase()) == false) { - newMembers.push(e2[this.state.csvSelected.text]); - this.addLog(`Will add ${e2[this.state.csvSelected.text]}`); - } - }); - - //send delete batches to the graph, if they exist - if (reqs.length > 0) { - this.addLog(`${reqs.length} Delete Batches Detected`); - reqs.forEach(r => { - if (r.requests.length > 0) { - this.addLog(`Deleting ${r.requests.length} users as a batch`); - client.api("$batch").version("v1.0").post(r, (er, re) => { - if (er) { this.addError(er.message, er); return; } - if (re) re.reponses.forEach(e3 => { if (e3.body.error) this.addError(e3.body.error.message, e3.body.error); }); - this.addLog(`Deleting Batch Done`); - }); + //filter the csv to look for users that do not exist the members list and add those to the add queue + this._data.forEach(async e2 => { + if (_members.some(m => m.mail.toLowerCase() === e2[this.state.csvSelected.text].toLowerCase()) == false) { + newMembers.push(e2[this.state.csvSelected.text]); + this.addLog(`Will add ${e2[this.state.csvSelected.text]}`); } }); - //once the delete batches are done call the add members function, if no new members are needed call the Done function - if (newMembers.length == 0) this.Done(); + + //send delete batches to the graph, if they exist + if (reqs.length > 0) { + this.addLog(`${reqs.length} Delete Batches Detected`); + reqs.forEach(r => { + if (r.requests.length > 0) { + this.addLog(`Deleting ${r.requests.length} users as a batch`); + client.api("$batch").version("v1.0").post(r, (er, re) => { + if (er) { this.addError(er.message, er); return; } + if (re) re.reponses.forEach(e3 => { if (e3.body.error) this.addError(e3.body.error.message, e3.body.error); }); + this.addLog(`Deleting Batch Done`); + }); + } + }); + //once the delete batches are done call the add members function, if no new members are needed call the Done function + if (newMembers.length == 0) this.Done(); + else this.addMembers(newMembers, client); + } //if no new members are needed call the Done function + else if (newMembers.length == 0) this.Done(); else this.addMembers(newMembers, client); - } //if no new members are needed call the Done function - else if (newMembers.length == 0) this.Done(); - else this.addMembers(newMembers, client); - }); + }); + } + else { + this.loadChannelMembers(client, `teams/${this.state.selectionDetails.key}/channels/${this.state.selectionChannel.key}/members`).then((_members) => { + console.debug(_members); + this.setState({ ...this.state, groupMembers: _members, stage: Stage.ComparingMembers }); + + this.addLog(`Found ${_members.length} members existing in the channel`); + + let _delete: Array = new Array(); + + //filter the members lists to find out if they no longer exist in the csv file and add those to the delete queue, ignore group owners + _members = _members.filter(m => { + if (this._data.some(value => value[this.state.csvSelected.text].toLowerCase() === m.email.toLowerCase()) || this.state.groupOwners.some(value => value.toLowerCase() === m.email.toLowerCase())) return m; + else { if (this.state.delete == true) { _delete.push(m); this.addLog(`Will delete ${m.email}`); } } + }); + + let reqs: IRequest[] = []; + if (this.state.delete == true) { + this.setState({ ...this.state, stage: Stage.RemovingOrphendMembers }); + let _i, _j, _k, temparray, chunk = 20; + for (_i = 0, _j = _delete.length, _k = 0; _i < _j; _i += chunk) { + temparray = _delete.slice(_i, _i + chunk); + reqs.push({ requests: temparray.map(e1 => { _k++; return { id: `${_k}`, method: "DELETE", url: `teams/${this.state.selectionDetails.key}/channels/${this.state.selectionChannel.key}/${e1.id}` }; }) }); + } + } + + let newMembers: string[] = []; + + //filter the csv to look for users that do not exist the members list and add those to the add queue + this._data.forEach(async e2 => { + if (_members.some(m => m.email.toLowerCase() === e2[this.state.csvSelected.text].toLowerCase()) == false) { + newMembers.push(e2[this.state.csvSelected.text]); + this.addLog(`Will add ${e2[this.state.csvSelected.text]}`); + } + }); + + //send delete batches to the graph, if they exist + if (reqs.length > 0) { + this.addLog(`${reqs.length} Delete Batches Detected`); + reqs.forEach(r => { + if (r.requests.length > 0) { + this.addLog(`Deleting ${r.requests.length} users as a batch`); + client.api("$batch").version("v1.0").post(r, (er, re) => { + if (er) { this.addError(er.message, er); return; } + if (re) re.reponses.forEach(e3 => { if (e3.body.error) this.addError(e3.body.error.message, e3.body.error); }); + this.addLog(`Deleting Batch Done`); + }); + } + }); + //once the delete batches are done call the add members function, if no new members are needed call the Done function + if (newMembers.length == 0) this.Done(); + else this.addMembers(newMembers, client); + } //if no new members are needed call the Done function + else if (newMembers.length == 0) this.Done(); + else this.addMembers(newMembers, client); + }); + } }); } @@ -279,9 +367,11 @@ export default class TeamsMembershipUpdater extends React.Component { - const { items, csvItems, orphanedMembersHelp, csvdata, csvcolumns, stage, csvSelected, logurl, logs, errors } = this.state; + const { items, csvItems, orphanedMembersHelp, csvdata, csvcolumns, stage, csvSelected, logurl, logs, errors, privateChannels } = this.state; const mg = mergeStyleSets({ callout: { width: 320, @@ -358,6 +448,7 @@ export default class TeamsMembershipUpdater extends React.Component {stage == Stage.CheckingOwnership && } {stage == Stage.ErrorOwnership && You are not an owner of this group. Please select another.} + diff --git a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/en-us.js b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/en-us.js index 3447c8b7c..a6ea76f7b 100644 --- a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/en-us.js +++ b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/en-us.js @@ -31,6 +31,8 @@ define([], function() { "orphanedMembersTitle": "Remove Orphaned Members", "orphanedMembersContent": "Removes members who are not present in the CSV file. Owners are not effected", "on": "On", - "off": "Off" + "off": "Off", + "selectChannel": "(Optional) Select channel", + "selectChannelPlaceholder": "Select a private channel" } }); diff --git a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/mystrings.d.ts b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/mystrings.d.ts index 9ed848fac..fe7b7d459 100644 --- a/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/mystrings.d.ts +++ b/samples/react-teams-membership-updater/src/webparts/teamsMembershipUpdater/loc/mystrings.d.ts @@ -31,6 +31,8 @@ declare interface ITeamsMembershipUpdaterWebPartStrings { orphanedMembersContent: string; on: string; off: string; + selectChannel: string; + selectChannelPlaceholder: string; } declare module 'TeamsMembershipUpdaterWebPartStrings' {