Skip to content
Merged
138 changes: 113 additions & 25 deletions packages/connect-react/src/components/ControlSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@
import { useCustomize } from "../hooks/customization-context";
import type { BaseReactSelectProps } from "../hooks/customization-context";
import { LoadMoreButton } from "./LoadMoreButton";
import {
isString, isOptionWithValue, OptionWithValue,

Check failure on line 15 in packages/connect-react/src/components/ControlSelect.tsx

View workflow job for this annotation

GitHub Actions / Lint Code Base

'isString' is defined but never used
} from "../utils/type-guards";

// XXX T and ConfigurableProp should be related
type ControlSelectProps<T> = {
Expand Down Expand Up @@ -41,7 +44,41 @@
] = 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,
])
Expand All @@ -67,11 +104,11 @@
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) {
Expand All @@ -84,8 +121,11 @@
}
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) {
Expand All @@ -95,12 +135,10 @@
}
} else {
ret = {
label: rawValue,
label: String(rawValue),
value: rawValue,
}
}
} else if (ret.__lv) {
ret = ret.__lv
}
}
return ret;
Expand All @@ -117,13 +155,14 @@
<components.MenuList {...props}>
{ children }
<div className="pt-4">
<LoadMoreButton onChange={onLoadMore}/>
<LoadMoreButton onChange={onLoadMore || (() => {})}/>
</div>
</components.MenuList>
)
}

const props = select.getProps("controlSelect", baseSelectProps)

if (showLoadMoreButton) {
props.components = {
// eslint-disable-next-line react/prop-types
Expand All @@ -133,24 +172,35 @@
}

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 = [
Expand All @@ -170,14 +220,14 @@
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,
});
Expand All @@ -198,19 +248,57 @@
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 (
<MaybeCreatableSelect
inputId={id}
instanceId={id}
options={selectOptions}
options={cleanedOptions}
value={selectValue}
isMulti={prop.type.endsWith("[]")}
isClearable={true}
required={!prop.optional}
getOptionLabel={(option) => {
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}
/>
);
}
16 changes: 13 additions & 3 deletions packages/connect-react/src/components/RemoteOptionsContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import { useFormFieldContext } from "../hooks/form-field-context";
import { useFrontendClient } from "../hooks/frontend-client-context";
import { ControlSelect } from "./ControlSelect";
import {
isString, isOptionWithValue,

Check failure on line 9 in packages/connect-react/src/components/RemoteOptionsContainer.tsx

View workflow job for this annotation

GitHub Actions / Lint Code Base

'isOptionWithValue' is defined but never used
} from "../utils/type-guards";

export type RemoteOptionsContainerProps = {
queryEnabled?: boolean;
Expand Down Expand Up @@ -138,9 +141,16 @@
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
}
Expand Down
14 changes: 14 additions & 0 deletions packages/connect-react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
43 changes: 43 additions & 0 deletions packages/connect-react/src/utils/type-guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
export interface OptionWithValue {
value: string | number;
label?: string;
__lv?: any;

Check failure on line 4 in packages/connect-react/src/utils/type-guards.ts

View workflow job for this annotation

GitHub Actions / Lint Code Base

Unexpected any. Specify a different type
}

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<OptionWithValue | string> {
if (!Array.isArray(options)) {
return [];
}
return options.map(normalizeOption);
}
16 changes: 12 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading