diff --git a/apps/web/app/api/workspace/objects/[name]/entries/options/route.ts b/apps/web/app/api/workspace/objects/[name]/entries/options/route.ts index 5f332e31e62..f8b765faa2a 100644 --- a/apps/web/app/api/workspace/objects/[name]/entries/options/route.ts +++ b/apps/web/app/api/workspace/objects/[name]/entries/options/route.ts @@ -65,20 +65,40 @@ export async function GET( `SELECT * FROM fields WHERE object_id = '${sqlEscape(obj.id)}' ORDER BY sort_order`, ); const displayFieldName = resolveDisplayField(obj, fields); + const displayFieldId = fields.find((field) => field.name === displayFieldName)?.id; // Optional search filter const url = new URL(req.url); const query = url.searchParams.get("q")?.trim() ?? ""; + const escapedQuery = sqlEscape(query.toLowerCase()); + const searchWhereSql = query + ? ` + AND ( + LOWER(e.id) LIKE '%${escapedQuery}%' + OR EXISTS ( + SELECT 1 + FROM entry_fields search_ef + WHERE search_ef.entry_id = e.id + AND search_ef.value IS NOT NULL + AND LOWER(search_ef.value) LIKE '%${escapedQuery}%' + ) + ) + ` + : ""; - // Fetch entries with their display field value + // Fetch entries and always label by the effective display/main field. + // Search can match any field, but labels stay stable for foreign-entry pickers. const rows = duckdbQueryOnFile<{ entry_id: string; label: string | null }>(dbFile, - `SELECT e.id as entry_id, ef.value as label + `SELECT + e.id as entry_id, + display_ef.value as label FROM entries e - LEFT JOIN entry_fields ef ON ef.entry_id = e.id - LEFT JOIN fields f ON f.id = ef.field_id AND f.name = '${sqlEscape(displayFieldName)}' + LEFT JOIN entry_fields display_ef + ON display_ef.entry_id = e.id + ${displayFieldId ? `AND display_ef.field_id = '${sqlEscape(displayFieldId)}'` : "AND 1 = 0"} WHERE e.object_id = '${sqlEscape(obj.id)}' - ${query ? `AND (ef.value IS NOT NULL AND LOWER(ef.value) LIKE '%${sqlEscape(query.toLowerCase())}%')` : ""} - ORDER BY ef.value ASC NULLS LAST + ${searchWhereSql} + ORDER BY COALESCE(display_ef.value, e.id) ASC LIMIT 200`, ); diff --git a/apps/web/app/components/workspace/entry-detail-modal.tsx b/apps/web/app/components/workspace/entry-detail-modal.tsx index 37b4c631176..60708e4bc1e 100644 --- a/apps/web/app/components/workspace/entry-detail-modal.tsx +++ b/apps/web/app/components/workspace/entry-detail-modal.tsx @@ -2,6 +2,7 @@ import { useEffect, useState, useCallback, useRef } from "react"; import { RelationSelect } from "./relation-select"; +import { FormattedFieldValue } from "./formatted-field-value"; function safeString(val: unknown): string { @@ -12,6 +13,9 @@ function safeString(val: unknown): string { return ""; } +const CREATED_AT_KEYS = ["created_at", "Created", "createdAt", "created"] as const; +const UPDATED_AT_KEYS = ["updated_at", "Updated", "updatedAt", "updated"] as const; + // --- Types --- type Field = { @@ -79,6 +83,36 @@ function parseRelationValue(value: string | null | undefined): string[] { return [trimmed]; } +function inputTypeForField(fieldType: string): React.HTMLInputTypeAttribute { + switch (fieldType) { + case "number": + return "number"; + case "date": + return "date"; + case "email": + return "email"; + case "phone": + return "tel"; + case "url": + return "url"; + default: + return "text"; + } +} + +function resolveEntryMetaValue( + entry: Record, + candidateKeys: readonly string[], +): unknown { + for (const key of candidateKeys) { + const value = entry[key]; + if (value !== null && value !== undefined && value !== "") { + return value; + } + } + return undefined; +} + // --- Cell renderers (lightweight variants of object-table ones) --- function EnumBadge({ @@ -148,7 +182,10 @@ function RelationChips({ {ids.map((id) => { const label = fieldLabels?.[id] ?? id; const handleClick = field.related_object_name && onNavigateEntry - ? () => onNavigateEntry(field.related_object_name!, id) + ? (e: React.MouseEvent) => { + e.stopPropagation(); + onNavigateEntry(field.related_object_name!, id); + } : undefined; return (