Added Delete User Dialog and graph calls
This commit is contained in:
parent
15f04cf57e
commit
4d06a84754
|
@ -13,7 +13,6 @@
|
|||
},
|
||||
"externals": {},
|
||||
"localizedResources": {
|
||||
"GroupMembershipManagerWebPartStrings": "lib/webparts/groupMembershipManager/loc/{locale}.js",
|
||||
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
|
||||
"GroupMembershipManagerWebPartStrings": "lib/webparts/groupMembershipManager/loc/{locale}.js"
|
||||
}
|
||||
}
|
|
@ -42,6 +42,10 @@
|
|||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "GroupMember.ReadWrite.All"
|
||||
},
|
||||
{
|
||||
"resource": "Microsoft Graph",
|
||||
"scope": "Group.ReadWrite.All"
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,7 +15,6 @@
|
|||
"@microsoft/sp-office-ui-fabric-core": "1.15.2",
|
||||
"@microsoft/sp-property-pane": "1.15.2",
|
||||
"@microsoft/sp-webpart-base": "1.15.2",
|
||||
"@pnp/spfx-controls-react": "^3.10.0",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"tslib": "2.3.1"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import * as React from 'react';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import styles from './GroupMembershipManager.module.scss';
|
||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
||||
import { Button, Checkbox, Divider, Input, Label, Spinner, useId } from "@fluentui/react-components";
|
||||
|
@ -8,6 +9,8 @@ import { WebPartContext } from '@microsoft/sp-webpart-base';
|
|||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
||||
|
||||
|
||||
export enum AddUserMode { Member, Owner }
|
||||
enum rState { Idle, Running, Error, Completed }
|
||||
|
@ -26,14 +29,29 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
const [users, setUsers] = React.useState<MicrosoftGraph.Person[]>([]);
|
||||
const [_error, setError] = React.useState<string>(null);
|
||||
const [running, setRunning] = React.useState<rState>(rState.Idle);
|
||||
const add = (): void => {
|
||||
setRunning(rState.Running);
|
||||
setTimeout(() => setRunning(rState.Completed), 10000);
|
||||
};
|
||||
|
||||
const handleError = (error: unknown): void => {
|
||||
console.error(error);
|
||||
setError(error.toString());
|
||||
setRunning(rState.Error);
|
||||
};
|
||||
|
||||
const add = async (): Promise<void> => {
|
||||
setRunning(rState.Running);
|
||||
try {
|
||||
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient("3");
|
||||
client.api(`/groups/${Group.id}`).update(Mode === AddUserMode.Member ? {
|
||||
"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}`)
|
||||
}, (e: GraphError, response ) => {
|
||||
if (e) throw e.message;
|
||||
else setRunning(rState.Completed)
|
||||
}).catch(handleError);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
|
@ -47,9 +65,9 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
React.useEffect(() => {
|
||||
if (searchTerm && searchTerm !== "") {
|
||||
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
||||
client.api('/me/people').search(searchTerm).filter("personType/subclass eq 'OrganizationUser'").get((error, response: { value: MicrosoftGraph.Person[] }) => {
|
||||
if (error) throw error;
|
||||
setSearchResults(response.value);
|
||||
client.api('/me/people').search(searchTerm).filter("personType/subclass eq 'OrganizationUser'").get((error: GraphError, response: { value: MicrosoftGraph.Person[] }) => {
|
||||
if (error) throw error.message;
|
||||
else setSearchResults(response.value);
|
||||
}).catch(handleError);
|
||||
}).catch(handleError);
|
||||
} else setSearchResults(null)
|
||||
|
@ -66,12 +84,12 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
<DialogSurface aria-label="label">
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.AddDialogTitleOwner : strings.AddDialogTitle}{Group.displayName}</DialogTitle>
|
||||
<DialogBody>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||
<div className={styles.stack}>
|
||||
{running !== rState.Error && _error && <Alert intent="error">{_error}</Alert>}
|
||||
{running === rState.Error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error}</Alert>}
|
||||
{running === rState.Completed && <Alert intent="success">{strings.Owners} {strings.Added} {Group.displayName}</Alert>}
|
||||
{users.length > 0 && <div style={{ display: 'flex', flexDirection: 'row', flexWrap: 'wrap', gap: '10px' }}>
|
||||
{users.map(u => <div key={u.id} style={{ display: 'flex', flexDirection: 'row', gap: '10px', maxWidth: 200, whiteSpace: 'nowrap' }}>
|
||||
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Added} {Group.displayName}</Alert>}
|
||||
{users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
||||
{users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
||||
{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)) } />}
|
||||
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.scoredEmailAddresses[0].address} />
|
||||
</div>)}
|
||||
|
@ -80,7 +98,7 @@ export default function AddUser({ Group, Mode, context, onCompleted }: Props): R
|
|||
<Divider />
|
||||
<Label htmlFor={inputId}>{strings.Search}</Label>
|
||||
<Input placeholder={strings.SearchPlaceholder} onChange={(e, d) => setSearchTerm(d.value)} id={inputId} />
|
||||
{searchResults && searchResults.map(u => <div key={u.id} style={{ display: 'flex', flexDirection: 'row', gap: '10px' }}>
|
||||
{searchResults && searchResults.map(u => <div key={u.id} className={styles.stackHoz}>
|
||||
<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} />
|
||||
</div>)}
|
||||
|
|
|
@ -0,0 +1,106 @@
|
|||
import * as React from 'react';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import styles from './GroupMembershipManager.module.scss';
|
||||
import * as strings from 'GroupMembershipManagerWebPartStrings'
|
||||
import { Dialog, DialogTrigger, DialogSurface, DialogTitle, DialogBody, DialogActions, Alert } from "@fluentui/react-components/unstable";
|
||||
import { Button, Spinner } from "@fluentui/react-components";
|
||||
import { AddUserMode } from './AddUser';
|
||||
import { WebPartContext } from '@microsoft/sp-webpart-base';
|
||||
import { PersonDeleteRegular } from '@fluentui/react-icons';
|
||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import { PersonaSize } from 'office-ui-fabric-react/lib/Persona';
|
||||
import SPFxPeopleCard from './SPFxPeopleCard';
|
||||
import { BatchRequestStep, BatchRequestContent, BatchRequestBody, BatchResponseContent } from '@microsoft/microsoft-graph-client';
|
||||
|
||||
enum rState { Idle, Running, Error, Completed }
|
||||
|
||||
type Props = {
|
||||
Group: MicrosoftGraph.Group,
|
||||
Mode: AddUserMode,
|
||||
Users: MicrosoftGraph.User[]
|
||||
context: WebPartContext,
|
||||
onCompleted?: () => void
|
||||
}
|
||||
|
||||
export default function RemoveUser({ Group, Users, Mode, context, onCompleted }: Props): React.ReactElement<Props> {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [_error, setError] = React.useState<string>(null);
|
||||
const [running, setRunning] = React.useState<rState>(rState.Idle);
|
||||
|
||||
const handleError = (error: unknown): void => {
|
||||
console.error(error);
|
||||
setError(error.toString());
|
||||
setRunning(rState.Error);
|
||||
};
|
||||
|
||||
const remove = async (): Promise<void> => {
|
||||
setRunning(rState.Running);
|
||||
try {
|
||||
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient("3");
|
||||
|
||||
const userRequestSteps: BatchRequestStep[] = Users.map((v, i) => ({
|
||||
id: i.toString(),
|
||||
request: new Request(`/groups/${Group.id}/${Mode === AddUserMode.Member ? 'members' : 'owners'}/${v.id}/$ref`, {
|
||||
method: "DELETE"
|
||||
})
|
||||
}));
|
||||
|
||||
const _content: BatchRequestBody = await (new BatchRequestContent(userRequestSteps).getContent());
|
||||
|
||||
// POST the batch request content to the /$batch endpoint
|
||||
const batchResponse = await client.api('/$batch').post(_content);
|
||||
|
||||
const errors: string[] = [];
|
||||
|
||||
new BatchResponseContent(batchResponse).getResponses().forEach((v, k) => {
|
||||
if (!v.ok) {
|
||||
errors.push(v.statusText);
|
||||
}
|
||||
});
|
||||
if (errors.length > 0) throw errors.join(', ');
|
||||
else setRunning(rState.Completed);
|
||||
} catch (error) {
|
||||
handleError(error);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
if (running === rState.Completed) setTimeout(() => {
|
||||
setOpen(false);
|
||||
setRunning(rState.Idle);
|
||||
if (onCompleted) onCompleted();
|
||||
}, 5000);
|
||||
}, [running]);
|
||||
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={(event, data) => setOpen(data.open)}>
|
||||
<DialogTrigger>
|
||||
<Button appearance='primary' icon={<PersonDeleteRegular />}>{strings.Remove}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogSurface aria-label="label">
|
||||
<DialogTitle>{Mode === AddUserMode.Owner ? strings.RemoveDialogTitleOwner : strings.RemoveDialogTitle}{Group.displayName}</DialogTitle>
|
||||
<DialogBody>
|
||||
<div className={styles.stack}>
|
||||
{running !== rState.Error && _error && <Alert intent="error">{_error}</Alert>}
|
||||
{running === rState.Error && <Alert intent="error" action={{ children: 'Retry', onClick: () => setRunning(rState.Running) }}>{_error}</Alert>}
|
||||
{running === rState.Completed && <Alert intent="success">{Mode === AddUserMode.Owner ? strings.Owners : strings.Members} {strings.Removed} {Group.displayName}</Alert>}
|
||||
{Users.length > 0 && <div className={styles.stackHoz} style={{ flexWrap: 'wrap' }}>
|
||||
{Users.map(u => <div key={u.id} className={styles.stackHoz} style={{ maxWidth: 200, whiteSpace: 'nowrap' }}>
|
||||
<SPFxPeopleCard primaryText={u.displayName} serviceScope={context.serviceScope} email={u.userPrincipalName} size={PersonaSize.size24} secondaryText={u.mail} />
|
||||
</div>)}
|
||||
</div>}
|
||||
{running === rState.Running && <Spinner labelPosition='below' label={strings.Removing} />}
|
||||
</div>
|
||||
</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>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
|
@ -4,11 +4,12 @@ import { IGroupMembershipManagerProps } from './IGroupMembershipManagerProps';
|
|||
import * as strings from 'GroupMembershipManagerWebPartStrings';
|
||||
import { MSGraphClientV3 } from '@microsoft/sp-http';
|
||||
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
|
||||
import { Button, Checkbox, CheckboxOnChangeData, Spinner, Subtitle1 } from '@fluentui/react-components';
|
||||
import { Checkbox, CheckboxOnChangeData, Spinner, Subtitle1, Textarea } from '@fluentui/react-components';
|
||||
import { Dropdown, Option, Toolbar } from "@fluentui/react-components/unstable";
|
||||
import { TableBody, TableCell, TableRow, Table } from '@fluentui/react-table';
|
||||
import { PersonDeleteRegular } from '@fluentui/react-icons';
|
||||
import AddUser, { AddUserMode } from './AddUser';
|
||||
import RemoveUser from './DeleteUser';
|
||||
import { GraphError } from '@microsoft/microsoft-graph-client';
|
||||
|
||||
type OnSelectData = {
|
||||
optionValue: string;
|
||||
|
@ -26,7 +27,7 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
|
||||
React.useEffect(() => {
|
||||
context.msGraphClientFactory.getClient("3").then((client: MSGraphClientV3) => {
|
||||
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,securityEnabled").get((error, response: { value: MicrosoftGraph.Group[] }) => {
|
||||
client.api('/me/ownedObjects').select("id,displayName,groupTypes,visibility,securityEnabled,description").get((error, response: { value: MicrosoftGraph.Group[] }) => {
|
||||
if (error) throw error;
|
||||
setGroups(response.value.sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
||||
}).catch(console.error);
|
||||
|
@ -39,11 +40,11 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
setToRemove([]);
|
||||
setRemoveOwner([]);
|
||||
const client: MSGraphClientV3 = await context.msGraphClientFactory.getClient("3");
|
||||
client.api(`/groups/${groups.filter(g => g.displayName === group)[0].id}/members`).select("id,displayName").get((error, response: { value: MicrosoftGraph.User[] }) => {
|
||||
client.api(`/groups/${groups.filter(g => g.displayName === group)[0].id}/members`).select("id,displayName").get((error: GraphError, response: { value: MicrosoftGraph.User[] }) => {
|
||||
if (error) throw error;
|
||||
setMembers(response.value.sort((a, b) => a.displayName.localeCompare(b.displayName)));
|
||||
}).catch(console.error);
|
||||
client.api(`/groups/${groups.filter(g => g.displayName === group)[0].id}/owners`).select("id,displayName").get((error, response: { value: MicrosoftGraph.User[] }) => {
|
||||
client.api(`/groups/${groups.filter(g => g.displayName === group)[0].id}/owners`).select("id,displayName").get((error: GraphError, response: { value: MicrosoftGraph.User[] }) => {
|
||||
if (error) throw error;
|
||||
setOwners(response.value);
|
||||
}).catch(console.error);
|
||||
|
@ -71,49 +72,52 @@ export default function GroupMembershipManager(props: IGroupMembershipManagerPro
|
|||
</Dropdown>
|
||||
</section>}
|
||||
{(!members || !owners) && groups && group && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
||||
{groups && group && <div className={`${styles.stackHoz} ${styles.spaceBetween}`} style={{marginTop: 10 }}>
|
||||
<div style={{width: '49%'}}>
|
||||
<Subtitle1>{strings.Members}</Subtitle1>
|
||||
{!members && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
||||
{members && <>
|
||||
{//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>
|
||||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Member} onCompleted={loadGroup} />
|
||||
<Button icon={<PersonDeleteRegular />} disabled={toRemove.length === 0}>{strings.Remove} {toRemove.length === 0 ? null : toRemove.length}</Button>
|
||||
</Toolbar>}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{members.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Checkbox label={user.displayName} onChange={(ev, data) => checkUser(data, user)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>}
|
||||
</div>
|
||||
<div style={{width: '49%'}}>
|
||||
<Subtitle1>{strings.Owners}</Subtitle1>
|
||||
{!owners && <Spinner labelPosition='below' label={strings.LoadingOwners} />}
|
||||
{owners && <>
|
||||
<Toolbar>
|
||||
<AddUser context={context} Group={groups.filter(g => g.displayName === group)[0]} Mode={AddUserMode.Owner} onCompleted={loadGroup} />
|
||||
<Button icon={<PersonDeleteRegular />} disabled={removeOwner.length === 0}>{strings.Remove} {removeOwner.length === 0 ? null : removeOwner.length }</Button>
|
||||
</Toolbar>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{owners.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Checkbox label={user.displayName} onChange={(ev, data) => checkOwner(data, user)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>}
|
||||
{groups && group && <div className={styles.stack} style={{marginTop: 10 }}>
|
||||
<Textarea defaultValue={groups.filter(g => g.displayName === group)[0].description} resize='vertical' readOnly />
|
||||
<div className={`${styles.stackHoz} ${styles.spaceBetween}`} style={{marginTop: 10 }}>
|
||||
<div style={{width: '49%'}}>
|
||||
<Subtitle1>{strings.Members}</Subtitle1>
|
||||
{!members && <Spinner labelPosition='below' label={strings.LoadingMembers} />}
|
||||
{members && <>
|
||||
{//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>
|
||||
<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} />
|
||||
</Toolbar>}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{members.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Checkbox label={user.displayName} onChange={(ev, data) => checkUser(data, user)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>}
|
||||
</div>
|
||||
<div style={{width: '49%'}}>
|
||||
<Subtitle1>{strings.Owners}</Subtitle1>
|
||||
{!owners && <Spinner labelPosition='below' label={strings.LoadingOwners} />}
|
||||
{owners && <>
|
||||
<Toolbar>
|
||||
<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} />
|
||||
</Toolbar>
|
||||
<Table>
|
||||
<TableBody>
|
||||
{owners.map(user => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>
|
||||
<Checkbox label={user.displayName} onChange={(ev, data) => checkOwner(data, user)} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</>}
|
||||
</div>
|
||||
</div>
|
||||
</div>}
|
||||
</section>
|
||||
|
|
|
@ -23,6 +23,10 @@ define([], function() {
|
|||
"SearchPlaceholder": "Start typing to search",
|
||||
"Retry": "Retry",
|
||||
"Adding": "Adding Users",
|
||||
"Added": "Added to"
|
||||
"Added": "Added to",
|
||||
"Removing": "Removing Users",
|
||||
"Removed": "Removed from",
|
||||
"RemoveDialogTitle": "Remove users from ",
|
||||
"RemoveDialogTitleOwner": "Remove owners from "
|
||||
}
|
||||
});
|
|
@ -23,6 +23,10 @@ declare interface IGroupMembershipManagerWebPartStrings {
|
|||
Retry: string;
|
||||
Adding: string;
|
||||
Added: string;
|
||||
Removing: string;
|
||||
Removed: string;
|
||||
RemoveDialogTitle: string;
|
||||
RemoveDialogTitleOwner: string;
|
||||
}
|
||||
|
||||
declare module 'GroupMembershipManagerWebPartStrings' {
|
||||
|
|
Loading…
Reference in New Issue