237 lines
8.1 KiB
TypeScript
237 lines
8.1 KiB
TypeScript
import React, { useState, useEffect, useRef } from 'react';
|
|
import apiService from '../services/apiService';
|
|
|
|
interface LinkFieldProps {
|
|
label: string;
|
|
doctype: string;
|
|
value: string;
|
|
onChange: (value: string) => void;
|
|
placeholder?: string;
|
|
disabled?: boolean;
|
|
filters?: Record<string, any>;
|
|
}
|
|
|
|
const LinkField: React.FC<LinkFieldProps> = ({
|
|
label,
|
|
doctype,
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
disabled = false,
|
|
filters = {},
|
|
}) => {
|
|
const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]);
|
|
const [searchText, setSearchText] = useState('');
|
|
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Fetch link options from ERPNext with filters
|
|
const searchLink = async (text: string = '') => {
|
|
try {
|
|
const params = new URLSearchParams({
|
|
doctype,
|
|
txt: text,
|
|
});
|
|
|
|
// Add filters if provided
|
|
if (filters && Object.keys(filters).length > 0) {
|
|
// Convert filters to JSON string for Frappe API
|
|
params.append('filters', JSON.stringify(filters));
|
|
}
|
|
|
|
const response = await apiService.apiCall<{ value: string; description?: string }[]>(
|
|
`/api/method/frappe.desk.search.search_link?${params.toString()}`
|
|
);
|
|
setSearchResults(response || []);
|
|
} catch (error) {
|
|
console.error(`Error fetching ${doctype} links:`, error);
|
|
setSearchResults([]);
|
|
}
|
|
};
|
|
|
|
// Fetch default options when dropdown opens or filters change
|
|
useEffect(() => {
|
|
if (isDropdownOpen) {
|
|
searchLink(searchText || '');
|
|
}
|
|
}, [isDropdownOpen, filters]); // Re-fetch when filters change
|
|
|
|
// Close dropdown when clicking outside
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setDropdownOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}, []);
|
|
|
|
return (
|
|
<div ref={containerRef} className="relative w-full mb-4">
|
|
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
|
|
{label}
|
|
</label>
|
|
|
|
<input
|
|
type="text"
|
|
value={value}
|
|
placeholder={placeholder || `Select ${label}`}
|
|
disabled={disabled}
|
|
className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md
|
|
focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700
|
|
bg-white dark:bg-gray-700 text-gray-900 dark:text-white`}
|
|
onFocus={() => !disabled && setDropdownOpen(true)}
|
|
onChange={(e) => {
|
|
const text = e.target.value;
|
|
setSearchText(text);
|
|
searchLink(text);
|
|
onChange(text);
|
|
}}
|
|
/>
|
|
|
|
{isDropdownOpen && searchResults.length > 0 && !disabled && (
|
|
<ul className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
|
rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg">
|
|
{searchResults.map((item, idx) => (
|
|
<li
|
|
key={idx}
|
|
onClick={() => {
|
|
onChange(item.value);
|
|
setDropdownOpen(false);
|
|
}}
|
|
className={`px-3 py-2 cursor-pointer
|
|
text-gray-900 dark:text-gray-100
|
|
hover:bg-blue-500 dark:hover:bg-blue-600 hover:text-white
|
|
${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`}
|
|
>
|
|
{item.value}
|
|
{item.description && (
|
|
<span className="text-gray-600 dark:text-gray-300 text-xs ml-2">
|
|
{item.description}
|
|
</span>
|
|
)}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
{/* Show message when no results found */}
|
|
{isDropdownOpen && searchResults.length === 0 && !disabled && (
|
|
<div className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
|
rounded-md mt-1 w-full shadow-lg p-3 text-center text-gray-500 dark:text-gray-400 text-sm">
|
|
No results found
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default LinkField;
|
|
|
|
|
|
// import React, { useState, useEffect, useRef } from 'react';
|
|
// import apiService from '../services/apiService'; // ✅ your ApiService
|
|
|
|
// interface LinkFieldProps {
|
|
// label: string;
|
|
// doctype: string;
|
|
// value: string;
|
|
// onChange: (value: string) => void;
|
|
// placeholder?: string;
|
|
// disabled?: boolean;
|
|
// filters?: Record<string, any>
|
|
// }
|
|
|
|
// const LinkField: React.FC<LinkFieldProps> = ({
|
|
// label,
|
|
// doctype,
|
|
// value,
|
|
// onChange,
|
|
// placeholder,
|
|
// disabled = false,
|
|
// }) => {
|
|
// const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]);
|
|
// const [searchText, setSearchText] = useState('');
|
|
// const [isDropdownOpen, setDropdownOpen] = useState(false);
|
|
// const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// // Fetch link options from ERPNext
|
|
// const searchLink = async (text: string = '') => {
|
|
// try {
|
|
// const params = new URLSearchParams({ doctype, txt: text });
|
|
// const response = await apiService.apiCall<{ value: string; description?: string }[]>(
|
|
// `/api/method/frappe.desk.search.search_link?${params.toString()}`
|
|
// );
|
|
// setSearchResults(response || []);
|
|
// } catch (error) {
|
|
// console.error(`Error fetching ${doctype} links:`, error);
|
|
// }
|
|
// };
|
|
|
|
// // Fetch default options when dropdown opens
|
|
// useEffect(() => {
|
|
// if (isDropdownOpen) searchLink('');
|
|
// }, [isDropdownOpen]);
|
|
|
|
// // Close dropdown when clicking outside
|
|
// useEffect(() => {
|
|
// const handleClickOutside = (event: MouseEvent) => {
|
|
// if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
// setDropdownOpen(false);
|
|
// }
|
|
// };
|
|
// document.addEventListener('mousedown', handleClickOutside);
|
|
// return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
// }, []);
|
|
|
|
// return (
|
|
// <div ref={containerRef} className="relative w-full mb-4">
|
|
// <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">{label}</label>
|
|
|
|
// <input
|
|
// type="text"
|
|
// value={value}
|
|
// placeholder={placeholder || `Select ${label}`}
|
|
// disabled={disabled}
|
|
// className={`w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md
|
|
// focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100 dark:disabled:bg-gray-700
|
|
// bg-white dark:bg-gray-700 text-gray-900 dark:text-white`}
|
|
// onFocus={() => !disabled && setDropdownOpen(true)}
|
|
// onChange={(e) => {
|
|
// const text = e.target.value;
|
|
// setSearchText(text);
|
|
// searchLink(text);
|
|
// onChange(text);
|
|
// }}
|
|
// />
|
|
|
|
// {isDropdownOpen && searchResults.length > 0 && !disabled && (
|
|
// <ul className="absolute z-50 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600
|
|
// rounded-md mt-1 max-h-48 overflow-auto w-full shadow-lg">
|
|
// {searchResults.map((item, idx) => (
|
|
// <li
|
|
// key={idx}
|
|
// onClick={() => {
|
|
// onChange(item.value);
|
|
// setDropdownOpen(false);
|
|
// }}
|
|
// className={`px-3 py-2 cursor-pointer
|
|
// text-gray-900 dark:text-gray-100
|
|
// hover:bg-blue-500 dark:hover:bg-blue-600
|
|
// ${value === item.value ? 'bg-blue-50 dark:bg-blue-700 font-semibold' : ''}`}
|
|
// >
|
|
// {item.value}
|
|
// {item.description && (
|
|
// <span className="text-gray-600 dark:text-gray-300 text-xs ml-2">{item.description}</span>
|
|
// )}
|
|
// </li>
|
|
// ))}
|
|
// </ul>
|
|
// )}
|
|
// </div>
|
|
// );
|
|
// };
|
|
|
|
// export default LinkField;
|