diff --git a/docs/pr-assets/data-table-link-autosuggest/autosuggest-dropdown.png b/docs/pr-assets/data-table-link-autosuggest/autosuggest-dropdown.png
new file mode 100644
index 000000000..58d55ef15
Binary files /dev/null and b/docs/pr-assets/data-table-link-autosuggest/autosuggest-dropdown.png differ
diff --git a/docs/pr-assets/data-table-link-autosuggest/selected.png b/docs/pr-assets/data-table-link-autosuggest/selected.png
new file mode 100644
index 000000000..e00a0c6eb
Binary files /dev/null and b/docs/pr-assets/data-table-link-autosuggest/selected.png differ
diff --git a/frontend/src/components/data-table/DataRecordFormLayout.tsx b/frontend/src/components/data-table/DataRecordFormLayout.tsx
index 85c006c40..025728d91 100644
--- a/frontend/src/components/data-table/DataRecordFormLayout.tsx
+++ b/frontend/src/components/data-table/DataRecordFormLayout.tsx
@@ -9,6 +9,7 @@ import {
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
+import { LinkFieldInput } from './LinkFieldInput';
import type { DataTableFieldDef } from '@/types/dataTable.types';
export interface LayoutSection {
@@ -190,14 +191,11 @@ export function FieldInput({ field, value, onChange }: FieldInputProps) {
)}
{field.fieldtype === 'Link' && (
- onChange(event.target.value)}
+ onChange={onChange}
disabled={isReadOnly}
- placeholder={`Link to ${field.options || 'table'}...`}
- className="h-8 text-sm"
/>
)}
diff --git a/frontend/src/components/data-table/LinkFieldInput.tsx b/frontend/src/components/data-table/LinkFieldInput.tsx
new file mode 100644
index 000000000..bb2bcaee6
--- /dev/null
+++ b/frontend/src/components/data-table/LinkFieldInput.tsx
@@ -0,0 +1,159 @@
+import { useEffect, useState, useMemo } from 'react';
+import { Combobox, ComboboxOption } from '@/components/ui/combobox';
+import { Input } from '@/components/ui/input';
+import { getTableRecords, getTableRecord } from '@/services/dataTableApi';
+import { db } from '@/lib/frappe-sdk';
+
+export interface LinkFieldInputProps {
+ targetDoctype?: string;
+ value: string;
+ onChange: (value: string) => void;
+ disabled?: boolean;
+}
+
+export function LinkFieldInput({ targetDoctype, value, onChange, disabled }: LinkFieldInputProps) {
+ const [options, setOptions] = useState([]);
+ const [search, setSearch] = useState('');
+ const [loading, setLoading] = useState(false);
+ const [titleField, setTitleField] = useState('name');
+
+ // Single selected record info to show label if not in current options list
+ const [selectedRecordLabel, setSelectedRecordLabel] = useState(null);
+
+ // Fetch title_field from schema
+ useEffect(() => {
+ if (!targetDoctype) return;
+
+ let isMounted = true;
+ const init = async () => {
+ try {
+ const result = await db.getDocList('Huf Data Table', {
+ fields: ['name', 'title_field_name'],
+ filters: [['doctype_name', '=', targetDoctype]],
+ limit: 1
+ });
+ if (isMounted && result && result.length > 0 && result[0].title_field_name) {
+ setTitleField(result[0].title_field_name);
+ }
+ } catch (err) {
+ console.error('Failed to fetch schema for link field', err);
+ }
+ };
+ init();
+
+ return () => { isMounted = false; };
+ }, [targetDoctype]);
+
+ // Fetch current selected record to show correct label initially
+ useEffect(() => {
+ if (!targetDoctype || !value) {
+ setSelectedRecordLabel(null);
+ return;
+ }
+
+ let isMounted = true;
+ const fetchSelected = async () => {
+ try {
+ const record = await getTableRecord(targetDoctype, value);
+ if (isMounted && record) {
+ const label = String(record[titleField] || record.name || value);
+ setSelectedRecordLabel(label);
+ }
+ } catch {
+ // If not found, just fallback
+ if (isMounted) setSelectedRecordLabel(value);
+ }
+ };
+
+ // We only need to fetch if the selected value isn't already in our options list
+ if (!options.some(opt => opt.value === value)) {
+ fetchSelected();
+ } else {
+ const opt = options.find(o => o.value === value);
+ setSelectedRecordLabel(opt?.label || value);
+ }
+
+ return () => { isMounted = false; };
+ }, [targetDoctype, value, titleField, options]);
+
+ // Debounced search
+ useEffect(() => {
+ if (!targetDoctype) return;
+
+ let isMounted = true;
+ const timeout = setTimeout(async () => {
+ setLoading(true);
+ try {
+ const filters: Array<[string, string, unknown]> = [];
+ if (search.trim()) {
+ // Typically we search by title_field or name. Let's use name if title_field is name.
+ filters.push([titleField, 'like', `%${search}%`]);
+ }
+
+ // We only need name and the title field
+ const fields = titleField === 'name' ? ['name'] : ['name', titleField];
+
+ const result = await getTableRecords(targetDoctype, {
+ filters: filters.length > 0 ? filters : undefined,
+ limit: 20,
+ fields
+ });
+
+ if (isMounted && result) {
+ const newOptions = result.items.map(item => ({
+ value: String(item.name),
+ label: String(item[titleField] || item.name)
+ }));
+ setOptions(newOptions);
+ }
+ } catch (err) {
+ console.error('Failed to search link field', err);
+ } finally {
+ if (isMounted) setLoading(false);
+ }
+ }, 300);
+
+ return () => {
+ isMounted = false;
+ clearTimeout(timeout);
+ };
+ }, [targetDoctype, search, titleField]);
+
+ // Ensure current value is included in options so Combobox can display it
+ const allOptions = useMemo(() => {
+ const list = [...options];
+ if (value && !list.some(opt => opt.value === value)) {
+ list.push({
+ value,
+ label: selectedRecordLabel || value
+ });
+ }
+ return list;
+ }, [options, value, selectedRecordLabel]);
+
+ if (!targetDoctype) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
diff --git a/frontend/src/components/ui/combobox.tsx b/frontend/src/components/ui/combobox.tsx
index aabc86350..14b07e66c 100644
--- a/frontend/src/components/ui/combobox.tsx
+++ b/frontend/src/components/ui/combobox.tsx
@@ -32,6 +32,8 @@ export interface ComboboxProps {
emptyText?: string;
searchPlaceholder?: string;
linkTo?: (value: string) => string | undefined;
+ onSearchChange?: (search: string) => void;
+ shouldFilter?: boolean;
}
export function Combobox({
@@ -43,6 +45,8 @@ export function Combobox({
emptyText = 'No option found.',
searchPlaceholder = 'Search...',
linkTo,
+ onSearchChange,
+ shouldFilter,
}: ComboboxProps) {
const [open, setOpen] = React.useState(false);
@@ -72,8 +76,8 @@ export function Combobox({
-
-
+
+
{emptyText}