Ahora si guarda y edita de forma correcta
This commit is contained in:
@@ -331,18 +331,48 @@ async function fetchApi<T = any>(
|
||||
// Manejo especial para errores 422 (validation error)
|
||||
if (response.status === 422) {
|
||||
// HTTPException(detail={ message, errors }) — catálogo / CSV parity
|
||||
const det = data.detail;
|
||||
const validationErrors = (errors: unknown[]) => errors as NonNullable<ApiResponse['validationErrors']>;
|
||||
const det = data.detail || (typeof data.message === 'object' ? data.message : null);
|
||||
if (
|
||||
det &&
|
||||
typeof det === 'object' &&
|
||||
typeof det === "object" &&
|
||||
!Array.isArray(det) &&
|
||||
Array.isArray((det as { errors?: unknown }).errors)
|
||||
) {
|
||||
const d = det as { message?: string; errors: unknown[] };
|
||||
const d = det as {
|
||||
message?: string;
|
||||
errors: Array<{ col?: string; msg?: string; field?: string; message?: string }>;
|
||||
};
|
||||
|
||||
// Mapping for catalog column names to DTO field names
|
||||
const colToField: Record<string, string> = {
|
||||
"CLAVE TRANSPORTISTA": "transporter_key",
|
||||
NOMBRE: "name",
|
||||
"NOMBRE CORTO": "short_name",
|
||||
RESPONSABLE: "responsible",
|
||||
RFC: "rfc",
|
||||
CALLES: "streets",
|
||||
"CODIGO POSTAL": "postal_code",
|
||||
CIUDAD: "city",
|
||||
ESTADO: "state",
|
||||
PAIS: "country",
|
||||
"CODIGO CARGADOR": "loader_code",
|
||||
"CODIGO CAAT": "caat_code",
|
||||
"CODIGO TRANS": "transport_code",
|
||||
"TIPO INTERFASE TRANS": "transport_interface_type",
|
||||
"SERVIDOR FTP": "ftp_server",
|
||||
"USUARIO FTP": "ftp_user",
|
||||
"CLAVE ACCESO FTP": "ftp_password",
|
||||
"DIRECTORIO FTP": "ftp_directory"
|
||||
};
|
||||
|
||||
const normalizedErrors = d.errors.map((err) => ({
|
||||
field: err.field || (err.col ? colToField[err.col] || err.col : ""),
|
||||
message: err.message || err.msg || "Error de validación"
|
||||
}));
|
||||
|
||||
return {
|
||||
error: d.message || 'Error de validación',
|
||||
validationErrors: validationErrors(d.errors),
|
||||
error: d.message || (typeof data.message === 'string' ? data.message : 'Error de validación'),
|
||||
validationErrors: normalizedErrors,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
@@ -350,21 +380,31 @@ async function fetchApi<T = any>(
|
||||
if (data.errors && Array.isArray(data.errors)) {
|
||||
return {
|
||||
error: data.message || 'Error de validación',
|
||||
validationErrors: validationErrors(data.errors),
|
||||
validationErrors: data.errors as NonNullable<ApiResponse['validationErrors']>,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
// Errores de validación de FastAPI (con detail)
|
||||
else if (data.detail) {
|
||||
let errorMessage = 'Error de validación: ';
|
||||
const vErrors: NonNullable<ApiResponse['validationErrors']> = [];
|
||||
|
||||
// FastAPI devuelve errores de validación en data.detail como array
|
||||
if (Array.isArray(data.detail)) {
|
||||
const errors = data.detail.map((err: any) => {
|
||||
data.detail.forEach((err: any) => {
|
||||
const fieldPath = err.loc ? err.loc.filter((l: any) => l !== 'body').join('.') : 'campo';
|
||||
const msg = humanizeValidationMessage(err.msg || 'error de validación');
|
||||
|
||||
vErrors.push({
|
||||
field: err.loc ? String(err.loc[err.loc.length - 1]) : 'campo',
|
||||
message: msg
|
||||
});
|
||||
});
|
||||
|
||||
errorMessage += data.detail.map((err: any) => {
|
||||
const field = err.loc ? err.loc.join('.') : 'campo desconocido';
|
||||
return `${field}: ${err.msg}`;
|
||||
}).join(', ');
|
||||
errorMessage += errors;
|
||||
} else if (typeof data.detail === 'string') {
|
||||
errorMessage = data.detail;
|
||||
} else {
|
||||
@@ -373,6 +413,7 @@ async function fetchApi<T = any>(
|
||||
|
||||
return {
|
||||
error: errorMessage,
|
||||
validationErrors: vErrors.length ? vErrors : undefined,
|
||||
status: response.status
|
||||
};
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-3xl">
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-3xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
|
||||
@@ -168,7 +168,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
|
||||
@@ -52,6 +52,7 @@
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let fieldErrors = $state<Record<string, string>>({});
|
||||
let countries = $state<Country[]>([]);
|
||||
let states = $state<State[]>([]);
|
||||
let refsLoading = $state(false);
|
||||
@@ -109,6 +110,7 @@
|
||||
$effect(() => {
|
||||
if (!open) {
|
||||
error = null;
|
||||
fieldErrors = {};
|
||||
loading = false;
|
||||
return;
|
||||
}
|
||||
@@ -120,9 +122,89 @@
|
||||
void loadReferenceData();
|
||||
});
|
||||
|
||||
function validateForm(): boolean {
|
||||
const errors: Record<string, string> = {};
|
||||
|
||||
// Clave del transportista
|
||||
if (!formData.transporter_key?.trim()) {
|
||||
errors.transporter_key = 'La clave es obligatoria';
|
||||
} else if (/\s/.test(formData.transporter_key)) {
|
||||
errors.transporter_key = 'La clave no puede contener espacios';
|
||||
} else if (!/^[A-Za-z0-9_-]+$/.test(formData.transporter_key)) {
|
||||
errors.transporter_key = 'La clave solo permite letras, números, guiones y guiones bajos';
|
||||
}
|
||||
|
||||
// Nombre / Razón Social
|
||||
if (!formData.name?.trim()) {
|
||||
errors.name = 'El nombre o razón social es obligatorio';
|
||||
}
|
||||
|
||||
// RFC (Opcional, pero si se pone debe ser válido si es MX)
|
||||
if (formData.rfc?.trim()) {
|
||||
const rfcRegex =
|
||||
/^([A-ZÑ&]{3,4}) ?(?:- ?)?(\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\d|3[01])) ?(?:- ?)?([A-Z\d]{2})([A-Z\d])$/i;
|
||||
if (formData.country === 'MEX' && !rfcRegex.test(formData.rfc.trim())) {
|
||||
errors.rfc = 'Formato de RFC inválido para México';
|
||||
}
|
||||
}
|
||||
|
||||
// Código Postal
|
||||
if (formData.postal_code?.trim()) {
|
||||
if (formData.country === 'MEX' && !/^\d{5}$/.test(formData.postal_code.trim())) {
|
||||
errors.postal_code = 'El código postal en México debe ser de 5 dígitos';
|
||||
} else if (!/^\d+$/.test(formData.postal_code.trim())) {
|
||||
errors.postal_code = 'El código postal debe ser numérico';
|
||||
}
|
||||
}
|
||||
|
||||
// Códigos de transporte
|
||||
if (formData.caat_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.caat_code)) {
|
||||
errors.caat_code = 'El código CAAT debe ser alfanumérico';
|
||||
}
|
||||
if (formData.transport_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.transport_code)) {
|
||||
errors.transport_code = 'El código de transporte debe ser alfanumérico';
|
||||
}
|
||||
if (formData.loader_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.loader_code)) {
|
||||
errors.loader_code = 'El código de cargador debe ser alfanumérico';
|
||||
}
|
||||
if (formData.filler_code?.trim() && !/^[A-Za-z0-9]+$/.test(formData.filler_code)) {
|
||||
errors.filler_code = 'El código de relleno debe ser alfanumérico';
|
||||
}
|
||||
|
||||
// Configuración FTP
|
||||
if (formData.ftp_server?.trim()) {
|
||||
const hostRegex =
|
||||
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$/;
|
||||
const ipRegex =
|
||||
/^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||
if (!hostRegex.test(formData.ftp_server) && !ipRegex.test(formData.ftp_server)) {
|
||||
errors.ftp_server = 'Servidor FTP inválido (debe ser un host o IP)';
|
||||
}
|
||||
}
|
||||
if (formData.ftp_user?.trim() && /\s/.test(formData.ftp_user)) {
|
||||
errors.ftp_user = 'El usuario FTP no puede contener espacios';
|
||||
}
|
||||
|
||||
fieldErrors = errors;
|
||||
return Object.keys(errors).length === 0;
|
||||
}
|
||||
|
||||
function clearFieldError(field: string) {
|
||||
if (fieldErrors[field]) {
|
||||
fieldErrors[field] = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
if (loading) return;
|
||||
error = null;
|
||||
fieldErrors = {};
|
||||
|
||||
if (!validateForm()) {
|
||||
error = 'Por favor, corrige los errores en el formulario';
|
||||
return;
|
||||
}
|
||||
|
||||
loading = true;
|
||||
|
||||
try {
|
||||
@@ -131,11 +213,6 @@
|
||||
throw new Error('No hay una compañía seleccionada');
|
||||
}
|
||||
|
||||
// Validación básica
|
||||
if (!formData.transporter_key.trim()) {
|
||||
throw new Error('La clave es requerida');
|
||||
}
|
||||
|
||||
let response;
|
||||
if (isEdit && item) {
|
||||
response = await transportersApi.update(item.transporter_key, formData, companyId);
|
||||
@@ -144,9 +221,17 @@
|
||||
}
|
||||
|
||||
if (response.error) {
|
||||
const ve = (response as { validationErrors?: { msg?: string }[] }).validationErrors;
|
||||
const ve = response.validationErrors;
|
||||
if (ve?.length) {
|
||||
throw new Error(ve.map((e) => e.msg).join(' · '));
|
||||
// Mapear errores de validación del backend si están disponibles
|
||||
const backendErrors: Record<string, string> = {};
|
||||
ve.forEach((err) => {
|
||||
if (err.field) {
|
||||
backendErrors[err.field] = err.message || 'Error de validación';
|
||||
}
|
||||
});
|
||||
fieldErrors = backendErrors;
|
||||
throw new Error('Errores de validación en el servidor');
|
||||
}
|
||||
throw new Error(response.error);
|
||||
}
|
||||
@@ -180,7 +265,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
||||
<Dialog.Content class="max-h-[90vh] overflow-y-auto sm:max-w-5xl" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
@@ -214,14 +299,28 @@
|
||||
id="transporter_key"
|
||||
bind:value={formData.transporter_key}
|
||||
disabled={isEdit}
|
||||
aria-invalid={!!fieldErrors.transporter_key}
|
||||
oninput={() => clearFieldError('transporter_key')}
|
||||
required
|
||||
maxlength={30}
|
||||
/>
|
||||
{#if fieldErrors.transporter_key}
|
||||
<p class="text-xs text-destructive">{fieldErrors.transporter_key}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="name">Nombre / Razón Social</Label>
|
||||
<Input id="name" bind:value={formData.name} maxlength={256} />
|
||||
<Label for="name">Nombre / Razón Social <span class="text-destructive">*</span></Label>
|
||||
<Input
|
||||
id="name"
|
||||
bind:value={formData.name}
|
||||
aria-invalid={!!fieldErrors.name}
|
||||
oninput={() => clearFieldError('name')}
|
||||
maxlength={256}
|
||||
/>
|
||||
{#if fieldErrors.name}
|
||||
<p class="text-xs text-destructive">{fieldErrors.name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -229,19 +328,42 @@
|
||||
<Input
|
||||
id="short_name"
|
||||
bind:value={formData.short_name}
|
||||
aria-invalid={!!fieldErrors.short_name}
|
||||
oninput={() => clearFieldError('short_name')}
|
||||
maxlength={10}
|
||||
placeholder="Máx. 10 car."
|
||||
/>
|
||||
{#if fieldErrors.short_name}
|
||||
<p class="text-xs text-destructive">{fieldErrors.short_name}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="rfc">RFC</Label>
|
||||
<Input id="rfc" bind:value={formData.rfc} maxlength={30} />
|
||||
<Input
|
||||
id="rfc"
|
||||
bind:value={formData.rfc}
|
||||
aria-invalid={!!fieldErrors.rfc}
|
||||
oninput={() => clearFieldError('rfc')}
|
||||
maxlength={30}
|
||||
/>
|
||||
{#if fieldErrors.rfc}
|
||||
<p class="text-xs text-destructive">{fieldErrors.rfc}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="responsible">Responsable</Label>
|
||||
<Input id="responsible" bind:value={formData.responsible} maxlength={100} />
|
||||
<Input
|
||||
id="responsible"
|
||||
bind:value={formData.responsible}
|
||||
aria-invalid={!!fieldErrors.responsible}
|
||||
oninput={() => clearFieldError('responsible')}
|
||||
maxlength={100}
|
||||
/>
|
||||
{#if fieldErrors.responsible}
|
||||
<p class="text-xs text-destructive">{fieldErrors.responsible}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -251,7 +373,16 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="caat_code">Código CAAT</Label>
|
||||
<Input id="caat_code" bind:value={formData.caat_code} maxlength={49} />
|
||||
<Input
|
||||
id="caat_code"
|
||||
bind:value={formData.caat_code}
|
||||
aria-invalid={!!fieldErrors.caat_code}
|
||||
oninput={() => clearFieldError('caat_code')}
|
||||
maxlength={49}
|
||||
/>
|
||||
{#if fieldErrors.caat_code}
|
||||
<p class="text-xs text-destructive">{fieldErrors.caat_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -259,9 +390,14 @@
|
||||
<Input
|
||||
id="transport_code"
|
||||
bind:value={formData.transport_code}
|
||||
aria-invalid={!!fieldErrors.transport_code}
|
||||
oninput={() => clearFieldError('transport_code')}
|
||||
maxlength={8}
|
||||
placeholder="Máx. 8 car."
|
||||
/>
|
||||
{#if fieldErrors.transport_code}
|
||||
<p class="text-xs text-destructive">{fieldErrors.transport_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -269,9 +405,14 @@
|
||||
<Input
|
||||
id="loader_code"
|
||||
bind:value={formData.loader_code}
|
||||
aria-invalid={!!fieldErrors.loader_code}
|
||||
oninput={() => clearFieldError('loader_code')}
|
||||
maxlength={9}
|
||||
placeholder="Máx. 9 car."
|
||||
/>
|
||||
{#if fieldErrors.loader_code}
|
||||
<p class="text-xs text-destructive">{fieldErrors.loader_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
@@ -279,13 +420,27 @@
|
||||
<Input
|
||||
id="transport_interface_type"
|
||||
bind:value={formData.transport_interface_type}
|
||||
aria-invalid={!!fieldErrors.transport_interface_type}
|
||||
oninput={() => clearFieldError('transport_interface_type')}
|
||||
maxlength={20}
|
||||
/>
|
||||
{#if fieldErrors.transport_interface_type}
|
||||
<p class="text-xs text-destructive">{fieldErrors.transport_interface_type}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="filler_code">Código Relleno</Label>
|
||||
<Input id="filler_code" bind:value={formData.filler_code} maxlength={20} />
|
||||
<Input
|
||||
id="filler_code"
|
||||
bind:value={formData.filler_code}
|
||||
aria-invalid={!!fieldErrors.filler_code}
|
||||
oninput={() => clearFieldError('filler_code')}
|
||||
maxlength={20}
|
||||
/>
|
||||
{#if fieldErrors.filler_code}
|
||||
<p class="text-xs text-destructive">{fieldErrors.filler_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between gap-3 rounded-lg border p-4">
|
||||
@@ -300,18 +455,44 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="streets">Calle y Número</Label>
|
||||
<Input id="streets" bind:value={formData.streets} maxlength={100} />
|
||||
<Input
|
||||
id="streets"
|
||||
bind:value={formData.streets}
|
||||
aria-invalid={!!fieldErrors.streets}
|
||||
oninput={() => clearFieldError('streets')}
|
||||
maxlength={100}
|
||||
/>
|
||||
{#if fieldErrors.streets}
|
||||
<p class="text-xs text-destructive">{fieldErrors.streets}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="city">Ciudad</Label>
|
||||
<Input id="city" bind:value={formData.city} maxlength={30} />
|
||||
<Input
|
||||
id="city"
|
||||
bind:value={formData.city}
|
||||
aria-invalid={!!fieldErrors.city}
|
||||
oninput={() => clearFieldError('city')}
|
||||
maxlength={30}
|
||||
/>
|
||||
{#if fieldErrors.city}
|
||||
<p class="text-xs text-destructive">{fieldErrors.city}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="country">País (clave americana)</Label>
|
||||
<Select.Root type="single" bind:value={formData.country} disabled={refsLoading}>
|
||||
<Select.Trigger class="w-full" id="country">
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={formData.country}
|
||||
disabled={refsLoading}
|
||||
onValueChange={() => clearFieldError('country')}
|
||||
>
|
||||
<Select.Trigger
|
||||
class={fieldErrors.country ? 'border-destructive' : ''}
|
||||
id="country"
|
||||
>
|
||||
{refsLoading
|
||||
? '...'
|
||||
: formData.country
|
||||
@@ -327,14 +508,25 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if fieldErrors.country}
|
||||
<p class="text-xs text-destructive">{fieldErrors.country}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="state">Estado / provincia</Label>
|
||||
<Select.Root type="single" bind:value={formData.state} disabled={refsLoading}>
|
||||
<Select.Trigger class="w-full" id="state">
|
||||
<Select.Root
|
||||
type="single"
|
||||
bind:value={formData.state}
|
||||
disabled={refsLoading}
|
||||
onValueChange={() => clearFieldError('state')}
|
||||
>
|
||||
<Select.Trigger
|
||||
class={fieldErrors.state ? 'border-destructive' : ''}
|
||||
id="state"
|
||||
>
|
||||
{refsLoading
|
||||
? '...'
|
||||
: formData.state ||
|
||||
@@ -349,10 +541,22 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
{#if fieldErrors.state}
|
||||
<p class="text-xs text-destructive">{fieldErrors.state}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="postal_code">C.P.</Label>
|
||||
<Input id="postal_code" bind:value={formData.postal_code} maxlength={15} />
|
||||
<Input
|
||||
id="postal_code"
|
||||
bind:value={formData.postal_code}
|
||||
aria-invalid={!!fieldErrors.postal_code}
|
||||
oninput={() => clearFieldError('postal_code')}
|
||||
maxlength={15}
|
||||
/>
|
||||
{#if fieldErrors.postal_code}
|
||||
<p class="text-xs text-destructive">{fieldErrors.postal_code}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -363,23 +567,60 @@
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="ftp_server">Servidor FTP</Label>
|
||||
<Input id="ftp_server" bind:value={formData.ftp_server} maxlength={200} />
|
||||
<Input
|
||||
id="ftp_server"
|
||||
bind:value={formData.ftp_server}
|
||||
aria-invalid={!!fieldErrors.ftp_server}
|
||||
oninput={() => clearFieldError('ftp_server')}
|
||||
maxlength={200}
|
||||
/>
|
||||
{#if fieldErrors.ftp_server}
|
||||
<p class="text-xs text-destructive">{fieldErrors.ftp_server}</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="ftp_user">Usuario</Label>
|
||||
<Input id="ftp_user" bind:value={formData.ftp_user} maxlength={200} />
|
||||
<Input
|
||||
id="ftp_user"
|
||||
bind:value={formData.ftp_user}
|
||||
aria-invalid={!!fieldErrors.ftp_user}
|
||||
oninput={() => clearFieldError('ftp_user')}
|
||||
maxlength={200}
|
||||
/>
|
||||
{#if fieldErrors.ftp_user}
|
||||
<p class="text-xs text-destructive">{fieldErrors.ftp_user}</p>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="ftp_password">Contraseña</Label>
|
||||
<Input id="ftp_password" type="password" bind:value={formData.ftp_password} maxlength={100} />
|
||||
<Input
|
||||
id="ftp_password"
|
||||
type="password"
|
||||
bind:value={formData.ftp_password}
|
||||
aria-invalid={!!fieldErrors.ftp_password}
|
||||
oninput={() => clearFieldError('ftp_password')}
|
||||
maxlength={100}
|
||||
/>
|
||||
{#if fieldErrors.ftp_password}
|
||||
<p class="text-xs text-destructive">{fieldErrors.ftp_password}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="ftp_directory">Directorio</Label>
|
||||
<Input id="ftp_directory" bind:value={formData.ftp_directory} maxlength={1000} />
|
||||
<Input
|
||||
id="ftp_directory"
|
||||
bind:value={formData.ftp_directory}
|
||||
aria-invalid={!!fieldErrors.ftp_directory}
|
||||
oninput={() => clearFieldError('ftp_directory')}
|
||||
maxlength={1000}
|
||||
/>
|
||||
{#if fieldErrors.ftp_directory}
|
||||
<p class="text-xs text-destructive">{fieldErrors.ftp_directory}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -215,7 +215,7 @@
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
<Dialog.Content class="max-h-[95vh] max-w-3xl overflow-y-auto">
|
||||
<Dialog.Content class="max-h-[95vh] max-w-3xl overflow-y-auto" onInteractOutside={(e) => e.preventDefault()}>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>{title}</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
|
||||
Reference in New Issue
Block a user