Added support for private channel population

This commit is contained in:
Nick Brown 2021-08-16 17:41:54 +01:00
parent 4ea97e510d
commit 2e2ccc919e
5 changed files with 167 additions and 72 deletions

View File

@ -4803,9 +4803,9 @@
} }
}, },
"@types/react": { "@types/react": {
"version": "17.0.14", "version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.14.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.18.tgz",
"integrity": "sha512-0WwKHUbWuQWOce61UexYuWTGuGY/8JvtUe/dtQ6lR4sZ3UiylHotJeWpf3ArP9+DSGUoLY3wbU59VyMrJps5VQ==", "integrity": "sha512-YTLgu7oS5zvSqq49X5Iue5oAbVGhgPc5Au29SJC4VeE17V6gASoOxVkUDy9pXFMRFxCWCD9fLeweNFizo3UzOg==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
@ -7888,9 +7888,9 @@
} }
}, },
"tar": { "tar": {
"version": "6.1.0", "version": "6.1.8",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.8.tgz",
"integrity": "sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA==", "integrity": "sha512-sb9b0cp855NbkMJcskdSYA7b11Q8JsX4qe4pyUAfHp+Y6jBjJeek2ZVlwEfWayshEIwlIzXx0Fain3QG9JPm2A==",
"dev": true, "dev": true,
"requires": { "requires": {
"chownr": "^2.0.0", "chownr": "^2.0.0",
@ -16541,9 +16541,9 @@
"integrity": "sha512-gcBs5HHr7tjkvk/+Ls10ttb3jEllRn7SvJitX/kx/gQq8BiFMSMKr1w+oNqXlh4EgkBHWUlJVPrYUu1KW/jVaQ==" "integrity": "sha512-gcBs5HHr7tjkvk/+Ls10ttb3jEllRn7SvJitX/kx/gQq8BiFMSMKr1w+oNqXlh4EgkBHWUlJVPrYUu1KW/jVaQ=="
}, },
"office-ui-fabric-react": { "office-ui-fabric-react": {
"version": "7.173.0", "version": "7.174.0",
"resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.173.0.tgz", "resolved": "https://registry.npmjs.org/office-ui-fabric-react/-/office-ui-fabric-react-7.174.0.tgz",
"integrity": "sha512-CXiIneX2lGL1l9vQa2t0nHnngKGUeCnOb0xoK58aGUeWKZuDpiVmxguokkvmrVr8F4hNb8thLfbyqFWpa7VJHQ==", "integrity": "sha512-hnuISSifwA7nSihuxpdNlh5plAmaPJqcDZUdhswak964Kb/8/ckMz/7BRQf+u9pGNs6LR14iDfRF/4RjLLzs6g==",
"requires": { "requires": {
"@fluentui/date-time-utilities": "^7.9.1", "@fluentui/date-time-utilities": "^7.9.1",
"@fluentui/react-focus": "^7.17.6", "@fluentui/react-focus": "^7.17.6",
@ -16947,9 +16947,9 @@
"dev": true "dev": true
}, },
"path-parse": { "path-parse": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="
}, },
"path-root": { "path-root": {
"version": "0.1.1", "version": "0.1.1",
@ -18324,9 +18324,9 @@
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
}, },
"react-papaparse": { "react-papaparse": {
"version": "3.16.1", "version": "3.17.1",
"resolved": "https://registry.npmjs.org/react-papaparse/-/react-papaparse-3.16.1.tgz", "resolved": "https://registry.npmjs.org/react-papaparse/-/react-papaparse-3.17.1.tgz",
"integrity": "sha512-xwijSNf6vXYK9RwGGYYLOe6PNop5nRfmD1oprUjO6qUPK7OHR28GTWLDX6+UKeIS8Tq4Pa8DSYfZYPtFVLMIYg==", "integrity": "sha512-Pe/eheSNMDJzJJWVtryp4A3GjzMzkop1G+a3xNPsBCkPN3YzgX3ed4Vfb4kbMjbYNqS3k2gevXjszHtyFZIWtg==",
"requires": { "requires": {
"@types/papaparse": "^5.0.4", "@types/papaparse": "^5.0.4",
"papaparse": "^5.2.0" "papaparse": "^5.2.0"
@ -21406,9 +21406,9 @@
} }
}, },
"url-parse": { "url-parse": {
"version": "1.5.1", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"querystringify": "^2.1.1", "querystringify": "^2.1.1",

View File

@ -17,10 +17,10 @@
"@microsoft/sp-webpart-base": "1.12.1", "@microsoft/sp-webpart-base": "1.12.1",
"@pnp/spfx-controls-react": "^3.2.1", "@pnp/spfx-controls-react": "^3.2.1",
"@pnp/spfx-property-controls": "^3.2.0", "@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": "16.9.0",
"react-dom": "16.9.0", "react-dom": "16.9.0",
"react-papaparse": "^3.16.1" "react-papaparse": "^3.17.1"
}, },
"resolutions": { "resolutions": {
"@types/react": "16.8.8" "@types/react": "16.8.8"
@ -31,7 +31,7 @@
"@microsoft/sp-module-interfaces": "1.12.1", "@microsoft/sp-module-interfaces": "1.12.1",
"@microsoft/sp-tslint-rules": "1.12.1", "@microsoft/sp-tslint-rules": "1.12.1",
"@microsoft/sp-webpart-workbench": "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/react-dom": "^17.0.8",
"@types/webpack-env": "^1.16.2", "@types/webpack-env": "^1.16.2",
"ajv": "~5.2.2", "ajv": "~5.2.2",

View File

@ -25,7 +25,9 @@ export enum Stage {
export interface ITeamsMembershipUpdaterState { export interface ITeamsMembershipUpdaterState {
items: IDropdownOption[]; items: IDropdownOption[];
privateChannels: IDropdownOption[];
selectionDetails: IDropdownOption; selectionDetails: IDropdownOption;
selectionChannel: IDropdownOption;
csvdata: any[]; csvdata: any[];
csvcolumns: IColumn[]; csvcolumns: IColumn[];
csvSelected: IDropdownOption; csvSelected: IDropdownOption;
@ -59,7 +61,9 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
this.state = { this.state = {
items: props.items, items: props.items,
privateChannels: [],
selectionDetails: null, selectionDetails: null,
selectionChannel: null,
csvdata: null, csvdata: null,
csvcolumns: [], csvcolumns: [],
csvSelected: null, csvSelected: null,
@ -125,7 +129,7 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
} }
public onChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { public onChange = (event: React.FormEvent<HTMLDivElement>, 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 => { this.props.context.msGraphClientFactory.getClient().then((client: MSGraphClient): void => {
client.api(`groups/${item.key}/owners`).version("v1.0").get((err, res) => { client.api(`groups/${item.key}/owners`).version("v1.0").get((err, res) => {
if (err) { if (err) {
@ -138,12 +142,25 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
_owners.push(element.userPrincipalName); _owners.push(element.userPrincipalName);
if (element.userPrincipalName == this.state.me) b = true; if (element.userPrincipalName == this.state.me) b = true;
}); });
if (b) this.setState({ ...this.state, selectionDetails: item, groupOwners: _owners, stage: Stage.Ready }); if (b) {
this.setState({ ...this.state, selectionDetails: item, groupOwners: _owners, stage: Stage.Ready });
client.api(`/teams/${item.key}/channels?$filter=membershipType eq 'private'`).get((err2, res2: { value: MicrosoftGraph.Channel[] }) => {
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 }); else this.setState({ ...this.state, stage: Stage.ErrorOwnership });
}); });
}); });
} }
public onChannelChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
this.setState({ ...this.state, stage: Stage.Ready, selectionChannel: item, logs: [], errors: [], logurl: null });
}
public onEmailChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => { public onEmailChange = (event: React.FormEvent<HTMLDivElement>, item: IDropdownOption): void => {
this.setState({ ...this.state, csvSelected: item }); this.setState({ ...this.state, csvSelected: item });
} }
@ -159,7 +176,20 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
this.addError(err.message, err); this.addError(err.message, err);
} }
let _m = res.value; let _m = res.value;
if (res['@odata.nextLink']) this.loadMembers(res['@odata.nextLink'], client).then((members) => { _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<Array<MicrosoftGraph.AadUserConversationMember>> => {
return new Promise<Array<MicrosoftGraph.AadUserConversationMember>>(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); result(_m);
}); });
}); });
@ -168,6 +198,7 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
public onRun = (e) => { public onRun = (e) => {
this.setState({ ...this.state, stage: Stage.LoadingCurrentMembers }); this.setState({ ...this.state, stage: Stage.LoadingCurrentMembers });
this.props.context.msGraphClientFactory.getClient().then((client: MSGraphClient): void => { this.props.context.msGraphClientFactory.getClient().then((client: MSGraphClient): void => {
if (this.state.selectionChannel === null) {
this.loadMembers(`groups/${this.state.selectionDetails.key}/members`, client).then((_members) => { this.loadMembers(`groups/${this.state.selectionDetails.key}/members`, client).then((_members) => {
console.debug(_members); console.debug(_members);
this.setState({ ...this.state, groupMembers: _members, stage: Stage.ComparingMembers }); this.setState({ ...this.state, groupMembers: _members, stage: Stage.ComparingMembers });
@ -222,6 +253,63 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
else if (newMembers.length == 0) this.Done(); else if (newMembers.length == 0) this.Done();
else this.addMembers(newMembers, client); 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<MicrosoftGraph.AadUserConversationMember> = new Array<MicrosoftGraph.AadUserConversationMember>();
//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<ITeamsMember
newreq.requests.push({ newreq.requests.push({
id: `${newreq.requests.length + 1}`, id: `${newreq.requests.length + 1}`,
method: "POST", method: "POST",
url: `groups/${this.state.selectionDetails.key}/members/$ref`, url: this.state.selectionChannel === null ? `groups/${this.state.selectionDetails.key}/members/$ref` : `teams/${this.state.selectionDetails.key}/channels/${this.state.selectionChannel.key}`,
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: { "@odata.id": `https://graph.microsoft.com/v1.0/directoryObjects/${e.body.id}` } body: this.state.selectionChannel === null ?
{ "@odata.id": `https://graph.microsoft.com/v1.0/directoryObjects/${e.body.id}` } :
{ "@odata.type": "#microsoft.graph.aadUserConversationMember", "roles": [], "user@odata.bind": `https://graph.microsoft.com/v1.0/users('${e.body.id}')` }
}); });
} }
}); });
@ -330,7 +420,7 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
} }
public render(): React.ReactElement<ITeamsMembershipUpdaterProps> { public render(): React.ReactElement<ITeamsMembershipUpdaterProps> {
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({ const mg = mergeStyleSets({
callout: { callout: {
width: 320, width: 320,
@ -358,6 +448,7 @@ export default class TeamsMembershipUpdater extends React.Component<ITeamsMember
<Dropdown label={strings.selectTeam} onChange={this.onChange} placeholder={selectTeamPlacehold} options={items} disabled={items.length == 0} /> <Dropdown label={strings.selectTeam} onChange={this.onChange} placeholder={selectTeamPlacehold} options={items} disabled={items.length == 0} />
{stage == Stage.CheckingOwnership && <ProgressIndicator label={strings.checkingOwner} description={strings.checkingOwnerDescription} />} {stage == Stage.CheckingOwnership && <ProgressIndicator label={strings.checkingOwner} description={strings.checkingOwnerDescription} />}
{stage == Stage.ErrorOwnership && <MessageBar messageBarType={MessageBarType.error} isMultiline={false}>You are not an owner of this group. Please select another.</MessageBar>} {stage == Stage.ErrorOwnership && <MessageBar messageBarType={MessageBarType.error} isMultiline={false}>You are not an owner of this group. Please select another.</MessageBar>}
<Dropdown label={strings.selectChannel } onChange={this.onChannelChange} placeholder={strings.selectChannelPlaceholder} options={privateChannels} disabled={privateChannels.length === 0} />
<FilePicker accepts={[".csv"]} buttonLabel={strings.selectFile} buttonIcon="ExcelDocument" label={strings.selectFileLabel} <FilePicker accepts={[".csv"]} buttonLabel={strings.selectFile} buttonIcon="ExcelDocument" label={strings.selectFileLabel}
hideStockImages hideOrganisationalAssetTab hideSiteFilesTab hideWebSearchTab hideLinkUploadTab onSave={this.fileChange} onChange={this.fileChange} context={this.props.context} /> hideStockImages hideOrganisationalAssetTab hideSiteFilesTab hideWebSearchTab hideLinkUploadTab onSave={this.fileChange} onChange={this.fileChange} context={this.props.context} />
<Dropdown label={strings.emailColumn} onChange={this.onEmailChange} placeholder={emailColumnPlaceholder} options={csvItems} disabled={!csvdata} /> <Dropdown label={strings.emailColumn} onChange={this.onEmailChange} placeholder={emailColumnPlaceholder} options={csvItems} disabled={!csvdata} />

View File

@ -31,6 +31,8 @@ define([], function() {
"orphanedMembersTitle": "Remove Orphaned Members", "orphanedMembersTitle": "Remove Orphaned Members",
"orphanedMembersContent": "Removes members who are not present in the CSV file. Owners are not effected", "orphanedMembersContent": "Removes members who are not present in the CSV file. Owners are not effected",
"on": "On", "on": "On",
"off": "Off" "off": "Off",
"selectChannel": "(Optional) Select channel",
"selectChannelPlaceholder": "Select a private channel"
} }
}); });

View File

@ -31,6 +31,8 @@ declare interface ITeamsMembershipUpdaterWebPartStrings {
orphanedMembersContent: string; orphanedMembersContent: string;
on: string; on: string;
off: string; off: string;
selectChannel: string;
selectChannelPlaceholder: string;
} }
declare module 'TeamsMembershipUpdaterWebPartStrings' { declare module 'TeamsMembershipUpdaterWebPartStrings' {