Skip to content

Commit 6d36c84

Browse files
Copilothotlong
andcommitted
fix(web): add resolveFields helper to eliminate non-null assertions on field names
Address code review feedback: - Add ResolvedField type with guaranteed name and label - Add resolveFields() helper to safely resolve field entries - Use resolveFields() in RecordTable and ObjectRecordPage - Eliminate all field.name! non-null assertions Co-authored-by: hotlong <50353452+hotlong@users.noreply.github.com>
1 parent 7948fd9 commit 6d36c84

File tree

3 files changed

+47
-15
lines changed

3 files changed

+47
-15
lines changed

apps/web/src/components/records/RecordTable.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
TableRow,
1616
} from '@/components/ui/table';
1717
import { FieldRenderer } from './FieldRenderer';
18-
import type { ObjectDefinition, RecordData } from '@/types/metadata';
18+
import type { ObjectDefinition, RecordData, ResolvedField } from '@/types/metadata';
19+
import { resolveFields } from '@/types/metadata';
1920

2021
interface RecordTableProps {
2122
objectDef: ObjectDefinition;
@@ -24,13 +25,19 @@ interface RecordTableProps {
2425
}
2526

2627
export function RecordTable({ objectDef, records, basePath }: RecordTableProps) {
27-
const columnNames =
28-
objectDef.listFields ??
29-
Object.keys(objectDef.fields).filter((k) => k !== 'id' && !objectDef.fields[k].readonly);
28+
const allResolved = resolveFields(objectDef.fields, ['id']);
3029

31-
const columns = columnNames
32-
.map((name) => ({ ...objectDef.fields[name], name }))
33-
.filter((f) => f.type);
30+
let columns: ResolvedField[];
31+
if (objectDef.listFields) {
32+
// Use specified list fields in order
33+
const fieldMap = new Map(allResolved.map((f) => [f.name, f]));
34+
columns = objectDef.listFields
35+
.map((name) => fieldMap.get(name))
36+
.filter((f): f is ResolvedField => !!f);
37+
} else {
38+
// Fallback: all non-readonly fields
39+
columns = allResolved.filter((f) => !f.readonly);
40+
}
3441

3542
if (records.length === 0) {
3643
return (
@@ -49,7 +56,7 @@ export function RecordTable({ objectDef, records, basePath }: RecordTableProps)
4956
<TableHeader>
5057
<TableRow>
5158
{columns.map((col) => (
52-
<TableHead key={col.name}>{col.label ?? col.name}</TableHead>
59+
<TableHead key={col.name}>{col.label}</TableHead>
5360
))}
5461
</TableRow>
5562
</TableHeader>

apps/web/src/pages/apps/object-record.tsx

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { FieldRenderer } from '@/components/records/FieldRenderer';
1212
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
1313
import { Button } from '@/components/ui/button';
1414
import { Separator } from '@/components/ui/separator';
15+
import { resolveFields } from '@/types/metadata';
1516

1617
export default function ObjectRecordPage() {
1718
const { appId, objectName, recordId } = useParams();
@@ -51,9 +52,7 @@ export default function ObjectRecordPage() {
5152
const recordTitle = String(record[primaryField] ?? record.id ?? 'Untitled');
5253

5354
// Group fields: primary info fields vs metadata/readonly fields
54-
const allFields = Object.entries(objectDef.fields)
55-
.filter(([key]) => key !== 'id')
56-
.map(([key, f]) => ({ ...f, name: f.name ?? key }));
55+
const allFields = resolveFields(objectDef.fields, ['id']);
5756
const editableFields = allFields.filter((f) => !f.readonly);
5857
const readonlyFields = allFields.filter((f) => f.readonly);
5958

@@ -81,10 +80,10 @@ export default function ObjectRecordPage() {
8180
{editableFields.map((field) => (
8281
<div key={field.name}>
8382
<dt className="text-sm font-medium text-muted-foreground">
84-
{field.label ?? field.name}
83+
{field.label}
8584
</dt>
8685
<dd className="mt-1 text-sm">
87-
<FieldRenderer field={field} value={record[field.name!]} />
86+
<FieldRenderer field={field} value={record[field.name]} />
8887
</dd>
8988
</div>
9089
))}
@@ -97,10 +96,10 @@ export default function ObjectRecordPage() {
9796
{readonlyFields.map((field) => (
9897
<div key={field.name}>
9998
<dt className="text-sm font-medium text-muted-foreground">
100-
{field.label ?? field.name}
99+
{field.label}
101100
</dt>
102101
<dd className="mt-1 text-sm">
103-
<FieldRenderer field={field} value={record[field.name!]} />
102+
<FieldRenderer field={field} value={record[field.name]} />
104103
</dd>
105104
</div>
106105
))}

apps/web/src/types/metadata.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,32 @@ export interface FieldDefinition {
8383
formula?: string;
8484
}
8585

86+
/**
87+
* A FieldDefinition with its `name` guaranteed to be present.
88+
* Use `resolveFields()` to convert raw object field entries.
89+
*/
90+
export interface ResolvedField extends FieldDefinition {
91+
name: string;
92+
label: string;
93+
}
94+
95+
/**
96+
* Resolve raw field entries from an ObjectDefinition, ensuring every
97+
* field has a `name` (from the record key) and a `label` (fallback to name).
98+
*/
99+
export function resolveFields(
100+
fields: Record<string, FieldDefinition>,
101+
exclude?: string[],
102+
): ResolvedField[] {
103+
return Object.entries(fields)
104+
.filter(([key]) => !exclude?.includes(key))
105+
.map(([key, f]) => ({
106+
...f,
107+
name: f.name ?? key,
108+
label: f.label ?? f.name ?? key,
109+
}));
110+
}
111+
86112
// ── Object Definition ───────────────────────────────────────────
87113

88114
export interface ObjectDefinition {

0 commit comments

Comments
 (0)