UNPKG

phx-react

Version:

PHX REACT

217 lines 12 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.classNames = classNames; const tslib_1 = require("tslib"); const outline_1 = require("@heroicons/react/24/outline"); const react_1 = tslib_1.__importStar(require("react")); const useDebounce_1 = tslib_1.__importDefault(require("../Func/useDebounce")); const ErrorMessage_1 = tslib_1.__importDefault(require("../../commons/ErrorMessage")); const PHXGrpcClientV3_1 = require("../Func/GRPC/PHXGrpcClientV3"); const constants_1 = require("../../utils/constants"); const helpers_1 = require("../../helpers/helpers"); const EmptySearchResult_1 = tslib_1.__importDefault(require("./EmptySearchResult")); function classNames(...classes) { return classes.filter(Boolean).join(' '); } const PHXSearchResultListV3 = ({ defaultInputText = '', disabled, error, errorMessageCustom, errorType, filterArray, getSearchResult, handleCustomRender, handleQueryValue, isArray, isFillInput, label, listSelected, name, placeholder, queryValueType = 'default', register, search, setValue, isUnaccentSearchValue = false, isClearAfterSelect = false, }) => { const [searchValue, setSearchValue] = (0, react_1.useState)(defaultInputText); const [isSearchLoading, setIsSearchLoading] = (0, react_1.useState)(false); const [isSearching, setIsSearching] = (0, react_1.useState)(false); const [searchDataResult, setSearchDataResult] = (0, react_1.useState)([]); // Refs for improved control const searchIdRef = (0, react_1.useRef)(0); const lastFetchedQueryRef = (0, react_1.useRef)(''); const lastFetchedAtRef = (0, react_1.useRef)(0); const lastUserEditAtRef = (0, react_1.useRef)(0); const filterQuery = (query, value) => { const formatValue = isUnaccentSearchValue ? (0, helpers_1.unaccentValue)(value) : value; return query.replace(/%@value%/g, `""%${formatValue.trim()}%""`); }; // fetchSearchData now accepts the query string to avoid closure issues const fetchSearchData = async (value) => { try { const searchQuery = search ? filterQuery(search.query, value) : ''; const res = await (0, PHXGrpcClientV3_1.PHXClientQueryV3)({ query: searchQuery, }); return res; } catch (e) { console.log(e); return null; } }; const getData = async (option) => { try { const startTime = performance.now(); const result = await option(); if (!result) return null; const data = result.data; const endTime = performance.now(); const duration = endTime - startTime; if (duration < constants_1.requestMaxDuration) { await new Promise((r) => setTimeout(r, 300)); } return data; } catch (e) { console.error(e); return null; } }; const searchChange = (e) => { const value = e.target.value.replace(/\\/g, ''); setSearchValue(value); setIsSearching(value !== ''); lastUserEditAtRef.current = Date.now(); // mark user edit time }; const debouncedSearch = (0, useDebounce_1.default)(searchValue, 300); // keep formatDynamicFields & message & handleClickSearchItem as-is const formatDynamicFields = (dataParams) => { const formattedValues = []; const searchData = {}; filterArray.forEach((item) => { if (item.keyParent) { const parentData = dataParams[item.keyParent] || {}; searchData[item.keyChild] = (parentData.length > 0 ? parentData[0][item.keyChild] : parentData[item.keyChild]) || ''; } else { searchData[item.keyChild] = dataParams[item.keyChild]; } }); for (const field in searchData) { if (searchData[field] !== null && searchData[field] !== undefined) { formattedValues.push(`${searchData[field]}` .toLowerCase() .split(' ') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' ')); } } return formattedValues.join(' - '); }; const handleClickSearchItem = (item) => { getSearchResult(item); setIsSearching(false); const formatted = formatDynamicFields(item); if (isArray || isClearAfterSelect) { setSearchValue(''); if (isClearAfterSelect) { setValue(name, ''); } } else if (isFillInput && handleCustomRender) { setSearchValue(handleCustomRender(item)); } else { setSearchValue(formatted); } }; const message = (type) => { let errorMessage = ''; switch (type) { case 'required-field': errorMessage = 'Vui lòng nhập thông tin'; break; case 'validate-input-field': errorMessage = 'Vui lòng nhập thông tin hợp lệ (độ dài 1 - 60 kí tự)'; break; case 'duplicate-field': errorMessage = 'Thông tin đã tồn tại'; break; case 'validate-phone-number': errorMessage = 'Số điện thoại chưa hợp lệ'; break; case 'validate-input-email': errorMessage = 'Định dạng email chưa đúng, vui lòng nhập lại'; break; case 'custom-message': errorMessage = errorMessageCustom; break; default: break; } return errorMessage; }; // Improved search function that accepts the debounced query const handleSearch = async (query) => { const currentId = ++searchIdRef.current; setIsSearchLoading(true); const data = await getData(() => fetchSearchData(query)); // if fetch failed or was null if (!data) { // only clear loading if still the la request if (currentId === searchIdRef.current) setIsSearchLoading(false); return; } // If another search has started meanwhile, drop this result if (currentId !== searchIdRef.current) { return; } const dataTable = search.keyResult; const result = queryValueType === 'default' ? data[dataTable] : handleQueryValue(data[dataTable]); let finalData = result; if (listSelected && listSelected.length > 0) { finalData = result.map((item) => { let isSelected; switch (name) { case 'user_assigned_tuition': isSelected = listSelected.some((selectedItem) => selectedItem.profile_student.user_code === item.profile_student.user_code); break; default: isSelected = listSelected.some((selectedItem) => selectedItem.id === item.id); break; } return isSelected ? { ...item, is_selected: true } : item; }); } setSearchDataResult(finalData); lastFetchedQueryRef.current = query; lastFetchedAtRef.current = Date.now(); // only clear loading if this is still the latest request if (currentId === searchIdRef.current) setIsSearchLoading(false); }; // NOTE: removed the old effect that set isSearchLoading(true) on every searchValue change. // We now rely on the debounced effect to start/stop loading. (0, react_1.useEffect)(() => { // If debouncedSearch is empty -> clear results and stop loading if (!debouncedSearch || debouncedSearch.trim() === '') { setSearchDataResult([]); setIsSearchLoading(false); return; } // If we already fetched this exact query and user hasn't edited since last fetch, skip fetching const userEditedAfterLastFetch = lastUserEditAtRef.current > lastFetchedAtRef.current; const needFetch = debouncedSearch !== lastFetchedQueryRef.current || userEditedAfterLastFetch; if (!needFetch) { // nothing to do return; } // run search for this debounced value handleSearch(debouncedSearch); setValue(name, debouncedSearch); // eslint-disable-next-line react-hooks/exhaustive-deps }, [debouncedSearch]); return (react_1.default.createElement("div", { className: 'relative w-full' }, react_1.default.createElement("div", { className: 'relative' }, label && react_1.default.createElement("label", { className: 'mb-1 block text-xs font-normal text-gray-700' }, label), react_1.default.createElement("div", { className: 'flex items-center' }, react_1.default.createElement("div", { className: 'relative w-full' }, react_1.default.createElement("span", { className: 'absolute inset-y-0 left-0 flex items-center pl-3 text-gray-500' }, react_1.default.createElement(outline_1.MagnifyingGlassIcon, { className: 'h-4 w-4' })), react_1.default.createElement("input", { autoComplete: 'off', className: classNames('block w-full rounded-lg border-[0.5px] border-gray-500 px-3 py-1.5 pl-9 pr-3 text-xs font-normal shadow-sm focus:border-gray-500 focus:outline-none focus:outline-offset-1 focus:outline-indigo-500 focus:ring-transparent', error ? 'border-red-800 bg-red-50 hover:bg-red-50 focus:border-red-800 focus:bg-red-50' : '', disabled ? 'bg-neutral-200 border-none cursor-not-allowed' : ''), name: name, onInput: searchChange, placeholder: placeholder, value: isArray || isFillInput ? searchValue : undefined, disabled: disabled, ...register, type: 'text' })))), error && errorType ? react_1.default.createElement(ErrorMessage_1.default, { message: message(errorType) }) : null, react_1.default.createElement("div", { className: 'absolute z-50 w-full drop-shadow-xl' }, isSearching && (searchDataResult === null || searchDataResult === void 0 ? void 0 : searchDataResult.length) > 0 ? (react_1.default.createElement("div", { className: 'my-2 min-w-full pb-2' }, react_1.default.createElement("div", { className: 'max-h-[300px] divide-gray-200 overflow-y-auto rounded-xl border border-gray-100 bg-white px-2 py-1' }, searchDataResult.map((item) => ( // @ts-ignore react_1.default.createElement("button", { key: item.id, "aria-expanded": 'false', "aria-haspopup": 'listbox', "aria-labelledby": 'headlessui-combobox-label-:rk: headlessui-combobox-button-:rm:', className: `${item.is_selected ? 'bg-gray-200 font-medium text-gray-900' : 'hover:bg-gray-100'} inset-y-0 right-0 my-1 mt-1 flex w-full items-center rounded-md px-2 py-[6px] pr-4 font-sans text-xs text-gray-600 transition-colors focus:outline-none`, "data-headlessui-state": '', disabled: item.is_selected, onClick: () => handleClickSearchItem(item), tabIndex: -1, type: 'button' }, react_1.default.createElement("div", { className: 'flex w-full justify-between' }, handleCustomRender ? react_1.default.createElement("p", null, handleCustomRender(item)) : react_1.default.createElement("p", null, formatDynamicFields(item)), item.is_selected ? react_1.default.createElement(outline_1.CheckIcon, { className: 'h-4 w-4' }) : null))))))) : (react_1.default.createElement(react_1.default.Fragment, null, isSearching && react_1.default.createElement(EmptySearchResult_1.default, { isSearchLoading: isSearchLoading })))))); }; exports.default = PHXSearchResultListV3; //# sourceMappingURL=SearchResultList.js.map