From 68015d6c143c046d24621f7160fc107cea55dc59 Mon Sep 17 00:00:00 2001 From: kumarabhirup Date: Tue, 3 Mar 2026 22:54:12 -0800 Subject: [PATCH] refactor(web): enhance entry detail and object table components for improved data handling This commit introduces several enhancements across the EntryDetailModal and ObjectTable components. Key changes include the addition of a FormattedFieldValue component for consistent display of various field types, improved handling of entry metadata, and the introduction of input type resolution for fields. Additionally, navigation callbacks for entries have been refined to support better interaction within the object table. These updates aim to streamline data presentation and enhance user experience. --- .../objects/[name]/entries/options/route.ts | 32 +- .../workspace/entry-detail-modal.tsx | 74 ++- .../workspace/formatted-field-value.tsx | 100 ++++ .../app/components/workspace/object-table.tsx | 150 +++++- apps/web/app/workspace/page.tsx | 1 + apps/web/lib/workspace-cell-format.test.ts | 110 ++++ apps/web/lib/workspace-cell-format.ts | 501 ++++++++++++++++++ apps/web/tsconfig.tsbuildinfo | 2 +- 8 files changed, 920 insertions(+), 50 deletions(-) create mode 100644 apps/web/app/components/workspace/formatted-field-value.tsx create mode 100644 apps/web/lib/workspace-cell-format.test.ts create mode 100644 apps/web/lib/workspace-cell-format.ts 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 (