flysoft-react-ui 0.4.0 → 0.5.2
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.
- package/dist/App.d.ts.map +1 -1
- package/dist/App.js +20 -4
- package/dist/components/form-controls/AutocompleteInput.d.ts +11 -3
- package/dist/components/form-controls/AutocompleteInput.d.ts.map +1 -1
- package/dist/components/form-controls/AutocompleteInput.js +410 -31
- package/dist/components/form-controls/Button.js +1 -1
- package/dist/components/form-controls/Checkbox.d.ts +14 -0
- package/dist/components/form-controls/Checkbox.d.ts.map +1 -0
- package/dist/components/form-controls/Checkbox.js +77 -0
- package/dist/components/form-controls/DateInput.d.ts +20 -4
- package/dist/components/form-controls/DateInput.d.ts.map +1 -1
- package/dist/components/form-controls/DateInput.js +425 -70
- package/dist/components/form-controls/DatePicker.d.ts +4 -3
- package/dist/components/form-controls/DatePicker.d.ts.map +1 -1
- package/dist/components/form-controls/DatePicker.js +26 -30
- package/dist/components/form-controls/Input.d.ts +10 -1
- package/dist/components/form-controls/Input.d.ts.map +1 -1
- package/dist/components/form-controls/Input.js +16 -10
- package/dist/components/form-controls/Pagination.d.ts +1 -0
- package/dist/components/form-controls/Pagination.d.ts.map +1 -1
- package/dist/components/form-controls/Pagination.js +3 -40
- package/dist/components/form-controls/RadioButtonGroup.d.ts +62 -0
- package/dist/components/form-controls/RadioButtonGroup.d.ts.map +1 -0
- package/dist/components/form-controls/RadioButtonGroup.js +220 -0
- package/dist/components/form-controls/SearchSelectInput-OLD.d.ts +68 -0
- package/dist/components/form-controls/SearchSelectInput-OLD.d.ts.map +1 -0
- package/dist/components/form-controls/SearchSelectInput-OLD.js +962 -0
- package/dist/components/form-controls/SearchSelectInput.d.ts +70 -0
- package/dist/components/form-controls/SearchSelectInput.d.ts.map +1 -0
- package/dist/components/form-controls/SearchSelectInput.js +335 -0
- package/dist/components/form-controls/index.d.ts +7 -1
- package/dist/components/form-controls/index.d.ts.map +1 -1
- package/dist/components/form-controls/index.js +3 -0
- package/dist/components/layout/AppLayout.d.ts +3 -2
- package/dist/components/layout/AppLayout.d.ts.map +1 -1
- package/dist/components/layout/AppLayout.js +104 -31
- package/dist/components/layout/Card.d.ts +4 -1
- package/dist/components/layout/Card.d.ts.map +1 -1
- package/dist/components/layout/Card.js +30 -1
- package/dist/components/layout/Collection.js +1 -1
- package/dist/components/layout/DataTable.d.ts +29 -0
- package/dist/components/layout/DataTable.d.ts.map +1 -0
- package/dist/components/layout/DataTable.js +165 -0
- package/dist/components/layout/index.d.ts +2 -0
- package/dist/components/layout/index.d.ts.map +1 -1
- package/dist/components/layout/index.js +1 -0
- package/dist/components/utils/Avatar.d.ts +49 -0
- package/dist/components/utils/Avatar.d.ts.map +1 -0
- package/dist/components/utils/Avatar.js +93 -0
- package/dist/components/utils/Badge.d.ts +3 -0
- package/dist/components/utils/Badge.d.ts.map +1 -1
- package/dist/components/utils/Badge.js +130 -26
- package/dist/components/utils/Dialog.d.ts.map +1 -1
- package/dist/components/utils/Dialog.js +5 -1
- package/dist/components/utils/DropdownMenu.d.ts +25 -0
- package/dist/components/utils/DropdownMenu.d.ts.map +1 -0
- package/dist/components/utils/DropdownMenu.js +145 -0
- package/dist/components/utils/Filter.d.ts +57 -0
- package/dist/components/utils/Filter.d.ts.map +1 -0
- package/dist/components/utils/Filter.js +580 -0
- package/dist/components/utils/FiltersDialog.d.ts +21 -0
- package/dist/components/utils/FiltersDialog.d.ts.map +1 -0
- package/dist/components/utils/FiltersDialog.js +104 -0
- package/dist/components/utils/Loader.js +1 -1
- package/dist/components/utils/RoadMap.d.ts +59 -0
- package/dist/components/utils/RoadMap.d.ts.map +1 -0
- package/dist/components/utils/RoadMap.js +138 -0
- package/dist/components/utils/Snackbar.d.ts +13 -0
- package/dist/components/utils/Snackbar.d.ts.map +1 -0
- package/dist/components/utils/Snackbar.js +121 -0
- package/dist/components/utils/SnackbarContainer.d.ts +7 -0
- package/dist/components/utils/SnackbarContainer.d.ts.map +1 -0
- package/dist/components/utils/SnackbarContainer.js +25 -0
- package/dist/components/utils/index.d.ts +12 -0
- package/dist/components/utils/index.d.ts.map +1 -1
- package/dist/components/utils/index.js +6 -0
- package/dist/contexts/AppLayoutContext.d.ts +40 -0
- package/dist/contexts/AppLayoutContext.d.ts.map +1 -0
- package/dist/contexts/AppLayoutContext.js +98 -0
- package/dist/contexts/ListCrudContext.d.ts +29 -0
- package/dist/contexts/ListCrudContext.d.ts.map +1 -0
- package/dist/contexts/ListCrudContext.js +209 -0
- package/dist/contexts/SnackbarContext.d.ts +26 -0
- package/dist/contexts/SnackbarContext.d.ts.map +1 -0
- package/dist/contexts/SnackbarContext.js +34 -0
- package/dist/contexts/index.d.ts +6 -0
- package/dist/contexts/index.d.ts.map +1 -1
- package/dist/contexts/index.js +6 -0
- package/dist/contexts/presets.js +6 -6
- package/dist/docs/AuthDocs.tsx/AuthDocsContent.js +3 -1
- package/dist/docs/AvatarDocs.d.ts +4 -0
- package/dist/docs/AvatarDocs.d.ts.map +1 -0
- package/dist/docs/AvatarDocs.js +7 -0
- package/dist/docs/BadgeDocs.d.ts.map +1 -1
- package/dist/docs/BadgeDocs.js +4 -2
- package/dist/docs/CardDocs.d.ts.map +1 -1
- package/dist/docs/CardDocs.js +7 -1
- package/dist/docs/CheckboxDocs.d.ts +4 -0
- package/dist/docs/CheckboxDocs.d.ts.map +1 -0
- package/dist/docs/CheckboxDocs.js +7 -0
- package/dist/docs/DataTableDocs.d.ts +4 -0
- package/dist/docs/DataTableDocs.d.ts.map +1 -0
- package/dist/docs/DataTableDocs.js +244 -0
- package/dist/docs/DateInputDocs.d.ts +1 -0
- package/dist/docs/DateInputDocs.d.ts.map +1 -1
- package/dist/docs/DateInputDocs.js +7 -9
- package/dist/docs/DatePickerDocs.d.ts +1 -0
- package/dist/docs/DatePickerDocs.d.ts.map +1 -1
- package/dist/docs/DatePickerDocs.js +6 -8
- package/dist/docs/DocAdmin.d.ts +4 -0
- package/dist/docs/DocAdmin.d.ts.map +1 -0
- package/dist/docs/DocAdmin.js +68 -0
- package/dist/docs/DocsMenu.d.ts.map +1 -1
- package/dist/docs/DocsMenu.js +1 -1
- package/dist/docs/DocsRouter.d.ts.map +1 -1
- package/dist/docs/DocsRouter.js +13 -1
- package/dist/docs/DropdownMenuDocs.d.ts +4 -0
- package/dist/docs/DropdownMenuDocs.d.ts.map +1 -0
- package/dist/docs/DropdownMenuDocs.js +66 -0
- package/dist/docs/ExampleFormDocs.d.ts +4 -0
- package/dist/docs/ExampleFormDocs.d.ts.map +1 -0
- package/dist/docs/ExampleFormDocs.js +148 -0
- package/dist/docs/FilterDocs.d.ts +4 -0
- package/dist/docs/FilterDocs.d.ts.map +1 -0
- package/dist/docs/FilterDocs.js +112 -0
- package/dist/docs/InputDocs.d.ts.map +1 -1
- package/dist/docs/InputDocs.js +11 -1
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocs.d.ts +11 -0
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocs.d.ts.map +1 -0
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocs.js +25 -0
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocsContentPersonas.d.ts +2 -0
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocsContentPersonas.d.ts.map +1 -0
- package/dist/docs/ListCrudDocs.tsx/ListCrudDocsContentPersonas.js +51 -0
- package/dist/docs/PaginationDocs.js +6 -6
- package/dist/docs/RadioButtonGroupDocs.d.ts +4 -0
- package/dist/docs/RadioButtonGroupDocs.d.ts.map +1 -0
- package/dist/docs/RadioButtonGroupDocs.js +46 -0
- package/dist/docs/RoadMapDocs.d.ts +4 -0
- package/dist/docs/RoadMapDocs.d.ts.map +1 -0
- package/dist/docs/RoadMapDocs.js +171 -0
- package/dist/docs/SearchSelectInputDocs.d.ts +4 -0
- package/dist/docs/SearchSelectInputDocs.d.ts.map +1 -0
- package/dist/docs/SearchSelectInputDocs.js +168 -0
- package/dist/docs/SnackbarDocs.d.ts +4 -0
- package/dist/docs/SnackbarDocs.d.ts.map +1 -0
- package/dist/docs/SnackbarDocs.js +50 -0
- package/dist/docs/TabsGroupDocs.d.ts.map +1 -1
- package/dist/docs/TabsGroupDocs.js +12 -1
- package/dist/docs/docMockServices/empresaService.d.ts +38 -0
- package/dist/docs/docMockServices/empresaService.d.ts.map +1 -0
- package/dist/docs/docMockServices/empresaService.js +116 -0
- package/dist/docs/docMockServices/index.d.ts +9 -0
- package/dist/docs/docMockServices/index.d.ts.map +1 -0
- package/dist/docs/docMockServices/index.js +8 -0
- package/dist/docs/docMockServices/initialData.d.ts +6 -0
- package/dist/docs/docMockServices/initialData.d.ts.map +1 -0
- package/dist/docs/docMockServices/initialData.js +132 -0
- package/dist/docs/docMockServices/interfaces.d.ts +26 -0
- package/dist/docs/docMockServices/interfaces.d.ts.map +1 -0
- package/dist/docs/docMockServices/interfaces.js +1 -0
- package/dist/docs/docMockServices/personaEmpresaService.d.ts +43 -0
- package/dist/docs/docMockServices/personaEmpresaService.d.ts.map +1 -0
- package/dist/docs/docMockServices/personaEmpresaService.js +113 -0
- package/dist/docs/docMockServices/personaService.d.ts +39 -0
- package/dist/docs/docMockServices/personaService.d.ts.map +1 -0
- package/dist/docs/docMockServices/personaService.js +180 -0
- package/dist/hooks/index.d.ts +2 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useAsyncRequest.d.ts +17 -0
- package/dist/hooks/useAsyncRequest.d.ts.map +1 -0
- package/dist/hooks/useAsyncRequest.js +70 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +5 -2
|
@@ -0,0 +1,962 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { useFormContext } from "react-hook-form";
|
|
4
|
+
import { Input } from "./Input";
|
|
5
|
+
import { Button } from "./Button";
|
|
6
|
+
import { Dialog } from "../utils/Dialog";
|
|
7
|
+
function SearchSelectInputInner({ value, onChange, onSearchPromiseFn, onSingleSearchPromiseFn, onSelectOption, dialogTitle = "Seleccione una opción", searchButtonPosition = "right", noResultsText = "Sin resultados", getOptionLabel, getOptionValue, getOptionDescription, renderOption, className = "", size = "md", ...inputProps }, ref) {
|
|
8
|
+
// Extraer onBlur de inputProps para manejarlo por separado
|
|
9
|
+
const { onBlur: registerOnBlur, ...restInputProps } = inputProps;
|
|
10
|
+
// Detectar si estamos en modo register: si viene 'name' de register, estamos en modo register
|
|
11
|
+
// register siempre pasa 'name', 'onChange', 'onBlur', y 'ref'
|
|
12
|
+
const isRegisterMode = React.useMemo(() => {
|
|
13
|
+
// Si viene 'name' en inputProps, es porque viene de register
|
|
14
|
+
return "name" in inputProps && inputProps.name !== undefined;
|
|
15
|
+
}, [inputProps]);
|
|
16
|
+
const fieldName = isRegisterMode && "name" in restInputProps
|
|
17
|
+
? restInputProps.name
|
|
18
|
+
: undefined;
|
|
19
|
+
// Obtener setValue del contexto del formulario
|
|
20
|
+
// Para usar objetos complejos con register, el formulario debe estar dentro de FormProvider
|
|
21
|
+
// useFormContext debe llamarse incondicionalmente (requisito de React Hooks)
|
|
22
|
+
// Si no hay FormProvider y se usa en modo register, useFormContext lanzará un error
|
|
23
|
+
// Para usar sin FormProvider, usar Controller en lugar de register
|
|
24
|
+
const formContext = useFormContext();
|
|
25
|
+
const setValue = formContext?.setValue;
|
|
26
|
+
// Estado interno para el texto mostrado en el input (siempre string)
|
|
27
|
+
const [internalDisplayValue, setInternalDisplayValue] = React.useState("");
|
|
28
|
+
const [displayValue, setDisplayValue] = React.useState("");
|
|
29
|
+
// Sincronizar el ref con el estado
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
displayValueRef.current = displayValue;
|
|
32
|
+
}, [displayValue]);
|
|
33
|
+
const [isDialogOpen, setIsDialogOpen] = React.useState(false);
|
|
34
|
+
const [options, setOptions] = React.useState([]);
|
|
35
|
+
// Sincronizar el ref con el estado de options
|
|
36
|
+
React.useEffect(() => {
|
|
37
|
+
optionsRef.current = options;
|
|
38
|
+
}, [options]);
|
|
39
|
+
const [isLoading, setIsLoading] = React.useState(false);
|
|
40
|
+
const [dialogSearchText, setDialogSearchText] = React.useState("");
|
|
41
|
+
const [hasSearched, setHasSearched] = React.useState(false);
|
|
42
|
+
const inputRef = React.useRef(null);
|
|
43
|
+
// Guardar la última opción seleccionada para poder mostrar su label después
|
|
44
|
+
const [lastSelectedOption, setLastSelectedOption] = React.useState(null);
|
|
45
|
+
// Ref para evitar múltiples búsquedas simultáneas del mismo valor
|
|
46
|
+
const searchingValueRef = React.useRef(null);
|
|
47
|
+
// Ref para evitar sincronización cuando acabamos de seleccionar una opción
|
|
48
|
+
const isSelectingRef = React.useRef(false);
|
|
49
|
+
// Ref para leer el displayValue actual sin incluirlo en dependencias
|
|
50
|
+
const displayValueRef = React.useRef("");
|
|
51
|
+
// Ref para rastrear el último valor del formulario sincronizado
|
|
52
|
+
const lastSyncedFormValueRef = React.useRef(undefined);
|
|
53
|
+
// Ref para mantener una referencia estable a syncDisplayValue
|
|
54
|
+
const syncDisplayValueRef = React.useRef(undefined);
|
|
55
|
+
// Ref para leer options sin incluirlo en dependencias
|
|
56
|
+
const optionsRef = React.useRef([]);
|
|
57
|
+
// Ref para rastrear el último value procesado exitosamente en modo Controller
|
|
58
|
+
const lastProcessedValueRef = React.useRef(undefined);
|
|
59
|
+
const labelGetter = React.useCallback((item) => {
|
|
60
|
+
if (getOptionLabel)
|
|
61
|
+
return getOptionLabel(item);
|
|
62
|
+
const anyItem = item;
|
|
63
|
+
return (anyItem.label ?? "").toString();
|
|
64
|
+
}, [getOptionLabel]);
|
|
65
|
+
const valueGetter = React.useCallback((item) => {
|
|
66
|
+
if (getOptionValue)
|
|
67
|
+
return getOptionValue(item);
|
|
68
|
+
const anyItem = item;
|
|
69
|
+
return anyItem.value ?? undefined;
|
|
70
|
+
}, [getOptionValue]);
|
|
71
|
+
const descriptionGetter = React.useCallback((item) => {
|
|
72
|
+
if (getOptionDescription)
|
|
73
|
+
return getOptionDescription(item);
|
|
74
|
+
const anyItem = item;
|
|
75
|
+
return anyItem.description;
|
|
76
|
+
}, [getOptionDescription]);
|
|
77
|
+
// Función helper para sincronizar displayValue con el valor del formulario en modo register
|
|
78
|
+
const syncDisplayValue = React.useCallback(() => {
|
|
79
|
+
// Evitar sincronización si acabamos de seleccionar una opción
|
|
80
|
+
if (isSelectingRef.current) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
if (isRegisterMode) {
|
|
84
|
+
// Si tenemos setValue, el valor del formulario es un objeto complejo (T | K)
|
|
85
|
+
if (fieldName && formContext) {
|
|
86
|
+
const formValue = formContext.watch(fieldName);
|
|
87
|
+
// Evitar sincronización si el valor no ha cambiado
|
|
88
|
+
if (lastSyncedFormValueRef.current === formValue) {
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
if (formValue !== undefined && formValue !== null && formValue !== "") {
|
|
92
|
+
// Actualizar el ref del último valor sincronizado
|
|
93
|
+
lastSyncedFormValueRef.current = formValue;
|
|
94
|
+
// Primero verificar si la última opción seleccionada coincide
|
|
95
|
+
if (lastSelectedOption) {
|
|
96
|
+
const lastValue = valueGetter(lastSelectedOption);
|
|
97
|
+
if (lastValue === formValue ||
|
|
98
|
+
(typeof formValue === "object" &&
|
|
99
|
+
formValue === lastSelectedOption)) {
|
|
100
|
+
setDisplayValue(labelGetter(lastSelectedOption));
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Buscar en las opciones ya cargadas (usar ref para evitar dependencias)
|
|
105
|
+
const matchingOption = optionsRef.current.find((opt) => valueGetter(opt) === formValue);
|
|
106
|
+
if (matchingOption) {
|
|
107
|
+
setDisplayValue(labelGetter(matchingOption));
|
|
108
|
+
setLastSelectedOption(matchingOption);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
// Si no encontramos, intentar usar getOptionLabel si formValue es un objeto
|
|
112
|
+
if (getOptionLabel &&
|
|
113
|
+
typeof formValue === "object" &&
|
|
114
|
+
formValue !== null) {
|
|
115
|
+
try {
|
|
116
|
+
const label = getOptionLabel(formValue);
|
|
117
|
+
if (label) {
|
|
118
|
+
setDisplayValue(label);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
// Si falla, continuar
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Si no encontramos nada y tenemos onSingleSearchPromiseFn, intentar buscar el elemento individual
|
|
127
|
+
if (onSingleSearchPromiseFn) {
|
|
128
|
+
// Si ya estamos buscando este valor, preservar el displayValue actual
|
|
129
|
+
if (searchingValueRef.current === formValue) {
|
|
130
|
+
// Ya estamos buscando este valor, preservar el displayValue actual
|
|
131
|
+
return (displayValueRef.current.trim() !== "" ||
|
|
132
|
+
lastSelectedOption !== null);
|
|
133
|
+
}
|
|
134
|
+
// Si ya tenemos lastSelectedOption con este valor, preservar el displayValue
|
|
135
|
+
// Esto evita buscar de nuevo cuando el valor ya se cargó previamente
|
|
136
|
+
if (lastSelectedOption) {
|
|
137
|
+
const lastValue = valueGetter(lastSelectedOption);
|
|
138
|
+
if (lastValue === formValue) {
|
|
139
|
+
// Ya tenemos la opción correcta, asegurarnos de que esté en options y preservar displayValue
|
|
140
|
+
if (!optionsRef.current.find((opt) => valueGetter(opt) === formValue)) {
|
|
141
|
+
setOptions((prev) => [...prev, lastSelectedOption]);
|
|
142
|
+
}
|
|
143
|
+
// El displayValue ya debería estar establecido, pero asegurémonos
|
|
144
|
+
setDisplayValue(labelGetter(lastSelectedOption));
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// Iniciar búsqueda solo si no hemos encontrado la opción todavía
|
|
149
|
+
searchingValueRef.current = formValue;
|
|
150
|
+
onSingleSearchPromiseFn(formValue)
|
|
151
|
+
.then((foundOption) => {
|
|
152
|
+
// Verificar que el valor sigue siendo el mismo (por si cambió mientras buscábamos)
|
|
153
|
+
if (fieldName && formContext) {
|
|
154
|
+
const currentFormValue = formContext.watch(fieldName);
|
|
155
|
+
if (currentFormValue === formValue) {
|
|
156
|
+
if (foundOption) {
|
|
157
|
+
// Si se encontró la opción, actualizar el displayValue
|
|
158
|
+
const label = labelGetter(foundOption);
|
|
159
|
+
setDisplayValue(label);
|
|
160
|
+
setLastSelectedOption(foundOption);
|
|
161
|
+
// Agregar la opción a las opciones disponibles si no está ya
|
|
162
|
+
setOptions((prev) => {
|
|
163
|
+
if (!prev.find((opt) => valueGetter(opt) === valueGetter(foundOption))) {
|
|
164
|
+
return [...prev, foundOption];
|
|
165
|
+
}
|
|
166
|
+
return prev;
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
// Si no se encontró (undefined), dejar displayValue vacío solo si realmente no hay valor
|
|
171
|
+
// Pero como formValue existe, no deberíamos limpiarlo aquí
|
|
172
|
+
// En realidad, si formValue existe pero no se encontró, dejar vacío es correcto
|
|
173
|
+
setDisplayValue("");
|
|
174
|
+
setLastSelectedOption(null);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
searchingValueRef.current = null;
|
|
179
|
+
})
|
|
180
|
+
.catch((error) => {
|
|
181
|
+
console.error("Error buscando elemento individual:", error);
|
|
182
|
+
searchingValueRef.current = null;
|
|
183
|
+
});
|
|
184
|
+
// Retornar true si ya tenemos un displayValue para preservarlo mientras buscamos
|
|
185
|
+
return (displayValueRef.current.trim() !== "" ||
|
|
186
|
+
lastSelectedOption !== null);
|
|
187
|
+
}
|
|
188
|
+
// Si no encontramos nada y no hay onSingleSearchPromiseFn, pero ya tenemos un displayValue
|
|
189
|
+
// (probablemente de una búsqueda anterior), preservarlo
|
|
190
|
+
if (displayValueRef.current.trim() !== "" ||
|
|
191
|
+
lastSelectedOption !== null) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
else {
|
|
197
|
+
// Actualizar el ref del último valor sincronizado
|
|
198
|
+
lastSyncedFormValueRef.current = formValue;
|
|
199
|
+
setDisplayValue("");
|
|
200
|
+
setLastSelectedOption(null);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else if (inputRef.current) {
|
|
205
|
+
// Fallback: leer del input nativo (valor string o serializado)
|
|
206
|
+
const formValue = inputRef.current.value;
|
|
207
|
+
if (formValue) {
|
|
208
|
+
// Buscar en las opciones para mostrar el label (usar ref para evitar dependencias)
|
|
209
|
+
const matchingOption = optionsRef.current.find((opt) => String(valueGetter(opt)) === String(formValue));
|
|
210
|
+
if (matchingOption) {
|
|
211
|
+
setDisplayValue(labelGetter(matchingOption));
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
setDisplayValue(formValue);
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
setDisplayValue("");
|
|
220
|
+
setLastSelectedOption(null);
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}, [
|
|
227
|
+
isRegisterMode,
|
|
228
|
+
fieldName,
|
|
229
|
+
formContext,
|
|
230
|
+
// Removido options - usar optionsRef.current en su lugar
|
|
231
|
+
labelGetter,
|
|
232
|
+
valueGetter,
|
|
233
|
+
getOptionLabel,
|
|
234
|
+
lastSelectedOption,
|
|
235
|
+
onSingleSearchPromiseFn,
|
|
236
|
+
]);
|
|
237
|
+
// Mantener el ref actualizado con la función syncDisplayValue
|
|
238
|
+
React.useEffect(() => {
|
|
239
|
+
syncDisplayValueRef.current = syncDisplayValue;
|
|
240
|
+
}, [syncDisplayValue]);
|
|
241
|
+
// Sincronizar displayValue cuando value cambia externamente (modo Controller)
|
|
242
|
+
React.useEffect(() => {
|
|
243
|
+
if (!isRegisterMode) {
|
|
244
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
245
|
+
let displayValueSet = false;
|
|
246
|
+
// Si value es un string, siempre buscar el label correspondiente
|
|
247
|
+
if (typeof value === "string") {
|
|
248
|
+
// Función helper para obtener el label de una opción
|
|
249
|
+
const getLabelFromOption = (option) => {
|
|
250
|
+
if (getOptionLabel) {
|
|
251
|
+
return getOptionLabel(option);
|
|
252
|
+
}
|
|
253
|
+
// Si no hay getOptionLabel, asumir que la opción tiene una propiedad "label"
|
|
254
|
+
const anyOption = option;
|
|
255
|
+
return anyOption?.label ?? String(value);
|
|
256
|
+
};
|
|
257
|
+
// Primero verificar si lastSelectedOption coincide con este valor
|
|
258
|
+
if (lastSelectedOption) {
|
|
259
|
+
const lastValue = valueGetter(lastSelectedOption);
|
|
260
|
+
if (String(lastValue) === value) {
|
|
261
|
+
const label = getLabelFromOption(lastSelectedOption);
|
|
262
|
+
setInternalDisplayValue(label);
|
|
263
|
+
lastProcessedValueRef.current = value;
|
|
264
|
+
displayValueSet = true;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Si no encontramos en lastSelectedOption, buscar en las opciones ya cargadas
|
|
268
|
+
if (!displayValueSet) {
|
|
269
|
+
const matchingOption = optionsRef.current.find((opt) => String(valueGetter(opt)) === value);
|
|
270
|
+
if (matchingOption) {
|
|
271
|
+
const label = getLabelFromOption(matchingOption);
|
|
272
|
+
setInternalDisplayValue(label);
|
|
273
|
+
setLastSelectedOption(matchingOption);
|
|
274
|
+
lastProcessedValueRef.current = value;
|
|
275
|
+
displayValueSet = true;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Si aún no encontramos y tenemos onSingleSearchPromiseFn, buscar el elemento individual
|
|
279
|
+
if (!displayValueSet && onSingleSearchPromiseFn) {
|
|
280
|
+
// Solo buscar si no estamos buscando ya este valor
|
|
281
|
+
if (searchingValueRef.current !== value) {
|
|
282
|
+
const currentValue = value; // Capturar el valor actual
|
|
283
|
+
searchingValueRef.current = value;
|
|
284
|
+
onSingleSearchPromiseFn(value)
|
|
285
|
+
.then((foundOption) => {
|
|
286
|
+
// Verificar que el valor sigue siendo el mismo
|
|
287
|
+
if (currentValue === value) {
|
|
288
|
+
if (foundOption) {
|
|
289
|
+
const label = getLabelFromOption(foundOption);
|
|
290
|
+
setInternalDisplayValue(label);
|
|
291
|
+
setLastSelectedOption(foundOption);
|
|
292
|
+
lastProcessedValueRef.current = value;
|
|
293
|
+
// Agregar la opción a las opciones disponibles si no está ya
|
|
294
|
+
setOptions((prev) => {
|
|
295
|
+
if (!prev.find((opt) => valueGetter(opt) === valueGetter(foundOption))) {
|
|
296
|
+
return [...prev, foundOption];
|
|
297
|
+
}
|
|
298
|
+
return prev;
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
// Si no se encontró, mostrar el string directamente
|
|
303
|
+
setInternalDisplayValue(currentValue);
|
|
304
|
+
lastProcessedValueRef.current = value;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
searchingValueRef.current = null;
|
|
308
|
+
})
|
|
309
|
+
.catch((error) => {
|
|
310
|
+
console.error("Error buscando elemento individual:", error);
|
|
311
|
+
searchingValueRef.current = null;
|
|
312
|
+
// En caso de error, mostrar el string directamente
|
|
313
|
+
setInternalDisplayValue(value);
|
|
314
|
+
lastProcessedValueRef.current = value;
|
|
315
|
+
});
|
|
316
|
+
// Mientras buscamos, no establecer displayValueSet para que no se sobrescriba
|
|
317
|
+
// El displayValue se actualizará cuando la promesa se resuelva
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
// Ya estamos buscando este valor, mantener el displayValue actual
|
|
321
|
+
displayValueSet = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
else if (!displayValueSet) {
|
|
325
|
+
// Si no hay onSingleSearchPromiseFn y no encontramos nada, mostrar el string directamente
|
|
326
|
+
setInternalDisplayValue(value);
|
|
327
|
+
lastProcessedValueRef.current = value;
|
|
328
|
+
displayValueSet = true;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
// Si value es T | K, obtener el label usando getOptionLabel o la propiedad label
|
|
333
|
+
// Si hay getOptionValue, el value puede ser K (no tiene label), así que necesitamos
|
|
334
|
+
// buscar el objeto original en las opciones recientes o usar getOptionLabel si está disponible
|
|
335
|
+
if (getOptionLabel) {
|
|
336
|
+
// Si hay getOptionLabel, intentar usarlo (puede que value sea K, no T)
|
|
337
|
+
// En ese caso, necesitamos buscar el objeto T correspondiente
|
|
338
|
+
// Por ahora, intentamos usar getOptionLabel directamente
|
|
339
|
+
try {
|
|
340
|
+
setInternalDisplayValue(getOptionLabel(value));
|
|
341
|
+
lastProcessedValueRef.current = value;
|
|
342
|
+
displayValueSet = true;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// Si falla, value es probablemente K, buscar en las opciones (usar ref para evitar dependencias)
|
|
346
|
+
const matchingOption = optionsRef.current.find((opt) => valueGetter(opt) === value);
|
|
347
|
+
if (matchingOption) {
|
|
348
|
+
setInternalDisplayValue(labelGetter(matchingOption));
|
|
349
|
+
setLastSelectedOption(matchingOption);
|
|
350
|
+
lastProcessedValueRef.current = value;
|
|
351
|
+
displayValueSet = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
// Intentar obtener la propiedad label directamente
|
|
357
|
+
const anyValue = value;
|
|
358
|
+
if (anyValue?.label) {
|
|
359
|
+
setInternalDisplayValue(anyValue.label);
|
|
360
|
+
lastProcessedValueRef.current = value;
|
|
361
|
+
displayValueSet = true;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
// Si no tiene label, puede ser K, buscar en las opciones (usar ref para evitar dependencias)
|
|
365
|
+
const matchingOption = optionsRef.current.find((opt) => valueGetter(opt) === value);
|
|
366
|
+
if (matchingOption) {
|
|
367
|
+
setInternalDisplayValue(labelGetter(matchingOption));
|
|
368
|
+
setLastSelectedOption(matchingOption);
|
|
369
|
+
lastProcessedValueRef.current = value;
|
|
370
|
+
displayValueSet = true;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
// Si no se encontró una opción coincidente, mostrar el valor como string
|
|
375
|
+
// (útil cuando hay un valor por defecto pero las opciones aún no se han cargado)
|
|
376
|
+
if (!displayValueSet) {
|
|
377
|
+
setInternalDisplayValue(String(value));
|
|
378
|
+
lastProcessedValueRef.current = value;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// Resetear el estado interno cuando value es undefined (por ejemplo, después de un reset)
|
|
384
|
+
setInternalDisplayValue("");
|
|
385
|
+
lastProcessedValueRef.current = undefined;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}, [
|
|
389
|
+
value,
|
|
390
|
+
getOptionLabel,
|
|
391
|
+
// Removido options - usar optionsRef.current en su lugar para evitar bucles infinitos
|
|
392
|
+
labelGetter,
|
|
393
|
+
valueGetter,
|
|
394
|
+
isRegisterMode,
|
|
395
|
+
onSingleSearchPromiseFn,
|
|
396
|
+
lastSelectedOption,
|
|
397
|
+
]);
|
|
398
|
+
// Ref para rastrear el último value procesado cuando options cambia
|
|
399
|
+
const lastValueProcessedRef = React.useRef(undefined);
|
|
400
|
+
const lastOptionsLengthRef = React.useRef(0);
|
|
401
|
+
// Actualizar internalDisplayValue cuando options cambia y hay un value que coincide
|
|
402
|
+
// Esto es necesario para cuando las opciones se cargan después de que se establece el value
|
|
403
|
+
React.useEffect(() => {
|
|
404
|
+
if (!isRegisterMode &&
|
|
405
|
+
value !== undefined &&
|
|
406
|
+
value !== null &&
|
|
407
|
+
value !== "") {
|
|
408
|
+
if (typeof value !== "string") {
|
|
409
|
+
// Solo procesar si el value cambió o si las options cambiaron (aumentaron)
|
|
410
|
+
const valueChanged = lastValueProcessedRef.current !== value;
|
|
411
|
+
const optionsChanged = options.length > lastOptionsLengthRef.current;
|
|
412
|
+
// Solo buscar si el value cambió o si las options aumentaron
|
|
413
|
+
if (valueChanged || optionsChanged) {
|
|
414
|
+
const matchingOption = options.find((opt) => valueGetter(opt) === value);
|
|
415
|
+
if (matchingOption) {
|
|
416
|
+
const newDisplay = labelGetter(matchingOption);
|
|
417
|
+
// Verificar que el displayValue actual no sea el mismo para evitar actualizaciones innecesarias
|
|
418
|
+
setInternalDisplayValue((prev) => {
|
|
419
|
+
if (prev !== newDisplay) {
|
|
420
|
+
lastValueProcessedRef.current = value;
|
|
421
|
+
lastOptionsLengthRef.current = options.length;
|
|
422
|
+
return newDisplay;
|
|
423
|
+
}
|
|
424
|
+
return prev;
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
428
|
+
// Actualizar el ref del length incluso si no encontramos coincidencia
|
|
429
|
+
lastOptionsLengthRef.current = options.length;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}, [options, value, isRegisterMode, valueGetter, labelGetter]);
|
|
435
|
+
// Sincronizar displayValue con el valor del formulario en modo register
|
|
436
|
+
React.useEffect(() => {
|
|
437
|
+
if (isRegisterMode) {
|
|
438
|
+
let attempts = 0;
|
|
439
|
+
const maxAttempts = 50;
|
|
440
|
+
const trySync = () => {
|
|
441
|
+
// Usar el ref en lugar de la función directamente para evitar dependencias
|
|
442
|
+
return syncDisplayValueRef.current?.() ?? false;
|
|
443
|
+
};
|
|
444
|
+
if (trySync()) {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
const intervalId = window.setInterval(() => {
|
|
448
|
+
attempts++;
|
|
449
|
+
if (trySync() || attempts >= maxAttempts) {
|
|
450
|
+
clearInterval(intervalId);
|
|
451
|
+
}
|
|
452
|
+
}, 100);
|
|
453
|
+
const timeouts = [];
|
|
454
|
+
[0, 50, 100, 200, 500, 1000].forEach((delay) => {
|
|
455
|
+
const timeoutId = window.setTimeout(() => {
|
|
456
|
+
trySync();
|
|
457
|
+
}, delay);
|
|
458
|
+
timeouts.push(timeoutId);
|
|
459
|
+
});
|
|
460
|
+
return () => {
|
|
461
|
+
clearInterval(intervalId);
|
|
462
|
+
timeouts.forEach(clearTimeout);
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
}, [isRegisterMode]); // Removido syncDisplayValue de las dependencias
|
|
466
|
+
// También sincronizar cuando cambia el valor del formulario
|
|
467
|
+
React.useEffect(() => {
|
|
468
|
+
if (isRegisterMode && fieldName && formContext) {
|
|
469
|
+
const subscription = formContext.watch((_data, { name }) => {
|
|
470
|
+
// Solo sincronizar cuando cambia el campo específico, no en cada cambio del formulario
|
|
471
|
+
if (name === fieldName) {
|
|
472
|
+
// Cuando cambia el valor del formulario, sincronizar el displayValue
|
|
473
|
+
// Usar el ref en lugar de la función directamente para evitar dependencias
|
|
474
|
+
syncDisplayValueRef.current?.();
|
|
475
|
+
}
|
|
476
|
+
});
|
|
477
|
+
return () => subscription.unsubscribe();
|
|
478
|
+
}
|
|
479
|
+
}, [isRegisterMode, fieldName, formContext]); // Removido syncDisplayValue de las dependencias
|
|
480
|
+
// También escuchar cambios en el input nativo para sincronizar cuando cambie
|
|
481
|
+
React.useEffect(() => {
|
|
482
|
+
if (isRegisterMode && inputRef.current) {
|
|
483
|
+
const input = inputRef.current;
|
|
484
|
+
const handleInputSync = () => {
|
|
485
|
+
// Usar el ref en lugar de la función directamente para evitar dependencias
|
|
486
|
+
syncDisplayValueRef.current?.();
|
|
487
|
+
};
|
|
488
|
+
input.addEventListener("input", handleInputSync);
|
|
489
|
+
input.addEventListener("change", handleInputSync);
|
|
490
|
+
const observer = new MutationObserver(() => {
|
|
491
|
+
// Usar el ref en lugar de la función directamente para evitar dependencias
|
|
492
|
+
syncDisplayValueRef.current?.();
|
|
493
|
+
});
|
|
494
|
+
observer.observe(input, {
|
|
495
|
+
attributes: true,
|
|
496
|
+
attributeFilter: ["value"],
|
|
497
|
+
});
|
|
498
|
+
return () => {
|
|
499
|
+
input.removeEventListener("input", handleInputSync);
|
|
500
|
+
input.removeEventListener("change", handleInputSync);
|
|
501
|
+
observer.disconnect();
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
}, [isRegisterMode]); // Removido syncDisplayValue de las dependencias
|
|
505
|
+
const handleDialogSearch = React.useCallback(async () => {
|
|
506
|
+
const textToSearch = dialogSearchText.trim();
|
|
507
|
+
if (!textToSearch)
|
|
508
|
+
return;
|
|
509
|
+
setIsLoading(true);
|
|
510
|
+
setHasSearched(true);
|
|
511
|
+
try {
|
|
512
|
+
const result = await onSearchPromiseFn(textToSearch);
|
|
513
|
+
// Si es PaginationInterface, extraer el array de list
|
|
514
|
+
if (result && typeof result === "object" && "list" in result) {
|
|
515
|
+
setOptions(result.list);
|
|
516
|
+
}
|
|
517
|
+
else {
|
|
518
|
+
setOptions(result);
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
console.error("Error en búsqueda:", error);
|
|
523
|
+
setOptions([]);
|
|
524
|
+
}
|
|
525
|
+
finally {
|
|
526
|
+
setIsLoading(false);
|
|
527
|
+
}
|
|
528
|
+
}, [dialogSearchText, onSearchPromiseFn]);
|
|
529
|
+
const handleInputChange = (event) => {
|
|
530
|
+
// El input principal ahora es readonly, así que esto no debería llamarse
|
|
531
|
+
// Pero lo mantenemos por compatibilidad
|
|
532
|
+
const newValue = event.target.value;
|
|
533
|
+
if (isRegisterMode) {
|
|
534
|
+
setDisplayValue(newValue);
|
|
535
|
+
}
|
|
536
|
+
else {
|
|
537
|
+
if (value === undefined || typeof value === "string") {
|
|
538
|
+
setInternalDisplayValue(newValue);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
if (onChange) {
|
|
542
|
+
const standardHandler = onChange;
|
|
543
|
+
standardHandler(event);
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
// Handler para cuando se hace foco en el input principal
|
|
547
|
+
// Abre el dialog automáticamente
|
|
548
|
+
const handleInputFocus = () => {
|
|
549
|
+
// Abrir el dialog cuando se hace foco
|
|
550
|
+
setIsDialogOpen(true);
|
|
551
|
+
setDialogSearchText(inputValue.trim());
|
|
552
|
+
};
|
|
553
|
+
// Función para hacer búsqueda desde un valor dado
|
|
554
|
+
const handleDialogSearchFromValue = React.useCallback(async (textToSearch) => {
|
|
555
|
+
if (!textToSearch.trim())
|
|
556
|
+
return;
|
|
557
|
+
setIsLoading(true);
|
|
558
|
+
setHasSearched(true);
|
|
559
|
+
try {
|
|
560
|
+
const result = await onSearchPromiseFn(textToSearch.trim());
|
|
561
|
+
if (result && typeof result === "object" && "list" in result) {
|
|
562
|
+
setOptions(result.list);
|
|
563
|
+
}
|
|
564
|
+
else {
|
|
565
|
+
setOptions(result);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
catch (error) {
|
|
569
|
+
console.error("Error en búsqueda:", error);
|
|
570
|
+
setOptions([]);
|
|
571
|
+
}
|
|
572
|
+
finally {
|
|
573
|
+
setIsLoading(false);
|
|
574
|
+
}
|
|
575
|
+
}, [onSearchPromiseFn]);
|
|
576
|
+
const handleKeyDown = (event) => {
|
|
577
|
+
// Cuando se presiona Enter, abrir el dialog
|
|
578
|
+
if (event.key === "Enter") {
|
|
579
|
+
event.preventDefault();
|
|
580
|
+
setIsDialogOpen(true);
|
|
581
|
+
setDialogSearchText(inputValue.trim());
|
|
582
|
+
// Si hay texto en el input, hacer búsqueda automática
|
|
583
|
+
if (inputValue.trim()) {
|
|
584
|
+
handleDialogSearchFromValue(inputValue.trim());
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
const handleSelect = (option) => {
|
|
589
|
+
const label = labelGetter(option);
|
|
590
|
+
// Marcar que estamos seleccionando para evitar sincronización
|
|
591
|
+
isSelectingRef.current = true;
|
|
592
|
+
// Resetear el ref del último valor sincronizado para forzar la próxima sincronización
|
|
593
|
+
lastSyncedFormValueRef.current = undefined;
|
|
594
|
+
// Guardar la opción seleccionada para poder mostrar su label después
|
|
595
|
+
setLastSelectedOption(option);
|
|
596
|
+
// Agregar la opción a las opciones disponibles si no está ya
|
|
597
|
+
if (!options.find((opt) => valueGetter(opt) === valueGetter(option))) {
|
|
598
|
+
setOptions((prev) => [...prev, option]);
|
|
599
|
+
}
|
|
600
|
+
// Determinar el valor a asignar: opción completa (T) o valor extraído (K)
|
|
601
|
+
const valueToAssign = getOptionValue ? valueGetter(option) : option;
|
|
602
|
+
if (isRegisterMode) {
|
|
603
|
+
// Actualizar el displayValue inmediatamente con el label
|
|
604
|
+
setDisplayValue(label);
|
|
605
|
+
// En modo register
|
|
606
|
+
if (fieldName && setValue) {
|
|
607
|
+
// Usar setValue para guardar el objeto completo directamente
|
|
608
|
+
setValue(fieldName, valueToAssign, {
|
|
609
|
+
shouldValidate: true,
|
|
610
|
+
shouldDirty: true,
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
// Fallback: actualizar el input nativo con el valor serializado
|
|
615
|
+
if (inputRef.current) {
|
|
616
|
+
const nativeInput = inputRef.current;
|
|
617
|
+
const valueString = typeof valueToAssign === "object" && valueToAssign !== null
|
|
618
|
+
? JSON.stringify(valueToAssign)
|
|
619
|
+
: String(valueToAssign);
|
|
620
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
|
621
|
+
if (nativeInputValueSetter) {
|
|
622
|
+
nativeInputValueSetter.call(nativeInput, valueString);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
nativeInput.value = valueString;
|
|
626
|
+
}
|
|
627
|
+
if (onChange) {
|
|
628
|
+
const changeEvent = {
|
|
629
|
+
target: nativeInput,
|
|
630
|
+
currentTarget: nativeInput,
|
|
631
|
+
};
|
|
632
|
+
onChange(changeEvent);
|
|
633
|
+
}
|
|
634
|
+
const inputEvent = new Event("input", {
|
|
635
|
+
bubbles: true,
|
|
636
|
+
cancelable: true,
|
|
637
|
+
});
|
|
638
|
+
nativeInput.dispatchEvent(inputEvent);
|
|
639
|
+
const changeEventNative = new Event("change", {
|
|
640
|
+
bubbles: true,
|
|
641
|
+
cancelable: true,
|
|
642
|
+
});
|
|
643
|
+
nativeInput.dispatchEvent(changeEventNative);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
else {
|
|
648
|
+
// Modo Controller o API personalizada
|
|
649
|
+
// Establecer internalDisplayValue inmediatamente para respuesta visual rápida
|
|
650
|
+
setInternalDisplayValue(label);
|
|
651
|
+
// Resetear lastProcessedValueRef para que el useEffect valide cuando el value cambie
|
|
652
|
+
lastProcessedValueRef.current = undefined;
|
|
653
|
+
if (onChange) {
|
|
654
|
+
// Intentar primero como onChange personalizado (acepta T | K directamente)
|
|
655
|
+
const customHandler = onChange;
|
|
656
|
+
if (typeof customHandler === "function") {
|
|
657
|
+
customHandler(valueToAssign);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
// Si no es función personalizada, es ChangeEventHandler
|
|
661
|
+
const serializedValue = typeof valueToAssign === "object" && valueToAssign !== null
|
|
662
|
+
? JSON.stringify(valueToAssign)
|
|
663
|
+
: String(valueToAssign);
|
|
664
|
+
const syntheticEvent = {
|
|
665
|
+
target: {
|
|
666
|
+
value: serializedValue,
|
|
667
|
+
name: restInputProps.name || "",
|
|
668
|
+
},
|
|
669
|
+
currentTarget: {
|
|
670
|
+
value: serializedValue,
|
|
671
|
+
name: restInputProps.name || "",
|
|
672
|
+
},
|
|
673
|
+
type: "change",
|
|
674
|
+
bubbles: true,
|
|
675
|
+
cancelable: true,
|
|
676
|
+
defaultPrevented: false,
|
|
677
|
+
eventPhase: 0,
|
|
678
|
+
isTrusted: false,
|
|
679
|
+
nativeEvent: {},
|
|
680
|
+
preventDefault: () => { },
|
|
681
|
+
isDefaultPrevented: () => false,
|
|
682
|
+
stopPropagation: () => { },
|
|
683
|
+
isPropagationStopped: () => false,
|
|
684
|
+
persist: () => { },
|
|
685
|
+
timeStamp: Date.now(),
|
|
686
|
+
};
|
|
687
|
+
const standardHandler = onChange;
|
|
688
|
+
standardHandler(syntheticEvent);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
onSelectOption?.(option, valueGetter(option));
|
|
693
|
+
setIsDialogOpen(false);
|
|
694
|
+
// Resetear el flag después de un breve delay para permitir que los efectos se ejecuten
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
isSelectingRef.current = false;
|
|
697
|
+
}, 100);
|
|
698
|
+
};
|
|
699
|
+
const handleIconClick = (event) => {
|
|
700
|
+
event.preventDefault();
|
|
701
|
+
// Si hay valor seleccionado, limpiarlo
|
|
702
|
+
if (hasSelectedValue) {
|
|
703
|
+
handleClear(event);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
// Si no hay valor, abrir el dialog (similar a handleInputFocus)
|
|
707
|
+
setIsDialogOpen(true);
|
|
708
|
+
setDialogSearchText(inputValue.trim());
|
|
709
|
+
// Si hay texto en el input, hacer búsqueda automática
|
|
710
|
+
if (inputValue.trim()) {
|
|
711
|
+
handleDialogSearchFromValue(inputValue.trim());
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
};
|
|
715
|
+
// Detectar si hay un valor seleccionado
|
|
716
|
+
const hasSelectedValue = React.useMemo(() => {
|
|
717
|
+
const currentDisplayValue = isRegisterMode
|
|
718
|
+
? displayValue
|
|
719
|
+
: internalDisplayValue;
|
|
720
|
+
const displayValueStr = isRegisterMode
|
|
721
|
+
? displayValue ?? ""
|
|
722
|
+
: internalDisplayValue ?? "";
|
|
723
|
+
return ((isRegisterMode
|
|
724
|
+
? displayValueStr.trim() !== ""
|
|
725
|
+
: value !== undefined && value !== null && value !== "") &&
|
|
726
|
+
typeof currentDisplayValue === "string" &&
|
|
727
|
+
currentDisplayValue.trim() !== "");
|
|
728
|
+
}, [value, internalDisplayValue, displayValue, isRegisterMode]);
|
|
729
|
+
// Función para limpiar el valor
|
|
730
|
+
const handleClear = React.useCallback((event) => {
|
|
731
|
+
event.preventDefault();
|
|
732
|
+
event.stopPropagation();
|
|
733
|
+
if (isRegisterMode) {
|
|
734
|
+
setDisplayValue("");
|
|
735
|
+
setLastSelectedOption(null);
|
|
736
|
+
if (fieldName && setValue) {
|
|
737
|
+
// Usar setValue para limpiar el valor
|
|
738
|
+
setValue(fieldName, undefined, {
|
|
739
|
+
shouldValidate: true,
|
|
740
|
+
shouldDirty: true,
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
else {
|
|
744
|
+
// Fallback: limpiar el input nativo
|
|
745
|
+
if (inputRef.current) {
|
|
746
|
+
const nativeInput = inputRef.current;
|
|
747
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
|
748
|
+
setter?.call(nativeInput, "");
|
|
749
|
+
const inputEvent = new Event("input", { bubbles: true });
|
|
750
|
+
nativeInput.dispatchEvent(inputEvent);
|
|
751
|
+
const changeEvent = new Event("change", { bubbles: true });
|
|
752
|
+
nativeInput.dispatchEvent(changeEvent);
|
|
753
|
+
}
|
|
754
|
+
if (onChange && inputRef.current) {
|
|
755
|
+
const changeEvent = {
|
|
756
|
+
target: inputRef.current,
|
|
757
|
+
currentTarget: inputRef.current,
|
|
758
|
+
};
|
|
759
|
+
onChange(changeEvent);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
else {
|
|
764
|
+
setInternalDisplayValue("");
|
|
765
|
+
}
|
|
766
|
+
// Resetear el valor llamando a onChange
|
|
767
|
+
if (!isRegisterMode && onChange) {
|
|
768
|
+
const customHandler = onChange;
|
|
769
|
+
if (typeof customHandler === "function") {
|
|
770
|
+
// Si hay getOptionValue, pasar undefined, sino pasar undefined también
|
|
771
|
+
customHandler(undefined);
|
|
772
|
+
}
|
|
773
|
+
else {
|
|
774
|
+
// Si es ChangeEventHandler, crear un evento sintético
|
|
775
|
+
const syntheticEvent = {
|
|
776
|
+
target: {
|
|
777
|
+
value: "",
|
|
778
|
+
name: inputProps.name || "",
|
|
779
|
+
},
|
|
780
|
+
currentTarget: {
|
|
781
|
+
value: "",
|
|
782
|
+
name: inputProps.name || "",
|
|
783
|
+
},
|
|
784
|
+
type: "change",
|
|
785
|
+
bubbles: true,
|
|
786
|
+
cancelable: true,
|
|
787
|
+
defaultPrevented: false,
|
|
788
|
+
eventPhase: 0,
|
|
789
|
+
isTrusted: false,
|
|
790
|
+
nativeEvent: {},
|
|
791
|
+
preventDefault: () => { },
|
|
792
|
+
isDefaultPrevented: () => false,
|
|
793
|
+
stopPropagation: () => { },
|
|
794
|
+
isPropagationStopped: () => false,
|
|
795
|
+
persist: () => { },
|
|
796
|
+
timeStamp: Date.now(),
|
|
797
|
+
};
|
|
798
|
+
const standardHandler = onChange;
|
|
799
|
+
standardHandler(syntheticEvent);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}, [onChange, isRegisterMode, setValue, fieldName, inputProps.name]);
|
|
803
|
+
// Determinar qué ícono mostrar: si hay valor seleccionado, mostrar "X", sino mostrar el ícono de búsqueda
|
|
804
|
+
const displayIcon = hasSelectedValue
|
|
805
|
+
? "fa-times"
|
|
806
|
+
: inputProps.icon || "fa-search";
|
|
807
|
+
const displayIconPosition = searchButtonPosition;
|
|
808
|
+
// handleIconClick ya maneja tanto limpiar como abrir el dialog
|
|
809
|
+
const displayOnIconClick = handleIconClick;
|
|
810
|
+
// Resetear el texto de búsqueda del dialog cuando se cierra
|
|
811
|
+
// Y buscar la opción individual si el valor del control no está en las opciones cargadas
|
|
812
|
+
React.useEffect(() => {
|
|
813
|
+
if (!isDialogOpen) {
|
|
814
|
+
setDialogSearchText("");
|
|
815
|
+
setHasSearched(false);
|
|
816
|
+
setOptions([]);
|
|
817
|
+
// Cuando se cierra el dialog, verificar si necesitamos buscar la opción individual
|
|
818
|
+
if (isRegisterMode &&
|
|
819
|
+
fieldName &&
|
|
820
|
+
onSingleSearchPromiseFn &&
|
|
821
|
+
formContext) {
|
|
822
|
+
const formValue = formContext.watch(fieldName);
|
|
823
|
+
if (formValue !== undefined && formValue !== null && formValue !== "") {
|
|
824
|
+
// Verificar si ya tenemos la opción en lastSelectedOption o en options
|
|
825
|
+
// Usar optionsRef.current para evitar dependencias y bucles infinitos
|
|
826
|
+
const hasOption = (lastSelectedOption &&
|
|
827
|
+
valueGetter(lastSelectedOption) === formValue) ||
|
|
828
|
+
optionsRef.current.some((opt) => valueGetter(opt) === formValue);
|
|
829
|
+
// Si no tenemos la opción y no estamos buscando ya, buscarla
|
|
830
|
+
if (!hasOption && searchingValueRef.current !== formValue) {
|
|
831
|
+
searchingValueRef.current = formValue;
|
|
832
|
+
onSingleSearchPromiseFn(formValue)
|
|
833
|
+
.then((foundOption) => {
|
|
834
|
+
// Verificar que el valor sigue siendo el mismo
|
|
835
|
+
if (fieldName && formContext) {
|
|
836
|
+
const currentFormValue = formContext.watch(fieldName);
|
|
837
|
+
if (currentFormValue === formValue) {
|
|
838
|
+
if (foundOption) {
|
|
839
|
+
// Si se encontró la opción, actualizar el displayValue
|
|
840
|
+
const label = labelGetter(foundOption);
|
|
841
|
+
setDisplayValue(label);
|
|
842
|
+
setLastSelectedOption(foundOption);
|
|
843
|
+
// Agregar la opción a las opciones disponibles si no está ya
|
|
844
|
+
setOptions((prev) => {
|
|
845
|
+
if (!prev.find((opt) => valueGetter(opt) === valueGetter(foundOption))) {
|
|
846
|
+
return [...prev, foundOption];
|
|
847
|
+
}
|
|
848
|
+
return prev;
|
|
849
|
+
});
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
searchingValueRef.current = null;
|
|
854
|
+
})
|
|
855
|
+
.catch((error) => {
|
|
856
|
+
console.error("Error buscando elemento individual al cerrar dialog:", error);
|
|
857
|
+
searchingValueRef.current = null;
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}, [
|
|
864
|
+
isDialogOpen,
|
|
865
|
+
isRegisterMode,
|
|
866
|
+
setValue,
|
|
867
|
+
fieldName,
|
|
868
|
+
formContext,
|
|
869
|
+
onSingleSearchPromiseFn,
|
|
870
|
+
lastSelectedOption,
|
|
871
|
+
// Removido options - usar optionsRef.current en su lugar para evitar bucles infinitos
|
|
872
|
+
valueGetter,
|
|
873
|
+
labelGetter,
|
|
874
|
+
]);
|
|
875
|
+
// Combinar refs: el ref del componente y el ref interno
|
|
876
|
+
const combinedRef = React.useCallback((node) => {
|
|
877
|
+
inputRef.current = node;
|
|
878
|
+
if (typeof ref === "function") {
|
|
879
|
+
ref(node);
|
|
880
|
+
}
|
|
881
|
+
else if (ref) {
|
|
882
|
+
ref.current = node;
|
|
883
|
+
}
|
|
884
|
+
// Cuando el ref se establece en modo register, sincronizar el displayValue
|
|
885
|
+
if (isRegisterMode && node) {
|
|
886
|
+
[0, 10, 50, 100, 200, 500].forEach((delay) => {
|
|
887
|
+
setTimeout(() => {
|
|
888
|
+
if (node && inputRef.current === node) {
|
|
889
|
+
// Usar el ref en lugar de la función directamente para evitar dependencias
|
|
890
|
+
syncDisplayValueRef.current?.();
|
|
891
|
+
}
|
|
892
|
+
}, delay);
|
|
893
|
+
});
|
|
894
|
+
}
|
|
895
|
+
}, [ref, isRegisterMode] // Removido syncDisplayValue de las dependencias
|
|
896
|
+
);
|
|
897
|
+
// Valor que se muestra en el input principal - completamente desacoplado del dialog
|
|
898
|
+
// Solo muestra el label de la opción seleccionada
|
|
899
|
+
const inputValue = React.useMemo(() => {
|
|
900
|
+
if (isRegisterMode) {
|
|
901
|
+
// En modo register, usar displayValue (que contiene el label de la opción seleccionada)
|
|
902
|
+
// displayValue se sincroniza automáticamente con el valor del formulario
|
|
903
|
+
return displayValue ?? "";
|
|
904
|
+
}
|
|
905
|
+
else {
|
|
906
|
+
// Modo Controller o API personalizada
|
|
907
|
+
if (value !== undefined && value !== null && value !== "") {
|
|
908
|
+
// Si el valor es una opción completa (T), mostrar su label
|
|
909
|
+
if (getOptionLabel && typeof value === "object") {
|
|
910
|
+
try {
|
|
911
|
+
return getOptionLabel(value);
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
// Si falla, puede ser K, buscar en las opciones
|
|
915
|
+
const matchingOption = options.find((opt) => valueGetter(opt) === value);
|
|
916
|
+
if (matchingOption) {
|
|
917
|
+
return labelGetter(matchingOption);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
// Si el valor es K (el valor extraído), buscar en las opciones
|
|
922
|
+
const matchingOption = options.find((opt) => valueGetter(opt) === value);
|
|
923
|
+
if (matchingOption) {
|
|
924
|
+
return labelGetter(matchingOption);
|
|
925
|
+
}
|
|
926
|
+
// Si no se encuentra, intentar mostrar el valor como string
|
|
927
|
+
return String(value);
|
|
928
|
+
}
|
|
929
|
+
return internalDisplayValue;
|
|
930
|
+
}
|
|
931
|
+
}, [
|
|
932
|
+
isRegisterMode,
|
|
933
|
+
displayValue,
|
|
934
|
+
value,
|
|
935
|
+
internalDisplayValue,
|
|
936
|
+
options,
|
|
937
|
+
labelGetter,
|
|
938
|
+
valueGetter,
|
|
939
|
+
getOptionLabel,
|
|
940
|
+
]);
|
|
941
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: `relative w-full ${className}`, children: _jsx(Input, { ...restInputProps, ref: combinedRef, value: inputValue, onChange: handleInputChange, onFocus: (e) => {
|
|
942
|
+
// Llamar al onFocus original si existe
|
|
943
|
+
if (restInputProps.onFocus) {
|
|
944
|
+
restInputProps.onFocus(e);
|
|
945
|
+
}
|
|
946
|
+
handleInputFocus(e);
|
|
947
|
+
}, onBlur: registerOnBlur, onKeyDown: handleKeyDown, size: size, icon: displayIcon, iconPosition: displayIconPosition, onIconClick: displayOnIconClick, readOnly: true }) }), _jsx(Dialog, { isOpen: isDialogOpen, title: dialogTitle, dialogBody: _jsxs("div", { className: "space-y-2", children: [_jsx(Input, { value: dialogSearchText, onChange: (e) => setDialogSearchText(e.target.value), onKeyDown: (e) => {
|
|
948
|
+
if (e.key === "Enter") {
|
|
949
|
+
e.preventDefault();
|
|
950
|
+
handleDialogSearch();
|
|
951
|
+
}
|
|
952
|
+
}, icon: "fa-search", iconPosition: "right", onIconClick: dialogSearchText.trim() ? handleDialogSearch : undefined, size: size, placeholder: "Buscar...", autoFocus: true }), isLoading ? (_jsx("div", { className: "flex items-center justify-center py-8", children: _jsx("i", { className: "fa fa-spinner fa-spin text-2xl text-[var(--color-primary)]" }) })) : options.length > 0 ? (_jsx("ul", { className: "space-y-1 max-h-96 overflow-y-auto", children: options.map((option, index) => {
|
|
953
|
+
const label = labelGetter(option);
|
|
954
|
+
const description = descriptionGetter(option);
|
|
955
|
+
const anyOption = option;
|
|
956
|
+
return (_jsx("li", { className: "px-3 py-2 cursor-pointer rounded-md flex items-start gap-2 text-sm\r\n text-[var(--color-text-primary)] hover:bg-[var(--color-bg-secondary)] transition-colors", onClick: () => handleSelect(option), children: renderOption ? (renderOption(option)) : (_jsxs(_Fragment, { children: [anyOption.icon && (_jsx("i", { className: `fa ${anyOption.icon} mt-0.5 text-[var(--color-text-muted)]` })), _jsxs("div", { className: "flex flex-col flex-1", children: [_jsx("span", { className: "font-[var(--font-default)]", children: label }), description !== undefined &&
|
|
957
|
+
description !== null && (_jsx("span", { className: "text-xs text-[var(--color-text-secondary)]", children: description }))] })] })) }, String(valueGetter(option) ?? label ?? index)));
|
|
958
|
+
}) })) : hasSearched ? (_jsx("div", { className: "px-3 py-8 text-center text-sm text-[var(--color-text-secondary)]", children: noResultsText })) : null] }), dialogActions: _jsx(Button, { variant: "outline", onClick: () => setIsDialogOpen(false), children: "Cerrar" }), onClose: () => setIsDialogOpen(false), closeOnOverlayClick: true })] }));
|
|
959
|
+
}
|
|
960
|
+
const SearchSelectInputForwarded = React.forwardRef(SearchSelectInputInner);
|
|
961
|
+
SearchSelectInputForwarded.displayName = "SearchSelectInput";
|
|
962
|
+
export const SearchSelectInput = SearchSelectInputForwarded;
|