Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 786c015

Browse files
committed
feat(frontend): add role change dialog to user detail page
1 parent 843f51a commit 786c015

File tree

2 files changed

+170
-10
lines changed

2 files changed

+170
-10
lines changed

services/frontend/src/components/admin/users/UserDetailGeneral.vue

Lines changed: 156 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,45 @@
11
<script setup lang="ts">
2-
import { computed } from 'vue'
2+
import { ref, computed } from 'vue'
33
import { useI18n } from 'vue-i18n'
4+
import { toast } from 'vue-sonner'
45
import { Badge } from '@/components/ui/badge'
6+
import { Button } from '@/components/ui/button'
57
import { DsCard } from '@/components/ui/ds-card'
8+
import {
9+
Dialog,
10+
DialogContent,
11+
DialogDescription,
12+
DialogFooter,
13+
DialogHeader,
14+
DialogTitle,
15+
DialogTrigger,
16+
} from '@/components/ui/dialog'
17+
import {
18+
Select,
19+
SelectContent,
20+
SelectItem,
21+
SelectTrigger,
22+
SelectValue,
23+
} from '@/components/ui/select'
624
import { Mail, Github, Shield } from 'lucide-vue-next'
25+
import { getEnv } from '@/utils/env'
726
import type { User } from '@/views/admin/users/types'
827
928
const props = defineProps<{
1029
user: User
1130
}>()
1231
32+
const emit = defineEmits<{
33+
roleChanged: []
34+
}>()
35+
1336
const { t } = useI18n()
1437
38+
// Dialog state
39+
const isDialogOpen = ref(false)
40+
const selectedRole = ref(props.user.role_id || 'global_user')
41+
const isChangingRole = ref(false)
42+
1543
// Computed properties for display
1644
const displayName = computed(() => {
1745
const firstName = props.user.first_name || ''
@@ -28,6 +56,64 @@ const authTypeBadge = computed(() => {
2856
text: isEmail ? t('adminUsers.userDetail.values.email') : t('adminUsers.userDetail.values.github')
2957
}
3058
})
59+
60+
// Role options
61+
const roleOptions = [
62+
{ value: 'global_admin', label: 'Global Administrator' },
63+
{ value: 'global_user', label: 'Global User' }
64+
]
65+
66+
// Change role handler
67+
async function handleChangeRole() {
68+
if (!selectedRole.value || selectedRole.value === props.user.role_id) {
69+
isDialogOpen.value = false
70+
return
71+
}
72+
73+
try {
74+
isChangingRole.value = true
75+
const apiUrl = getEnv('VITE_DEPLOYSTACK_BACKEND_URL')
76+
77+
const response = await fetch(`${apiUrl}/api/admin/users/${props.user.id}/role`, {
78+
method: 'PUT',
79+
headers: {
80+
'Content-Type': 'application/json',
81+
},
82+
credentials: 'include',
83+
body: JSON.stringify({
84+
role_id: selectedRole.value
85+
})
86+
})
87+
88+
const data = await response.json()
89+
90+
if (!response.ok) {
91+
throw new Error(data.error || 'Failed to change role')
92+
}
93+
94+
toast.success('Role changed successfully', {
95+
description: `User role updated to ${roleOptions.find(r => r.value === selectedRole.value)?.label}`
96+
})
97+
98+
isDialogOpen.value = false
99+
emit('roleChanged')
100+
} catch (error) {
101+
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
102+
toast.error('Failed to change role', {
103+
description: errorMessage
104+
})
105+
} finally {
106+
isChangingRole.value = false
107+
}
108+
}
109+
110+
// Reset selected role when dialog opens
111+
function handleDialogOpen(open: boolean) {
112+
isDialogOpen.value = open
113+
if (open) {
114+
selectedRole.value = props.user.role_id || 'global_user'
115+
}
116+
}
31117
</script>
32118

33119
<template>
@@ -59,14 +145,6 @@ const authTypeBadge = computed(() => {
59145
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">{{ user.email }}</dd>
60146
</div>
61147

62-
<!-- Role -->
63-
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4">
64-
<dt class="text-sm font-medium text-gray-900">{{ t('adminUsers.userDetail.fields.role') }}</dt>
65-
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
66-
{{ user.role ? user.role.name : t('adminUsers.userDetail.values.noRoleAssigned') }}
67-
</dd>
68-
</div>
69-
70148
<!-- Registration Method -->
71149
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4">
72150
<dt class="text-sm font-medium text-gray-900">{{ t('adminUsers.userDetail.fields.registrationMethod') }}</dt>
@@ -103,6 +181,75 @@ const authTypeBadge = computed(() => {
103181
</dl>
104182
</DsCard>
105183

184+
<!-- Role Card -->
185+
<DsCard :title="t('adminUsers.userDetail.fields.role')">
186+
<p class="text-sm text-muted-foreground mb-6">
187+
User role and permissions within the DeployStack system
188+
</p>
189+
190+
<dl class="divide-y divide-gray-100">
191+
<!-- Role ID -->
192+
<div class="py-4 sm:grid sm:grid-cols-3 sm:gap-4">
193+
<dt class="text-sm font-medium text-gray-900">Role</dt>
194+
<dd class="mt-1 text-sm text-gray-700 sm:col-span-2 sm:mt-0">
195+
{{ user.role_id || t('adminUsers.userDetail.values.noRoleAssigned') }}
196+
</dd>
197+
</div>
198+
</dl>
199+
200+
<template #footer-actions>
201+
<Dialog :open="isDialogOpen" @update:open="handleDialogOpen">
202+
<DialogTrigger as-child>
203+
<Button>
204+
Change Role
205+
</Button>
206+
</DialogTrigger>
207+
<DialogContent class="sm:max-w-[425px]">
208+
<DialogHeader>
209+
<DialogTitle>Change User Role</DialogTitle>
210+
<DialogDescription>
211+
Update the role for {{ user.username }}. This will affect their permissions across the system.
212+
</DialogDescription>
213+
</DialogHeader>
214+
215+
<div class="space-y-4 py-4">
216+
<div class="space-y-2">
217+
<label class="text-sm font-medium">Select Role</label>
218+
<Select v-model="selectedRole">
219+
<SelectTrigger>
220+
<SelectValue placeholder="Select a role" />
221+
</SelectTrigger>
222+
<SelectContent>
223+
<SelectItem
224+
v-for="option in roleOptions"
225+
:key="option.value"
226+
:value="option.value"
227+
>
228+
{{ option.label }}
229+
</SelectItem>
230+
</SelectContent>
231+
</Select>
232+
</div>
233+
</div>
234+
235+
<DialogFooter>
236+
<Button variant="outline" @click="isDialogOpen = false" :disabled="isChangingRole">
237+
Cancel
238+
</Button>
239+
<Button
240+
@click="handleChangeRole"
241+
:loading="isChangingRole"
242+
loading-text="Changing..."
243+
:disabled="isChangingRole || selectedRole === user.role_id"
244+
>
245+
Change Role
246+
</Button>
247+
</DialogFooter>
248+
</DialogContent>
249+
</Dialog>
250+
</template>
251+
</DsCard>
252+
106253
<!-- Permissions Card -->
107254
<DsCard v-if="user.role && user.role.permissions.length > 0" :title="t('adminUsers.userDetail.fields.permissions')">
108255
<ul role="list" class="divide-y divide-gray-100 rounded-md border border-gray-200">

services/frontend/src/views/admin/users/[id].vue

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,15 @@ const displayName = computed(() => {
138138
return fullName || user.value.username
139139
})
140140
141+
// Handler for role change event
142+
async function handleRoleChanged() {
143+
try {
144+
user.value = await fetchUser(userId)
145+
} catch (err) {
146+
error.value = err instanceof Error ? err.message : 'An unknown error occurred'
147+
}
148+
}
149+
141150
// Load user on component mount
142151
onMounted(async () => {
143152
setBreadcrumbs([
@@ -278,7 +287,11 @@ onMounted(async () => {
278287

279288
<!-- Content Area -->
280289
<div class="flex-1">
281-
<UserDetailGeneral v-if="currentSection === 'general'" :user="user" />
290+
<UserDetailGeneral
291+
v-if="currentSection === 'general'"
292+
:user="user"
293+
@role-changed="handleRoleChanged"
294+
/>
282295
<UserDetailTeams v-else-if="currentSection === 'teams'" :user-id="userId" />
283296
</div>
284297
</div>

0 commit comments

Comments
 (0)