Skip to content

[Schema] - Validate integration field API response shape before Configure step#4190

Open
geodem127 wants to merge 8 commits into
devfrom
fix/4092-schema-integration-field-configure-step-is-silently-empty-when
Open

[Schema] - Validate integration field API response shape before Configure step#4190
geodem127 wants to merge 8 commits into
devfrom
fix/4092-schema-integration-field-configure-step-is-silently-empty-when

Conversation

@geodem127

@geodem127 geodem127 commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

RCA:

The Connect step of the Integration field wizard (useIntegrationField.fetchApiData) treated any 200-OK + parseable JSON response as a successful connection, with no check on whether the response shape could actually be used. Four shapes silently broke the next step (null, [], a bare object with no nested array, an array of primitives): ConfigureDisplayOptions.tsx's key-selector effect derives rootPathOptions/apiPathOptions from the response, and for all four shapes those come back empty — leaving the "Select Keys to Display" panel completely blank, Done permanently disabled, and zero explanation of what went wrong.

FIX:

  • useIntegrationField.ts — added an exported classifyApiResponse(apiData) that classifies a response as usable or not, deliberately built on the same shape-walking helpers ConfigureDisplayOptions uses to derive its own options, so a response classified "ok" is guaranteed to actually produce selectable options downstream (and vice versa). fetchApiData now runs this classification before setting status: "success"; on failure it sets a new status: "invalid" plus a specific invalidReason string instead.
  • Configure/keyPathHelpers.ts (new) — extracted isObj/getObjectKeyPaths/getAllArrayKeyPaths out of ConfigureDisplayOptions.tsx (previously inline/unexported) into a shared module so both the classifier and the renderer use one source of truth.
  • Configure/ConfigureDisplayOptions.tsx — now imports the helpers from the shared module; behavior unchanged.
  • Configure/ConnectToApi.tsx — added an "invalid" entry to the existing CONNECTION_STATUSES failure-overlay map (reusing the existing UI pattern, no new UI) and renders the dynamic invalidReason as the subtitle. The existing button handler already keeps the user on the Connect step for any non-success status, so no other change was needed to block advancement.

Regression test added: cypress/e2e/schema/integration.spec.jsAdd Field > Invalid Response Shape > blocks advancing and shows an error for: <shape> (parameterized over the four bad shapes).

TESTING PLAN:

Prerequisites: none — the test mocks **/get-url?url=* with each bad shape.

  1. Open Schema → any model → Add Field → Integration.
  2. Enter any endpoint URL, click Connect.
  3. For a response of null, [], {name:"x", id:1}, or [1,2,3]: confirm the overlay shows "Unsupported Response Format" with a shape-specific reason, and clicking the action button keeps you on the Connect step (does not advance to Display Type).
  4. For a normal array-of-objects response: confirm "Connection Successful" still appears and "Next" still advances (no regression to the happy path).

Expected outcome: all four bad shapes show a specific, actionable error and block advancement; the happy path is unchanged — verified via manual browser testing (fetch-mocked, since the issue's originally-seeded QA endpoints have since been taken down) and the new Cypress spec.

RECORDING:

gh-issue-ctrl-4092-verification

Related work:

PR #4188 (open) separately adds an "Edit API URL" re-entry point for editing existing integration fields and independently extracts the same three helper functions into its own keyPathResolution.ts. The two PRs will produce one trivial, one-line import-path merge conflict in ConfigureDisplayOptions.tsx — confirmed via a local test-merge, easily resolved by whichever merges second.

Closes #4092

🤖 Generated with Claude Code

geodem127 added 2 commits July 6, 2026 14:34
…ng past Connect

The Connect step accepted any 200-OK + parseable JSON as success, even
shapes the Configure step can't use (null, [], a bare object with no
nested array, an array of primitives) — leaving the user on a silently
empty, permanently-disabled Configure screen with no explanation.

Classify the response right after connecting, reusing the same
shape-walking helpers ConfigureDisplayOptions uses to derive its
key-selector options (extracted to a shared keyPathHelpers.ts so the
two can never diverge), and surface a specific reason on the existing
Connect-step failure overlay instead of advancing.

