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
4 changes: 4 additions & 0 deletions .changeset/12829-flow-runs-flat-items.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
---

Console Flow Runs page: the flow list reads the served flat `meta.getItems('flow')` item only (`name` and `label` on the item itself), and the page's test fixtures feed that shape. The removed `spec`-wrapped arm had no producer: no server serves that shape, so no behaviour a user can reach changes and nothing is released (objectui#12829).
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const { STORED, approvalsApiStub, ADAPTER, AUTH, I18N } = vi.hoisted(() => {
// its flow and run loads on it, so a fresh one per call re-ran them on every
// render and its runs table never settled (objectui#11865 surfaced it).
const client = {
meta: { getItems: async (type: string) => (type === 'flow' ? [{ spec: { name: 'sync_flow', label: 'Sync' } }] : []) },
meta: { getItems: async (type: string) => (type === 'flow' ? [{ name: 'sync_flow', label: 'Sync' }] : []) },
automation: { execute: async () => ({ success: true }), listRuns, getRun: async () => ({ run: null }) },
};
const ADAPTER = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,19 @@
* 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.
* other suites fed `{ 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);
* - a `spec`-wrapped item is NOT read (objectui#12829): no server serves that
* shape, so the page keeps no arm for it and such an item lists no flow;
* - 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.
* DIRECTION, observed on unchanged `main` at objectui#12783: the flattened case
* was red there (the option read the bare name); the unlabelled case is green
* by design, as a no-regression guard. The `spec`-wrapped case is red on any
* page that still reads a `spec` arm (objectui#12829).
*/

import '@testing-library/jest-dom/vitest';
Expand Down Expand Up @@ -100,19 +101,16 @@ describe('the flow picker names a labelled flow by its label (objectui#12783)',
]);
});

it('a spec-wrapped answer: the same text', async () => {
it('a spec-wrapped item is not read: it lists no flow (objectui#12829)', async () => {
catalog.answer = {
type: 'flow',
items: [
{ spec: { name: 'showcase_batch_reminders', label: 'Batch Task Reminders (Loop)', variables: [] } },
{ spec: { name: 'nightly_sync', variables: [] } },
],
};
render(<FlowRunsPage />);
expect(await optionTexts()).toEqual([
'Batch Task Reminders (Loop) (showcase_batch_reminders)',
'nightly_sync',
]);
expect(await screen.findByText('No flow definitions found.')).toBeInTheDocument();
expect(screen.queryByRole('combobox')).not.toBeInTheDocument();
});

it('an unlabelled flow: its bare name, never the name doubled as a label', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { cleanup, fireEvent, render, screen, waitFor, within } from '@testing-library/react';

const { execute, listRuns, catalog, ADAPTER } = vi.hoisted(() => {
const catalog = { flows: [] as Array<{ spec: Record<string, unknown> }> };
const catalog = { flows: [] as Array<Record<string, unknown>> };
const execute = vi.fn(async () => ({ success: true, status: 'completed', durationMs: 1 }));
const listRuns = vi.fn(async () => ({ runs: [] }));
// A STABLE singleton: a fresh object per render loops the page's fetch effects.
Expand All @@ -56,8 +56,9 @@ vi.mock('@object-ui/app-shell', async (importOriginal) => ({
// Imported AFTER the mocks so the page picks them up.
import { FlowRunsPage } from './FlowRunsPage';

const REASSIGN = { spec: { name: 'reassign_wizard', label: 'Reassign', variables: [] } };
const NIGHTLY = { spec: { name: 'nightly_sync', variables: [] } };
// Flows as the server lists them: flattened, `name` and `label` on the item (objectui#12829).
const REASSIGN = { name: 'reassign_wizard', label: 'Reassign', variables: [] };
const NIGHTLY = { name: 'nightly_sync', variables: [] };

beforeEach(() => {
catalog.flows = [REASSIGN, NIGHTLY];
Expand Down
3 changes: 2 additions & 1 deletion apps/console/src/pages/developer/FlowRunsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ const { execute, ADAPTER, authFetch } = vi.hoisted(() => {
}));
const ADAPTER = {
getClient: () => ({
meta: { getItems: async (type: string) => (type === 'flow' ? [{ spec: flow }] : []) },
// The served list item IS the flow definition (objectui#12829).
meta: { getItems: async (type: string) => (type === 'flow' ? [flow] : []) },
automation: { execute, listRuns: async () => ({ runs: [] }) },
}),
};
Expand Down
16 changes: 7 additions & 9 deletions apps/console/src/pages/developer/FlowRunsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ interface FlowItem {
name: string;
label?: string;
spec?: any;
updatedAt?: string;
}

interface RunSummary {
Expand Down Expand Up @@ -218,15 +217,14 @@ 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).
// The served list is flattened: each item IS the flow definition, its
// `name` and `label` on the item itself, so the picker names a labelled
// flow `label (name)` (objectui#12783). No server wraps an item in a
// `spec` member, so none is read (objectui#12829).
const normalized: FlowItem[] = items.map(it => ({
name: it?.spec?.name ?? it?.name,
label: it?.spec?.label ?? it?.label,
spec: it?.spec ?? it,
updatedAt: it?.updatedAt ?? it?.updated_at,
name: it?.name,
label: it?.label,
spec: it,
})).filter(f => f.name);
setFlows(normalized);
if (!selectedFlowName && normalized.length > 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,12 @@ vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } }));
import { FlowRunsPage } from './FlowRunsPage';
import { PublicFormsPage } from './PublicFormsPage';

/** A flow definition, as `meta.getItems('flow')` rows carry it. */
/** A flow definition, as `meta.getItems('flow')` rows carry it: the item IS
* the definition, flattened, with no `spec` member (objectui#12829). */
const FLOW = {
spec: {
name: 'reassign_wizard',
label: 'Reassign',
variables: [{ name: 'recordId', type: 'text', isInput: true }],
},
name: 'reassign_wizard',
label: 'Reassign',
variables: [{ name: 'recordId', type: 'text', isInput: true }],
};

/** A published public form, on the `ViewItem` shape `GET /meta/view` serves:
Expand Down
Loading