Skip to content

Commit 77afdb1

Browse files
authored
add UI for changing user role (#4554)
1 parent 7fa442f commit 77afdb1

File tree

2 files changed

+107
-0
lines changed

2 files changed

+107
-0
lines changed

apps/frontend/src/locales/en-US/index.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,9 @@
11061106
"profile.button.billing": {
11071107
"message": "Manage user billing"
11081108
},
1109+
"profile.button.edit-role": {
1110+
"message": "Edit role"
1111+
},
11091112
"profile.button.info": {
11101113
"message": "View user details"
11111114
},

apps/frontend/src/pages/user/[id].vue

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,37 @@
22
<div v-if="user" class="experimental-styles-within">
33
<ModalCreation ref="modal_creation" />
44
<CollectionCreateModal ref="modal_collection_creation" />
5+
<NewModal ref="editRoleModal" header="Edit role">
6+
<div class="flex w-80 flex-col gap-4">
7+
<div class="flex flex-col gap-2">
8+
<TeleportDropdownMenu
9+
v-model="selectedRole"
10+
:options="roleOptions"
11+
name="edit-role"
12+
placeholder="Select a role"
13+
/>
14+
</div>
15+
<div class="flex justify-end gap-2">
16+
<ButtonStyled>
17+
<button @click="cancelRoleEdit">
18+
<XIcon />
19+
Cancel
20+
</button>
21+
</ButtonStyled>
22+
<ButtonStyled color="brand">
23+
<button
24+
:disabled="!selectedRole || selectedRole === user.role || isSavingRole"
25+
@click="saveRoleEdit"
26+
>
27+
<template v-if="isSavingRole">
28+
<SpinnerIcon class="animate-spin" /> Saving...
29+
</template>
30+
<template v-else> <SaveIcon /> Save changes </template>
31+
</button>
32+
</ButtonStyled>
33+
</div>
34+
</div>
35+
</NewModal>
536
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
637
<div class="flex flex-col gap-3">
738
<div class="flex flex-col gap-1">
@@ -127,6 +158,10 @@
127158
},
128159
{ id: 'copy-id', action: () => copyId() },
129160
{ id: 'copy-permalink', action: () => copyPermalink() },
161+
{
162+
divider: true,
163+
shown: auth.user && isAdmin(auth.user),
164+
},
130165
{
131166
id: 'open-billing',
132167
action: () => navigateTo(`/admin/billing/${user.id}`),
@@ -137,6 +172,11 @@
137172
action: () => $refs.userDetailsModal.show(),
138173
shown: auth.user && isStaff(auth.user),
139174
},
175+
{
176+
id: 'edit-role',
177+
action: () => openRoleEditModal(),
178+
shown: auth.user && isAdmin(auth.user),
179+
},
140180
]"
141181
aria-label="More options"
142182
>
@@ -165,6 +205,10 @@
165205
<InfoIcon aria-hidden="true" />
166206
{{ formatMessage(messages.infoButton) }}
167207
</template>
208+
<template #edit-role>
209+
<EditIcon aria-hidden="true" />
210+
{{ formatMessage(messages.editRoleButton) }}
211+
</template>
168212
</OverflowMenu>
169213
</ButtonStyled>
170214
</template>
@@ -355,17 +399,22 @@ import {
355399
LockIcon,
356400
MoreVerticalIcon,
357401
ReportIcon,
402+
SaveIcon,
403+
SpinnerIcon,
358404
XIcon,
359405
} from '@modrinth/assets'
360406
import {
361407
Avatar,
362408
ButtonStyled,
363409
commonMessages,
364410
ContentPageHeader,
411+
injectNotificationManager,
365412
NewModal,
366413
OverflowMenu,
414+
TeleportDropdownMenu,
367415
useRelativeTime,
368416
} from '@modrinth/ui'
417+
import { isAdmin } from '@modrinth/utils'
369418
import { IntlFormatted } from '@vintl/vintl/components'
370419
371420
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
@@ -398,6 +447,8 @@ const formatCompactNumber = useCompactNumber(true)
398447
399448
const formatRelativeTime = useRelativeTime()
400449
450+
const { addNotification } = injectNotificationManager()
451+
401452
const messages = defineMessages({
402453
profileProjectsStats: {
403454
id: 'profile.stats.projects',
@@ -472,6 +523,10 @@ const messages = defineMessages({
472523
id: 'profile.button.info',
473524
defaultMessage: 'View user details',
474525
},
526+
editRoleButton: {
527+
id: 'profile.button.edit-role',
528+
defaultMessage: 'Edit role',
529+
},
475530
userNotFoundError: {
476531
id: 'profile.error.not-found',
477532
defaultMessage: 'User not found',
@@ -648,6 +703,55 @@ const navLinks = computed(() => [
648703
.slice()
649704
.sort((a, b) => a.label.localeCompare(b.label)),
650705
])
706+
707+
const selectedRole = ref(user.value.role)
708+
const isSavingRole = ref(false)
709+
710+
const roleOptions = ['developer', 'moderator', 'admin']
711+
712+
const editRoleModal = useTemplateRef('editRoleModal')
713+
714+
const openRoleEditModal = () => {
715+
selectedRole.value = user.value.role
716+
editRoleModal.value?.show()
717+
}
718+
719+
const cancelRoleEdit = () => {
720+
selectedRole.value = user.value.role
721+
editRoleModal.value?.hide()
722+
}
723+
724+
function saveRoleEdit() {
725+
if (!selectedRole.value || selectedRole.value === user.value.role) {
726+
return
727+
}
728+
729+
isSavingRole.value = true
730+
731+
useBaseFetch(`user/${user.value.id}`, {
732+
method: 'PATCH',
733+
body: {
734+
role: selectedRole.value,
735+
},
736+
})
737+
.then(() => {
738+
user.value.role = selectedRole.value
739+
740+
editRoleModal.value?.hide()
741+
})
742+
.catch(() => {
743+
console.error('Failed to update user role:', error)
744+
745+
addNotification({
746+
type: 'error',
747+
title: 'Failed to update role',
748+
message: 'An error occurred while updating the user role. Please try again.',
749+
})
750+
})
751+
.finally(() => {
752+
isSavingRole.value = false
753+
})
754+
}
651755
</script>
652756
<script>
653757
export default defineNuxtComponent({

0 commit comments

Comments
 (0)