Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .changeset/record-picker-select-label.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
"@object-ui/fields": patch
---

The lookup "Browse all records" Record Picker now formats its columns with
the same field metadata the list view uses (objectui#3333). Previously the
dialog handed cell renderers a bare `{ name, type }` descriptor, so a
`select` column had no `options` and fell back to title-casing the raw
stored value (`manufacturing` rendered as "Manufacturing" instead of the
authored option label, e.g. "03 制造") — while the same field displayed
correctly in the list view and on the record detail page.

`RecordPickerDialog` gains an optional `fieldsMeta` prop (the referenced
object's schema `fields` map). When provided, each column's field descriptor
is enriched from the schema — `options` (run through the shared i18n option
translation), `currency`, `scale`, `precision`, `format`, `reference_to`, … —
and columns authored as plain strings in `lookup_columns` inherit the schema
field's `type`, so they format identically to typed columns. `LookupField`
passes the referenced object's schema it already fetches for `titleFormat`.
Callers that don't pass `fieldsMeta` keep the previous behavior.
1 change: 1 addition & 0 deletions packages/fields/src/widgets/LookupField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1248,6 +1248,7 @@ export function LookupField({ value, onChange, field, readonly, ...props }: Fiel
lookupFilters={lookupFilters}
baseFilter={dependentFilter}
cellRenderer={getCellRendererResolver()}
fieldsMeta={refObjectSchema?.fields}
filterColumns={filterColumns}
/>
)}
Expand Down
112 changes: 112 additions & 0 deletions packages/fields/src/widgets/RecordPickerDialog.selectLabel.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
/**
* 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.
*/

/**
* Record Picker select-column label resolution — #3333.
*
* The "Browse all records" picker table must format cells with the SAME
* field metadata the list view uses. Pre-fix the dialog handed the cell
* renderer a bare `{ name, type }` descriptor, so a `select` column had no
* `options` and fell back to title-casing the raw stored value
* (`manufacturing` → "Manufacturing" instead of the authored label).
*
* With `fieldsMeta` (the referenced object's schema `fields` map) the dialog
* enriches each column's field descriptor — options, currency, scale, … —
* so select columns resolve their option labels, and string-authored
* `lookup_columns` (no `type` on the column def) inherit the schema field's
* type and format identically.
*/

import { render, screen, waitFor } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { RecordPickerDialog } from './RecordPickerDialog';
import { getCellRenderer } from '../index';

const projects = [
{ id: 'p1', name: 'Line A retooling', project_phase: 'manufacturing' },
];

const projectFields = {
name: { type: 'text', label: 'Name' },
project_phase: {
type: 'select',
label: 'Project Phase',
options: [
{ label: '01 立项', value: 'initiation' },
{ label: '03 制造', value: 'manufacturing' },
],
},
};

function makeDataSource() {
const find = vi.fn(async () => ({ data: projects, total: projects.length }));
return { find } as any;
}

describe('RecordPickerDialog — select columns resolve option labels (#3333)', () => {
it('renders the authored option label when fieldsMeta provides options', async () => {
render(
<RecordPickerDialog
open
onOpenChange={() => {}}
dataSource={makeDataSource()}
objectName="projects"
columns={[
{ field: 'name', label: 'Name', type: 'text' },
{ field: 'project_phase', label: 'Project Phase', type: 'select' },
]}
onSelect={() => {}}
cellRenderer={getCellRenderer}
fieldsMeta={projectFields}
/>,
);

await waitFor(() => {
expect(screen.getByText('03 制造')).toBeInTheDocument();
});
expect(screen.queryByText('Manufacturing')).not.toBeInTheDocument();
});

it('string-authored columns without a type inherit the schema field type', async () => {
render(
<RecordPickerDialog
open
onOpenChange={() => {}}
dataSource={makeDataSource()}
objectName="projects"
columns={['name', 'project_phase']}
onSelect={() => {}}
cellRenderer={getCellRenderer}
fieldsMeta={projectFields}
/>,
);

await waitFor(() => {
expect(screen.getByText('03 制造')).toBeInTheDocument();
});
expect(screen.queryByText('Manufacturing')).not.toBeInTheDocument();
});

it('falls back to the humanized raw value without fieldsMeta (legacy behavior)', async () => {
render(
<RecordPickerDialog
open
onOpenChange={() => {}}
dataSource={makeDataSource()}
objectName="projects"
columns={[{ field: 'project_phase', label: 'Project Phase', type: 'select' }]}
onSelect={() => {}}
cellRenderer={getCellRenderer}
/>,
);

await waitFor(() => {
expect(screen.getByText('Manufacturing')).toBeInTheDocument();
});
});
});
50 changes: 45 additions & 5 deletions packages/fields/src/widgets/RecordPickerDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
X,
} from 'lucide-react';
import type { DataSource, LookupColumnDef, LookupFilterDef } from '@object-ui/types';
import { useSafeFieldLabel } from '@object-ui/i18n';
import { useFieldTranslation } from './useFieldTranslation';
import { useRecordQuery } from './useRecordQuery';

