From c6fba5532fcf590a0cb6fcbd1dd88d2dd6b7fcec Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:20:47 -0700 Subject: [PATCH 1/7] Fixes GH-17399 --- .../src/components/ControlSelect.tsx | 127 ++++++++++++++---- .../src/components/RemoteOptionsContainer.tsx | 14 +- packages/connect-react/src/index.ts | 14 ++ .../connect-react/src/utils/type-guards.ts | 43 ++++++ pnpm-lock.yaml | 29 ++-- 5 files changed, 188 insertions(+), 39 deletions(-) create mode 100644 packages/connect-react/src/utils/type-guards.ts diff --git a/packages/connect-react/src/components/ControlSelect.tsx b/packages/connect-react/src/components/ControlSelect.tsx index fcc312f665098..c580e11591a1a 100644 --- a/packages/connect-react/src/components/ControlSelect.tsx +++ b/packages/connect-react/src/components/ControlSelect.tsx @@ -11,6 +11,7 @@ import { useFormFieldContext } from "../hooks/form-field-context"; import { useCustomize } from "../hooks/customization-context"; import type { BaseReactSelectProps } from "../hooks/customization-context"; import { LoadMoreButton } from "./LoadMoreButton"; +import { isString, isOptionWithValue, OptionWithValue } from "../utils/type-guards"; // XXX T and ConfigurableProp should be related type ControlSelectProps = { @@ -41,7 +42,41 @@ export function ControlSelect({ ] = useState(value); useEffect(() => { - setSelectOptions(options) + // Ensure all options have proper primitive values for label/value + const sanitizedOptions = options.map(option => { + if (typeof option === 'string') return option; + + // If option has __lv wrapper, extract the inner option + if (option && typeof option === 'object' && '__lv' in option) { + const innerOption = option.__lv; + return { + label: String(innerOption?.label || innerOption?.value || ''), + value: innerOption?.value + }; + } + + // Handle nested label and value objects + let actualLabel = ''; + let actualValue = option.value; + + // Extract nested label + if (option.label && typeof option.label === 'object' && 'label' in option.label) { + actualLabel = String(option.label.label || ''); + } else { + actualLabel = String(option.label || option.value || ''); + } + + // Extract nested value + if (option.value && typeof option.value === 'object' && 'value' in option.value) { + actualValue = option.value.value; + } + + return { + label: actualLabel, + value: actualValue + }; + }); + setSelectOptions(sanitizedOptions) }, [ options, ]) @@ -67,11 +102,11 @@ export function ControlSelect({ if (ret != null) { if (Array.isArray(ret)) { // if simple, make lv (XXX combine this with other place this happens) - if (typeof ret[0] !== "object") { + if (!isOptionWithValue(ret[0])) { const lvs = []; for (const o of ret) { let obj = { - label: o, + label: String(o), value: o, } for (const item of selectOptions) { @@ -84,8 +119,11 @@ export function ControlSelect({ } ret = lvs; } - } else if (typeof ret !== "object") { - const lvOptions = selectOptions?.[0] && typeof selectOptions[0] === "object"; + } else if (ret && typeof ret === "object" && "__lv" in ret) { + // Extract the actual option from __lv wrapper + ret = ret.__lv; + } else if (!isOptionWithValue(ret)) { + const lvOptions = selectOptions?.[0] && isOptionWithValue(selectOptions[0]); if (lvOptions) { for (const item of selectOptions) { if (item.value === rawValue) { @@ -95,12 +133,10 @@ export function ControlSelect({ } } else { ret = { - label: rawValue, + label: String(rawValue), value: rawValue, } } - } else if (ret.__lv) { - ret = ret.__lv } } return ret; @@ -117,13 +153,14 @@ export function ControlSelect({ { children }
- + {})}/>
) } const props = select.getProps("controlSelect", baseSelectProps) + if (showLoadMoreButton) { props.components = { // eslint-disable-next-line react/prop-types @@ -133,24 +170,29 @@ export function ControlSelect({ } const handleCreate = (inputValue: string) => { - const createOption = (input: unknown) => { - if (typeof input === "object") return input + const createOption = (input: unknown): OptionWithValue => { + if (isOptionWithValue(input)) return input + const strValue = String(input); return { - label: input, - value: input, + label: strValue, + value: strValue, } } const newOption = createOption(inputValue) let newRawValue = newOption - const newSelectOptions = selectOptions - ? [ - newOption, - ...selectOptions, - ] - : [ - newOption, - ] + + // NEVER add wrapped objects to selectOptions - only clean {label, value} objects + const cleanSelectOptions = selectOptions.map(opt => { + if (typeof opt === 'string') return opt; + if (opt && typeof opt === 'object' && '__lv' in opt) { + return {label: String(opt.__lv?.label || ''), value: opt.__lv?.value}; + } + return opt; + }); + + const newSelectOptions = [newOption, ...cleanSelectOptions]; setSelectOptions(newSelectOptions); + if (prop.type.endsWith("[]")) { if (Array.isArray(rawValue)) { newRawValue = [ @@ -170,14 +212,14 @@ export function ControlSelect({ const handleChange = (o: unknown) => { if (o) { if (Array.isArray(o)) { - if (typeof o[0] === "object" && "value" in o[0]) { + if (typeof o[0] === "object" && o[0] && "value" in o[0]) { onChange({ __lv: o, }); } else { onChange(o); } - } else if (typeof o === "object" && "value" in o) { + } else if (typeof o === "object" && o && "value" in o) { onChange({ __lv: o, }); @@ -198,19 +240,54 @@ export function ControlSelect({ const MaybeCreatableSelect = isCreatable ? CreatableSelect : Select; + + // Final safety check - ensure NO __lv wrapped objects reach react-select + const cleanedOptions = selectOptions.map(opt => { + if (typeof opt === 'string') return opt; + if (opt && typeof opt === 'object' && '__lv' in opt && opt.__lv) { + let actualLabel = ''; + let actualValue = opt.__lv.value; + + // Handle nested label in __lv + if (opt.__lv.label && typeof opt.__lv.label === 'object' && 'label' in opt.__lv.label) { + actualLabel = String(opt.__lv.label.label || ''); + } else { + actualLabel = String(opt.__lv.label || opt.__lv.value || ''); + } + + // Handle nested value in __lv + if (opt.__lv.value && typeof opt.__lv.value === 'object' && 'value' in opt.__lv.value) { + actualValue = opt.__lv.value.value; + } + + return { + label: actualLabel, + value: actualValue + }; + } + return opt; + }); + + return ( { + return typeof option === 'string' ? option : String(option?.label || option?.value || ''); + }} + getOptionValue={(option) => { + return typeof option === 'string' ? option : String(option?.value || ''); + }} + onChange={handleChange} {...props} {...selectProps} {...additionalProps} - onChange={handleChange} /> ); } diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index 5dd526f79265b..66d8285675008 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -5,6 +5,7 @@ import { useFormContext } from "../hooks/form-context"; import { useFormFieldContext } from "../hooks/form-field-context"; import { useFrontendClient } from "../hooks/frontend-client-context"; import { ControlSelect } from "./ControlSelect"; +import { isString, isOptionWithValue } from "../utils/type-guards"; export type RemoteOptionsContainerProps = { queryEnabled?: boolean; @@ -138,9 +139,16 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP const newOptions = [] const allValues = new Set(pageable.values) for (const o of _options || []) { - const value = typeof o === "string" - ? o - : o.value + let value: string | number; + if (isString(o)) { + value = o; + } else if (o && typeof o === "object" && "value" in o && o.value != null) { + value = o.value; + } else { + // Skip items that don't match expected format + console.warn('Skipping invalid option:', o); + continue; + } if (allValues.has(value)) { continue } diff --git a/packages/connect-react/src/index.ts b/packages/connect-react/src/index.ts index b8ff8968f8893..c27a397096f57 100644 --- a/packages/connect-react/src/index.ts +++ b/packages/connect-react/src/index.ts @@ -31,3 +31,17 @@ export * from "./hooks/use-app"; export * from "./hooks/use-apps"; export * from "./hooks/use-component"; export * from "./hooks/use-components"; + +// Debug info for development +import packageJson from '../package.json'; + +export const DEBUG_INFO = { + version: `${packageJson.version}-dev`, + buildTime: new Date().toISOString(), + source: "local-development" +}; + +// Auto-log debug info in development +if (typeof window !== 'undefined') { + console.log('🔧 @pipedream/connect-react DEBUG:', DEBUG_INFO); +} diff --git a/packages/connect-react/src/utils/type-guards.ts b/packages/connect-react/src/utils/type-guards.ts new file mode 100644 index 0000000000000..8f887f7315fa5 --- /dev/null +++ b/packages/connect-react/src/utils/type-guards.ts @@ -0,0 +1,43 @@ +export interface OptionWithValue { + value: string | number; + label?: string; + __lv?: any; +} + +export function isString(value: unknown): value is string { + return typeof value === 'string'; +} + +export function isOptionWithValue(value: unknown): value is OptionWithValue { + return ( + value !== null && + typeof value === 'object' && + !Array.isArray(value) && + 'value' in value + ); +} + +export function isStringArray(value: unknown): value is string[] { + return Array.isArray(value) && value.every(item => typeof item === 'string'); +} + +export function isOptionArray(value: unknown): value is OptionWithValue[] { + return Array.isArray(value) && value.every(item => isOptionWithValue(item)); +} + +export function normalizeOption(option: unknown): OptionWithValue | string { + if (isString(option)) { + return option; + } + if (isOptionWithValue(option)) { + return option; + } + return String(option); +} + +export function normalizeOptions(options: unknown): Array { + if (!Array.isArray(options)) { + return []; + } + return options.map(normalizeOption); +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 58a3859a8ef1e..545ca13bda498 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1042,8 +1042,7 @@ importers: components/ascora: {} - components/ashby_job_postings_api: - specifiers: {} + components/ashby_job_postings_api: {} components/asin_data_api: {} @@ -2593,8 +2592,7 @@ importers: specifier: ^0.0.1-security version: 0.0.1-security - components/click_sign: - specifiers: {} + components/click_sign: {} components/clickfunnels: dependencies: @@ -6204,8 +6202,7 @@ importers: components/helpdesk: {} - components/helpdocs: - specifiers: {} + components/helpdocs: {} components/helper_functions: dependencies: @@ -16002,6 +15999,14 @@ importers: specifier: ^6.0.0 version: 6.2.0 + modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/cjs: {} + + modelcontextprotocol/node_modules2/@modelcontextprotocol/sdk/dist/esm: {} + + modelcontextprotocol/node_modules2/zod-to-json-schema/dist/cjs: {} + + modelcontextprotocol/node_modules2/zod-to-json-schema/dist/esm: {} + packages/ai: dependencies: '@pipedream/sdk': @@ -16154,7 +16159,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.30))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.4)(typescript@5.7.2)(yaml@2.6.1) @@ -16197,7 +16202,7 @@ importers: version: 3.1.0 jest: specifier: ^29.1.2 - version: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0) type-fest: specifier: ^4.15.0 version: 4.27.0 @@ -37305,6 +37310,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: @@ -51386,7 +51393,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -51400,10 +51407,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 8.0.0-alpha.13 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) + babel-jest: 29.7.0(@babel/core@7.26.0) ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): dependencies: From 679ab02952e7ac8eb4d4ebe449d33e19b3ccd9fb Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:21:26 -0700 Subject: [PATCH 2/7] Update pnpm-lock.yaml --- pnpm-lock.yaml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 545ca13bda498..0da1455f230af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29985,22 +29985,22 @@ packages: superagent@3.8.1: resolution: {integrity: sha512-VMBFLYgFuRdfeNQSMLbxGSLfmXL/xc+OO+BZp41Za/NRDBet/BNbkRJrYzCUu0u4GU0i/ml2dtT8b9qgkw9z6Q==} engines: {node: '>= 4.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superagent@4.1.0: resolution: {integrity: sha512-FT3QLMasz0YyCd4uIi5HNe+3t/onxMyEho7C3PSqmti3Twgy2rXT4fmkTz6wRL6bTF4uzPcfkUCa8u4JWHw8Ag==} engines: {node: '>= 6.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superagent@5.3.1: resolution: {integrity: sha512-wjJ/MoTid2/RuGCOFtlacyGNxN9QLMgcpYLDQlWFIhhdJ93kNscFonGvrpAHSCVjRVj++DGCglocF7Aej1KHvQ==} engines: {node: '>= 7.0.0'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net superagent@7.1.6: resolution: {integrity: sha512-gZkVCQR1gy/oUXr+kxJMLDjla434KmSOKbx5iGD30Ql+AkJQ/YlPKECJy2nhqOsHLjGHzoDTXNSjhnvWhzKk7g==} engines: {node: '>=6.4.0 <13 || >=14'} - deprecated: Please upgrade to v9.0.0+ as we have fixed a public vulnerability with formidable dependency. Note that v9.0.0+ requires Node.js v14.18.0+. See https://github.com/ladjs/superagent/pull/1800 for insight. This project is supported and maintained by the team at Forward Email @ https://forwardemail.net + deprecated: Please upgrade to superagent v10.2.2+, see release notes at https://github.com/forwardemail/superagent/releases/tag/v10.2.2 - maintenance is supported by Forward Email @ https://forwardemail.net supports-color@2.0.0: resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==} @@ -37310,8 +37310,6 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) - transitivePeerDependencies: - - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: From 68e599c4596d52036f752efa207157ef59084485 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:22:08 -0700 Subject: [PATCH 3/7] Update pnpm-lock.yaml --- pnpm-lock.yaml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0da1455f230af..cc23f35678cd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16159,7 +16159,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.30))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.4)(typescript@5.7.2)(yaml@2.6.1) @@ -16202,7 +16202,7 @@ importers: version: 3.1.0 jest: specifier: ^29.1.2 - version: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0) type-fest: specifier: ^4.15.0 version: 4.27.0 @@ -37310,6 +37310,8 @@ snapshots: '@putout/operator-filesystem': 5.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3)) '@putout/operator-json': 2.2.0 putout: 36.13.1(eslint@8.57.1)(typescript@5.6.3) + transitivePeerDependencies: + - supports-color '@putout/operator-regexp@1.0.0(putout@36.13.1(eslint@8.57.1)(typescript@5.6.3))': dependencies: @@ -51391,7 +51393,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -51405,10 +51407,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 7.26.0 + '@babel/core': 8.0.0-alpha.13 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@7.26.0) + babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): dependencies: From 7fa33d904873944d0ef675e38685f1b4148b106d Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:22:51 -0700 Subject: [PATCH 4/7] Update pnpm-lock.yaml --- pnpm-lock.yaml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cc23f35678cd4..9676df0a475d0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16159,7 +16159,7 @@ importers: version: 3.1.7 ts-jest: specifier: ^29.2.5 - version: 29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) + version: 29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2) tsup: specifier: ^8.3.6 version: 8.3.6(@microsoft/api-extractor@7.47.12(@types/node@20.17.30))(jiti@2.4.2)(postcss@8.4.49)(tsx@4.19.4)(typescript@5.7.2)(yaml@2.6.1) @@ -16202,7 +16202,7 @@ importers: version: 3.1.0 jest: specifier: ^29.1.2 - version: 29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0) + version: 29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0) type-fest: specifier: ^4.15.0 version: 4.27.0 @@ -51393,7 +51393,7 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): + ts-jest@29.2.5(@babel/core@7.26.0)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@7.26.0))(jest@29.7.0(@types/node@20.17.30)(babel-plugin-macros@3.1.0))(typescript@5.7.2): dependencies: bs-logger: 0.2.6 ejs: 3.1.10 @@ -51407,10 +51407,10 @@ snapshots: typescript: 5.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@babel/core': 8.0.0-alpha.13 + '@babel/core': 7.26.0 '@jest/transform': 29.7.0 '@jest/types': 29.6.3 - babel-jest: 29.7.0(@babel/core@8.0.0-alpha.13) + babel-jest: 29.7.0(@babel/core@7.26.0) ts-jest@29.2.5(@babel/core@8.0.0-alpha.13)(@jest/transform@29.7.0)(@jest/types@29.6.3)(babel-jest@29.7.0(@babel/core@8.0.0-alpha.13))(jest@29.7.0(@types/node@20.17.6)(babel-plugin-macros@3.1.0))(typescript@5.6.3): dependencies: From 0dea0ed14dae9e6ceef88370cb9250ef49277b66 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:29:40 -0700 Subject: [PATCH 5/7] Linting --- .../src/components/ControlSelect.tsx | 93 +++++++++++-------- .../src/components/RemoteOptionsContainer.tsx | 6 +- packages/connect-react/src/index.ts | 8 +- .../connect-react/src/utils/type-guards.ts | 12 +-- 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/packages/connect-react/src/components/ControlSelect.tsx b/packages/connect-react/src/components/ControlSelect.tsx index c580e11591a1a..5e0d1e889001f 100644 --- a/packages/connect-react/src/components/ControlSelect.tsx +++ b/packages/connect-react/src/components/ControlSelect.tsx @@ -11,7 +11,9 @@ import { useFormFieldContext } from "../hooks/form-field-context"; import { useCustomize } from "../hooks/customization-context"; import type { BaseReactSelectProps } from "../hooks/customization-context"; import { LoadMoreButton } from "./LoadMoreButton"; -import { isString, isOptionWithValue, OptionWithValue } from "../utils/type-guards"; +import { + isString, isOptionWithValue, OptionWithValue, +} from "../utils/type-guards"; // XXX T and ConfigurableProp should be related type ControlSelectProps = { @@ -43,37 +45,37 @@ export function ControlSelect({ useEffect(() => { // Ensure all options have proper primitive values for label/value - const sanitizedOptions = options.map(option => { - if (typeof option === 'string') return option; - + const sanitizedOptions = options.map((option) => { + if (typeof option === "string") return option; + // If option has __lv wrapper, extract the inner option - if (option && typeof option === 'object' && '__lv' in option) { + if (option && typeof option === "object" && "__lv" in option) { const innerOption = option.__lv; return { - label: String(innerOption?.label || innerOption?.value || ''), - value: innerOption?.value + label: String(innerOption?.label || innerOption?.value || ""), + value: innerOption?.value, }; } - + // Handle nested label and value objects - let actualLabel = ''; + let actualLabel = ""; let actualValue = option.value; - + // Extract nested label - if (option.label && typeof option.label === 'object' && 'label' in option.label) { - actualLabel = String(option.label.label || ''); + if (option.label && typeof option.label === "object" && "label" in option.label) { + actualLabel = String(option.label.label || ""); } else { - actualLabel = String(option.label || option.value || ''); + actualLabel = String(option.label || option.value || ""); } - + // Extract nested value - if (option.value && typeof option.value === 'object' && 'value' in option.value) { + if (option.value && typeof option.value === "object" && "value" in option.value) { actualValue = option.value.value; } - + return { label: actualLabel, - value: actualValue + value: actualValue, }; }); setSelectOptions(sanitizedOptions) @@ -160,7 +162,7 @@ export function ControlSelect({ } const props = select.getProps("controlSelect", baseSelectProps) - + if (showLoadMoreButton) { props.components = { // eslint-disable-next-line react/prop-types @@ -180,19 +182,25 @@ export function ControlSelect({ } const newOption = createOption(inputValue) let newRawValue = newOption - + // NEVER add wrapped objects to selectOptions - only clean {label, value} objects - const cleanSelectOptions = selectOptions.map(opt => { - if (typeof opt === 'string') return opt; - if (opt && typeof opt === 'object' && '__lv' in opt) { - return {label: String(opt.__lv?.label || ''), value: opt.__lv?.value}; + const cleanSelectOptions = selectOptions.map((opt) => { + if (typeof opt === "string") return opt; + if (opt && typeof opt === "object" && "__lv" in opt) { + return { + label: String(opt.__lv?.label || ""), + value: opt.__lv?.value, + }; } return opt; }); - - const newSelectOptions = [newOption, ...cleanSelectOptions]; + + const newSelectOptions = [ + newOption, + ...cleanSelectOptions, + ]; setSelectOptions(newSelectOptions); - + if (prop.type.endsWith("[]")) { if (Array.isArray(rawValue)) { newRawValue = [ @@ -242,33 +250,32 @@ export function ControlSelect({ : Select; // Final safety check - ensure NO __lv wrapped objects reach react-select - const cleanedOptions = selectOptions.map(opt => { - if (typeof opt === 'string') return opt; - if (opt && typeof opt === 'object' && '__lv' in opt && opt.__lv) { - let actualLabel = ''; + const cleanedOptions = selectOptions.map((opt) => { + if (typeof opt === "string") return opt; + if (opt && typeof opt === "object" && "__lv" in opt && opt.__lv) { + let actualLabel = ""; let actualValue = opt.__lv.value; - + // Handle nested label in __lv - if (opt.__lv.label && typeof opt.__lv.label === 'object' && 'label' in opt.__lv.label) { - actualLabel = String(opt.__lv.label.label || ''); + if (opt.__lv.label && typeof opt.__lv.label === "object" && "label" in opt.__lv.label) { + actualLabel = String(opt.__lv.label.label || ""); } else { - actualLabel = String(opt.__lv.label || opt.__lv.value || ''); + actualLabel = String(opt.__lv.label || opt.__lv.value || ""); } - + // Handle nested value in __lv - if (opt.__lv.value && typeof opt.__lv.value === 'object' && 'value' in opt.__lv.value) { + if (opt.__lv.value && typeof opt.__lv.value === "object" && "value" in opt.__lv.value) { actualValue = opt.__lv.value.value; } - + return { label: actualLabel, - value: actualValue + value: actualValue, }; } return opt; }); - return ( ({ isClearable={true} required={!prop.optional} getOptionLabel={(option) => { - return typeof option === 'string' ? option : String(option?.label || option?.value || ''); + return typeof option === "string" + ? option + : String(option?.label || option?.value || ""); }} getOptionValue={(option) => { - return typeof option === 'string' ? option : String(option?.value || ''); + return typeof option === "string" + ? option + : String(option?.value || ""); }} onChange={handleChange} {...props} diff --git a/packages/connect-react/src/components/RemoteOptionsContainer.tsx b/packages/connect-react/src/components/RemoteOptionsContainer.tsx index 66d8285675008..9696bc054b350 100644 --- a/packages/connect-react/src/components/RemoteOptionsContainer.tsx +++ b/packages/connect-react/src/components/RemoteOptionsContainer.tsx @@ -5,7 +5,9 @@ import { useFormContext } from "../hooks/form-context"; import { useFormFieldContext } from "../hooks/form-field-context"; import { useFrontendClient } from "../hooks/frontend-client-context"; import { ControlSelect } from "./ControlSelect"; -import { isString, isOptionWithValue } from "../utils/type-guards"; +import { + isString, isOptionWithValue, +} from "../utils/type-guards"; export type RemoteOptionsContainerProps = { queryEnabled?: boolean; @@ -146,7 +148,7 @@ export function RemoteOptionsContainer({ queryEnabled }: RemoteOptionsContainerP value = o.value; } else { // Skip items that don't match expected format - console.warn('Skipping invalid option:', o); + console.warn("Skipping invalid option:", o); continue; } if (allValues.has(value)) { diff --git a/packages/connect-react/src/index.ts b/packages/connect-react/src/index.ts index c27a397096f57..c86640ebc2f9c 100644 --- a/packages/connect-react/src/index.ts +++ b/packages/connect-react/src/index.ts @@ -33,15 +33,15 @@ export * from "./hooks/use-component"; export * from "./hooks/use-components"; // Debug info for development -import packageJson from '../package.json'; +import packageJson from "../package.json"; export const DEBUG_INFO = { version: `${packageJson.version}-dev`, buildTime: new Date().toISOString(), - source: "local-development" + source: "local-development", }; // Auto-log debug info in development -if (typeof window !== 'undefined') { - console.log('🔧 @pipedream/connect-react DEBUG:', DEBUG_INFO); +if (typeof window !== "undefined") { + console.log("🔧 @pipedream/connect-react DEBUG:", DEBUG_INFO); } diff --git a/packages/connect-react/src/utils/type-guards.ts b/packages/connect-react/src/utils/type-guards.ts index 8f887f7315fa5..158293b148082 100644 --- a/packages/connect-react/src/utils/type-guards.ts +++ b/packages/connect-react/src/utils/type-guards.ts @@ -5,24 +5,24 @@ export interface OptionWithValue { } export function isString(value: unknown): value is string { - return typeof value === 'string'; + return typeof value === "string"; } export function isOptionWithValue(value: unknown): value is OptionWithValue { return ( value !== null && - typeof value === 'object' && + typeof value === "object" && !Array.isArray(value) && - 'value' in value + "value" in value ); } export function isStringArray(value: unknown): value is string[] { - return Array.isArray(value) && value.every(item => typeof item === 'string'); + return Array.isArray(value) && value.every((item) => typeof item === "string"); } export function isOptionArray(value: unknown): value is OptionWithValue[] { - return Array.isArray(value) && value.every(item => isOptionWithValue(item)); + return Array.isArray(value) && value.every((item) => isOptionWithValue(item)); } export function normalizeOption(option: unknown): OptionWithValue | string { @@ -40,4 +40,4 @@ export function normalizeOptions(options: unknown): Array Date: Tue, 22 Jul 2025 14:29:59 -0700 Subject: [PATCH 6/7] Merge branch 'danny/connect-react/gh-17399' of github.com:PipedreamHQ/pipedream into danny/connect-react/gh-17399 From 8a0207506783b7bf7be2b50bf75af6bb9eb6121f Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Tue, 22 Jul 2025 14:44:04 -0700 Subject: [PATCH 7/7] Linting and documenting the utility functions --- .../src/components/ControlSelect.tsx | 78 +--------- .../src/components/RemoteOptionsContainer.tsx | 4 +- packages/connect-react/src/index.ts | 10 +- .../connect-react/src/utils/type-guards.ts | 136 +++++++++++++++++- 4 files changed, 142 insertions(+), 86 deletions(-) diff --git a/packages/connect-react/src/components/ControlSelect.tsx b/packages/connect-react/src/components/ControlSelect.tsx index 5e0d1e889001f..3cd4c9f4dd313 100644 --- a/packages/connect-react/src/components/ControlSelect.tsx +++ b/packages/connect-react/src/components/ControlSelect.tsx @@ -12,7 +12,7 @@ import { useCustomize } from "../hooks/customization-context"; import type { BaseReactSelectProps } from "../hooks/customization-context"; import { LoadMoreButton } from "./LoadMoreButton"; import { - isString, isOptionWithValue, OptionWithValue, + isOptionWithValue, OptionWithValue, sanitizeOption, } from "../utils/type-guards"; // XXX T and ConfigurableProp should be related @@ -44,44 +44,11 @@ export function ControlSelect({ ] = useState(value); useEffect(() => { - // Ensure all options have proper primitive values for label/value - const sanitizedOptions = options.map((option) => { - if (typeof option === "string") return option; - - // If option has __lv wrapper, extract the inner option - if (option && typeof option === "object" && "__lv" in option) { - const innerOption = option.__lv; - return { - label: String(innerOption?.label || innerOption?.value || ""), - value: innerOption?.value, - }; - } - - // Handle nested label and value objects - let actualLabel = ""; - let actualValue = option.value; - - // Extract nested label - if (option.label && typeof option.label === "object" && "label" in option.label) { - actualLabel = String(option.label.label || ""); - } else { - actualLabel = String(option.label || option.value || ""); - } - - // Extract nested value - if (option.value && typeof option.value === "object" && "value" in option.value) { - actualValue = option.value.value; - } - - return { - label: actualLabel, - value: actualValue, - }; - }); - setSelectOptions(sanitizedOptions) + const sanitizedOptions = options.map(sanitizeOption); + setSelectOptions(sanitizedOptions); }, [ options, - ]) + ]); useEffect(() => { setRawValue(value) @@ -184,16 +151,7 @@ export function ControlSelect({ let newRawValue = newOption // NEVER add wrapped objects to selectOptions - only clean {label, value} objects - const cleanSelectOptions = selectOptions.map((opt) => { - if (typeof opt === "string") return opt; - if (opt && typeof opt === "object" && "__lv" in opt) { - return { - label: String(opt.__lv?.label || ""), - value: opt.__lv?.value, - }; - } - return opt; - }); + const cleanSelectOptions = selectOptions.map(sanitizeOption); const newSelectOptions = [ newOption, @@ -250,31 +208,7 @@ export function ControlSelect({ : Select; // Final safety check - ensure NO __lv wrapped objects reach react-select - const cleanedOptions = selectOptions.map((opt) => { - if (typeof opt === "string") return opt; - if (opt && typeof opt === "object" && "__lv" in opt && opt.__lv) { - let actualLabel = ""; - let actualValue = opt.__lv.value; - - // Handle nested label in __lv - if (opt.__lv.label && typeof opt.__lv.label === "object" && "label" in opt.__lv.label) { - actualLabel = String(opt.__lv.label.label || ""); - } else { - actualLabel = String(opt.__lv.label || opt.__lv.value || ""); - } - - // Handle nested value in __lv - if (opt.__lv.value && typeof opt.__lv.value === "object" && "value" in opt.__lv.value) { - actualValue = opt.__lv.value.value; - } - - return { - label: actualLabel, - value: actualValue, - }; - } - return opt; - }); + const cleanedOptions = selectOptions.map(sanitizeOption); return ( ).value === "string" || typeof (value as Record).value === "number") ); } +/** + * Type guard to check if a value is an array of strings. + * @param value - The value to check + * @returns true if the value is a string array + */ export function isStringArray(value: unknown): value is string[] { return Array.isArray(value) && value.every((item) => typeof item === "string"); } +/** + * Type guard to check if a value is an array of OptionWithValue objects. + * @param value - The value to check + * @returns true if the value is an array of valid OptionWithValue objects + */ export function isOptionArray(value: unknown): value is OptionWithValue[] { return Array.isArray(value) && value.every((item) => isOptionWithValue(item)); } +/** + * Normalizes an unknown value into either a string or OptionWithValue. + * Used for basic option processing where the input format is uncertain. + * @param option - The option to normalize + * @returns A normalized string or OptionWithValue object + */ export function normalizeOption(option: unknown): OptionWithValue | string { if (isString(option)) { return option; @@ -35,9 +70,106 @@ export function normalizeOption(option: unknown): OptionWithValue | string { return String(option); } +/** + * Normalizes an array of unknown values into an array of strings or OptionWithValue objects. + * Handles cases where the input might not be an array by returning an empty array. + * @param options - The options array to normalize + * @returns An array of normalized options + */ export function normalizeOptions(options: unknown): Array { if (!Array.isArray(options)) { return []; } return options.map(normalizeOption); } + +/** + * Sanitizes an option to ensure it has proper primitive values for label/value. + * This is the main utility for processing complex nested option structures that can + * come from various sources (APIs, form data, etc.) into a format compatible with react-select. + * + * Handles multiple nesting scenarios: + * 1. String options: returned as-is (e.g., "simple-option") + * 2. __lv wrapper objects: extracts inner option from {__lv: {label: "...", value: "..."}} + * 3. Nested label/value objects: handles {label: {label: "Documents"}, value: {value: "123"}} + * + * This function was created to fix React error #31 where nested objects were being + * passed to React components that expected primitive values. + * + * @param option - The option to sanitize (can be string, object, or complex nested structure) + * @returns A clean option with primitive label/value or a string + * + * @example + * // Simple string + * sanitizeOption("hello") // returns "hello" + * + * @example + * // Nested object structure + * sanitizeOption({ + * label: {label: "Documents", value: "123"}, + * value: {label: "Documents", value: "123"} + * }) // returns {label: "Documents", value: "123"} + * + * @example + * // __lv wrapper + * sanitizeOption({ + * __lv: {label: "Test", value: "test-id"} + * }) // returns {label: "Test", value: "test-id"} + */ +export function sanitizeOption(option: unknown): { label: string; value: unknown } | string { + if (typeof option === "string") return option; + + if (!option || typeof option !== "object") { + return { + label: "", + value: "", + }; + } + + // If option has __lv wrapper, extract the inner option + if ("__lv" in option) { + const innerOption = (option as Record).__lv; + + let actualLabel = ""; + let actualValue = innerOption?.value; + + // Handle nested label in __lv + if (innerOption?.label && typeof innerOption.label === "object" && "label" in innerOption.label) { + actualLabel = String(innerOption.label.label || ""); + } else { + actualLabel = String(innerOption?.label || innerOption?.value || ""); + } + + // Handle nested value in __lv + if (innerOption?.value && typeof innerOption.value === "object" && "value" in innerOption.value) { + actualValue = innerOption.value.value; + } + + return { + label: actualLabel, + value: actualValue, + }; + } + + // Handle nested label and value objects + const optionObj = option as Record; + let actualLabel = ""; + let actualValue = optionObj.value; + + // Extract nested label + if (optionObj.label && typeof optionObj.label === "object" && "label" in optionObj.label) { + actualLabel = String(optionObj.label.label || ""); + } else { + actualLabel = String(optionObj.label || optionObj.value || ""); + } + + // Extract nested value + if (optionObj.value && typeof optionObj.value === "object" && "value" in optionObj.value) { + actualValue = optionObj.value.value; + } + + return { + label: actualLabel, + value: actualValue, + }; +}