Skip to content

Commit d7e92db

Browse files
committed
fix(frontend): preserve team permissions after update and improve delete modal UX
- Fix critical bug where team permissions (role, is_admin, is_owner) were reset after updating team details by merging response data instead of replacing - Replace AlertDialog with Dialog component for team deletion modal and increase width to 600px - Add confirmation input requiring "sudo delete team" text before enabling delete button - Remove red styling from consequences list in delete modal for less alarming appearance - Remove icons from Save Changes, Delete Team, and Add Member buttons for cleaner UI - Fix button loading states to properly display Spinner component alongside text - Add new translation keys for deletion confirmation workflow
1 parent 6d82fb3 commit d7e92db

File tree

4 files changed

+89
-84
lines changed

4 files changed

+89
-84
lines changed

services/frontend/src/components/teams/manage/TeamInfo.vue

Lines changed: 69 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,26 @@ import { toast } from 'vue-sonner'
66
import { Button } from '@/components/ui/button'
77
import { Spinner } from '@/components/ui/spinner'
88
import { Input } from '@/components/ui/input'
9+
import { Label } from '@/components/ui/label'
910
import { Badge } from '@/components/ui/badge'
1011
import { Alert, AlertDescription } from '@/components/ui/alert'
1112
import {
12-
Save,
1313
Lock,
1414
Calendar,
1515
Users,
1616
Hash,
1717
AlertTriangle,
18-
Trash2,
1918
XCircle,
2019
Shield
2120
} from 'lucide-vue-next'
2221
import {
23-
AlertDialog,
24-
AlertDialogCancel,
25-
AlertDialogContent,
26-
AlertDialogDescription,
27-
AlertDialogFooter,
28-
AlertDialogHeader,
29-
AlertDialogTitle,
30-
} from '@/components/ui/alert-dialog'
22+
Dialog,
23+
DialogContent,
24+
DialogDescription,
25+
DialogFooter,
26+
DialogHeader,
27+
DialogTitle,
28+
} from '@/components/ui/dialog'
3129
import { TeamService, type Team } from '@/services/teamService'
3230
import { DsCard } from '@/components/ui/ds-card'
3331
import { z } from 'zod'
@@ -56,6 +54,7 @@ const saveError = ref<string | null>(null)
5654
const isDeleting = ref(false)
5755
const deleteError = ref<string | null>(null)
5856
const showDeleteDialog = ref(false)
57+
const confirmationText = ref('')
5958
6059
// Form data
6160
const formData = ref({
@@ -97,6 +96,10 @@ const formattedUpdatedDate = computed(() => {
9796
})
9897
})
9998
99+
const isConfirmationValid = computed(() => {
100+
return confirmationText.value === 'sudo delete team'
101+
})
102+
100103
// Save team changes
101104
const saveTeam = async () => {
102105
try {
@@ -288,11 +291,9 @@ watch(() => props.team, () => {
288291
<Button
289292
@click="saveTeam"
290293
:disabled="!hasChanges || isSaving"
291-
class="gap-2"
292294
>
293295
<Spinner v-if="isSaving" class="mr-2" />
294-
<Save v-else class="h-4 w-4" />
295-
{{ t('teams.manage.save') }}
296+
{{ isSaving ? t('teams.manage.saving') : t('teams.manage.save') }}
296297
</Button>
297298
</template>
298299
</DsCard>
@@ -323,34 +324,19 @@ watch(() => props.team, () => {
323324

324325
<!-- Delete Team Content -->
325326
<div v-else class="space-y-4">
326-
<p class="text-sm text-muted-foreground">
327+
<p class="text-sm">
327328
{{ t('teams.manage.dangerZone.deleteTeamDescription') }}
328329
</p>
329330

330331
<!-- What gets deleted -->
331-
<div class="bg-muted/50 border rounded-lg p-4">
332-
<h4 class="text-sm font-medium mb-3">{{ t('teams.manage.dangerZone.willDelete.title') }}</h4>
333-
<ul class="text-xs space-y-2">
334-
<li class="flex items-start gap-2">
335-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
336-
{{ t('teams.manage.dangerZone.willDelete.servers') }}
337-
</li>
338-
<li class="flex items-start gap-2">
339-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
340-
{{ t('teams.manage.dangerZone.willDelete.credentials') }}
341-
</li>
342-
<li class="flex items-start gap-2">
343-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
344-
{{ t('teams.manage.dangerZone.willDelete.variables') }}
345-
</li>
346-
<li class="flex items-start gap-2">
347-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
348-
{{ t('teams.manage.dangerZone.willDelete.history') }}
349-
</li>
350-
<li class="flex items-start gap-2">
351-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
352-
{{ t('teams.manage.dangerZone.willDelete.members') }}
353-
</li>
332+
<div>
333+
<h4 class="text-sm font-medium mb-2">{{ t('teams.manage.dangerZone.willDelete.title') }}</h4>
334+
<ul class="text-sm space-y-1 list-disc list-inside">
335+
<li>{{ t('teams.manage.dangerZone.willDelete.servers') }}</li>
336+
<li>{{ t('teams.manage.dangerZone.willDelete.credentials') }}</li>
337+
<li>{{ t('teams.manage.dangerZone.willDelete.variables') }}</li>
338+
<li>{{ t('teams.manage.dangerZone.willDelete.history') }}</li>
339+
<li>{{ t('teams.manage.dangerZone.willDelete.members') }}</li>
354340
</ul>
355341
</div>
356342
</div>
@@ -359,69 +345,74 @@ watch(() => props.team, () => {
359345
<Button
360346
variant="destructive"
361347
@click="showDeleteDialog = true"
362-
class="gap-2"
363348
>
364-
<Trash2 class="h-4 w-4" />
365349
{{ t('teams.manage.dangerZone.deleteButton') }}
366350
</Button>
367351
</template>
368352
</DsCard>
369353

370354
<!-- Delete Confirmation Dialog -->
371-
<AlertDialog :open="showDeleteDialog" @update:open="showDeleteDialog = $event">
372-
<AlertDialogContent class="sm:max-w-[500px]">
373-
<AlertDialogHeader>
374-
<AlertDialogTitle class="flex items-center gap-2 text-destructive">
375-
<AlertTriangle class="h-5 w-5" />
376-
{{ t('teams.manage.deleteDialog.title') }}
377-
</AlertDialogTitle>
378-
<AlertDialogDescription class="space-y-4">
355+
<Dialog
356+
:open="showDeleteDialog"
357+
@update:open="(value) => {
358+
showDeleteDialog = value
359+
if (!value) confirmationText = ''
360+
}"
361+
>
362+
<DialogContent class="sm:max-w-[600px]">
363+
<DialogHeader>
364+
<DialogTitle>{{ t('teams.manage.deleteDialog.title') }}</DialogTitle>
365+
<DialogDescription class="space-y-4">
379366
<p>{{ t('teams.manage.deleteDialog.warning') }}</p>
380367

381368
<div class="rounded-lg border p-3 bg-muted/50">
382369
<p class="font-medium text-sm mb-1">{{ t('teams.manage.deleteDialog.teamName') }}:</p>
383370
<p class="font-mono text-sm">"{{ team.name }}"</p>
384371
</div>
385372

386-
<div class="rounded-lg border-destructive/50 bg-destructive/5 p-4 space-y-3">
387-
<p class="text-sm font-medium text-destructive">{{ t('teams.manage.deleteDialog.consequences') }}</p>
388-
<ul class="text-xs space-y-2">
389-
<li class="flex items-start gap-2">
390-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
391-
{{ t('teams.manage.deleteDialog.consequencesList.servers') }}
392-
</li>
393-
<li class="flex items-start gap-2">
394-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
395-
{{ t('teams.manage.deleteDialog.consequencesList.credentials') }}
396-
</li>
397-
<li class="flex items-start gap-2">
398-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
399-
{{ t('teams.manage.deleteDialog.consequencesList.variables') }}
400-
</li>
401-
<li class="flex items-start gap-2">
402-
<XCircle class="h-3 w-3 text-destructive mt-0.5 shrink-0" />
403-
{{ t('teams.manage.deleteDialog.consequencesList.history') }}
404-
</li>
373+
<div>
374+
<p class="text-sm font-medium mb-2">{{ t('teams.manage.deleteDialog.consequences') }}</p>
375+
<ul class="text-sm space-y-1 list-disc list-inside">
376+
<li>{{ t('teams.manage.deleteDialog.consequencesList.servers') }}</li>
377+
<li>{{ t('teams.manage.deleteDialog.consequencesList.credentials') }}</li>
378+
<li>{{ t('teams.manage.deleteDialog.consequencesList.variables') }}</li>
379+
<li>{{ t('teams.manage.deleteDialog.consequencesList.history') }}</li>
405380
</ul>
406381
</div>
407-
</AlertDialogDescription>
408-
</AlertDialogHeader>
409-
<AlertDialogFooter>
410-
<AlertDialogCancel @click="showDeleteDialog = false">
382+
383+
<div class="space-y-2">
384+
<Label for="delete-confirmation">
385+
{{ t('teams.manage.deleteDialog.confirmationLabel') }}
386+
</Label>
387+
<Input
388+
id="delete-confirmation"
389+
v-model="confirmationText"
390+
:placeholder="t('teams.manage.deleteDialog.confirmationPlaceholder')"
391+
class="font-mono"
392+
/>
393+
<p class="text-xs text-muted-foreground">
394+
{{ t('teams.manage.deleteDialog.confirmationRequired') }}
395+
</p>
396+
</div>
397+
</DialogDescription>
398+
</DialogHeader>
399+
<DialogFooter>
400+
<Button
401+
variant="outline"
402+
@click="showDeleteDialog = false"
403+
>
411404
{{ t('teams.manage.deleteDialog.cancel') }}
412-
</AlertDialogCancel>
405+
</Button>
413406
<Button
414407
variant="destructive"
415408
@click="deleteTeam"
416-
:disabled="isDeleting"
417-
class="gap-2"
409+
:disabled="!isConfirmationValid || isDeleting"
418410
>
419411
<Spinner v-if="isDeleting" class="mr-2" />
420-
<Trash2 v-else class="h-4 w-4" />
421-
{{ t('teams.manage.deleteDialog.confirm') }}
412+
{{ isDeleting ? t('teams.manage.deleteDialog.deleting') : t('teams.manage.deleteDialog.confirm') }}
422413
</Button>
423-
</AlertDialogFooter>
424-
</AlertDialogContent>
425-
</AlertDialog>
414+
</DialogFooter>
415+
</DialogContent>
416+
</Dialog>
426417
</div>
427418
</template>

services/frontend/src/components/teams/manage/members/MembersList.vue

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { useI18n } from 'vue-i18n'
44
import { Button } from '@/components/ui/button'
55
import { Alert, AlertDescription } from '@/components/ui/alert'
66
import { Skeleton } from '@/components/ui/skeleton'
7-
import { Users, UserPlus, Info, AlertTriangle } from 'lucide-vue-next'
7+
import { Users, Info, AlertTriangle } from 'lucide-vue-next'
88
import MemberRow from './MemberRow.vue'
99
import { DsCard } from '@/components/ui/ds-card'
1010
import type { Team } from '@/services/teamService'
@@ -249,8 +249,7 @@ defineExpose({
249249
<p class="text-sm text-muted-foreground mb-4">
250250
{{ t('teams.manage.members.noMembers.description') }}
251251
</p>
252-
<Button v-if="canAddMembers" class="gap-2" @click="handleAddMember">
253-
<UserPlus class="h-4 w-4" />
252+
<Button v-if="canAddMembers" @click="handleAddMember">
254253
{{ t('teams.manage.members.noMembers.addFirstMember') }}
255254
</Button>
256255
</div>
@@ -268,10 +267,8 @@ defineExpose({
268267

269268
<template v-if="canAddMembers" #footer-actions>
270269
<Button
271-
class="gap-2"
272270
@click="handleAddMember"
273271
>
274-
<UserPlus class="h-4 w-4" />
275272
{{ t('teams.manage.members.addMember') }}
276273
</Button>
277274
</template>

services/frontend/src/i18n/locales/en/teams.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,9 @@ export default {
277277
variables: 'All global environment variables',
278278
history: 'Complete deployment history and logs'
279279
},
280+
confirmationRequired: 'Type "sudo delete team" to confirm',
281+
confirmationLabel: 'Confirmation',
282+
confirmationPlaceholder: 'Type: sudo delete team',
280283
cancel: 'Cancel',
281284
confirm: 'Delete Team',
282285
deleting: 'Deleting...'

services/frontend/src/views/teams/manage/[id].vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,21 @@ const loadTeam = async () => {
8383
8484
// Handle team updates
8585
const handleTeamUpdated = (updatedTeam: Team) => {
86-
team.value = updatedTeam
86+
// Merge updated team data while preserving permission fields
87+
// that may not be returned by the update endpoint
88+
if (team.value) {
89+
team.value = {
90+
...team.value,
91+
...updatedTeam,
92+
// Explicitly preserve permission fields if they're not in the update response
93+
role: updatedTeam.role ?? team.value.role,
94+
is_admin: updatedTeam.is_admin ?? team.value.is_admin,
95+
is_owner: updatedTeam.is_owner ?? team.value.is_owner,
96+
member_count: updatedTeam.member_count ?? team.value.member_count,
97+
}
98+
} else {
99+
team.value = updatedTeam
100+
}
87101
}
88102
89103
// Handle team selection from sidebar

0 commit comments

Comments
 (0)