Skip to content

Commit 937dc14

Browse files
committed
feat(ui): enhance components with accessibility, UX improvements, and new features
- Add navigation links for history, saved queries, conversations, and AI memory - Enhance 2FA setup modals with improved UX and error handling - Add user/role creation dialogs with better validation feedback - Improve settings pages (AI providers, system prompt, profile, security) - Add actionToast utility for consistent user feedback - Enhance sidebar with new sections and active state management
1 parent c3136ca commit 937dc14

19 files changed

Lines changed: 296 additions & 127 deletions

resources/js/components/AppSidebar.vue

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script setup lang="ts">
22
import { computed } from 'vue';
33
import { Link, usePage } from '@inertiajs/vue3';
4-
import { Database, HardDrive, HardDriveDownload, History, MessageSquare, ShieldCheck, Users } from 'lucide-vue-next';
4+
import { Brain, Database, HardDrive, HardDriveDownload, History, MessageSquare, MessageCircle, Save, ShieldCheck, Users } from 'lucide-vue-next';
55
import AppLogo from '@/components/AppLogo.vue';
66
import NavMain from '@/components/NavMain.vue';
77
import NavUser from '@/components/NavUser.vue';
@@ -28,6 +28,7 @@ const canViewAudit = computed<boolean>(() => permissions.value.includes('audit.v
2828
const canRunQueries = computed<boolean>(() => permissions.value.includes('queries.execute'));
2929
const canExportQueries = computed<boolean>(() => permissions.value.includes('queries.export'));
3030
const canManagePlatform = computed<boolean>(() => permissions.value.includes('connections.create'));
31+
const canUseAi = computed<boolean>(() => permissions.value.includes('queries.ai_generate'));
3132
3233
const mainNavItems = computed<NavItem[]>(() => {
3334
const items: NavItem[] = [
@@ -49,15 +50,33 @@ const mainNavItems = computed<NavItem[]>(() => {
4950
if (canRunQueries.value) {
5051
items.push({
5152
title: 'Historial de consultas',
52-
href: '/dashboard#query-history',
53+
href: '/queries/history',
5354
icon: History,
5455
});
56+
items.push({
57+
title: 'Consultas guardadas',
58+
href: '/queries/saved',
59+
icon: Save,
60+
});
61+
}
62+
63+
if (canUseAi.value) {
64+
items.push({
65+
title: 'Conversaciones IA',
66+
href: '/conversations',
67+
icon: MessageCircle,
68+
});
69+
items.push({
70+
title: 'Memoria IA',
71+
href: '/ai-memory',
72+
icon: Brain,
73+
});
5574
}
5675
5776
if (canViewAudit.value) {
5877
items.push({
5978
title: 'Auditoría',
60-
href: '/dashboard#audit',
79+
href: '/audit',
6180
icon: ShieldCheck,
6281
});
6382
}

resources/js/components/AppearanceTabs.vue

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { useAppearance } from '@/composables/useAppearance';
55
const { appearance, updateAppearance } = useAppearance();
66
77
const tabs = [
8-
{ value: 'light', Icon: Sun, label: 'Light' },
9-
{ value: 'dark', Icon: Moon, label: 'Dark' },
10-
{ value: 'system', Icon: Monitor, label: 'System' },
8+
{ value: 'light', Icon: Sun, label: 'Claro' },
9+
{ value: 'dark', Icon: Moon, label: 'Oscuro' },
10+
{ value: 'system', Icon: Monitor, label: 'Sistema' },
1111
] as const;
1212
</script>
1313

resources/js/components/CreateConnectionDialog.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { Input } from '@/components/ui/input';
2828
import { Label } from '@/components/ui/label';
2929
import { Spinner } from '@/components/ui/spinner';
3030
import { Badge } from '@/components/ui/badge';
31+
import { toastActionError, toastActionSuccess } from '@/lib/actionToast';
3132
3233
type Driver = 'pgsql' | 'mysql' | 'mariadb';
3334
@@ -139,10 +140,12 @@ async function handleSave() {
139140
140141
open.value = false;
141142
emit('created', createdConnectionId);
143+
toastActionSuccess('Conexión creada.');
142144
143145
router.reload({ only: ['connections'] });
144146
} catch (error) {
145147
serverError.value = error instanceof Error ? error.message : 'Error al crear la conexión.';
148+
toastActionError(error, 'No se pudo guardar la conexión.');
146149
} finally {
147150
isSubmitting.value = false;
148151
}

resources/js/components/CreateRoleDialog.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '@/components/ui/dialog';
1515
import { Input } from '@/components/ui/input';
1616
import { Label } from '@/components/ui/label';
17+
import { toastActionError, toastActionSuccess } from '@/lib/actionToast';
1718
1819
type PermissionOption = {
1920
id: number;
@@ -110,7 +111,7 @@ async function handleSave() {
110111
111112
const roleId = isEditing.value ? props.role!.id : await findNewRoleId();
112113
if (roleId) {
113-
await fetch(`/admin/roles/${roleId}/permissions`, {
114+
const permissionsResponse = await fetch(`/admin/roles/${roleId}/permissions`, {
114115
method: 'POST',
115116
headers: {
116117
'Content-Type': 'application/json',
@@ -121,12 +122,19 @@ async function handleSave() {
121122
credentials: 'same-origin',
122123
body: JSON.stringify({ permission_ids: selectedPermissionIds.value }),
123124
});
125+
126+
if (!permissionsResponse.ok) {
127+
const body = await permissionsResponse.json().catch(() => ({}));
128+
throw new Error(body.message || 'No se pudieron guardar los permisos del rol.');
129+
}
124130
}
125131
126132
open.value = false;
133+
toastActionSuccess(isEditing.value ? 'Rol actualizado.' : 'Rol creado.');
127134
router.reload({ only: ['roles', 'permissions'] });
128135
} catch (error) {
129136
serverError.value = error instanceof Error ? error.message : 'Error al guardar.';
137+
toastActionError(error, 'No se pudo guardar el rol.');
130138
} finally {
131139
isSubmitting.value = false;
132140
}

resources/js/components/CreateUserDialog.vue

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '@/components/ui/dialog';
1616
import { Input } from '@/components/ui/input';
1717
import { Label } from '@/components/ui/label';
18+
import { toastActionError, toastActionSuccess } from '@/lib/actionToast';
1819
1920
type RoleOption = {
2021
id: number;
@@ -107,7 +108,7 @@ async function handleSave() {
107108
if (selectedRoleIds.value.length > 0 || isEditing.value) {
108109
const userId = isEditing.value ? props.user!.id : await findNewUserId();
109110
if (userId) {
110-
await fetch(`/admin/users/${userId}/roles`, {
111+
const rolesResponse = await fetch(`/admin/users/${userId}/roles`, {
111112
method: 'POST',
112113
headers: {
113114
'Content-Type': 'application/json',
@@ -118,13 +119,20 @@ async function handleSave() {
118119
credentials: 'same-origin',
119120
body: JSON.stringify({ role_ids: selectedRoleIds.value }),
120121
});
122+
123+
if (!rolesResponse.ok) {
124+
const body = await rolesResponse.json().catch(() => ({}));
125+
throw new Error(body.message || 'No se pudieron guardar los roles del usuario.');
126+
}
121127
}
122128
}
123129
124130
open.value = false;
131+
toastActionSuccess(isEditing.value ? 'Usuario actualizado.' : 'Usuario creado.');
125132
router.reload({ only: ['users'] });
126133
} catch (error) {
127134
serverError.value = error instanceof Error ? error.message : 'Error al guardar.';
135+
toastActionError(error, 'No se pudo guardar el usuario.');
128136
} finally {
129137
isSubmitting.value = false;
130138
}

resources/js/components/DeleteUser.vue

Lines changed: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
DialogTrigger,
1818
} from '@/components/ui/dialog';
1919
import { Label } from '@/components/ui/label';
20+
import { toastActionError } from '@/lib/actionToast';
2021
2122
const passwordInput = useTemplateRef('passwordInput');
2223
</script>
@@ -25,29 +26,29 @@ const passwordInput = useTemplateRef('passwordInput');
2526
<div class="space-y-6">
2627
<Heading
2728
variant="small"
28-
title="Delete account"
29-
description="Delete your account and all of its resources"
29+
title="Eliminar cuenta"
30+
description="Elimina tu cuenta y todos sus recursos"
3031
/>
3132
<div
3233
class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10"
3334
>
3435
<div class="relative space-y-0.5 text-red-600 dark:text-red-100">
35-
<p class="font-medium">Warning</p>
36+
<p class="font-medium">Advertencia</p>
3637
<p class="text-sm">
37-
Please proceed with caution, this cannot be undone.
38+
Procede con precaución, esta acción no se puede deshacer.
3839
</p>
3940
</div>
4041
<Dialog>
4142
<DialogTrigger as-child>
4243
<Button variant="destructive" data-test="delete-user-button"
43-
>Delete account</Button
44+
>Eliminar cuenta</Button
4445
>
4546
</DialogTrigger>
4647
<DialogContent>
47-
<Form
48+
<Form
4849
v-bind="ProfileController.destroy.form()"
4950
reset-on-success
50-
@error="() => passwordInput?.focus()"
51+
@error="(errors) => { passwordInput?.focus(); toastActionError(errors, 'No se pudo eliminar la cuenta.'); }"
5152
:options="{
5253
preserveScroll: true,
5354
}"
@@ -56,27 +57,25 @@ const passwordInput = useTemplateRef('passwordInput');
5657
>
5758
<DialogHeader class="space-y-3">
5859
<DialogTitle
59-
>Are you sure you want to delete your
60-
account?</DialogTitle
60+
>¿Seguro que deseas eliminar tu cuenta?</DialogTitle
6161
>
6262
<DialogDescription>
63-
Once your account is deleted, all of its
64-
resources and data will also be permanently
65-
deleted. Please enter your password to confirm
66-
you would like to permanently delete your
67-
account.
63+
Una vez eliminada tu cuenta, todos sus recursos
64+
y datos también se eliminarán de forma
65+
permanente. Ingresa tu contraseña para confirmar
66+
que deseas eliminar tu cuenta permanentemente.
6867
</DialogDescription>
6968
</DialogHeader>
7069

7170
<div class="grid gap-2">
7271
<Label for="password" class="sr-only"
73-
>Password</Label
72+
>Contraseña</Label
7473
>
7574
<PasswordInput
7675
id="password"
7776
name="password"
7877
ref="passwordInput"
79-
placeholder="Password"
78+
placeholder="Contraseña"
8079
/>
8180
<InputError :message="errors.password" />
8281
</div>
@@ -92,7 +91,7 @@ const passwordInput = useTemplateRef('passwordInput');
9291
}
9392
"
9493
>
95-
Cancel
94+
Cancelar
9695
</Button>
9796
</DialogClose>
9897

@@ -102,7 +101,7 @@ const passwordInput = useTemplateRef('passwordInput');
102101
:disabled="processing"
103102
data-test="confirm-delete-user-button"
104103
>
105-
Delete account
104+
Eliminar cuenta
106105
</Button>
107106
</DialogFooter>
108107
</Form>

resources/js/components/TwoFactorRecoveryCodes.vue

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,12 @@ onMounted(async () => {
4242
<Card class="w-full">
4343
<CardHeader>
4444
<CardTitle class="flex gap-3">
45-
<LockKeyhole class="size-4" />2FA recovery codes
45+
<LockKeyhole class="size-4" />Códigos de recuperación 2FA
4646
</CardTitle>
4747
<CardDescription>
48-
Recovery codes let you regain access if you lose your 2FA
49-
device. Store them in a secure password manager.
48+
Los códigos de recuperación te permiten volver a acceder si
49+
pierdes tu dispositivo 2FA. Guárdalos en un gestor de
50+
contraseñas seguro.
5051
</CardDescription>
5152
</CardHeader>
5253
<CardContent>
@@ -58,8 +59,8 @@ onMounted(async () => {
5859
:is="isRecoveryCodesVisible ? EyeOff : Eye"
5960
class="size-4"
6061
/>
61-
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery
62-
codes
62+
{{ isRecoveryCodesVisible ? 'Ocultar' : 'Ver' }} códigos
63+
de recuperación
6364
</Button>
6465

6566
<Form
@@ -75,7 +76,7 @@ onMounted(async () => {
7576
type="submit"
7677
:disabled="processing"
7778
>
78-
<RefreshCw /> Regenerate codes
79+
<RefreshCw /> Regenerar códigos
7980
</Button>
8081
</Form>
8182
</div>
@@ -111,10 +112,10 @@ onMounted(async () => {
111112
</div>
112113
</div>
113114
<p class="text-xs text-muted-foreground select-none">
114-
Each recovery code can be used once to access your
115-
account and will be removed after use. If you need more,
116-
click
117-
<span class="font-bold">Regenerate codes</span> above.
115+
Cada código de recuperación se puede usar una vez para
116+
acceder a tu cuenta y se elimina después de usarlo. Si
117+
necesitas más, haz clic en
118+
<span class="font-bold">Regenerar códigos</span> arriba.
118119
</p>
119120
</div>
120121
</div>

resources/js/components/TwoFactorSetupModal.vue

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,26 @@ const pinInputContainerRef = useTemplateRef('pinInputContainerRef');
4646
const modalConfig = computed<TwoFactorConfigContent>(() => {
4747
if (props.twoFactorEnabled) {
4848
return {
49-
title: 'Two-factor authentication enabled',
49+
title: 'Autenticación de dos factores activada',
5050
description:
51-
'Two-factor authentication is now enabled. Scan the QR code or enter the setup key in your authenticator app.',
52-
buttonText: 'Close',
51+
'La autenticación de dos factores ya está activa. Escanea el código QR o ingresa la clave en tu app autenticadora.',
52+
buttonText: 'Cerrar',
5353
};
5454
}
5555
5656
if (showVerificationStep.value) {
5757
return {
58-
title: 'Verify authentication code',
59-
description: 'Enter the 6-digit code from your authenticator app',
60-
buttonText: 'Continue',
58+
title: 'Verifica el código de autenticación',
59+
description: 'Ingresa el código de 6 dígitos de tu app autenticadora',
60+
buttonText: 'Continuar',
6161
};
6262
}
6363
6464
return {
65-
title: 'Enable two-factor authentication',
65+
title: 'Activa la autenticación de dos factores',
6666
description:
67-
'To finish enabling two-factor authentication, scan the QR code or enter the setup key in your authenticator app',
68-
buttonText: 'Continue',
67+
'Para terminar de activar la autenticación de dos factores, escanea el código QR o ingresa la clave en tu app autenticadora',
68+
buttonText: 'Continuar',
6969
};
7070
});
7171
@@ -197,7 +197,7 @@ watch(
197197
class="absolute inset-0 top-1/2 h-px w-full bg-border"
198198
/>
199199
<span class="relative bg-card px-2 py-1"
200-
>or, enter the code manually</span
200+
>o ingresa el código manualmente</span
201201
>
202202
</div>
203203

@@ -279,14 +279,14 @@ watch(
279279
@click="showVerificationStep = false"
280280
:disabled="processing"
281281
>
282-
Back
282+
Atrás
283283
</Button>
284284
<Button
285285
type="submit"
286286
class="w-auto flex-1"
287287
:disabled="processing || code.length < 6"
288288
>
289-
Confirm
289+
Confirmar
290290
</Button>
291291
</div>
292292
</div>

resources/js/layouts/AuthLayout.vue

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<script setup lang="ts">
22
import AuthLayout from '@/layouts/auth/AuthSplitLayout.vue';
3+
import { Toaster } from '@/components/ui/sonner';
34
45
const { title = '', description = '' } = defineProps<{
56
title?: string;
@@ -10,5 +11,6 @@ const { title = '', description = '' } = defineProps<{
1011
<template>
1112
<AuthLayout :title="title" :description="description">
1213
<slot />
14+
<Toaster />
1315
</AuthLayout>
1416
</template>

0 commit comments

Comments
 (0)