You've already forked AstralRinth
forked from didirus/AstralRinth
add UI for changing user role (#4554)
This commit is contained in:
@@ -1106,6 +1106,9 @@
|
|||||||
"profile.button.billing": {
|
"profile.button.billing": {
|
||||||
"message": "Manage user billing"
|
"message": "Manage user billing"
|
||||||
},
|
},
|
||||||
|
"profile.button.edit-role": {
|
||||||
|
"message": "Edit role"
|
||||||
|
},
|
||||||
"profile.button.info": {
|
"profile.button.info": {
|
||||||
"message": "View user details"
|
"message": "View user details"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,6 +2,37 @@
|
|||||||
<div v-if="user" class="experimental-styles-within">
|
<div v-if="user" class="experimental-styles-within">
|
||||||
<ModalCreation ref="modal_creation" />
|
<ModalCreation ref="modal_creation" />
|
||||||
<CollectionCreateModal ref="modal_collection_creation" />
|
<CollectionCreateModal ref="modal_collection_creation" />
|
||||||
|
<NewModal ref="editRoleModal" header="Edit role">
|
||||||
|
<div class="flex w-80 flex-col gap-4">
|
||||||
|
<div class="flex flex-col gap-2">
|
||||||
|
<TeleportDropdownMenu
|
||||||
|
v-model="selectedRole"
|
||||||
|
:options="roleOptions"
|
||||||
|
name="edit-role"
|
||||||
|
placeholder="Select a role"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<ButtonStyled>
|
||||||
|
<button @click="cancelRoleEdit">
|
||||||
|
<XIcon />
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
<ButtonStyled color="brand">
|
||||||
|
<button
|
||||||
|
:disabled="!selectedRole || selectedRole === user.role || isSavingRole"
|
||||||
|
@click="saveRoleEdit"
|
||||||
|
>
|
||||||
|
<template v-if="isSavingRole">
|
||||||
|
<SpinnerIcon class="animate-spin" /> Saving...
|
||||||
|
</template>
|
||||||
|
<template v-else> <SaveIcon /> Save changes </template>
|
||||||
|
</button>
|
||||||
|
</ButtonStyled>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</NewModal>
|
||||||
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
|
<NewModal v-if="auth.user && isStaff(auth.user)" ref="userDetailsModal" header="User details">
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
@@ -127,6 +158,10 @@
|
|||||||
},
|
},
|
||||||
{ id: 'copy-id', action: () => copyId() },
|
{ id: 'copy-id', action: () => copyId() },
|
||||||
{ id: 'copy-permalink', action: () => copyPermalink() },
|
{ id: 'copy-permalink', action: () => copyPermalink() },
|
||||||
|
{
|
||||||
|
divider: true,
|
||||||
|
shown: auth.user && isAdmin(auth.user),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'open-billing',
|
id: 'open-billing',
|
||||||
action: () => navigateTo(`/admin/billing/${user.id}`),
|
action: () => navigateTo(`/admin/billing/${user.id}`),
|
||||||
@@ -137,6 +172,11 @@
|
|||||||
action: () => $refs.userDetailsModal.show(),
|
action: () => $refs.userDetailsModal.show(),
|
||||||
shown: auth.user && isStaff(auth.user),
|
shown: auth.user && isStaff(auth.user),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'edit-role',
|
||||||
|
action: () => openRoleEditModal(),
|
||||||
|
shown: auth.user && isAdmin(auth.user),
|
||||||
|
},
|
||||||
]"
|
]"
|
||||||
aria-label="More options"
|
aria-label="More options"
|
||||||
>
|
>
|
||||||
@@ -165,6 +205,10 @@
|
|||||||
<InfoIcon aria-hidden="true" />
|
<InfoIcon aria-hidden="true" />
|
||||||
{{ formatMessage(messages.infoButton) }}
|
{{ formatMessage(messages.infoButton) }}
|
||||||
</template>
|
</template>
|
||||||
|
<template #edit-role>
|
||||||
|
<EditIcon aria-hidden="true" />
|
||||||
|
{{ formatMessage(messages.editRoleButton) }}
|
||||||
|
</template>
|
||||||
</OverflowMenu>
|
</OverflowMenu>
|
||||||
</ButtonStyled>
|
</ButtonStyled>
|
||||||
</template>
|
</template>
|
||||||
@@ -355,6 +399,8 @@ import {
|
|||||||
LockIcon,
|
LockIcon,
|
||||||
MoreVerticalIcon,
|
MoreVerticalIcon,
|
||||||
ReportIcon,
|
ReportIcon,
|
||||||
|
SaveIcon,
|
||||||
|
SpinnerIcon,
|
||||||
XIcon,
|
XIcon,
|
||||||
} from '@modrinth/assets'
|
} from '@modrinth/assets'
|
||||||
import {
|
import {
|
||||||
@@ -362,10 +408,13 @@ import {
|
|||||||
ButtonStyled,
|
ButtonStyled,
|
||||||
commonMessages,
|
commonMessages,
|
||||||
ContentPageHeader,
|
ContentPageHeader,
|
||||||
|
injectNotificationManager,
|
||||||
NewModal,
|
NewModal,
|
||||||
OverflowMenu,
|
OverflowMenu,
|
||||||
|
TeleportDropdownMenu,
|
||||||
useRelativeTime,
|
useRelativeTime,
|
||||||
} from '@modrinth/ui'
|
} from '@modrinth/ui'
|
||||||
|
import { isAdmin } from '@modrinth/utils'
|
||||||
import { IntlFormatted } from '@vintl/vintl/components'
|
import { IntlFormatted } from '@vintl/vintl/components'
|
||||||
|
|
||||||
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
|
import TenMClubBadge from '~/assets/images/badges/10m-club.svg?component'
|
||||||
@@ -398,6 +447,8 @@ const formatCompactNumber = useCompactNumber(true)
|
|||||||
|
|
||||||
const formatRelativeTime = useRelativeTime()
|
const formatRelativeTime = useRelativeTime()
|
||||||
|
|
||||||
|
const { addNotification } = injectNotificationManager()
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
profileProjectsStats: {
|
profileProjectsStats: {
|
||||||
id: 'profile.stats.projects',
|
id: 'profile.stats.projects',
|
||||||
@@ -472,6 +523,10 @@ const messages = defineMessages({
|
|||||||
id: 'profile.button.info',
|
id: 'profile.button.info',
|
||||||
defaultMessage: 'View user details',
|
defaultMessage: 'View user details',
|
||||||
},
|
},
|
||||||
|
editRoleButton: {
|
||||||
|
id: 'profile.button.edit-role',
|
||||||
|
defaultMessage: 'Edit role',
|
||||||
|
},
|
||||||
userNotFoundError: {
|
userNotFoundError: {
|
||||||
id: 'profile.error.not-found',
|
id: 'profile.error.not-found',
|
||||||
defaultMessage: 'User not found',
|
defaultMessage: 'User not found',
|
||||||
@@ -648,6 +703,55 @@ const navLinks = computed(() => [
|
|||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => a.label.localeCompare(b.label)),
|
.sort((a, b) => a.label.localeCompare(b.label)),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
const selectedRole = ref(user.value.role)
|
||||||
|
const isSavingRole = ref(false)
|
||||||
|
|
||||||
|
const roleOptions = ['developer', 'moderator', 'admin']
|
||||||
|
|
||||||
|
const editRoleModal = useTemplateRef('editRoleModal')
|
||||||
|
|
||||||
|
const openRoleEditModal = () => {
|
||||||
|
selectedRole.value = user.value.role
|
||||||
|
editRoleModal.value?.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
const cancelRoleEdit = () => {
|
||||||
|
selectedRole.value = user.value.role
|
||||||
|
editRoleModal.value?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveRoleEdit() {
|
||||||
|
if (!selectedRole.value || selectedRole.value === user.value.role) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
isSavingRole.value = true
|
||||||
|
|
||||||
|
useBaseFetch(`user/${user.value.id}`, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: {
|
||||||
|
role: selectedRole.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
user.value.role = selectedRole.value
|
||||||
|
|
||||||
|
editRoleModal.value?.hide()
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
console.error('Failed to update user role:', error)
|
||||||
|
|
||||||
|
addNotification({
|
||||||
|
type: 'error',
|
||||||
|
title: 'Failed to update role',
|
||||||
|
message: 'An error occurred while updating the user role. Please try again.',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
isSavingRole.value = false
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
export default defineNuxtComponent({
|
export default defineNuxtComponent({
|
||||||
|
|||||||
Reference in New Issue
Block a user