From 225d63e38444d06a71af648f4d7ad4ca1a7e3978 Mon Sep 17 00:00:00 2001 From: Duradundi Hadimani Date: Thu, 13 Nov 2025 16:33:24 +0530 Subject: [PATCH] Added Linkfield.tsx --- src/components/LinkField.tsx | 201 +++++++++++++++++++++++++++++++++++ src/pages/AssetDetail.tsx | 59 ++++++++-- 2 files changed, 250 insertions(+), 10 deletions(-) create mode 100644 src/components/LinkField.tsx diff --git a/src/components/LinkField.tsx b/src/components/LinkField.tsx new file mode 100644 index 0000000..f09d7d2 --- /dev/null +++ b/src/components/LinkField.tsx @@ -0,0 +1,201 @@ +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; +} + +const LinkField: React.FC = ({ + 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(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 ( +
+ + + !disabled && setDropdownOpen(true)} + onChange={(e) => { + const text = e.target.value; + setSearchText(text); + searchLink(text); + onChange(text); + }} + className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 + disabled:bg-gray-100 dark:disabled:bg-gray-700 + border-gray-300 dark:border-gray-600 + bg-white dark:bg-gray-700 text-gray-900 dark:text-white`} + /> + + {isDropdownOpen && searchResults.length > 0 && ( +
    + {searchResults.map((item, idx) => ( +
  • { + onChange(item.value); + setDropdownOpen(false); + }} + className={`px-3 py-2 hover:bg-blue-100 dark:hover:bg-gray-700 cursor-pointer + ${value === item.value ? 'bg-blue-50 dark:bg-gray-600 font-semibold' : ''}`} + > +
    {item.value}
    + {item.description && ( +
    {item.description}
    + )} +
  • + ))} +
+ )} +
+ ); +}; + +export default LinkField; + + + + + +// import React, { useState, useEffect, useRef } from 'react'; +// import apiService from '../services/apiService';// ✅ uses your existing ApiService + +// interface LinkFieldProps { +// label: string; +// doctype: string; +// value: string; +// onChange: (value: string) => void; +// placeholder?: string; +// } + +// const LinkField: React.FC = ({ label, doctype, value, onChange, placeholder }) => { +// const [searchResults, setSearchResults] = useState<{ value: string; description?: string }[]>([]); +// const [searchText, setSearchText] = useState(''); +// const [isDropdownOpen, setDropdownOpen] = useState(false); +// const containerRef = useRef(null); + +// // Function to call ERPNext link search API +// 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); +// } +// }; + +// // Load default results 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 ( +//
+// {/* */} +// +// setDropdownOpen(true)} +// onChange={(e) => { +// const text = e.target.value; +// setSearchText(text); +// searchLink(text); +// onChange(text); +// }} +// /> +// {isDropdownOpen && searchResults.length > 0 && ( +//
    +// {searchResults.map((item, idx) => ( +//
  • { +// onChange(item.value); +// setDropdownOpen(false); +// }} +// className={`p-2 hover:bg-blue-100 cursor-pointer ${ +// value === item.value ? 'bg-blue-50 font-semibold' : '' +// }`} +// > +// {item.value} +// {item.description && {item.description}} +//
  • +// ))} +//
+// )} +//
+// ); +// }; + +// export default LinkField; diff --git a/src/pages/AssetDetail.tsx b/src/pages/AssetDetail.tsx index 1a8cbf5..c3064fc 100644 --- a/src/pages/AssetDetail.tsx +++ b/src/pages/AssetDetail.tsx @@ -4,6 +4,9 @@ import { useAssetDetails, useAssetMutations } from '../hooks/useAsset'; import { FaArrowLeft, FaSave, FaEdit, FaQrcode } from 'react-icons/fa'; import type { CreateAssetData } from '../services/assetService'; +import LinkField from '../components/LinkField'; + + const AssetDetail: React.FC = () => { const { assetName } = useParams<{ assetName: string }>(); const navigate = useNavigate(); @@ -264,7 +267,7 @@ const AssetDetail: React.FC = () => { /> -
+ {/*
@@ -282,9 +285,17 @@ const AssetDetail: React.FC = () => { -
+
*/} -
+ setFormData({ ...formData, custom_asset_type: val })} + /> + + + {/*
@@ -302,7 +313,14 @@ const AssetDetail: React.FC = () => { -
+
*/} + setFormData({ ...formData, custom_modality: val })} + /> +
-
+ {/*
@@ -397,7 +415,14 @@ const AssetDetail: React.FC = () => { disabled={!isEditing} 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" /> -
+
*/} + + setFormData({ ...formData, manufacturer: val })} + />