From d841908f4298bc4d46832bf4b861b416eabcbd19 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 23:39:30 +0000 Subject: [PATCH] fix(console): the Flow Runs picker reads a flattened flow item's label (objectui#12783) objectstack's getMetaItems serves meta.getItems('flow') as flattened items: name and label sit on the item, with no spec member. The page read the name from either form but the label from spec.label only, so on a real server the picker listed every flow by its API name. The label is now read the way the name is, spec first and then the item itself. Pins: a flattened answer shows `label (name)`; a spec-wrapped answer shows the same text; an unlabelled flow shows its bare name. Claude-Session: https://claude.ai/code/session_01Vk15bz8xHtKGUfXfCm16h4 Co-authored-by: Claude --- .changeset/12783-flow-runs-picker-label.md | 9 ++ ...FlowRunsPage.flattenedLabel-12783.test.tsx | 123 ++++++++++++++++++ .../src/pages/developer/FlowRunsPage.tsx | 6 +- 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 .changeset/12783-flow-runs-picker-label.md create mode 100644 apps/console/src/pages/developer/FlowRunsPage.flattenedLabel-12783.test.tsx diff --git a/.changeset/12783-flow-runs-picker-label.md b/.changeset/12783-flow-runs-picker-label.md new file mode 100644 index 0000000000..7de3b075c7 --- /dev/null +++ b/.changeset/12783-flow-runs-picker-label.md @@ -0,0 +1,9 @@ +--- +'@object-ui/console': patch +--- + +The developer Flow Runs page's flow picker names each labelled flow by its label again, on the list a real server serves (objectui#12783). + +objectstack's metadata list answers `meta.getItems('flow')` with flattened items: each flow's `name` and `label` sit on the item itself, with no `spec` member. The page read the name from either form, but the label from `spec.label` only, so against a server every flow was listed by its API name alone, for example `showcase_batch_reminders`. The label is now read the way the name is, `spec` first and then the item itself, so the picker shows `Batch Task Reminders (Loop) (showcase_batch_reminders)`, the `label (name)` form the page builds. A flow that declares no label still shows its bare name. + +Nothing else on the page changes: no export, prop, type member or i18n key is added or removed. diff --git a/apps/console/src/pages/developer/FlowRunsPage.flattenedLabel-12783.test.tsx b/apps/console/src/pages/developer/FlowRunsPage.flattenedLabel-12783.test.tsx new file mode 100644 index 0000000000..9485bd23f9 --- /dev/null +++ b/apps/console/src/pages/developer/FlowRunsPage.flattenedLabel-12783.test.tsx @@ -0,0 +1,123 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. + +/** + * The Flow Runs flow picker names a labelled flow `label (name)` on the list + * the server actually serves (objectui#12783). + * + * objectstack's `getMetaItems` answers `meta.getItems('flow')` with FLATTENED + * items: each flow's `name` and `label` sit at the top level of the item, and + * no item carries a `spec` member. The page read the name with a fallback to + * the flat item but the label from `it.spec.label` alone, so on a real server + * every flow was listed by its API name only, `showcase_batch_reminders` where + * "Batch Task Reminders (Loop) (showcase_batch_reminders)" belongs. The page's + * other suites feed `{ spec: {...} }` items, which is why none of them saw it. + * + * What is pinned: + * - a flattened answer, shaped as the served list is: the option text is + * `label (name)`; + * - a `spec`-wrapped answer: the same text (the page still reads that form, + * `spec` first, as it reads the name); + * - an unlabelled flow: its bare name, not the name standing in for a label. + * + * DIRECTION, observed on unchanged `main`: the flattened case is red there (the + * option reads the bare name); the other two are green there by design, as + * no-regression guards for the forms the fix must not disturb. + */ + +import '@testing-library/jest-dom/vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { cleanup, fireEvent, render, screen, within } from '@testing-library/react'; + +const { catalog, ADAPTER } = vi.hoisted(() => { + const catalog: { answer: unknown } = { answer: [] }; + // A STABLE singleton: a fresh object per render loops the page's fetch effects. + const client = { + meta: { getItems: async (type: string) => (type === 'flow' ? catalog.answer : []) }, + automation: { + execute: async () => ({ success: true, status: 'completed', durationMs: 1 }), + listRuns: async () => ({ runs: [] }), + }, + }; + return { catalog, ADAPTER: { getClient: () => client } }; +}); + +vi.mock('@object-ui/app-shell', async (importOriginal) => ({ + ...(await importOriginal>()), + useAdapter: () => ADAPTER, + useMetadata: () => ({ objects: [] }), +})); + +// Imported AFTER the mocks so the page picks them up. +import { FlowRunsPage } from './FlowRunsPage'; + +/** + * One flow as the showcase server lists it: the `{ type, items }` envelope, + * the item flattened, its `name` and `label` top-level, no `spec` member. + */ +const SERVED_BATCH_REMINDERS = { + name: 'showcase_batch_reminders', + label: 'Batch Task Reminders (Loop)', + type: 'autolaunched', + status: 'active', + version: 1, + variables: [{ name: 'tasks', type: 'list', isInput: true }], + _packageId: 'com.example.showcase', + _provenance: 'package', +}; + +/** A served flow that declares no label. */ +const SERVED_UNLABELLED = { + name: 'nightly_sync', + type: 'autolaunched', + status: 'active', + version: 1, + variables: [], + _packageId: 'com.example.showcase', + _provenance: 'package', +}; + +beforeEach(() => { + catalog.answer = []; +}); +afterEach(cleanup); + +/** Open the picker from the keyboard once the list has loaded; return the option texts, in order. */ +async function optionTexts(): Promise { + await screen.findByRole('button', { name: /Run Flow/i }); + const trigger = await screen.findByRole('combobox'); + fireEvent.keyDown(trigger, { key: 'ArrowDown' }); + const listbox = await screen.findByRole('listbox'); + return within(listbox).getAllByRole('option').map((o) => o.textContent ?? ''); +} + +describe('the flow picker names a labelled flow by its label (objectui#12783)', () => { + it('a flattened meta.getItems answer, as the server serves it: label (name)', async () => { + catalog.answer = { type: 'flow', items: [SERVED_BATCH_REMINDERS, SERVED_UNLABELLED] }; + render(); + expect(await optionTexts()).toEqual([ + 'Batch Task Reminders (Loop) (showcase_batch_reminders)', + 'nightly_sync', + ]); + }); + + it('a spec-wrapped answer: the same text', async () => { + catalog.answer = { + type: 'flow', + items: [ + { spec: { name: 'showcase_batch_reminders', label: 'Batch Task Reminders (Loop)', variables: [] } }, + { spec: { name: 'nightly_sync', variables: [] } }, + ], + }; + render(); + expect(await optionTexts()).toEqual([ + 'Batch Task Reminders (Loop) (showcase_batch_reminders)', + 'nightly_sync', + ]); + }); + + it('an unlabelled flow: its bare name, never the name doubled as a label', async () => { + catalog.answer = { type: 'flow', items: [SERVED_UNLABELLED] }; + render(); + expect(await optionTexts()).toEqual(['nightly_sync']); + }); +}); diff --git a/apps/console/src/pages/developer/FlowRunsPage.tsx b/apps/console/src/pages/developer/FlowRunsPage.tsx index cc58e654f5..5147dc4b43 100644 --- a/apps/console/src/pages/developer/FlowRunsPage.tsx +++ b/apps/console/src/pages/developer/FlowRunsPage.tsx @@ -218,9 +218,13 @@ export function FlowRunsPage() { : Array.isArray(result?.items) ? result.items : []; + // The served list is flattened: `name` and `label` sit on the item + // itself, with no `spec` member. Both are read the same way, `spec` + // first, so the picker names a labelled flow `label (name)` on the list + // the server answers (objectui#12783). const normalized: FlowItem[] = items.map(it => ({ name: it?.spec?.name ?? it?.name, - label: it?.spec?.label, + label: it?.spec?.label ?? it?.label, spec: it?.spec ?? it, updatedAt: it?.updatedAt ?? it?.updated_at, })).filter(f => f.name);