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

Commit c7a4c09

Browse files
committed
fix(plugin-timeline): a multi-valued groupByField keeps the group key it had (objectui#12104)
`recordDisplayValueAt` reads no display value off an array, so routing a multiselect, tags or multi-lookup value through it moved every such row under "Unassigned". An array keeps the stringified key it had on main; how a multi-valued row should group is not decided by this change. Pinned as a control. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AswpQDLCKiZos2jCXknwKz
1 parent e32f4ed commit c7a4c09

3 files changed

Lines changed: 26 additions & 6 deletions

File tree

‎.changeset/12104-timeline-group-key.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@ An object timeline with a `groupByField` draws one group per value, headed by th
77
- **A lookup groups by the referenced record's name.** The timeline's own query expands every declared relation, so a lookup value arrives as the related record, and the group header used to read "[object Object]" for every row: a timeline grouped by an owner or an account drew one group. The group key now reads the field's display value through the same resolver the entry title and description use. A select field still groups by its option's label. An empty value groups under "Unassigned", and when no row has a display value the timeline keeps its date buckets. A lookup that arrives unexpanded groups under its id, which is what the title shows for it too.
88
- **A key's rows are gathered into one group.** Entries are sorted by start date, and a group used to end wherever the next entry had another value, so a field whose values alternate in date order (a channel, say) drew the same header many times. Each value now heads exactly one group. Groups appear in the order of their earliest entry, and entries stay in date order inside each group, so a timeline whose values were already contiguous renders as before.
99

10-
Unchanged: a timeline with no `groupByField` (its date buckets), and the horizontal timeline, which draws no groups and keeps its date order. Nothing is added to the package entry: no export, prop or language-pack key.
10+
Unchanged: a timeline with no `groupByField` (its date buckets), the horizontal timeline, which draws no groups and keeps its date order, and the group key of a multi-valued field (a multiselect, tags, or a lookup with `multiple`), which reads as before. Nothing is added to the package entry: no export, prop or language-pack key.

‎packages/plugin-timeline/src/ObjectTimeline.tsx‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -816,16 +816,23 @@ export const ObjectTimeline: React.FC<ObjectTimelineProps> = ({
816816
// unexpanded foreign key groups under the id itself, which is what the
817817
// title slot shows for it too (AGENTS.md #0.1).
818818
//
819+
// An ARRAY (a multi-valued field: `multiselect`, `tags`, a lookup with
820+
// `multiple`) keeps the key it had, its stringified value. The resolver
821+
// reads no display value off an array, so routing one through it would
822+
// move every such row under "Unassigned", and how a multi-valued row
823+
// should group (one combined group, or one group per value) is a
824+
// decision this change does not make.
825+
//
819826
// One group per key is drawn by the renderer's vertical branch, which
820827
// buckets by this key in the order of each key's earliest entry. The
821828
// order is NOT changed here: the horizontal branch draws the same array,
822829
// draws no groups, and must keep its date order.
823830
const groupSelectOptions = optionMap(groupByField);
824831
const groupKeys = mapped.map((m): string | undefined => {
825832
const opt = m.group != null && m.group !== '' ? groupSelectOptions[String(m.group)] : undefined;
826-
return opt?.label
827-
? fieldOptionLabel(objectName, groupByField, String(m.group), opt.label)
828-
: recordDisplayValueAt(m._data, groupByField);
833+
if (opt?.label) return fieldOptionLabel(objectName, groupByField, String(m.group), opt.label);
834+
if (Array.isArray(m.group)) return String(m.group);
835+
return recordDisplayValueAt(m._data, groupByField);
829836
});
830837
if (groupKeys.some((key) => key !== undefined)) {
831838
return mapped.map((m, i) => ({ ...m, group: groupKeys[i] ?? t('timeline.bucket.unassigned') }));

‎packages/plugin-timeline/src/__tests__/ObjectTimeline.groupKey-12104.test.tsx‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@
3636
* ── The controls ────────────────────────────────────────────────────────────
3737
* The date buckets (no `groupByField`), an unexpanded lookup id (the
3838
* resolver's own answer: the id itself, no name is invented for it, AGENTS.md
39-
* #0.1), and the horizontal rail, which draws no groups and keeps its date
40-
* order. Each was right before the fix and must stay right after it. The
39+
* #0.1), a multi-valued field (its key is left as it was), and the horizontal
40+
* rail, which draws no groups and keeps its date order. Each was right before the fix and must stay right after it. The
4141
* horizontal arm is also why the bucketing lives in the renderer's vertical
4242
* branch and not in the composer: both branches draw the same item array.
4343
*
@@ -243,6 +243,19 @@ describe('an explicit groupByField draws one group per display value (objectui#1
243243
expect(feed()).toEqual(['u1: First, Second', 'u2: Third']);
244244
});
245245

246+
// An array has no display value under the resolver, so a multi-valued field
247+
// keeps the key it had (its stringified value) rather than moving every row
248+
// under "Unassigned". How a multi-valued row should group is not decided here.
249+
it('CONTROL: a multi-valued field keeps the group key it had', () => {
250+
mountInline(block({ groupByField: 'tags' }), [
251+
{ id: 't1', subject: 'First', due: '2026-03-01', tags: ['urgent', 'vip'] },
252+
{ id: 't2', subject: 'Second', due: '2026-03-02', tags: ['urgent', 'vip'] },
253+
{ id: 't3', subject: 'Third', due: '2026-03-03', tags: ['vip'] },
254+
]);
255+
256+
expect(feed()).toEqual(['urgent,vip: First, Second', 'vip: Third']);
257+
});
258+
246259
it('CONTROL: a timeline with no groupByField draws its date buckets, in date order, exactly as before', () => {
247260
vi.useFakeTimers({ toFake: ['Date'] });
248261
// Tuesday 2026-10-06, noon, local time; every value is date-only, which

0 commit comments

Comments
 (0)