@zauru-sdk/components 2.0.118 → 2.0.120

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,8 +10,7 @@ type Props = {
10
10
  helpText?: string;
11
11
  hint?: string;
12
12
  onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
13
- disabled?: boolean;
14
- readOnly?: boolean;
13
+ readOnly?: boolean; // <-- Usamos readOnly en lugar de disabled
15
14
  fileTypes?: string[];
16
15
  showAvailableTypes?: boolean;
17
16
  className?: string;
@@ -28,12 +27,11 @@ export const FileUploadField = (props: Props) => {
28
27
  helpText,
29
28
  hint,
30
29
  onChange,
31
- disabled = false,
32
30
  readOnly = false,
33
31
  fileTypes = [],
34
32
  showAvailableTypes = false,
35
33
  className,
36
- defaultValue = undefined,
34
+ defaultValue,
37
35
  download = false,
38
36
  required = false,
39
37
  } = props;
@@ -41,113 +39,126 @@ export const FileUploadField = (props: Props) => {
41
39
  const {
42
40
  register: tempRegister,
43
41
  formState: { errors },
44
- } = useFormContext() || { formState: {} }; // Obtener el contexto solo si existe
45
- const error = errors ? errors[props.name ?? "-1"] : undefined;
46
- const register = tempRegister
47
- ? tempRegister(props.name ?? "-1", { required })
48
- : undefined; // Solo usar register si está disponible
42
+ } = useFormContext() || { formState: {} };
43
+
44
+ const error = errors ? errors[name] : undefined;
45
+ const register = tempRegister ? tempRegister(name, { required }) : undefined;
49
46
 
50
47
  const [showTooltip, setShowTooltip] = useState<boolean>(false);
51
48
 
52
- if (typeof defaultValue == "string") {
49
+ // Para mostrar en el hint los tipos de archivo permitidos (opcional)
50
+ let hintMessage = hint;
51
+ if (showAvailableTypes && fileTypes.length > 0) {
52
+ hintMessage = `${hint || ""} Archivos permitidos: ${fileTypes.join(", ")}`;
53
+ }
54
+
55
+ // Clases de estilo basadas en si hay error (color rojo) o no (gris),
56
+ // pero ahora ignoramos el "disabled" y nos centramos en "readOnly".
57
+ const color = error ? "red" : "gray";
58
+ const isReadOnly = readOnly;
59
+ // En modo readOnly, puedes poner un fondo distinto, o dejarlo en blanco
60
+ const bgColor = isReadOnly ? "bg-gray-100" : `bg-${color}-50`;
61
+ const textColor = isReadOnly ? "text-gray-700" : `text-${color}-900`;
62
+ const borderColor = error ? "border-red-500" : `border-${color}-500`;
63
+
64
+ /**
65
+ * onChange normal del input.
66
+ * Sólo se llama cuando readOnly es false (porque si es true ni renderizamos el input).
67
+ */
68
+ const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
69
+ onChange && onChange(event);
70
+ // Si usas register, la parte interna de react-hook-form también se encargará
71
+ // del cambio, no necesitas llamarlo manualmente.
72
+ };
73
+
74
+ /**
75
+ * Para el "preview" cuando `defaultValue` es string:
76
+ * - Si `download` es true, mostramos un icono de descarga.
77
+ * - Si `download` es false, mostramos la imagen en miniatura.
78
+ * El click abre la URL en nueva ventana.
79
+ */
80
+ function renderPreview(defaultValue: string) {
53
81
  if (download) {
82
+ // Botón de descarga
54
83
  return (
55
84
  <div
56
85
  role="button"
57
86
  tabIndex={0}
58
- onClick={() => {
59
- window.open(defaultValue, "_blank");
60
- }}
87
+ onClick={() => window.open(defaultValue, "_blank")}
61
88
  onKeyDown={(event) => {
62
- // Permite que el evento se active con la tecla Enter
63
89
  if (event.key === "Enter") {
64
90
  window.open(defaultValue, "_blank");
65
91
  }
66
92
  }}
93
+ className="inline-flex items-center cursor-pointer"
67
94
  >
68
- {title && (
69
- <label
70
- htmlFor={name}
71
- className="block mb-1 text-sm font-medium text-gray-700"
72
- >
73
- {title}
74
- </label>
75
- )}{" "}
76
95
  <DownloadIconSVG />
96
+ <span className="ml-1 text-blue-600 underline">
97
+ Descargar archivo
98
+ </span>
77
99
  </div>
78
100
  );
79
- }
80
- return (
81
- <div className={`col-span-6 sm:col-span-3 ${className}`}>
82
- {title && (
83
- <label
84
- htmlFor={name}
85
- className={`block mb-1 text-sm font-medium text-gray-700`}
86
- >
87
- {title}
88
- {required && <span className="text-red-500">*</span>}
89
- </label>
90
- )}{" "}
101
+ } else {
102
+ // Vista previa como imagen
103
+ return (
91
104
  <div
92
105
  role="button"
93
106
  tabIndex={0}
94
- onClick={() => {
95
- if (register) {
96
- register.onChange({
97
- target: {
98
- value: defaultValue,
99
- },
100
- });
101
- }
102
- window.open(defaultValue, "_blank");
103
- }}
107
+ onClick={() => window.open(defaultValue, "_blank")}
104
108
  onKeyDown={(event) => {
105
- // Permite que el evento se active con la tecla Enter
106
109
  if (event.key === "Enter") {
107
- if (register) {
108
- register.onChange({
109
- target: {
110
- value: defaultValue,
111
- },
112
- });
113
- }
114
110
  window.open(defaultValue, "_blank");
115
111
  }
116
112
  }}
113
+ className="inline-block cursor-pointer"
117
114
  >
118
115
  <img
119
116
  src={defaultValue}
120
117
  alt={name}
121
- className={`h-48 w-48 inline mr-1 pb-1`}
118
+ className="h-48 w-48 inline mr-1 pb-1"
122
119
  style={{
123
- stroke: "currentColor",
124
- strokeWidth: 2,
125
- strokeLinecap: "round",
126
- strokeLinejoin: "round",
127
- fill: "none",
120
+ objectFit: "contain",
128
121
  backgroundColor: "transparent",
129
122
  }}
130
123
  />
131
124
  </div>
132
- </div>
133
- );
125
+ );
126
+ }
134
127
  }
