Skip to content

Commit

Permalink
Add UI for changing a member's role within a group
Browse files Browse the repository at this point in the history
Add a "Role" column to the Members tab of the group settings UI. This displays a
dropdown with the different roles that the current user can assign to the target
user within the group. If the current user cannot change the role, the column
displays the current role as plain text.
  • Loading branch information
robertknight committed Nov 25, 2024
1 parent a1693d9 commit 2d24608
Show file tree
Hide file tree
Showing 2 changed files with 122 additions and 7 deletions.
127 changes: 121 additions & 6 deletions h/static/scripts/group-forms/components/EditGroupMembersForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@ import {
Scroll,
TrashIcon,
IconButton,
Select,
} from '@hypothesis/frontend-shared';
import { useContext, useEffect, useMemo, useState } from 'preact/hooks';

import { Config } from '../config';
import type { APIConfig, Group } from '../config';
import ErrorNotice from './ErrorNotice';
import FormContainer from './forms/FormContainer';
import type { GroupMembersResponse } from '../utils/api';
import type { GroupMembersResponse, Role } from '../utils/api';
import { callAPI } from '../utils/api';
import { pluralize } from '../utils/pluralize';
import GroupFormHeader from './GroupFormHeader';
Expand All @@ -21,6 +22,19 @@ type MemberRow = {
username: string;
userid: string;
canDelete: boolean;
role: Role;
availableRoles: Role[];
};

/**
* Mappings between roles and labels. The keys are sorted in descending order
* of permissions.
*/
const roleStrings: Record<Role, string> = {
owner: 'Owner',
admin: 'Admin',
moderator: 'Moderator',
member: 'Member',
};

async function fetchMembers(
Expand All @@ -33,11 +47,21 @@ async function fetchMembers(
headers,
signal,
});
return members.map(member => ({
userid: member.userid,
username: member.username,
canDelete: member.actions.includes('delete'),
}));

const possibleRoles: Role[] = Object.keys(roleStrings) as Role[];
return members.map(member => {
const availableRoles = possibleRoles.filter(role =>
member.actions.includes(`updates.roles.${role}`),
);

return {
userid: member.userid,
username: member.username,
canDelete: member.actions.includes('delete'),
role: member.roles[0] ?? 'member',
availableRoles,
};
});
}

async function removeMember(
Expand All @@ -52,6 +76,49 @@ async function removeMember(
});
}

async function setMemberRoles(
groupid: string,
userid: string,
roles: Role[],
headers?: Record<PropertyKey, unknown>,
) {
const url = `/api/groups/${groupid}/members/${userid}`;
await callAPI(url, {
method: 'PATCH',
headers,
json: {
roles,
},
});
}

type RoleSelectProps = {
/** The current role of the member. */
current: Role;

/** Ordered list of possible roles that the current user can assign to the member. */
available: Role[];

/** Callback for when the user requests to change the role of the member. */
onChange: (r: Role) => void;
};

function RoleSelect({ current, available, onChange }: RoleSelectProps) {
return (
<Select
value={current}
onChange={onChange}
buttonContent={roleStrings[current]}
>
{available.map(role => (
<Select.Option key={role} value={role}>
{roleStrings[role]}
</Select.Option>
))}
</Select>
);
}

export type EditGroupMembersFormProps = {
/** The saved group details. */
group: Group;
Expand Down Expand Up @@ -87,6 +154,10 @@ export default function EditGroupMembersForm({
field: 'username' as keyof MemberRow,
label: 'Username',
},
{
field: 'role' as keyof MemberRow,
label: 'Role',
},
{
field: 'canDelete' as keyof MemberRow,
label: '',
Expand Down Expand Up @@ -123,6 +194,39 @@ export default function EditGroupMembersForm({
}
};

const changeRole = async (member: MemberRow, role: Role) => {
setMembers(
members =>
members?.map(m => {
if (m === member) {
return { ...m, role };
} else {
return m;
}
}) ?? null,
);
try {
await setMemberRoles(
group.pubid,
member.userid,
[role],
config.api.readGroupMembers?.headers,
);
} catch (err) {
setMembers(
members =>
members?.map(m => {
if (m === member) {
return { ...m, role: member.role };
} else {
return m;
}
}) ?? null,
);
setErrorMessage(err.message);
}
};

const renderRow = (user: MemberRow, field: keyof MemberRow) => {
switch (field) {
case 'username':
Expand All @@ -135,6 +239,17 @@ export default function EditGroupMembersForm({
{user.username}
</div>
);
case 'role':
if (user.availableRoles.length <= 1) {
return roleStrings[user.role];
}
return (
<RoleSelect
current={user.role}
available={user.availableRoles}
onChange={role => changeRole(user, role)}
/>
);
case 'canDelete':
return user.canDelete ? (
<IconButton
Expand Down
2 changes: 1 addition & 1 deletion h/static/scripts/group-forms/utils/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
export type GroupType = 'private' | 'restricted' | 'open';

/** Member role within a group. */
export type Role = 'owner' | 'admin' | 'member';
export type Role = 'owner' | 'admin' | 'moderator' | 'member';

/**
* Request to create or update a group.
Expand Down

0 comments on commit 2d24608

Please sign in to comment.