Refs #4092
@geodem127 geodem127 added bug Something isn't working enhancement Improvement to an existing feature labels Jul 6, 2026
@geodem127 geodem127 self-assigned this Jul 6, 2026
@geodem127 geodem127 requested review from agalin920 and finnar-bin July 6, 2026 06:36
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Review: [Schema] - Validate integration field API response shape before Configure step

Summary: Solid fix for a real silent-failure bug. The approach — extracting the helper functions, building a classifier that mirrors the downstream rendering logic, and adding a new status instead of overloading failed — is correct and clean. The Cypress coverage matches the four identified bad shapes. A few issues below, one of which (misleading error message edge case) is worth fixing before merge.


What's good

  • Single source of truth — extracting isObj/getObjectKeyPaths/getAllArrayKeyPaths into keyPathHelpers.ts ensures the classifier and the renderer can't silently diverge. This is the right architecture.
  • classifyApiResponse is exported and pure — makes it straightforwardly testable if unit tests are ever added.
  • Reuses existing UI pattern — adding invalid to CONNECTION_STATUSES rather than special-casing new JSX keeps the overlay rendering consistent.
  • data-cy on the subtitle — the new data-cy="integrationConnectionStatusSubtitle" attribute enables the Cypress test to verify the message is non-empty without brittle text matching. Good.
  • Cleanup is symmetricinvalidReason is cleared in both fetchApiData and the useEffect teardown, matching the existing status/apiData reset pattern.

Issues

1. Empty-object array produces a self-contradictory error message (logic bug)

classifyApiResponse on [{}] returns "API returned an array of objects. Expected an array of objects." because getObjectKeyPaths({}) returns [] (no leaf keys), so the length check fires, but typeof {} is "object". The branch is correct to reject a response that can't produce selectable keys, but the message needs to distinguish no-leaf-key objects from non-objects. (Inline comment posted on the relevant line.)

2. as object cast bypasses type safety on the array-item check

getObjectKeyPaths(apiData[0] as object) coerces without a prior isObj guard. If apiData[0] is a primitive, for...in silently returns nothing and the right answer falls out accidentally. (Inline comment posted on the relevant line.)

3. subTitle in the invalid entry is dead code

The field is defined in CONNECTION_STATUSES.invalid but the render branch explicitly reads invalidReason for the "invalid" status — subTitle is never reached. Either remove the field (and make subTitle optional in the type) or use it as a fallback when invalidReason is empty. (Inline comment posted on the relevant line.)

4. Minor: fontSize="small" vs sx={{ fontSize: 40 }} on the button icon

The new invalid entry copies failed's AutorenewRoundedIcon with conflicting props — fontSize="small" is overridden by sx. Pre-existing issue on failed, but worth cleaning up since you're touching this code.


Cypress tests

  • The four shapes (null / [] / flat object / primitive array) map directly to the classifier's four rejection branches — complete coverage of the added logic.
  • No cy.login() call in the new context — consistent with the surrounding spec, so not a new gap, but CLAUDE.md guidance says to call it in beforeEach. Worth a follow-up.
  • The tests verify the action button keeps the user on the Connect step (integrationEndpointInput exists, integrationSelectDisplayOptionsDialog doesn't). ✓

Comment on lines +28 to +35
};
}
if (!getObjectKeyPaths(apiData[0] as object).length) {
return {
ok: false,
reason: `API returned an array of ${typeof apiData[0]}s. Expected an array of objects.`,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two issues here worth fixing together:

1. Self-contradictory message on [{}]
getObjectKeyPaths({}) returns [], so [{}] fails this check with the message "API returned an array of objects. Expected an array of objects." — which is nonsensical. The case you want to reject here is "array items have no selectable leaf keys", not "array items are not objects". Consider:

Suggested change
};
}
if (!getObjectKeyPaths(apiData[0] as object).length) {
return {
ok: false,
reason: `API returned an array of ${typeof apiData[0]}s. Expected an array of objects.`,
};
}
if (!isObj(apiData[0])) {
return {
ok: false,
reason: `API returned an array of ${typeof apiData[0]}s. Expected an array of objects.`,
};
}
if (!getObjectKeyPaths(apiData[0]).length) {
return {
ok: false,
reason:
"API returned an array of objects with no selectable keys. Make sure each object has at least one scalar property.",
};
}

