|
2 | 2 | <div v-if="user" class="experimental-styles-within"> |
3 | 3 | <ModalCreation ref="modal_creation" /> |
4 | 4 | <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> |
5 | 36 | <NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details"> |
6 | 37 | <div class="flex flex-col gap-3"> |
7 | 38 | <div class="flex flex-col gap-1"> |
|
127 | 158 | }, |
128 | 159 | { id: 'copy-id', action: () => copyId() }, |
129 | 160 | { id: 'copy-permalink', action: () => copyPermalink() }, |
| 161 | + { |
| 162 | + divider: true, |
| 163 | + shown: auth.user && isAdmin(auth.user), |
| 164 | + }, |
130 | 165 | { |
131 | 166 | id: 'open-billing', |
132 | 167 | action: () => navigateTo(`/admin/billing/${user.id}`), |
|
137 | 172 | action: () => $refs.userDetailsModal.show(), |
138 | 173 | shown: auth.user && isStaff(auth.user), |
139 | 174 | }, |
| 175 | + { |
| 176 | + id: 'edit-role', |
| 177 | + action: () => openRoleEditModal(), |
| 178 | + shown: auth.user && isAdmin(auth.user), |
| 179 | + }, |
140 | 180 | ]" |
141 | 181 | aria-label="More options" |
142 | 182 | > |
|
165 | 205 | <InfoIcon aria-hidden="true" /> |
166 | 206 | {{ formatMessage(messages.infoButton) }} |
167 | 207 | </template> |
| 208 | + <template #edit-role> |
| 209 | + <EditIcon aria-hidden="true" /> |
| 210 | + {{ formatMessage(messages.editRoleButton) }} |
| 211 | + </template> |
168 | 212 | </OverflowMenu> |
169 | 213 | </ButtonStyled> |
170 | 214 | </template> |
@@ -355,17 +399,22 @@ import { |
355 | 399 | LockIcon, |
356 | 400 | MoreVerticalIcon, |
357 | 401 | ReportIcon, |
| 402 | + SaveIcon, |
| 403 | + SpinnerIcon, |
358 | 404 | XIcon, |
359 | 405 | } from '@modrinth/assets' |
360 | 406 | import { |
361 | 407 | Avatar, |
362 | 408 | ButtonStyled, |
363 | 409 | commonMessages, |
364 | 410 | ContentPageHeader, |
| 411 | + injectNotificationManager, |
365 | 412 | NewModal, |
366 | 413 | OverflowMenu, |
| 414 | + TeleportDropdownMenu, |
367 | 415 | useRelativeTime, |
368 | 416 | } from '@modrinth/ui' |
| 417 | +import { isAdmin } from '@modrinth/utils' |
369 | 418 | import { IntlFormatted } from '@vintl/vintl/components' |
370 | 419 |
|
371 | 420 | import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component' |
@@ -398,6 +447,8 @@ const formatCompactNumber = useCompactNumber(true) |
398 | 447 |
|
399 | 448 | const formatRelativeTime = useRelativeTime() |
400 | 449 |
|
| 450 | +const { addNotification } = injectNotificationManager() |
| 451 | +
|
401 | 452 | const messages = defineMessages({ |
402 | 453 | profileProjectsStats: { |
403 | 454 | id: 'profile.stats.projects', |
@@ -472,6 +523,10 @@ const messages = defineMessages({ |
472 | 523 | id: 'profile.button.info', |
473 | 524 | defaultMessage: 'View user details', |
474 | 525 | }, |
| 526 | + editRoleButton: { |
| 527 | + id: 'profile.button.edit-role', |
| 528 | + defaultMessage: 'Edit role', |
| 529 | + }, |
475 | 530 | userNotFoundError: { |
476 | 531 | id: 'profile.error.not-found', |
477 | 532 | defaultMessage: 'User not found', |
@@ -648,6 +703,55 @@ const navLinks = computed(() => [ |
648 | 703 | .slice() |
649 | 704 | .sort((a, b) => a.label.localeCompare(b.label)), |
650 | 705 | ]) |
| 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 | +} |
651 | 755 | </script> |
652 | 756 | <script> |
653 | 757 | export default defineNuxtComponent({ |
|
0 commit comments