135
128
 
136
- const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
137
- onChange && onChange(event);
138
- };
129
+ /**
130
+ * 1) Si readOnly = true:
131
+ * - Si defaultValue es string -> Sólo mostramos el preview (descarga/imagen).
132
+ * - Si no hay defaultValue (o es File) -> mostramos "nada" o un texto de "Sin archivo".
133
+ */
134
+ if (readOnly) {
135
+ return (
136
+ <div className={`col-span-6 sm:col-span-3 ${className}`}>
137
+ {title && (
138
+ <label
139
+ htmlFor={name}
140
+ className={`block mb-1 text-sm font-medium text-gray-700`}
141
+ >
142
+ {title}
143
+ </label>
144
+ )}
139
145
 
140
- let hintMessage = hint;
141
- if (showAvailableTypes && fileTypes.length > 0) {
142
- hintMessage = `${hint} Archivos permitidos: ${fileTypes.join(", ")}`;
146
+ {typeof defaultValue === "string" && defaultValue ? (
147
+ renderPreview(defaultValue)
148
+ ) : (
149
+ <div className="text-sm italic text-gray-400">
150
+ No hay archivo disponible
151
+ </div>
152
+ )}
153
+ </div>
154
+ );
143
155
  }
144
156
 
145
- const color = error ? "red" : "gray";
146
- const isReadOnly = disabled || readOnly;
147
- const bgColor = isReadOnly ? "bg-gray-200" : `bg-${color}-50`;
148
- const textColor = isReadOnly ? "text-gray-500" : `text-${color}-900`;
149
- const borderColor = isReadOnly ? "border-gray-300" : `border-${color}-500`;
150
-
157
+ /**
158
+ * 2) readOnly = false:
159
+ * - Si hay defaultValue y es string, mostramos la vista previa + el input
160
+ * - Si no hay defaultValue (o no es string) mostramos solo el input
161
+ */
151
162
  return (
152
163
  <div className={`col-span-6 sm:col-span-3 ${className}`}>
153
164
  {title && (
@@ -156,19 +167,28 @@ export const FileUploadField = (props: Props) => {
156
167
  className={`block mb-1 text-sm font-medium text-${color}-700`}
157
168
  >
158
169
  {title}
170
+ {required && <span className="text-red-500 ml-1">*</span>}
159
171
  </label>
160
172
  )}
173
+
174
+ {/* Mostrar la vista previa si defaultValue es string */}
175
+ {typeof defaultValue === "string" && defaultValue && (
176
+ <div className="mb-2">{renderPreview(defaultValue)}</div>
177
+ )}
178
+
179
+ {/* Input para cambiar/cargar archivo */}
161
180
  <div className="flex relative items-center">
162
181
  <input
163
182
  type="file"
164
- name={name}
165
183
  id={id ?? name}
166
- disabled={disabled}
167
- readOnly={readOnly}
168
184
  accept={fileTypes.map((ft) => `.${ft}`).join(", ")}
169
- onChange={handleInputChange}
170
185
  className={`block w-full rounded-md ${bgColor} ${borderColor} ${textColor} shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm`}
186
+ {...(register ?? {})}
187
+ name={name}
188
+ onChange={handleInputChange}
171
189
  />
190
+
191
+ {/* Botón de ayuda con tooltip */}
172
192
  {helpText && (
173
193
  <div className="flex items-center relative ml-3">
174
194
  <div
@@ -186,11 +206,14 @@ export const FileUploadField = (props: Props) => {
186
206
  </div>
187
207
  )}
188
208
  </div>
209
+
210
+ {/* Mensaje de error */}
189
211
  {error && (
190
- <p className={`mt-2 text-sm text-${color}-600`}>
212
+ <p className={`mt-2 text-sm text-red-600`}>
191
213
  <span className="font-medium">Oops!</span> {error.message?.toString()}
192
214
  </p>
193
215
  )}
216
+ {/* Hint (si no hay error) */}
194
217
  {!error && hintMessage && (
195
218
  <p className={`mt-2 italic text-sm text-${color}-500`}>{hintMessage}</p>
196
219
  )}
@@ -44,10 +44,23 @@ export const ReactZodForm = (props: Props) => {
44
44
  const handleSubmit: SubmitHandler<FieldValues> = (data, event) => {
45
45
  if (onSubmit) {
46
46
  onSubmit(data, event);
47
- } else {
48
- // If no onSubmit is provided, use Remix's submit function
49
- submit(event?.target as HTMLFormElement, { method });
47
+ return;
50
48
  }
49
+
50
+ const form = event?.target as HTMLFormElement;
51
+ const nativeEvent = event?.nativeEvent as SubmitEvent;
52
+
53
+ // Armamos el FormData a partir del propio <form>
54
+ const formData = new FormData(form);
55
+
56
+ // Detectamos el botón que disparó el submit (submitter),
57
+ // en caso de que tenga un name/value, lo agregamos al formData
58
+ const submitter = nativeEvent.submitter;
59
+ if (submitter instanceof HTMLButtonElement && submitter.name) {
60
+ formData.append(submitter.name, submitter.value);
61
+ }
62
+
63
+ submit(formData, { method, encType });
51
64
  };
52
65
 
53
66
  return (