2. Unsafe as object cast
The original code calls getObjectKeyPaths(apiData[0] as object) without a prior isObj check. If apiData[0] is a primitive, for...in over a boxed primitive returns nothing and the correct result falls out by accident. The suggestion above fixes both by checking isObj explicitly first.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geodem127 is point 1 valid?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, point 1 is valid — confirmed. getObjectKeyPaths({}) returns [], so [{}] fell into the "not an object" branch and produced the self-contradictory "API returned an array of objects. Expected an array of objects." Fixed in 7b1f337 by splitting the isObj check from the selectable-keys check.

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Coverage Report

Overall Coverage

Lines Statements Branches Functions
59.99% 59.64% 54.81% 54.39%

Changed Files Coverage

File Lines Statements Branches Functions
src/shell/components/FieldTypeIntegration/Configure/ConnectToApi.tsx 80.76% 77.58% 79.48% 65.00%
src/shell/components/FieldTypeIntegration/Configure/keyPathResolution.ts 100.00% 87.50% 75.00% 100.00%
src/shell/components/FieldTypeIntegration/useIntegrationField.ts 88.52% 85.71% 77.77% 100.00%

Comment on lines +28 to +35
};
}
if (!getObjectKeyPaths(apiData[0] as object).length) {
return {
ok: false,
reason: `API returned an array of ${typeof apiData[0]}s. Expected an array of objects.`,
};
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@geodem127 is point 1 valid?

…e keys

Splits the isObj check from the key-path check in classifyApiResponse so an
array of empty objects ([{}]) gets an accurate error instead of "API
returned an array of objects. Expected an array of objects."

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review: [Schema] - Validate integration field API response shape before Configure step

Overall this is a well-scoped fix. The classifyApiResponse function cleanly encodes the contract between the connect and configure steps, the shared keyPathHelpers module eliminates the duplication risk identified in the description, and the Cypress parameterisation covers the four documented bad shapes plus a fifth ([{}]) that the description missed. A few issues to address before merge:


Bug — single-object path accepts { items: [{}] } as valid

classifyApiResponse's isObj(apiData) branch calls getAllArrayKeyPaths(apiData) and returns ok: true if any path is found. But getAllArrayKeyPaths only requires value.some(isObj) to push a path, and {} satisfies that. So { items: [{}] } is classified as ok.

In ConfigureDisplayOptions, the single-object path then does:

const dataRoot = get(apiData, rootPath)?.[0];   // → {}
const optionsRaw = getObjectKeyPaths(dataRoot);  // → []
setRootPathOptions(optionsRaw);                  // → empty, panel blank again

The array branch already guards against this with !getObjectKeyPaths(apiData[0]).length. The single-object branch needs an equivalent check: for each arrayPath, verify that get(apiData, arrayPath)?.[0] is a non-empty object with at least one scalar key.


invalid.subTitle is dead code

CONNECTION_STATUSES.invalid.subTitle is populated but never shown — the render unconditionally uses invalidReason for the "invalid" case. This will mislead a future maintainer into thinking the static string is the displayed text. Either remove it from the invalid entry or refactor the rendering so the static string serves as a visible fallback.


Empty invalidReason silently drops the subtitle

If a future code path sets status = "invalid" without calling setInvalidReason, the subtitle renders as an empty string (no visual indication). A simple guard:

{status === "invalid"
  ? invalidReason || CONNECTION_STATUSES[status].subTitle
  : CONNECTION_STATUSES[status].subTitle}

…would at minimum show the generic static text instead of nothing.


Minor — buttonIcon fontSize props conflict (pre-existing, not introduced here)

Both failed and invalid entries use <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />. The fontSize="small" MUI prop is overridden by the sx rule. Keeping it for consistency with the existing entry is fine, but worth a cleanup pass if either entry is touched again.

Comment thread src/shell/components/FieldTypeIntegration/useIntegrationField.ts
…ield validation

- classifyApiResponse: the single-object branch accepted {items:[{}]} as
  valid because getAllArrayKeyPaths only checks that an array contains
  objects, not that they have selectable keys. Now verifies at least one
  nested array's first item has selectable keys, using lodash get() to
  resolve dotted/bracketed paths the same way ConfigureDisplayOptions does.
- ConnectToApi: the static "invalid" subTitle was dead code since the
  render always used invalidReason. Now falls back to it if invalidReason
  is ever empty.
- Cypress: added coverage for [{}] and {items:[{}]} invalid shapes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@geodem127

Copy link
Copy Markdown
Contributor Author

Addressed all outstanding review feedback:

useIntegrationField.ts

  • Split the isObj check from the selectable-keys check in classifyApiResponse's array branch, fixing the self-contradictory [{}] message and the unsafe as object cast (discussion).
  • Fixed the single-object branch accepting { items: [{}] } as valid — it now verifies at least one nested array's first item has selectable keys, resolved via lodash.get() to match how ConfigureDisplayOptions actually reads paths (discussion).

ConnectToApi.tsx

  • The static invalid.subTitle was dead code since the render always used invalidReason. It's now a fallback for the case where invalidReason is ever empty (discussion 1, discussion 2).

Cypress

  • Added [{}] and { items: [{}] } cases to the invalid-shape parameterized test.

Pushed in f0cfdd5. npx tsc --noEmit is clean on the changed files.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review

Summary: This PR correctly fixes a silent failure in the integration field wizard where invalid API response shapes (null, empty array, bare object, array of primitives) would silently pass the Connect step and leave the Configure step blank with no explanation. The fix is well-structured: helper extraction into a shared module, a classifier that deliberately mirrors the downstream shape-walking logic, and a new invalid status entry that reuses the existing overlay UI pattern.

Overall this is clean and well-reasoned work. A couple of minor issues worth addressing:


Bug: typeof null produces a contradictory error message

In classifyApiResponse, the branch that handles an array of non-objects uses typeof apiData[0] to describe the element type. However, typeof null === "object", so if the response is an array containing null elements (e.g. [null]), the error message reads:

"API returned an array of objects. Expected an array of objects."

This is contradictory. isObj already correctly rejects null (since it checks v !== null), but the subsequent message construction doesn't account for this case. Fix: special-case null before reaching for typeof.

There's also no test case for the [null] shape in the new Cypress spec — the suite tests a top-level null response and arrays of numbers, but not arrays whose elements are null.


Nit: subTitle in the invalid CONNECTION_STATUSES entry is unreachable

The rendering logic in ConnectToApi.tsx is:

{status === "invalid"
  ? invalidReason || CONNECTION_STATUSES[status].subTitle
  : CONNECTION_STATUSES[status].subTitle}

Since classifyApiResponse always returns a non-empty reason string, and invalidReason is only set immediately before setStatus("invalid"), the || CONNECTION_STATUSES["invalid"].subTitle fallback is unreachable in practice. The static subTitle in the invalid entry is effectively dead from a user-facing perspective. Either remove the subTitle field from the invalid entry (and drop the || fallback) to make this explicit, or rename it to something like defaultReason to signal that it's a fallback. As-is it's harmless but misleading.


Minor: Cypress tests could share a beforeEach

The new Invalid Response Shape context inlines the full navigation sequence (visit, AddFieldBtn, FieldItem_integration, etc.) inside every individual test, whereas the sibling HTTP Headers context extracts this into a beforeEach. The parameterized intercept must happen before the visit, so a beforeEach for navigation (called after the intercept) would keep the tests DRY. Not a blocker.

Comment thread src/shell/components/FieldTypeIntegration/useIntegrationField.ts
Comment thread cypress/e2e/schema/integration.spec.js
@geodem127 geodem127 requested a review from agalin920 July 7, 2026 07:01
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review: [Schema] - Validate integration field API response shape before Configure step

Summary: This is a solid fix. The root cause was clear, the solution is well-scoped, and the key design decision — making classifyApiResponse walk the same shape helpers that ConfigureDisplayOptions uses downstream — is the right call. It guarantees that anything classified "ok" will actually produce selectable options, eliminating any future drift between validation and rendering.


Strengths

  • Mirror design is correct. classifyApiResponse deliberately mirrors ConfigureDisplayOptions's apiData[0] / get(apiData, path)?.[0] access patterns, so the validator and the renderer agree by construction, not by convention. The comment on lines 10–12 of useIntegrationField.ts explains this — keep it.
  • Clean extraction. Moving isObj/getObjectKeyPaths/getAllArrayKeyPaths into keyPathHelpers.ts is the right call with the two call sites now in play.
  • Existing UI pattern reused. The invalid entry in CONNECTION_STATUSES slots in cleanly; no new UI machinery needed.
  • Test coverage is thorough. Six parameterized cases, all mocked via cy.intercept (no real network calls), using data-cy selectors throughout. The two extra cases beyond the four in the bug report ([{}] and { items: [{}] }) are good additions.

Issues

1. buttonIcon has dead fontSize prop in both failed and invalid entries

In ConnectToApi.tsx:

buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,

sx={{ fontSize: 40 }} overrides the MUI fontSize="small" variant — the prop has no effect. The invalid entry copies this from failed, so both share the same dead prop. Since this PR introduces the invalid entry, it's the right time to fix both: either drop fontSize="small" (let sx win, which is already happening) or drop the sx and use the string variant (fontSize="large").

2. invalidReason fallback in subtitle rendering is unnecessary

{status === "invalid"
  ? invalidReason || CONNECTION_STATUSES[status].subTitle
  : CONNECTION_STATUSES[status].subTitle}

invalidReason is always set (non-empty) before status is set to "invalid", and is reset to "" at the start of every new request. The || fallback branch can never execute. The expression can be simplified to:

{status === "invalid" ? invalidReason : CONNECTION_STATUSES[status].subTitle}

Minor observations (no action required)

  • classification.ok === false (line 111 of useIntegrationField.ts) — !classification.ok is equivalent and slightly more idiomatic, but the explicit form is fine given the discriminated union.
  • for...in + hasOwnProperty in the walker functions is correct. These are moved unchanged from ConfigureDisplayOptions.tsx, so no behavior change.
  • invalidReason typed as string (not string | null) means the "not set" state is represented by "". That's consistent internally, but null would make the unset state more explicit. Not a blocker.
  • PR description mentions a one-line import-path conflict with PR [Schema] - Add Edit API URL entry point and endpoint-mismatch warning to integration fields #4188 — appreciated heads-up; whoever merges second just needs to fix the ConfigureDisplayOptions.tsx import.

Overall: Approve after the buttonIcon dead prop is cleaned up (or acknowledged as intentional to match the failed style). The subtitle simplification is a nice-to-have.

geodem127 added 2 commits July 8, 2026 00:57
…tep-is-silently-empty-when

# Conflicts:
#	src/shell/components/FieldTypeIntegration/Configure/ConfigureDisplayOptions.tsx
…d-configure-step-is-silently-empty-when' into fix/4092-schema-integration-field-configure-step-is-silently-empty-when
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overview

This PR correctly fixes a silent failure in the integration field wizard: API responses that can't produce selectable keys in the Configure step were silently accepted, leaving the display-options panel blank with no explanation. The approach — classify the response using the same shape-walking logic the renderer uses, then surface a specific error — is the right design.

Main concern: duplicate helper implementations

The PR creates Configure/keyPathHelpers.ts exporting isObj, getObjectKeyPaths, and getAllArrayKeyPaths. But Configure/keyPathResolution.ts (landed in PR #4188) already exports getObjectKeyPaths and getAllArrayKeyPaths with identical implementations. This leaves two sources of truth for the same functions. A divergence (e.g. a future edge-case fix to one but not the other) will cause the classifier and the renderer to disagree on which responses are usable — exactly the class of bug this PR was designed to prevent.

The fix is straightforward: export isObj from keyPathResolution.ts and import all three helpers from there in useIntegrationField.ts, then delete keyPathHelpers.ts. That keeps the "classifier uses the same helpers as the renderer" guarantee the PR description promises, with a single file as the source of truth.

Other issues

Dead-code fallback in the subtitle expression (ConnectToApi.tsx ~line 431):

{status === "invalid"
  ? invalidReason || CONNECTION_STATUSES[status].subTitle
  : CONNECTION_STATUSES[status].subTitle}

classifyApiResponse always returns a non-empty reason string when ok === false, and fetchApiData always sets invalidReason to that string before setting status: "invalid". So invalidReason is never falsy here; the || CONNECTION_STATUSES[status].subTitle branch is dead code and adds noise for future readers.

Oversized button icon in invalid status (ConnectToApi.tsx ~line 88):

buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,

fontSize="small" (~20 px) is overridden by sx={{ fontSize: 40 }} (40 px). An icon twice the intended size will be visually out of place inside a size="small" button. The same bug exists in the pre-existing failed entry — worth fixing both while the file is open.

Positive notes

  • The classification logic in classifyApiResponse correctly mirrors ConfigureDisplayOptions's useEffect chain (array root → first-item key check; object root → nested-array paths → first-item key check), so the guarantee in the PR description holds.
  • The Cypress tests are well-structured: they mock the endpoint rather than hitting a real API, cover all four originally-identified bad shapes plus two additional edge cases, and check both the error surface and that the button keeps the user on the Connect step.
  • Using data-cy selectors throughout is consistent with project convention.

};
walk(obj, "");
return acc;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file duplicates getObjectKeyPaths and getAllArrayKeyPaths verbatim from keyPathResolution.ts (which already exports them after PR #4188). Two identical implementations means a future bug fix to one won't automatically reach the other — which breaks the "classifier uses the same helpers as the renderer" guarantee stated in the PR description.

The simpler path: export isObj from keyPathResolution.ts and import all three helpers from there in useIntegrationField.ts, then delete this file entirely. ConfigureDisplayOptions.tsx already imports from keyPathResolution.ts, so the single-source guarantee holds with no new file needed.

subTitle:
"The API connected, but its response can't be used to configure this field.",
buttonLabel: "Try Again",
buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sx={{ fontSize: 40 }} overrides fontSize="small" (~20 px), producing a 40 px icon inside a size="small" button — visually too large. The same issue exists in the pre-existing failed entry (line 76). The sx prop should be dropped from both:

Suggested change
buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,
buttonIcon: <AutorenewRoundedIcon fontSize="small" />,

Comment on lines +430 to +432
{status === "invalid"
? invalidReason || CONNECTION_STATUSES[status].subTitle
: CONNECTION_STATUSES[status].subTitle}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classifyApiResponse always returns a non-empty reason and fetchApiData always assigns it to invalidReason before setting status: "invalid", so invalidReason is never falsy here. The || CONNECTION_STATUSES[status].subTitle fallback is dead code. Simplify to:

Suggested change
{status === "invalid"
? invalidReason || CONNECTION_STATUSES[status].subTitle
: CONNECTION_STATUSES[status].subTitle}
{status === "invalid"
? invalidReason
: CONNECTION_STATUSES[status].subTitle}

The dev merge renamed keyPathHelpers.ts to keyPathResolution.ts but the
conflict resolution missed useIntegrationField.ts, which still imported
the old module (and keyPathResolution.ts didn't export isObj, which
useIntegrationField.ts needs).
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Code Review

Overall: Well-structured, targeted bug fix. The discriminated union return type on classifyApiResponse is the right pattern, the shared-helpers approach guarantees classifier/renderer parity, and the Cypress test coverage is solid. A few issues to address before merge.


Bug: typeof null produces a misleading error message

In classifyApiResponse, when apiData is an array whose first element is null, isObj(null) correctly returns false, but typeof null === "object" — so the error message reads "API returned an array of objects" for an input like [null, null]. The right fix is a specific null check before the typeof branch:

if (!isObj(apiData[0])) {
  const kind = apiData[0] === null ? "null" : typeof apiData[0];
  return {
    ok: false,
    reason: `API returned an array of ${kind}s. Expected an array of objects.`,
  };
}

The existing test suite doesn't cover [null] so this slips through — worth adding.


Dead fallback in subtitle rendering

In ConnectToApi.tsx:

{status === "invalid"
  ? invalidReason || CONNECTION_STATUSES[status].subTitle
  : CONNECTION_STATUSES[status].subTitle}

invalidReason is always a non-empty string when status === "invalid" — both are set in the same synchronous block before React batches the render. The || CONNECTION_STATUSES[status].subTitle branch is unreachable dead code. Per the project's own conventions ("Don't add error handling, fallbacks, or validation for scenarios that can't happen"), simplify to:

{status === "invalid" ? invalidReason : CONNECTION_STATUSES[status].subTitle}

Conflicting fontSize props on buttonIcon

The invalid entry's buttonIcon is:

<AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />

fontSize="small" sets a CSS class (~20px), then sx={{ fontSize: 40 }} overwrites it at higher specificity, making the prop a no-op. This is copied from the failed entry (pre-existing), but worth cleaning up in both places while touching this file. Remove fontSize="small" — the sx prop alone is sufficient.


Minor: modelZUID env var in tests

The new Cypress tests call cy.visit(`/schema/${Cypress.env("modelZUID")}/fields`) — ensure modelZUID is documented in the CI env var list / cypress.env.json template if it isn't already. Not a blocker if existing passing tests already rely on it (it looks like they do from the surrounding spec), just flagging for future contributors.


What's working well

  • The { ok: true } | { ok: false; reason: string } discriminated union is exactly the right return shape — exhaustive and self-documenting.
  • Walking only apiData[0] (not the whole array) keeps classification O(1) in array length, consistent with how ConfigureDisplayOptions samples the response.
  • All six parametrized Cypress cases properly use cy.intercept, cy.wait, and cy.getBySelector — no hard waits, no class selectors.
  • The new data-cy="integrationConnectionStatusSubtitle" attribute makes the subtitle testable without relying on MUI internals.

}
if (!isObj(apiData[0])) {
return {
ok: false,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: typeof null === "object" produces a misleading message.

If apiData is [null], isObj(null) correctly returns false (the v !== null guard), but then typeof null is "object", so this message reads "API returned an array of objects" — the opposite of what happened.

Suggested change
ok: false,
const kind = apiData[0] === null ? "null" : typeof apiData[0];
return {
ok: false,
reason: `API returned an array of ${kind}s. Expected an array of objects.`,
};

Also worth adding { name: "array of nulls", body: [null] } to the Cypress invalidShapes table to cover this case.

Comment on lines +430 to +432
{status === "invalid"
? invalidReason || CONNECTION_STATUSES[status].subTitle
: CONNECTION_STATUSES[status].subTitle}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead fallback — unreachable code.

invalidReason is always non-empty when status === "invalid": setInvalidReason(classification.reason) and setStatus("invalid") are called in the same synchronous block, so React batches them into a single render. The || CONNECTION_STATUSES[status].subTitle branch is never reached.

Per the project's own convention ("Don't add error handling or fallbacks for scenarios that can't happen"), prefer the simpler form:

Suggested change
{status === "invalid"
? invalidReason || CONNECTION_STATUSES[status].subTitle
: CONNECTION_STATUSES[status].subTitle}
{status === "invalid"
? invalidReason
: CONNECTION_STATUSES[status].subTitle}

subTitle:
"The API connected, but its response can't be used to configure this field.",
buttonLabel: "Try Again",
buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fontSize="small" is a no-op here. The sx={{ fontSize: 40 }} prop takes higher CSS specificity and overrides the class set by fontSize="small" (~20px → 40px). The explicit MUI prop is misleading since it has no effect.

Same issue exists on the failed entry at line 76 — worth fixing both while touching this file.

Suggested change
buttonIcon: <AutorenewRoundedIcon fontSize="small" sx={{ fontSize: 40 }} />,
buttonIcon: <AutorenewRoundedIcon sx={{ fontSize: 40 }} />,

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working enhancement Improvement to an existing feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Schema - Integration field Configure step is silently empty when API returns null/[]/{}/[primitives]

2 participants