From b641bc36f436742e0eb1d4cbfe732b3214b99e7b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:05:03 +0000 Subject: [PATCH 1/3] fix(grid): a lookup or master_detail cell draws no tooltip carrying the raw id (objectui#13377) data-table's cell wrapper set its title tooltip to String(cellValue), and for a reference column that value is the referenced record's id, so hovering beside the drawn name, or beside "Restricted record", showed the id. - @object-ui/types: TableColumn gains optional `foreignKey` (zod mirror typed; the static table column tombstones it, like masked and chips). - @object-ui/components: data-table skips the title tooltip for it. - @object-ui/plugin-grid: ObjectGrid's last column pass stamps it on lookup / master_detail / tree / user columns (isExpandableFieldType over the authored and the object-declared type). Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude --- .changeset/13377-lookup-cell-tooltip.md | 13 ++ ...a-table-foreign-key-tooltip-13377.test.tsx | 114 +++++++++++++++ .../src/renderers/complex/data-table.tsx | 7 +- packages/plugin-grid/src/ObjectGrid.tsx | 11 +- .../lookupCellTooltip-13377.test.tsx | 130 ++++++++++++++++++ .../lookupRestrictedRecord-13335.test.tsx | 5 +- .../static-table-narrow-surface.test.ts | 4 +- packages/types/src/data-display.ts | 24 ++++ packages/types/src/zod/data-display.zod.ts | 8 ++ 9 files changed, 310 insertions(+), 6 deletions(-) create mode 100644 .changeset/13377-lookup-cell-tooltip.md create mode 100644 packages/components/src/renderers/complex/__tests__/data-table-foreign-key-tooltip-13377.test.tsx create mode 100644 packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx diff --git a/.changeset/13377-lookup-cell-tooltip.md b/.changeset/13377-lookup-cell-tooltip.md new file mode 100644 index 0000000000..db18283480 --- /dev/null +++ b/.changeset/13377-lookup-cell-tooltip.md @@ -0,0 +1,13 @@ +--- +'@object-ui/types': minor +'@object-ui/components': patch +'@object-ui/plugin-grid': patch +--- + +Grids: hovering a `lookup` or `master_detail` cell no longer shows the referenced record's raw id (objectui#13377). + +The table's cell wrapper set its `title` tooltip to the cell's raw value, and for a reference column that value is the related record's id. So hovering beside the drawn name, or beside "Restricted record" for a record the viewer cannot read, still showed the id. + +- **`@object-ui/types`:** `TableColumn` gains an optional `foreignKey` boolean: the column's raw value is the id of a referenced record. The zod mirror (`TableColumnSchema`) keeps it, and the static `table` column refuses it by name, like `masked` and `chips`. +- **`@object-ui/components`:** a `data-table` column with `foreignKey: true` draws no `title` tooltip. Nothing else changes for it: copy, CSV export, inline edit, search and sort behave as before. +- **`@object-ui/plugin-grid`:** `ObjectGrid` sets `foreignKey` on every `lookup`, `master_detail`, `tree` and `user` column, read from the column's own type or the object's field type. Every other column keeps its tooltip. diff --git a/packages/components/src/renderers/complex/__tests__/data-table-foreign-key-tooltip-13377.test.tsx b/packages/components/src/renderers/complex/__tests__/data-table-foreign-key-tooltip-13377.test.tsx new file mode 100644 index 0000000000..ff819e8851 --- /dev/null +++ b/packages/components/src/renderers/complex/__tests__/data-table-foreign-key-tooltip-13377.test.tsx @@ -0,0 +1,114 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * `TableColumn.foreignKey` — the cell wrapper draws no `title` tooltip on a + * column whose raw value is a referenced record's id (objectui#13377). + * + * The producer (`ObjectGrid`) draws the record's name through `cell` and sets + * the flag; the wrapper's tooltip was `String(cellValue)`, so hovering beside + * the drawn name showed the id. Pinned three ways in one file: the flagged + * column has no tooltip; an ordinary column in the same table keeps its own; + * and the SAME column with the flag absent (or `false`) carries the id, which + * proves the flag is what withholds it. + */ +import { describe, it, expect, afterEach, vi } from 'vitest'; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import React from 'react'; +import { ComponentRegistry } from '@object-ui/core'; +import { TableColumnSchema } from '@object-ui/types/zod'; +import '../data-table'; + +const RAW_ID = 'CASE-ID-13377'; +const DRAWN = 'Printer jam'; +const CONTROL = 'KA-0025'; +const ROWS = [{ id: '1', title: CONTROL, source_case: RAW_ID }]; + +afterEach(() => { + vi.restoreAllMocks(); + cleanup(); +}); + +/** `foreignKey` undefined ⇒ the key is ABSENT from the column, not `false`. */ +function renderTable(foreignKey: boolean | undefined) { + const DataTable = ComponentRegistry.get('data-table') as any; + if (!DataTable) throw new Error('data-table not registered'); + return render( + DRAWN, + ...(foreignKey === undefined ? {} : { foreignKey }), + }, + ], + pagination: false, + searchable: false, + }} + />, + ); +} + +/** The body cell under the header `label`. */ +function cellUnder(label: string): HTMLElement { + const headers = Array.from(document.querySelectorAll('thead th')); + const index = headers.findIndex((th) => (th.textContent ?? '').trim() === label); + expect(index, `CONTROL: a "${label}" header rendered`).toBeGreaterThanOrEqual(0); + const cell = document.querySelector('tbody tr')?.children[index] as HTMLElement | undefined; + expect(cell?.tagName, `CONTROL: the "${label}" body cell exists`).toBe('TD'); + return cell!; +} + +describe('data-table — `foreignKey: true` draws no tooltip carrying the raw id (objectui#13377)', () => { + it('the foreign-key cell has no `title`; the ordinary cell keeps its value as its tooltip', () => { + renderTable(true); + expect( + cellUnder('Title').querySelector('[title]')?.getAttribute('title'), + 'CONTROL: an ordinary cell keeps its tooltip', + ).toBe(CONTROL); + const ref = cellUnder('Source Case'); + expect(ref.textContent, 'CONTROL: the producer drew the name').toContain(DRAWN); + expect(ref.querySelector('[title]'), 'the foreign-key cell carries no tooltip').toBeNull(); + expect(document.body.innerHTML, 'the raw id is nowhere in the DOM').not.toContain(RAW_ID); + }); + + it('withholds only the tooltip: Ctrl+C still copies the raw value (unlike `masked`)', () => { + const writeText = vi.fn().mockResolvedValue(undefined); + Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText } }); + renderTable(true); + fireEvent.keyDown(cellUnder('Source Case'), { key: 'c', ctrlKey: true }); + expect(writeText.mock.calls.map((call) => call[0])).toEqual([RAW_ID]); + }); +}); + +describe('data-table — the flag ABSENT keeps the tooltip (objectui#13377 control)', () => { + it('absent: the cell carries the raw value as its tooltip, as before', () => { + renderTable(undefined); + expect(cellUnder('Source Case').querySelector('[title]')?.getAttribute('title')).toBe(RAW_ID); + }); + + it('`foreignKey: false` is the same as absent', () => { + renderTable(false); + expect(cellUnder('Source Case').querySelector('[title]')?.getAttribute('title')).toBe(RAW_ID); + }); +}); + +describe('`foreignKey` survives the zod mirror (objectui#13377)', () => { + it('a parsed column keeps the flag instead of stripping it', () => { + const parsed = TableColumnSchema.parse({ header: 'Source Case', accessorKey: 'source_case', foreignKey: true }); + expect(parsed.foreignKey).toBe(true); + expect(TableColumnSchema.safeParse({ header: 'A', accessorKey: 'a', foreignKey: 'yes' }).success).toBe(false); + }); +}); diff --git a/packages/components/src/renderers/complex/data-table.tsx b/packages/components/src/renderers/complex/data-table.tsx index d2d9a2d7b0..df568e9c4b 100644 --- a/packages/components/src/renderers/complex/data-table.tsx +++ b/packages/components/src/renderers/complex/data-table.tsx @@ -3920,8 +3920,11 @@ const DataTableRenderer = ({ schema }: { schema: DataTableSchema }) => { } // No tooltip on a MASKED column (objectui#10583): // the title carried the raw value, so a hover - // showed what the cell's mask hides. - title={!isFit && !col.masked && cellValue != null && typeof cellValue !== 'object' ? String(cellValue) : undefined} + // showed what the cell's mask hides. None on a + // FOREIGN-KEY column either (objectui#13377): its + // raw value is the referenced record's id, which + // the drawn name (or "Restricted record") withholds. + title={!isFit && !col.masked && !col.foreignKey && cellValue != null && typeof cellValue !== 'object' ? String(cellValue) : undefined} // The cell half of the drawn-size read the // auto width is estimated from (objectui#11682). data-auto-width-key={autoWidthKey(col)} diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index 8cfe56695b..ba0e39e602 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -5045,9 +5045,18 @@ export const ObjectGrid: React.FC = ({ // column from a text one. It floors a chip column at its widest drawn // chip. Written only when true, so every other column is unchanged. const chips = !masked && isChipFieldType(producerType ?? objectFields?.[col.accessorKey]?.type); + // objectui#13377 — THE FOREIGN-KEY FLAG, for the same reason again: the + // fold drops `lookup` / `master_detail` / `tree` / `user`, so `data-table` + // could not tell that the raw value its cell `title` shows is the + // referenced record's id. Read as a union of the authored and the + // object-declared type, like the masked flag, since it only withholds. + // A masked column already carries no tooltip. Written only when true. + const foreignKey = !masked && ( + isExpandableFieldType({ type: producerType }) || isExpandableFieldType(objectFields?.[col.accessorKey]) + ); const rest = masked ? { ...draft, masked: true } - : chips ? { ...draft, chips: true } : draft; + : { ...draft, ...(chips ? { chips: true } : {}), ...(foreignKey ? { foreignKey: true } : {}) }; if (producerType == null) return rest; const normalized = normalizeTableColumnType(producerType); if (normalized === undefined) return rest; diff --git a/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx b/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx new file mode 100644 index 0000000000..785a2c99d1 --- /dev/null +++ b/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx @@ -0,0 +1,130 @@ +/** + * ObjectUI + * Copyright (c) 2024-present ObjectStack Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * objectui#13377 — a reference cell's hover tooltip carried the raw id. + * + * `data-table`'s cell wrapper set `title` to `String(cellValue)`, and for a + * `lookup` / `master_detail` column that value is the referenced record's id. + * So hovering beside the drawn name, or beside "Restricted record" + * (objectui#13335), showed the id the cell's text withholds. `ObjectGrid` now + * stamps `foreignKey` on such a column and the table draws no tooltip for it. + * + * Driven through the real `ObjectGrid` with a readable and a restricted + * reference in both a lookup and a master_detail column. The control is the + * plain text column of the same grid, which keeps its value as its tooltip. + */ + +import React from 'react'; +import { describe, it, expect, vi, beforeAll } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { ObjectGrid } from '../ObjectGrid'; +import { registerAllFields } from '@object-ui/fields'; +import { ActionProvider, SchemaRendererProvider } from '@object-ui/react'; + +registerAllFields(); + +const OBJECT = 'kb_article_13377'; +const REFERENCED = 'kb_case_13377'; +const RESTRICTED_ID = 'CASE-RESTRICTED-13377'; +const READABLE = { id: 'CASE-READABLE-13377', name: 'Printer jam' }; +const ROWS = [ + { id: 'a1', title: 'KA-0025', source_case: RESTRICTED_ID, parent_case: READABLE.id }, + { id: 'a2', title: 'KA-0026', source_case: READABLE.id, parent_case: RESTRICTED_ID }, +]; + +beforeAll(() => { + if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = vi.fn() as any; + } +}); + +/** The body cells under the header `label`, one per row. */ +function cellsUnder(container: HTMLElement, label: string): HTMLElement[] { + const headers = Array.from(container.querySelectorAll('thead th')); + const index = headers.findIndex((th) => (th.textContent ?? '').trim() === label); + expect(index, `CONTROL: a "${label}" header rendered`).toBeGreaterThanOrEqual(0); + const cells = Array.from(container.querySelectorAll('tbody tr')).map((tr) => tr.children[index] as HTMLElement); + expect(cells, `CONTROL: one "${label}" cell per row`).toHaveLength(ROWS.length); + return cells; +} + +/** Every `title` attribute inside a cell. */ +const titlesIn = (cell: HTMLElement) => + Array.from(cell.querySelectorAll('[title]')).map((el) => el.getAttribute('title') ?? ''); + +async function renderGrid() { + const ds = { + find: vi.fn(async () => ({ data: ROWS, total: ROWS.length, hasMore: false, pageSize: 50 })), + findOne: vi.fn(async (objectName: string, id: string) => + objectName === REFERENCED && id === READABLE.id ? READABLE : null, + ), + getObjectSchema: async (name: string) => + name === REFERENCED + ? { name, fields: { id: { type: 'text' }, name: { type: 'text' } } } + : { + name, + fields: { + id: { type: 'text' }, + title: { type: 'text', label: 'Title' }, + source_case: { type: 'lookup', label: 'Source Case', reference: REFERENCED }, + parent_case: { type: 'master_detail', label: 'Parent Case', reference: REFERENCED }, + }, + }, + } as any; + const schema: any = { + type: 'object-grid', + objectName: OBJECT, + data: ROWS, + pagination: { pageSize: 50 }, + columns: [ + { field: 'title', label: 'Title' }, + { field: 'source_case', label: 'Source Case', type: 'lookup' }, + // No authored type: the flag comes from the object's declared type. + { field: 'parent_case', label: 'Parent Case' }, + ], + }; + const utils = render( + + + + + , + ); + await waitFor(() => expect(screen.getAllByText('Printer jam')).toHaveLength(2), { timeout: 4000 }); + await waitFor(() => expect(screen.getAllByText('Restricted record')).toHaveLength(2), { timeout: 4000 }); + return utils; +} + +describe('objectui#13377 — ObjectGrid: a reference cell draws no tooltip carrying the raw id', () => { + it('a lookup and a master_detail cell, readable and restricted, carry no `title` with the id', async () => { + const { container } = await renderGrid(); + for (const label of ['Source Case', 'Parent Case']) { + for (const cell of cellsUnder(container, label)) { + const titles = titlesIn(cell); + expect(titles.some((t) => t.includes(RESTRICTED_ID)), `${label}: no tooltip shows the restricted id`).toBe(false); + expect(titles.some((t) => t.includes(READABLE.id)), `${label}: no tooltip shows the readable id`).toBe(false); + // The wrapper `data-table` owns, specifically: it carries no tooltip at all. + const wrapper = cell.querySelector('[data-auto-width-key]'); + expect(wrapper, `CONTROL: ${label} has the table's cell wrapper`).not.toBeNull(); + expect(wrapper!.hasAttribute('title'), `${label}: the wrapper has no title`).toBe(false); + } + } + // The ids are nowhere in the grid's DOM, text or attribute. + expect(container.innerHTML).not.toContain(RESTRICTED_ID); + expect(container.innerHTML).not.toContain(READABLE.id); + }); + + it('CONTROL — the plain text column of the same grid keeps its value as its tooltip', async () => { + const { container } = await renderGrid(); + const [first, second] = cellsUnder(container, 'Title'); + expect(first!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe('KA-0025'); + expect(second!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe('KA-0026'); + }); +}); diff --git a/packages/plugin-grid/src/__tests__/lookupRestrictedRecord-13335.test.tsx b/packages/plugin-grid/src/__tests__/lookupRestrictedRecord-13335.test.tsx index b8a8a269c1..be90cf98e8 100644 --- a/packages/plugin-grid/src/__tests__/lookupRestrictedRecord-13335.test.tsx +++ b/packages/plugin-grid/src/__tests__/lookupRestrictedRecord-13335.test.tsx @@ -82,8 +82,9 @@ describe('objectui#13335 — ObjectGrid: an unreadable lookup cell reads "Restri const marks = container.querySelectorAll('[data-slot="restricted-reference"]'); expect(marks).toHaveLength(1); // What the lookup renderer draws carries no id and no link. (The table's - // own cell wrapper still puts the raw value in its `title`, as it does for - // every primitive cell; that wrapper is `data-table`'s, not this renderer's.) + // own cell wrapper, `data-table`'s and not this renderer's, put the raw + // value in its `title` too, until objectui#13377: that half is pinned in + // `lookupCellTooltip-13377.test.tsx`.) expect(marks[0]!.outerHTML).not.toContain(RESTRICTED_ID); expect(marks[0]!.closest('td')!.querySelector('a')).toBeNull(); expect(container.querySelectorAll('[data-slot="unresolved-reference"]')).toHaveLength(0); diff --git a/packages/types/src/__tests__/static-table-narrow-surface.test.ts b/packages/types/src/__tests__/static-table-narrow-surface.test.ts index ed64a1240e..15ad100fff 100644 --- a/packages/types/src/__tests__/static-table-narrow-surface.test.ts +++ b/packages/types/src/__tests__/static-table-narrow-surface.test.ts @@ -123,6 +123,7 @@ const RETIRED_COLUMN_KEYS: Record = { wrap: true, masked: true, chips: true, + foreignKey: true, }; /** Every key the rich `TableColumn` interface declares. `satisfies` keeps the @@ -152,6 +153,7 @@ const RICH_COLUMN_KEYS = [ 'wrap', 'masked', 'chips', + 'foreignKey', ] as const satisfies readonly (keyof TableColumn)[]; type _RichKeyListExhaustive = Expect>; @@ -328,7 +330,7 @@ describe('the tombstone refusal reaches the author with its remediation text (ob expect(StaticTableColumnSchema.safeParse(LIVE_COLUMN).success).toBe(true); expect(TableZod.safeParse(STATIC_TABLE).success).toBe(true); - for (const key of ['headerIcon', 'fitContent', 'format', 'options', 'currency', 'wrap', 'masked', 'chips'] as const) { + for (const key of ['headerIcon', 'fitContent', 'format', 'options', 'currency', 'wrap', 'masked', 'chips', 'foreignKey'] as const) { const result = StaticTableColumnSchema.safeParse({ header: 'Amount', accessorKey: 'amount', diff --git a/packages/types/src/data-display.ts b/packages/types/src/data-display.ts index a858a447f9..147be3454b 100644 --- a/packages/types/src/data-display.ts +++ b/packages/types/src/data-display.ts @@ -832,6 +832,22 @@ export interface TableColumn { * renderer is opaque to it (objectui#12708). */ chips?: boolean; + /** + * The column's raw value is a FOREIGN KEY: the id of a referenced record + * (a `lookup`, `master_detail`, `tree` or `user` field), while its `cell` + * renderer draws that record's name (for a lookup the viewer cannot read, + * "Restricted record"). The cell carries no `title` tooltip, which would show + * the raw id the drawn cell withholds. Absent or `false`, the tooltip is as + * before. + * + * Unlike {@link TableColumn.masked}, it withholds nothing else: copy, CSV + * export, inline edit, search and sort are unchanged. + * + * Written by `ObjectGrid` from the column's field type, because the table + * cannot tell a reference from text: `type` has no reference member + * (objectui#13377). + */ + foreignKey?: boolean; } /** @@ -907,6 +923,14 @@ export interface StaticTableColumn { * @deprecated Not part of the static `table` renderer's contract. */ chips?: never; + /** + * NOT on the static `table` surface (objectui#13377) — declared on the rich + * {@link TableColumn} only, where `data-table`'s cell tooltip reads it. The + * static renderer draws no `title` tooltip for it to withhold. + * Use `data-table` for the interactive set. + * @deprecated Not part of the static `table` renderer's contract. + */ + foreignKey?: never; /** * RETIRED from the static `table` surface (objectui#5474, ADR-0049) — the * static renderer never read it; a right-aligned column authored here was diff --git a/packages/types/src/zod/data-display.zod.ts b/packages/types/src/zod/data-display.zod.ts index e7e1f9764f..94b7e367ae 100644 --- a/packages/types/src/zod/data-display.zod.ts +++ b/packages/types/src/zod/data-display.zod.ts @@ -351,6 +351,13 @@ export const TableColumnSchema = z.object({ .boolean() .optional() .describe('Chip column: its cells draw chips, so the table floors its width at the widest drawn chip of the loaded rows and a single-word value is never ellipsized'), + // objectui#13377. Serializable metadata, so the mirror TYPES it — + // `z.boolean()`, like `masked` and `chips`; without this line the + // non-strict object would silently STRIP it. + foreignKey: z + .boolean() + .optional() + .describe('Foreign-key column: its raw value is the id of a referenced record, so the cell carries no title tooltip showing that id; copy, export, edit, search and sort are unchanged'), }); /** @@ -409,6 +416,7 @@ export const StaticTableColumnSchema = z.object({ wrap: retirementTombstone('NOT on the static table surface (objectui#6650) — declared on the rich TableColumn only; use data-table'), masked: retirementTombstone('NOT on the static table surface (objectui#10583) — declared on the rich TableColumn only; use data-table'), chips: retirementTombstone('NOT on the static table surface (objectui#12708) — declared on the rich TableColumn only; use data-table'), + foreignKey: retirementTombstone('NOT on the static table surface (objectui#13377) — declared on the rich TableColumn only; use data-table'), }); /** From 3114b300df8e6d72e817133f73aafa0e63354fd4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:19:19 +0000 Subject: [PATCH 2/3] fix(grid): read the foreign-key flag from the type the cell is drawn by (objectui#13377) Same precedence as the chip flag (authored type first, then the object's): a view column that authors `type: 'text'` over a lookup draws the id as its text, so it keeps its tooltip. Pinned as a second control in the grid test. Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude --- packages/plugin-grid/src/ObjectGrid.tsx | 11 +++++----- .../lookupCellTooltip-13377.test.tsx | 20 +++++++++++++++++-- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/packages/plugin-grid/src/ObjectGrid.tsx b/packages/plugin-grid/src/ObjectGrid.tsx index ba0e39e602..eed3d11a33 100644 --- a/packages/plugin-grid/src/ObjectGrid.tsx +++ b/packages/plugin-grid/src/ObjectGrid.tsx @@ -5048,12 +5048,11 @@ export const ObjectGrid: React.FC = ({ // objectui#13377 — THE FOREIGN-KEY FLAG, for the same reason again: the // fold drops `lookup` / `master_detail` / `tree` / `user`, so `data-table` // could not tell that the raw value its cell `title` shows is the - // referenced record's id. Read as a union of the authored and the - // object-declared type, like the masked flag, since it only withholds. - // A masked column already carries no tooltip. Written only when true. - const foreignKey = !masked && ( - isExpandableFieldType({ type: producerType }) || isExpandableFieldType(objectFields?.[col.accessorKey]) - ); + // referenced record's id. Read from the same type as the chip flag (the + // authored one first), the one the cell is drawn by: a view that authors + // `type: 'text'` over a lookup draws the id as text, and keeps the + // tooltip. A masked column already carries none. Written only when true. + const foreignKey = !masked && isExpandableFieldType({ type: producerType ?? objectFields?.[col.accessorKey]?.type }); const rest = masked ? { ...draft, masked: true } : { ...draft, ...(chips ? { chips: true } : {}), ...(foreignKey ? { foreignKey: true } : {}) }; diff --git a/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx b/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx index 785a2c99d1..9fd59e1136 100644 --- a/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx +++ b/packages/plugin-grid/src/__tests__/lookupCellTooltip-13377.test.tsx @@ -17,7 +17,10 @@ * * Driven through the real `ObjectGrid` with a readable and a restricted * reference in both a lookup and a master_detail column. The control is the - * plain text column of the same grid, which keeps its value as its tooltip. + * plain text column of the same grid, which keeps its value as its tooltip, + * and a view column that authors `type: 'text'` over the lookup field: it draws + * the id as its text, so the flag (read from the authored type first, like + * `chips`) is not set and its tooltip stays. */ import React from 'react'; @@ -59,7 +62,7 @@ function cellsUnder(container: HTMLElement, label: string): HTMLElement[] { const titlesIn = (cell: HTMLElement) => Array.from(cell.querySelectorAll('[title]')).map((el) => el.getAttribute('title') ?? ''); -async function renderGrid() { +async function renderGrid(extraColumns: Array> = []) { const ds = { find: vi.fn(async () => ({ data: ROWS, total: ROWS.length, hasMore: false, pageSize: 50 })), findOne: vi.fn(async (objectName: string, id: string) => @@ -88,6 +91,7 @@ async function renderGrid() { { field: 'source_case', label: 'Source Case', type: 'lookup' }, // No authored type: the flag comes from the object's declared type. { field: 'parent_case', label: 'Parent Case' }, + ...extraColumns, ], }; const utils = render( @@ -127,4 +131,16 @@ describe('objectui#13377 — ObjectGrid: a reference cell draws no tooltip carry expect(first!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe('KA-0025'); expect(second!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe('KA-0026'); }); + + it('CONTROL — a view column authoring `type: \'text\'` over the lookup draws the id as text and keeps its tooltip', async () => { + const { container } = await renderGrid([{ field: 'source_case', label: 'As Text', type: 'text' }]); + const [first, second] = cellsUnder(container, 'As Text'); + expect(first!.textContent, 'CONTROL: the authored type draws the raw id').toBe(RESTRICTED_ID); + expect(first!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe(RESTRICTED_ID); + expect(second!.querySelector('[data-auto-width-key]')?.getAttribute('title')).toBe(READABLE.id); + // The lookup-typed column over the same field still carries none. + for (const cell of cellsUnder(container, 'Source Case')) { + expect(cell.querySelector('[data-auto-width-key]')!.hasAttribute('title')).toBe(false); + } + }); }); From 615a7656cf01b148355db7ea842aae264a1441ba Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 16:34:54 +0000 Subject: [PATCH 3/3] docs(data-table): list TableColumn.foreignKey in the Schema block (objectui#13377) Claude-Session: https://claude.ai/code/session_01WJozybDmRH8z2c5SD7vJkj Co-authored-by: Claude --- content/docs/components/complex/data-table.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/content/docs/components/complex/data-table.mdx b/content/docs/components/complex/data-table.mdx index b7476e72ca..4e8cdf163a 100644 --- a/content/docs/components/complex/data-table.mdx +++ b/content/docs/components/complex/data-table.mdx @@ -23,6 +23,7 @@ interface TableColumn { sortable?: boolean; // Enable sorting for this column masked?: boolean; // Withhold the raw value (see Masked columns) chips?: boolean; // Cells draw chips: never narrower than the widest one (see Chip columns) + foreignKey?: boolean; // Raw value is a referenced record's id: the cell draws no title tooltip } interface DataTableSchema {