phx-react
Version:
PHX REACT
217 lines • 12 kB
JavaScript
"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