feat: boton de mostrar/ocultar clave en todos los campos de contraseña
Componente reutilizable PasswordInput (icono de ojo) usado en login, registro, restablecer clave, cambiar clave, crear usuario y la clave SMTP de Configuracion, para que el usuario pueda verificar que la esta escribiendo correctamente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,15 @@
|
|||||||
# Changelog - project.linkedcorp.net
|
# Changelog - project.linkedcorp.net
|
||||||
|
|
||||||
|
## [1.4.0] - 2026-09-09
|
||||||
|
### Added
|
||||||
|
- Boton de revelar/ocultar clave (icono de ojo) en todos los campos de contraseña de la app:
|
||||||
|
login, registro, restablecer clave, cambiar clave (usuario logueado), crear usuario y la clave
|
||||||
|
SMTP en Configuración. Nuevo componente reutilizable `PasswordInput`.
|
||||||
|
### Fixed
|
||||||
|
- El fix de espacios en SMTP del v1.3.0 usaba `trim()`, que solo recorta los bordes. El caso real
|
||||||
|
en producción tenía el espacio a mitad del host (`"mail.linkedcorp." + " " + "net"`), asi que
|
||||||
|
seguia fallando tras el primer fix. Corregido para quitar cualquier espacio, no solo los bordes.
|
||||||
|
|
||||||
## [1.3.0] - 2026-09-06
|
## [1.3.0] - 2026-09-06
|
||||||
### Added
|
### Added
|
||||||
- Configuración → Correo (SMTP): botón "Enviar prueba" con campo de destinatario. Ejecuta un envío
|
- Configuración → Correo (SMTP): botón "Enviar prueba" con campo de destinatario. Ejecuta un envío
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-backend",
|
"name": "project-linkedcorp-backend",
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/server.js",
|
"main": "src/server.js",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "project-linkedcorp-frontend",
|
"name": "project-linkedcorp-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.3.0",
|
"version": "1.4.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Flyout, FlyoutItem, FlyoutSeparator } from '../components/Flyout.jsx';
|
|||||||
import { DetailPopup } from '../components/DetailPopup.jsx';
|
import { DetailPopup } from '../components/DetailPopup.jsx';
|
||||||
import { Field } from '../components/Field.jsx';
|
import { Field } from '../components/Field.jsx';
|
||||||
import { Button } from '../components/Button.jsx';
|
import { Button } from '../components/Button.jsx';
|
||||||
|
import { PasswordInput } from '../components/PasswordInput.jsx';
|
||||||
import { useAuth } from '../context/auth-context.jsx';
|
import { useAuth } from '../context/auth-context.jsx';
|
||||||
import { useUi } from '../context/ui-context.jsx';
|
import { useUi } from '../context/ui-context.jsx';
|
||||||
import { useAsync } from '../hooks/useAsync.js';
|
import { useAsync } from '../hooks/useAsync.js';
|
||||||
@@ -131,13 +132,13 @@ export default function AppShell() {
|
|||||||
<DetailPopup open={changingPassword} title={t('change_password_title')} onClose={() => setChangingPassword(false)} width={420} height={380}>
|
<DetailPopup open={changingPassword} title={t('change_password_title')} onClose={() => setChangingPassword(false)} width={420} height={380}>
|
||||||
{passwordError && <div className="login-error">{passwordError}</div>}
|
{passwordError && <div className="login-error">{passwordError}</div>}
|
||||||
<Field label={t('change_password_current')}>
|
<Field label={t('change_password_current')}>
|
||||||
<input className="input" type="password" value={passwordForm.currentPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, currentPassword: e.target.value }))} autoFocus />
|
<PasswordInput value={passwordForm.currentPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, currentPassword: e.target.value }))} autoFocus />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('reset_password_new')}>
|
<Field label={t('reset_password_new')}>
|
||||||
<input className="input" type="password" value={passwordForm.newPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, newPassword: e.target.value }))} />
|
<PasswordInput value={passwordForm.newPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, newPassword: e.target.value }))} />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('register_password_confirm')}>
|
<Field label={t('register_password_confirm')}>
|
||||||
<input className="input" type="password" value={passwordForm.confirmPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, confirmPassword: e.target.value }))} />
|
<PasswordInput value={passwordForm.confirmPassword} onChange={(e) => setPasswordForm((f) => ({ ...f, confirmPassword: e.target.value }))} />
|
||||||
</Field>
|
</Field>
|
||||||
<div style={{ display: 'flex', gap: 8 }}>
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
<Button variant="accent" icon={Save} onClick={submitChangePassword}>{t('common_save')}</Button>
|
<Button variant="accent" icon={Save} onClick={submitChangePassword}>{t('common_save')}</Button>
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
import { Eye, EyeOff } from 'lucide-react';
|
||||||
|
import { useUi } from '../context/ui-context.jsx';
|
||||||
|
|
||||||
|
export function PasswordInput({ value, onChange, required, autoFocus, placeholder, id }) {
|
||||||
|
const { t } = useUi();
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
return (
|
||||||
|
<div className="password-input">
|
||||||
|
<input
|
||||||
|
id={id}
|
||||||
|
className="input"
|
||||||
|
type={visible ? 'text' : 'password'}
|
||||||
|
value={value}
|
||||||
|
onChange={onChange}
|
||||||
|
required={required}
|
||||||
|
autoFocus={autoFocus}
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="icon-btn password-toggle"
|
||||||
|
onClick={() => setVisible((v) => !v)}
|
||||||
|
tabIndex={-1}
|
||||||
|
aria-label={visible ? t('password_hide') : t('password_show')}
|
||||||
|
title={visible ? t('password_hide') : t('password_show')}
|
||||||
|
>
|
||||||
|
{visible ? <EyeOff /> : <Eye />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -59,6 +59,8 @@ export const messages = {
|
|||||||
smtp_test_send: 'Enviar prueba',
|
smtp_test_send: 'Enviar prueba',
|
||||||
smtp_test_sending: 'Enviando...',
|
smtp_test_sending: 'Enviando...',
|
||||||
smtp_test_success: 'Correo de prueba enviado correctamente a',
|
smtp_test_success: 'Correo de prueba enviado correctamente a',
|
||||||
|
password_show: 'Mostrar clave',
|
||||||
|
password_hide: 'Ocultar clave',
|
||||||
common_new: 'Nuevo',
|
common_new: 'Nuevo',
|
||||||
common_edit: 'Editar',
|
common_edit: 'Editar',
|
||||||
common_save: 'Guardar',
|
common_save: 'Guardar',
|
||||||
@@ -242,6 +244,8 @@ export const messages = {
|
|||||||
smtp_test_send: 'Send test',
|
smtp_test_send: 'Send test',
|
||||||
smtp_test_sending: 'Sending...',
|
smtp_test_sending: 'Sending...',
|
||||||
smtp_test_success: 'Test email sent successfully to',
|
smtp_test_success: 'Test email sent successfully to',
|
||||||
|
password_show: 'Show password',
|
||||||
|
password_hide: 'Hide password',
|
||||||
common_new: 'New',
|
common_new: 'New',
|
||||||
common_edit: 'Edit',
|
common_edit: 'Edit',
|
||||||
common_save: 'Save',
|
common_save: 'Save',
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { getSmtpConfig, updateSmtpConfig, testSmtpConfig } from '../../services/
|
|||||||
import { Field, FieldRow } from '../../components/Field.jsx';
|
import { Field, FieldRow } from '../../components/Field.jsx';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
import { InfoBar } from '../../components/InfoBar.jsx';
|
import { InfoBar } from '../../components/InfoBar.jsx';
|
||||||
|
import { PasswordInput } from '../../components/PasswordInput.jsx';
|
||||||
|
|
||||||
export default function SmtpSettingsPanel() {
|
export default function SmtpSettingsPanel() {
|
||||||
const { t, pushToast } = useUi();
|
const { t, pushToast } = useUi();
|
||||||
@@ -66,7 +67,7 @@ export default function SmtpSettingsPanel() {
|
|||||||
</Field>
|
</Field>
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
<Field label={t('smtp_pass')} hint={config.configured ? t('smtp_pass_configured_hint') : undefined}>
|
<Field label={t('smtp_pass')} hint={config.configured ? t('smtp_pass_configured_hint') : undefined}>
|
||||||
<input className="input" type="password" value={form.pass} onChange={(e) => setForm((f) => ({ ...f, pass: e.target.value }))} />
|
<PasswordInput value={form.pass} onChange={(e) => setForm((f) => ({ ...f, pass: e.target.value }))} />
|
||||||
</Field>
|
</Field>
|
||||||
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
<Button variant="accent" icon={Save} onClick={save}>{t('common_save')}</Button>
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuth } from '../../context/auth-context.jsx';
|
|||||||
import { useUi } from '../../context/ui-context.jsx';
|
import { useUi } from '../../context/ui-context.jsx';
|
||||||
import { Field } from '../../components/Field.jsx';
|
import { Field } from '../../components/Field.jsx';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
|
import { PasswordInput } from '../../components/PasswordInput.jsx';
|
||||||
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
||||||
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ export default function LoginPage() {
|
|||||||
<input className="input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
<input className="input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required autoFocus />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('login_password')}>
|
<Field label={t('login_password')}>
|
||||||
<input className="input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
<Button variant="accent" icon={LogIn} type="submit" disabled={loading} style={{ width: '100%', justifyContent: 'center' }}>
|
<Button variant="accent" icon={LogIn} type="submit" disabled={loading} style={{ width: '100%', justifyContent: 'center' }}>
|
||||||
{t('login_submit')}
|
{t('login_submit')}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useUi } from '../../context/ui-context.jsx';
|
|||||||
import { register } from '../../services/auth.js';
|
import { register } from '../../services/auth.js';
|
||||||
import { Field } from '../../components/Field.jsx';
|
import { Field } from '../../components/Field.jsx';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
|
import { PasswordInput } from '../../components/PasswordInput.jsx';
|
||||||
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
||||||
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
||||||
|
|
||||||
@@ -61,10 +62,10 @@ export default function RegisterPage() {
|
|||||||
<input className="input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
<input className="input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('login_password')}>
|
<Field label={t('login_password')}>
|
||||||
<input className="input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('register_password_confirm')}>
|
<Field label={t('register_password_confirm')}>
|
||||||
<input className="input" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required />
|
<PasswordInput value={confirm} onChange={(e) => setConfirm(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
<Button variant="accent" icon={UserPlus} type="submit" disabled={loading} style={{ width: '100%', justifyContent: 'center' }}>
|
<Button variant="accent" icon={UserPlus} type="submit" disabled={loading} style={{ width: '100%', justifyContent: 'center' }}>
|
||||||
{t('register_submit')}
|
{t('register_submit')}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useUi } from '../../context/ui-context.jsx';
|
|||||||
import { resetPassword } from '../../services/auth.js';
|
import { resetPassword } from '../../services/auth.js';
|
||||||
import { Field } from '../../components/Field.jsx';
|
import { Field } from '../../components/Field.jsx';
|
||||||
import { Button } from '../../components/Button.jsx';
|
import { Button } from '../../components/Button.jsx';
|
||||||
|
import { PasswordInput } from '../../components/PasswordInput.jsx';
|
||||||
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
import logoDark from '../../assets/logoLinkedCorpTxt.png';
|
||||||
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
import logoLight from '../../assets/logoLinkedCorpTxtBlack.png';
|
||||||
|
|
||||||
@@ -49,10 +50,10 @@ export default function ResetPasswordPage() {
|
|||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Field label={t('reset_password_new')}>
|
<Field label={t('reset_password_new')}>
|
||||||
<input className="input" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required autoFocus />
|
<PasswordInput value={password} onChange={(e) => setPassword(e.target.value)} required autoFocus />
|
||||||
</Field>
|
</Field>
|
||||||
<Field label={t('register_password_confirm')}>
|
<Field label={t('register_password_confirm')}>
|
||||||
<input className="input" type="password" value={confirm} onChange={(e) => setConfirm(e.target.value)} required />
|
<PasswordInput value={confirm} onChange={(e) => setConfirm(e.target.value)} required />
|
||||||
</Field>
|
</Field>
|
||||||
<Button variant="accent" icon={KeyRound} type="submit" disabled={loading || !token} style={{ width: '100%', justifyContent: 'center' }}>
|
<Button variant="accent" icon={KeyRound} type="submit" disabled={loading || !token} style={{ width: '100%', justifyContent: 'center' }}>
|
||||||
{t('reset_password_submit')}
|
{t('reset_password_submit')}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { Field, FieldRow } from '../../components/Field.jsx';
|
|||||||
import { ComboBox } from '../../components/ComboBox.jsx';
|
import { ComboBox } from '../../components/ComboBox.jsx';
|
||||||
import { Badge } from '../../components/Badge.jsx';
|
import { Badge } from '../../components/Badge.jsx';
|
||||||
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
import { DetailPopup } from '../../components/DetailPopup.jsx';
|
||||||
|
import { PasswordInput } from '../../components/PasswordInput.jsx';
|
||||||
|
|
||||||
function flatten(nodes, acc = []) {
|
function flatten(nodes, acc = []) {
|
||||||
for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); }
|
for (const n of nodes) { acc.push(n); if (n.children?.length) flatten(n.children, acc); }
|
||||||
@@ -169,7 +170,7 @@ export default function UsersPanel() {
|
|||||||
</Field>
|
</Field>
|
||||||
{!selected && (
|
{!selected && (
|
||||||
<Field label={t('user_password')}>
|
<Field label={t('user_password')}>
|
||||||
<input className="input" type="password" value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} />
|
<PasswordInput value={form.password} onChange={(e) => setForm((f) => ({ ...f, password: e.target.value }))} />
|
||||||
</Field>
|
</Field>
|
||||||
)}
|
)}
|
||||||
</FieldRow>
|
</FieldRow>
|
||||||
|
|||||||
@@ -124,6 +124,15 @@ button { font-family: inherit; }
|
|||||||
.search-input input { padding-left: 32px; }
|
.search-input input { padding-left: 32px; }
|
||||||
.search-input svg { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: rgb(var(--muted)); }
|
.search-input svg { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); width: 15px; height: 15px; color: rgb(var(--muted)); }
|
||||||
|
|
||||||
|
/* --- Password reveal toggle: input queda dentro para no romper el layout de .field --- */
|
||||||
|
.password-input { position: relative; width: 100%; }
|
||||||
|
.password-input input { padding-right: 34px; }
|
||||||
|
.password-input .password-toggle {
|
||||||
|
position: absolute; right: 2px; top: 50%; transform: translateY(-50%);
|
||||||
|
width: 28px; height: 28px;
|
||||||
|
}
|
||||||
|
.password-input .password-toggle svg { width: 16px; height: 16px; }
|
||||||
|
|
||||||
.list-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); overflow: hidden; }
|
.list-card { background: rgb(var(--surface)); border: 1px solid rgb(var(--line)); border-radius: var(--r-lg); overflow: hidden; }
|
||||||
table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
table.list-table { width: 100%; border-collapse: collapse; font-size: .8rem; }
|
||||||
.list-table th {
|
.list-table th {
|
||||||
|
|||||||
Reference in New Issue
Block a user