Skip to content

Commit

Permalink
Enable owners to change user roles (#648)
Browse files Browse the repository at this point in the history
### Summary & Motivation

Enable owners to change the roles of other users, such as switching from
Member to Admin or Owner. This functionality is implemented in a new
modal dialog with a dropdown for selecting roles. A new `UserRole.ts`
file has been introduced to support translations of all options in the
`UserRole` enum.

The `AlertDialog` has been updated to support configurations without
action buttons (e.g., OK and Cancel), as selecting a role in the
dropdown automatically submits the change. Additionally, a new size
option for the header has been added to allow for smaller headings.

To ensure tenant integrity, it is not possible for users to change their
own role, preventing scenarios where a tenant could be left without an
owner. This is a UI change only, as there are other ways to lose access
to the sole owner.

### Checklist

- [x] I have added a Label to the pull-request
- [x] I have added tests, and done manual regression tests
- [x] I have updated the documentation, if necessary
  • Loading branch information
tjementum authored Dec 27, 2024
2 parents f87c29a + a7b60f5 commit 2e13892
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 38 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import { EllipsisVerticalIcon, Trash2Icon, UserIcon } from "lucide-react";
import { EllipsisVerticalIcon, PencilIcon, Trash2Icon, UserIcon } from "lucide-react";
import type { SortDescriptor } from "react-aria-components";
import { MenuTrigger, TableBody } from "react-aria-components";
import { useCallback, useState } from "react";
import { Cell, Column, Row, Table, TableHeader } from "@repo/ui/components/Table";
import { Badge } from "@repo/ui/components/Badge";
import { Pagination } from "@repo/ui/components/Pagination";
import { Select, SelectItem } from "@repo/ui/components/Select";
import { Menu, MenuItem, MenuSeparator } from "@repo/ui/components/Menu";
import { Button } from "@repo/ui/components/Button";
import { Avatar } from "@repo/ui/components/Avatar";
import { api, type components, SortableUserProperties, SortOrder, useApi } from "@/shared/lib/api/client";
import { api, type components, SortableUserProperties, SortOrder, UserRole, useApi } from "@/shared/lib/api/client";
import { useNavigate, useSearch } from "@tanstack/react-router";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { AlertDialog } from "@repo/ui/components/AlertDialog";
import { Modal } from "@repo/ui/components/Modal";
import { useUserInfo } from "@repo/infrastructure/auth/hooks";
import { getUserRoleLabel } from "@/shared/lib/api/userRole";

type UserDetails = components["schemas"]["UserDetails"];

Expand Down Expand Up @@ -42,6 +44,7 @@ export function UserTable() {
});

const [userToDelete, setUserToDelete] = useState<UserDetails | null>(null);
const [userToChangeRole, setUserToChangeRole] = useState<UserDetails | null>(null);

