Skip to content
This repository was archived by the owner on Oct 26, 2026. It is now read-only.

Commit ee7e3a6

Browse files
fix(plugin-calendar): show the lookup display name for a lookup titleField (#13480)
<!-- claude-projects-attribution: {"github_login":"os-elon-musk"} --> _Requested by **Elon** · [project thread](https://claude.ai/code/project/chan_01WedDzV26nniwxv7jC88B9D?thread=cmsg_01WedDzV26nniwxv7jC88B9D2bmCsjnmZkrozUjaCZ8Xgr)_ Fixes #13471 Clause-②: no Metadata-protocol: none ## What changed `ObjectCalendar` resolved the author's `calendar.titleField` itself (`String(record[titleField])` for any truthy value), so a lookup, which arrives expanded as an object, printed the literal `[object Object]` on every event. The title now goes to `getRecordDisplayName(objectSchema, record, { titleField })`, which takes the field as its explicit rung (ADR-0079) and already reduces an expanded reference to its display name. Same fix family as objectui#12188 and objectui#8464. Precedence is unchanged: the explicit `titleField` wins when it yields a value, otherwise the object-level ladder (`nameField`, `titleFormat`, type-aware derivation, `Record #id`) runs as before. The change is the one function body in `packages/plugin-calendar/src/ObjectCalendar.tsx`, plus a new test and a changeset. ## Acceptance notes - Mechanism check: the PM hypothesis held on `origin/main` (cd70b78). `resolveTitle` returned `String(v)` for an object value. No new fallback or alias was added in the renderer; the existing core resolver is the single authority (AGENTS.md #0.1). - Pin: `packages/plugin-calendar/src/__tests__/ObjectCalendar.lookupTitleField-13471.test.tsx` covers an expanded `{ _id, name }` and `{ id, name }` lookup (shows the name), a raw id string (unchanged), and an expanded object with no usable name or a null value (never `[object Object]`, falls through to `nameField`). - Reverse verification (fix committed first, then `ObjectCalendar.tsx` checked out from the base commit with a restore trap): 2 of 3 tests red on the base source (expanded name, no-usable-name), the raw-id test green on both sides as intended. After restore `git diff HEAD` was empty and the implementation line was present. - Local gates on head 9e316d4, all exit 0: vitest `packages/plugin-calendar/` from the repo root (64 files passed, 465 tests passed, 95 skipped); `pnpm --filter @object-ui/plugin-calendar run type-check` (the test tsconfig lists the new test file) after building the workspace dependency closure; `pnpm --filter @object-ui/plugin-calendar run lint` (0 errors, warnings pre-existing); `node scripts/check-changeset-presence.mjs`; `pnpm run changeset:check`; `node scripts/check-changeset-claims.mjs`; `pnpm run check:new-line-citations` (0 new citations). - Behaviour note: a numeric `0` or boolean `false` titleField value used to fall through to the object-level title (falsy); the shared resolver counts it as a value and prints it. Not a real-world title case; noted for completeness. - Not measured: a live browser check against the hotsrm audit calendar; CI covers the full suite. --- 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude <noreply@anthropic.com>
1 parent b41ed7f commit ee7e3a6

3 files changed

Lines changed: 138 additions & 14 deletions

File tree

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
"@object-ui/plugin-calendar": patch
3+
---
4+
5+
Clause-②: no
6+
Metadata-protocol: none
7+
8+
Fix calendar events reading `[object Object]` when `calendar.titleField` names a lookup field. `ObjectCalendar` now passes the author's `titleField` to the unified record display-name resolver instead of stringifying the value itself, so an expanded lookup shows the referenced record's display name, a raw id string still shows as it did, and an expanded reference with no usable name falls through to the object-level title ladder.

‎packages/plugin-calendar/src/ObjectCalendar.tsx‎

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,20 +1013,21 @@ export const ObjectCalendar: React.FC<ObjectCalendarComponentProps> = ({
10131013
const resolveColorFieldValue = createFieldColorResolver(
10141014
(objectSchema?.fields as Record<string, any> | undefined)?.[colorField ?? ''],
10151015
);
1016-
const resolveTitle = (record: Record<string, any>): string => {
1017-
// 1. Explicit titleField wins when present on the record.
1018-
if (titleField) {
1019-
const v = record[titleField];
1020-
const s = typeof v === 'string' ? v.trim() : v;
1021-
if (s) return String(s);
1022-
}
1023-
// 2-4. Unified object-level resolver (ADR-0079): objectSchema.titleFormat
1024-
// → objectSchema.displayNameField → type-aware field derivation →
1025-
// `Record #<id>` floor. Replaces the old per-view chain (template render
1026-
// → NAME_FIELD_KEY → hard-coded name list → "Untitled") so an event
1027-
// object whose name lives in e.g. `activity_name` shows the real name.
1028-
return getRecordDisplayName(objectSchema, record);
1029-
};
1016+
// The author-chosen `titleField` goes to the unified resolver as its
1017+
// explicit rung rather than being stringified here: a lookup arrives
1018+
// expanded to an object (`{ id, name, … }`) and `String(object)` is the
1019+
// literal `[object Object]`. The resolver reduces an expanded reference to
1020+
// its display name, treats a whitespace-only value as empty, and leaves a
1021+
// raw id string as it is.
1022+
//
1023+
// Precedence: explicit titleField (when it yields a value) →
1024+
// objectSchema.nameField / displayNameField → objectSchema.titleFormat →
1025+
// type-aware field derivation → `Record #<id>` floor (ADR-0079). That
1026+
// replaces the old per-view chain (template render → NAME_FIELD_KEY →
1027+
// hard-coded name list → "Untitled") so an event object whose name lives
1028+
// in e.g. `activity_name` shows the real name.
1029+
const resolveTitle = (record: Record<string, any>): string =>
1030+
getRecordDisplayName(objectSchema, record, { titleField });
10301031

10311032
const scheduled: CalendarViewEvent[] = [];
10321033
const unscheduled: UnscheduledRecord[] = [];
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectui#13471 — a calendar whose `titleField` names a LOOKUP.
11+
*
12+
* The record query asks for the lookup expanded, so the value arrives as an
13+
* object (`{ _id, name }`). `ObjectCalendar` used to stringify the author's
14+
* `titleField` itself, so every event read the literal `[object Object]`. It now
15+
* hands the field to the unified record display-name resolver as the explicit
16+
* rung, which reduces an expanded reference to its display name.
17+
*
18+
* Events are read off the props `CalendarView` receives, the same seam the
19+
* `colorField` ladder test uses.
20+
*/
21+
22+
import React from 'react';
23+
import { render, waitFor, screen } from '@testing-library/react';
24+
import { describe, it, expect, vi } from 'vitest';
25+
import { ObjectCalendar } from '../ObjectCalendar';
26+
27+
vi.mock('@object-ui/plugin-detail', async (importOriginal) => ({
28+
...(await importOriginal<typeof import('@object-ui/plugin-detail')>()),
29+
RecordDetailDrawer: () => null,
30+
deriveRecordPageHref: () => null,
31+
}));
32+
33+
let lastEvents: any[] = [];
34+
35+
vi.mock('../CalendarView', async (importOriginal) => {
36+
const actual = await importOriginal<any>();
37+
return {
38+
...actual,
39+
CalendarView: ({ events }: any) => {
40+
lastEvents = events;
41+
return <div data-testid="calendar-view" data-event-count={String(events.length)} />;
42+
},
43+
};
44+
});
45+
46+
const OBJECT_SCHEMA = {
47+
name: 'srm_supplier_audit',
48+
nameField: 'audit_no',
49+
fields: {
50+
audit_no: { name: 'audit_no', type: 'text' },
51+
supplier: { name: 'supplier', type: 'lookup', reference_to: 'srm_supplier' },
52+
planned_date: { name: 'planned_date', type: 'datetime' },
53+
},
54+
};
55+
56+
function makeDataSource(rows: any[]) {
57+
return {
58+
find: vi.fn(async () => ({ data: rows, total: rows.length })),
59+
findOne: vi.fn(),
60+
create: vi.fn(),
61+
update: vi.fn(),
62+
delete: vi.fn(),
63+
getObjectSchema: vi.fn(async () => OBJECT_SCHEMA),
64+
} as any;
65+
}
66+
67+
async function titlesFor(rows: any[]): Promise<string[]> {
68+
lastEvents = [];
69+
const schema: any = {
70+
type: 'object-calendar',
71+
objectName: 'srm_supplier_audit',
72+
calendar: { startDateField: 'planned_date', titleField: 'supplier' },
73+
};
74+
const { unmount } = render(<ObjectCalendar schema={schema} dataSource={makeDataSource(rows)} />);
75+
try {
76+
let settled: any[] = [];
77+
await waitFor(() => {
78+
const events = lastEvents;
79+
expect(events).toHaveLength(rows.length);
80+
settled = events;
81+
});
82+
return settled.map((e) => e.title);
83+
} finally {
84+
unmount();
85+
expect(screen.queryAllByTestId('calendar-view')).toHaveLength(0);
86+
}
87+
}
88+
89+
describe('objectui#13471 — calendar titleField naming a lookup', () => {
90+
it('an expanded lookup object shows the referenced record name', async () => {
91+
const titles = await titlesFor([
92+
{ id: 'a1', audit_no: 'AUD-1', planned_date: '2026-10-05T09:00:00Z', supplier: { _id: 's1', name: 'Acme Metals' } },
93+
{ id: 'a2', audit_no: 'AUD-2', planned_date: '2026-10-06T09:00:00Z', supplier: { id: 's2', name: 'Borealis Plastics' } },
94+
]);
95+
expect(titles).toEqual(['Acme Metals', 'Borealis Plastics']);
96+
});
97+
98+
it('a raw id string still shows as it did', async () => {
99+
const titles = await titlesFor([
100+
{ id: 'a1', audit_no: 'AUD-1', planned_date: '2026-10-05T09:00:00Z', supplier: 'sup-001' },
101+
]);
102+
expect(titles).toEqual(['sup-001']);
103+
});
104+
105+
it('an expanded object with no usable name never prints [object Object]', async () => {
106+
const titles = await titlesFor([
107+
{ id: 'a1', audit_no: 'AUD-1', planned_date: '2026-10-05T09:00:00Z', supplier: { _id: 's1' } },
108+
{ id: 'a2', audit_no: 'AUD-2', planned_date: '2026-10-06T09:00:00Z', supplier: null },
109+
]);
110+
expect(titles.some((t) => t.includes('[object Object]'))).toBe(false);
111+
// Falls through to the object-level resolver (`nameField`), the same ladder
112+
// an empty titleField always took.
113+
expect(titles).toEqual(['AUD-1', 'AUD-2']);
114+
});
115+
});

0 commit comments

Comments
 (0)