Expand Down Expand Up @@ -278,6 +279,18 @@ export interface RecordPickerDialogProps {
*/
cellRenderer?: CellRendererResolver;

/**
* The referenced object's schema `fields` map (field name → field
* definition). When provided, cell renderers receive the FULL field
* metadata — `options`, `currency`, `scale`, `precision`, `format`,
* `reference_to`, … — exactly like the list view enriches its columns from
* the object schema. Without it a `select` column falls back to
* title-casing the raw stored value instead of resolving the option label
* (#3333: `manufacturing` rendered as "Manufacturing" instead of the
* authored option label).
*/
fieldsMeta?: Record<string, any>;

/**
* Filter bar column definitions.
* When provided, shows an inline filter bar below the search input.
Expand Down Expand Up @@ -344,11 +357,13 @@ export function RecordPickerDialog({
lookupFilters,
baseFilter,
cellRenderer,
fieldsMeta,
filterColumns,
renderFilterBar,
renderGrid,
}: RecordPickerDialogProps) {
const { t } = useFieldTranslation();
const { translateOptions } = useSafeFieldLabel();

// Query state (records/loading/error/total + page/search/sort) lives in the
// shared useRecordQuery kernel — instantiated after mergedFilter below.
Expand Down Expand Up @@ -385,6 +400,29 @@ export function RecordPickerDialog({
return [{ field: displayField, label: fieldToLabel(displayField) }];
}, [columnsProp, displayField]);

// Field descriptors handed to the type-aware cell renderers, enriched from
// the referenced object's schema (`fieldsMeta`) the same way the list view
// enriches its columns. This is what lets a `select` column resolve its
// option label (options + i18n) instead of title-casing the raw value
// (#3333). Columns whose def carries no `type` inherit the schema field's
// type so authored string `lookup_columns` format identically.
const columnFieldDescriptors = useMemo<Record<string, any>>(() => {
const map: Record<string, any> = {};
for (const col of resolvedColumns) {
const meta = fieldsMeta?.[col.field];
const type = col.type ?? meta?.type;
if (!type) continue;
const descriptor: any = meta
? { ...meta, name: col.field, type }
: { name: col.field, type };
if (Array.isArray(descriptor.options) && objectName) {
descriptor.options = translateOptions(objectName, col.field, descriptor.options);
}
map[col.field] = descriptor;
}
return map;
}, [resolvedColumns, fieldsMeta, objectName, translateOptions]);

// Auto-generate filter columns from lookupFilters when no explicit filterColumns given.
// Each LookupFilterDef becomes a filterable field with inferred type.
const effectiveFilterColumns = useMemo<RecordPickerFilterColumn[] | undefined>(() => {
Expand Down Expand Up @@ -637,11 +675,13 @@ export function RecordPickerDialog({

const val = record[col.field];

// Use type-aware renderer when column type and resolver are available
if (col.type && cellRenderer) {
const Renderer = cellRenderer(col.type);
// Use type-aware renderer when a field descriptor (column `type`, or the
// schema field's type via `fieldsMeta`) and a resolver are available.
const descriptor = columnFieldDescriptors[col.field];
if (descriptor && cellRenderer) {
const Renderer = cellRenderer(descriptor.type);
if (Renderer) {
return <Renderer value={val} field={{ name: col.field, type: col.type } as any} />;
return <Renderer value={val} field={descriptor} />;
}
}

Expand All @@ -657,7 +697,7 @@ export function RecordPickerDialog({
}
if (typeof val === 'boolean') return val ? 'Yes' : 'No';
return String(val);
}, [cellRenderer, titleFormat, displayField]);
}, [cellRenderer, titleFormat, displayField, columnFieldDescriptors]);

// Render sort indicator for a column
const renderSortIcon = useCallback((field: string) => {
Expand Down
Loading