const handlePageChange = useCallback(
(page: number) => {
Expand Down Expand Up @@ -81,11 +84,66 @@ export function UserTable() {
setUserToDelete(null);
}, [userToDelete]);

const handleUserRoleChange = useCallback(
async (newUserRole: UserRole) => {
if (!userToChangeRole) return;

await api.put("/api/account-management/users/{id}/change-user-role", {
params: { path: { id: userToChangeRole.id } },
body: { userRole: newUserRole }
});

setRefreshKey((prev) => prev + 1);
setUserToChangeRole(null);
},
[userToChangeRole]
);

const currentPage = (data?.currentPageOffset ?? 0) + 1;

return (
<>
<Modal isOpen={userToDelete !== null} onOpenChange={() => setUserToDelete(null)} blur={false}>
<Modal
isOpen={userToChangeRole !== null}
onOpenChange={() => setUserToChangeRole(null)}
blur={false}
isDismissable={true}
>
<AlertDialog title={t`Change User Role`}>
<p className="text-muted-foreground text-sm">
<Trans>
Select a new role for{" "}
<b>
{`${userToChangeRole?.firstName ?? ""} ${userToChangeRole?.lastName ?? ""}`.trim() ||
userToChangeRole?.email}
</b>
</Trans>
</p>

<div className="flex flex-col gap-4 mt-4">
<Select
autoFocus
aria-label={t`User Role`}
selectedKey={userToChangeRole?.role}
onSelectionChange={(key) => handleUserRoleChange(key as UserRole)}
className="flex w-full flex-col"
>
{Object.values(UserRole).map((userRole) => (
<SelectItem id={userRole} key={userRole}>
{getUserRoleLabel(userRole)}
</SelectItem>
))}
</Select>
</div>
</AlertDialog>
</Modal>

<Modal
isOpen={userToDelete !== null}
onOpenChange={() => setUserToDelete(null)}
blur={false}
isDismissable={true}
>
<AlertDialog
title={t`Delete User`}
variant="destructive"
Expand All @@ -95,7 +153,7 @@ export function UserTable() {
>
<Trans>
Are you sure you want to delete{" "}
{`${userToDelete?.firstName ?? ""} ${userToDelete?.lastName ?? ""}`.trim() || userToDelete?.email}?
<b>{`${userToDelete?.firstName ?? ""} ${userToDelete?.lastName ?? ""}`.trim() || userToDelete?.email}?</b>
</Trans>
</AlertDialog>
</Modal>
Expand All @@ -122,7 +180,7 @@ export function UserTable() {
<Column minWidth={65} defaultWidth={120} allowsSorting id={SortableUserProperties.ModifiedAt}>
<Trans>Last Seen</Trans>
</Column>
<Column minWidth={65} defaultWidth={75} allowsSorting id={SortableUserProperties.Role}>
<Column minWidth={100} defaultWidth={75} allowsSorting id={SortableUserProperties.Role}>
<Trans>Role</Trans>
</Column>
<Column width={114}>
Expand Down Expand Up @@ -159,7 +217,7 @@ export function UserTable() {
<Cell>{toFormattedDate(user.createdAt)}</Cell>
<Cell>{toFormattedDate(user.modifiedAt)}</Cell>
<Cell>
<Badge variant="outline">{user.role}</Badge>
<Badge variant="outline">{getUserRoleLabel(user.role)}</Badge>
</Cell>
<Cell>
<div className="group flex gap-2 w-full">
Expand All @@ -175,23 +233,25 @@ export function UserTable() {
<Button variant="icon" aria-label={t`Menu`}>
<EllipsisVerticalIcon className="w-5 h-5 text-muted-foreground" />
</Button>
<Menu
onAction={(key) => {
if (key === "viewProfile") {
alert("open");
} else if (key === "deleteUser") {
setUserToDelete(user);
}
}}
>
<Menu>
<MenuItem id="viewProfile">
<UserIcon className="w-4 h-4" />
<Trans>View Profile</Trans>
</MenuItem>
<MenuItem
id="changeRole"
isDisabled={userInfo?.role !== "Owner" || userInfo?.id === user.id}
onAction={() => setUserToChangeRole(user)}
>
<PencilIcon className="w-4 h-4 group-disabled:text-muted-foreground" />
<span className="group-disabled:text-muted-foreground">
<Trans>Change Role</Trans>
</span>
</MenuItem>
<MenuSeparator />
<MenuItem
id="deleteUser"
isDisabled={user.id === userInfo?.id}
isDisabled={userInfo?.role !== "Owner" || user.id === userInfo?.id}
onAction={() => setUserToDelete(user)}
>
<Trash2Icon className="w-4 h-4 text-destructive" />
Expand Down
16 changes: 16 additions & 0 deletions application/account-management/WebApp/shared/lib/api/userRole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { t } from "@lingui/core/macro";
import { UserRole } from "@/shared/lib/api/client";

export function getUserRoleLabel(role: UserRole): string {
switch (role) {
case UserRole.Member:
return t`Member`;
case UserRole.Admin:
return t`Admin`;
case UserRole.Owner:
return t`Owner`;
default: {
return String(role);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ msgstr "Tilføj flere i brugermenuen"
msgid "Added"
msgstr "Tilføjet"

msgid "Admin"
msgstr "Admin"

msgid "All Users"
msgstr "Alle brugere"

Expand All @@ -48,8 +51,8 @@ msgid "An error occurred while processing your request. {0}"
msgstr "Der opstod en fejl under behandlingen af din anmodning. {0}"

#. placeholder {0}: `${userToDelete?.firstName ?? ""} ${userToDelete?.lastName ?? ""}`.trim() || userToDelete?.email
msgid "Are you sure you want to delete {0}?"
msgstr "Er du sikker på, at du vil slette {0}?"
msgid "Are you sure you want to delete <0>{0}?</0>"
msgstr "Er du sikker på, at du vil slette <0>{0}?</0>"

msgid "By continuing, you accept our policies"
msgstr "Ved at fortsætte accepterer du vores vilkår"
Expand All @@ -63,6 +66,12 @@ msgstr "Annuller"
msgid "Change avatar options"
msgstr "Skift avatarindstillinger"

msgid "Change Role"
msgstr "Skift rolle"

msgid "Change User Role"
msgstr "Skift brugerrolle"

msgid "Continue"
msgstr "Fortsæt"

Expand Down Expand Up @@ -183,6 +192,9 @@ msgstr "Administrer din konto her."
msgid "Manage your users and permissions here."
msgstr "Administrer dine brugere og tilladelser her."

msgid "Member"
msgstr "Medlem"

msgid "Menu"
msgstr "Menu"

Expand All @@ -201,6 +213,9 @@ msgstr "Ingen aktiv tilmeldingssession"
msgid "Organization"
msgstr "Organisation"

msgid "Owner"
msgstr "Ejer"

msgid "Pending"
msgstr "Afventer"

Expand Down Expand Up @@ -246,6 +261,10 @@ msgstr "Skærmbilleder af dashboard-projektet med mobilversioner"
msgid "Search"
msgstr "Søg"

#. placeholder {0}: `${userToChangeRole?.firstName ?? ""} ${userToChangeRole?.lastName ?? ""}`.trim() || userToChangeRole?.email
msgid "Select a new role for <0>{0}</0>"
msgstr "Vælg en ny rolle for <0>{0}</0>"

msgid "Send invite"
msgstr "Send invitation"

Expand Down Expand Up @@ -288,6 +307,9 @@ msgstr "Upload foto"
msgid "User profile"
msgstr "Brugerprofil"

msgid "User Role"
msgstr "Brugerrolle"

msgid "[email protected]"
msgstr "[email protected]"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ msgstr "Add more in the Users menu"
msgid "Added"
msgstr "Added"

msgid "Admin"
msgstr "Admin"

msgid "All Users"
msgstr "All Users"

Expand All @@ -48,8 +51,8 @@ msgid "An error occurred while processing your request. {0}"
msgstr "An error occurred while processing your request. {0}"

#. placeholder {0}: `${userToDelete?.firstName ?? ""} ${userToDelete?.lastName ?? ""}`.trim() || userToDelete?.email
msgid "Are you sure you want to delete {0}?"
msgstr "Are you sure you want to delete {0}?"
msgid "Are you sure you want to delete <0>{0}?</0>"
msgstr "Are you sure you want to delete <0>{0}?</0>"

msgid "By continuing, you accept our policies"
msgstr "By continuing, you accept our policies"
Expand All @@ -63,6 +66,12 @@ msgstr "Cancel"
msgid "Change avatar options"
msgstr "Change avatar options"

msgid "Change Role"
msgstr "Change Role"

msgid "Change User Role"
msgstr "Change User Role"

msgid "Continue"
msgstr "Continue"

Expand Down Expand Up @@ -183,6 +192,9 @@ msgstr "Manage your account here."
msgid "Manage your users and permissions here."
msgstr "Manage your users and permissions here."

msgid "Member"
msgstr "Member"

msgid "Menu"
msgstr "Menu"

Expand All @@ -201,6 +213,9 @@ msgstr "No active signup session"
msgid "Organization"
msgstr "Organization"

msgid "Owner"
msgstr "Owner"

msgid "Pending"
msgstr "Pending"

Expand Down Expand Up @@ -246,6 +261,10 @@ msgstr "Screenshots of the dashboard project with mobile versions"
msgid "Search"
msgstr "Search"

#. placeholder {0}: `${userToChangeRole?.firstName ?? ""} ${userToChangeRole?.lastName ?? ""}`.trim() || userToChangeRole?.email
msgid "Select a new role for <0>{0}</0>"
msgstr "Select a new role for <0>{0}</0>"

msgid "Send invite"
msgstr "Send invite"

Expand Down Expand Up @@ -288,6 +307,9 @@ msgstr "Upload photo"
msgid "User profile"
msgstr "User profile"

msgid "User Role"
msgstr "User Role"

msgid "[email protected]"
msgstr "[email protected]"

Expand Down
Loading

0 comments on commit 2e13892

Please sign in to comment.