Skip to content

Commit 8a02075

Browse files
Linting and documenting the utility functions
1 parent 7c6155b commit 8a02075

File tree

4 files changed

+142
-86
lines changed

4 files changed

+142
-86
lines changed

packages/connect-react/src/components/ControlSelect.tsx

Lines changed: 6 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { useCustomize } from "../hooks/customization-context";
1212
import type { BaseReactSelectProps } from "../hooks/customization-context";
1313
import { LoadMoreButton } from "./LoadMoreButton";
1414
import {
15-
isString, isOptionWithValue, OptionWithValue,
15+
isOptionWithValue, OptionWithValue, sanitizeOption,
1616
} from "../utils/type-guards";
1717

1818
// XXX T and ConfigurableProp should be related
@@ -44,44 +44,11 @@ export function ControlSelect<T>({
4444
] = useState(value);
4545

4646
useEffect(() => {
47-
// Ensure all options have proper primitive values for label/value
48-
const sanitizedOptions = options.map((option) => {
49-
if (typeof option === "string") return option;
50-
51-
// If option has __lv wrapper, extract the inner option
52-
if (option && typeof option === "object" && "__lv" in option) {
53-
const innerOption = option.__lv;
54-
return {
55-
label: String(innerOption?.label || innerOption?.value || ""),
56-
value: innerOption?.value,
57-
};
58-
}
59-
60-
// Handle nested label and value objects
61-
let actualLabel = "";
62-
let actualValue = option.value;
63-
64-
// Extract nested label
65-
if (option.label && typeof option.label === "object" && "label" in option.label) {
66-
actualLabel = String(option.label.label || "");
67-
} else {
68-
actualLabel = String(option.label || option.value || "");
69-
}
70-
71-
// Extract nested value
72-
if (option.value && typeof option.value === "object" && "value" in option.value) {
73-
actualValue = option.value.value;
74-
}
75-
76-
return {
77-
label: actualLabel,
78-
value: actualValue,
79-
};
80-
});
81-
setSelectOptions(sanitizedOptions)
47+
const sanitizedOptions = options.map(sanitizeOption);
48+
setSelectOptions(sanitizedOptions);
8249
}, [
8350
options,
84-
])
51+
]);
8552

8653
useEffect(() => {
8754
setRawValue(value)
@@ -184,16 +151,7 @@ export function ControlSelect<T>({
184151
let newRawValue = newOption
185152

186153
// NEVER add wrapped objects to selectOptions - only clean {label, value} objects
187-
const cleanSelectOptions = selectOptions.map((opt) => {
188-
if (typeof opt === "string") return opt;
189-
if (opt && typeof opt === "object" && "__lv" in opt) {
190-
return {
191-
label: String(opt.__lv?.label || ""),
192-
value: opt.__lv?.value,
193-
};
194-
}
195-
return opt;
196-
});
154+
const cleanSelectOptions = selectOptions.map(sanitizeOption);
197155

198156
const newSelectOptions = [
199157
newOption,
@@ -250,31 +208,7 @@ export function ControlSelect<T>({
250208
: Select;
251209

252210
// Final safety check - ensure NO __lv wrapped objects reach react-select
253-
const cleanedOptions = selectOptions.map((opt) => {
254-
if (typeof opt === "string") return opt;
255-
if (opt && typeof opt === "object" && "__lv" in opt && opt.__lv) {
256-
let actualLabel = "";
257-
let actualValue = opt.__lv.value;
258-
259-
// Handle nested label in __lv
260-
if (opt.__lv.label && typeof opt.__lv.label === "object" && "label" in opt.__lv.label) {
261-
actualLabel = String(opt.__lv.label.label || "");
262-
} else {
263-
actualLabel = String(opt.__lv.label || opt.__lv.value || "");
264-
}
265-
266-
// Handle nested value in __lv
267-
if (opt.__lv.value && typeof opt.__lv.value === "object" && "value" in opt.__lv.value) {
268-
actualValue = opt.__lv.value.value;
269-
}
270-
271-
return {
272-
label: actualLabel,
273-
value: actualValue,
274-
};
275-
}
276-
return opt;
277-
});
211+
const cleanedOptions = selectOptions.map(sanitizeOption);
278212

279213
return (
280214
<MaybeCreatableSelect

packages/connect-react/src/components/RemoteOptionsContainer.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,7 @@ import { useFormContext } from "../hooks/form-context";
55
import { useFormFieldContext } from "../hooks/form-field-context";
66
import { useFrontendClient } from "../hooks/frontend-client-context";
77
import { ControlSelect } from "./ControlSelect";
8-
import {
9-
isString, isOptionWithValue,
10-
} from "../utils/type-guards";
8+
import { isString } from "../utils/type-guards";
119

1210
export type RemoteOptionsContainerProps = {
1311
queryEnabled?: boolean;

packages/connect-react/src/index.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -32,16 +32,8 @@ export * from "./hooks/use-apps";
3232
export * from "./hooks/use-component";
3333
export * from "./hooks/use-components";
3434

35-
// Debug info for development
36-
import packageJson from "../package.json";
37-
35+
// Debug info for development - consumers can choose to log this if needed
3836
export const DEBUG_INFO = {
39-
version: `${packageJson.version}-dev`,
4037
buildTime: new Date().toISOString(),
4138
source: "local-development",
4239
};
43-
44-
// Auto-log debug info in development
45-
if (typeof window !== "undefined") {
46-
console.log("🔧 @pipedream/connect-react DEBUG:", DEBUG_INFO);
47-
}

packages/connect-react/src/utils/type-guards.ts

Lines changed: 134 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,65 @@
1+
/**
2+
* Represents an option object with a value and optional label.
3+
* Used by react-select and similar components.
4+
*/
15
export interface OptionWithValue {
6+
/** The actual value of the option (string or number) */
27
value: string | number;
8+
/** Optional display label for the option */
39
label?: string;
4-
__lv?: any;
10+
/** Internal wrapper object (used by form handling logic) */
11+
__lv?: unknown;
512
}
613

14+
/**
15+
* Type guard to check if a value is a string.
16+
* @param value - The value to check
17+
* @returns true if the value is a string
18+
*/
719
export function isString(value: unknown): value is string {
820
return typeof value === "string";
921
}
1022

23+
/**
24+
* Type guard to check if a value is a valid OptionWithValue object.
25+
* Validates that the object has a 'value' property that is either a string or number.
26+
* @param value - The value to check
27+
* @returns true if the value is a valid OptionWithValue
28+
*/
1129
export function isOptionWithValue(value: unknown): value is OptionWithValue {
1230
return (
1331
value !== null &&
1432
typeof value === "object" &&
1533
!Array.isArray(value) &&
16-
"value" in value
34+
"value" in value &&
35+
(typeof (value as Record<string, unknown>).value === "string" || typeof (value as Record<string, unknown>).value === "number")
1736
);
1837
}
1938

39+
/**
40+
* Type guard to check if a value is an array of strings.
41+
* @param value - The value to check
42+
* @returns true if the value is a string array
43+
*/
2044
export function isStringArray(value: unknown): value is string[] {
2145
return Array.isArray(value) && value.every((item) => typeof item === "string");
2246
}
2347

48+
/**
49+
* Type guard to check if a value is an array of OptionWithValue objects.
50+
* @param value - The value to check
51+
* @returns true if the value is an array of valid OptionWithValue objects
52+
*/
2453
export function isOptionArray(value: unknown): value is OptionWithValue[] {
2554
return Array.isArray(value) && value.every((item) => isOptionWithValue(item));
2655
}
2756

57+
/**
58+
* Normalizes an unknown value into either a string or OptionWithValue.
59+
* Used for basic option processing where the input format is uncertain.
60+
* @param option - The option to normalize
61+
* @returns A normalized string or OptionWithValue object
62+
*/
2863
export function normalizeOption(option: unknown): OptionWithValue | string {
2964
if (isString(option)) {
3065
return option;
@@ -35,9 +70,106 @@ export function normalizeOption(option: unknown): OptionWithValue | string {
3570
return String(option);
3671
}
3772

73+
/**
74+
* Normalizes an array of unknown values into an array of strings or OptionWithValue objects.
75+
* Handles cases where the input might not be an array by returning an empty array.
76+
* @param options - The options array to normalize
77+
* @returns An array of normalized options
78+
*/
3879
export function normalizeOptions(options: unknown): Array<OptionWithValue | string> {
3980
if (!Array.isArray(options)) {
4081
return [];
4182
}
4283
return options.map(normalizeOption);
4384
}
85+
86+
/**
87+
* Sanitizes an option to ensure it has proper primitive values for label/value.
88+
* This is the main utility for processing complex nested option structures that can
89+
* come from various sources (APIs, form data, etc.) into a format compatible with react-select.
90+
*
91+
* Handles multiple nesting scenarios:
92+
* 1. String options: returned as-is (e.g., "simple-option")
93+
* 2. __lv wrapper objects: extracts inner option from {__lv: {label: "...", value: "..."}}
94+
* 3. Nested label/value objects: handles {label: {label: "Documents"}, value: {value: "123"}}
95+
*
96+
* This function was created to fix React error #31 where nested objects were being
97+
* passed to React components that expected primitive values.
98+
*
99+
* @param option - The option to sanitize (can be string, object, or complex nested structure)
100+
* @returns A clean option with primitive label/value or a string
101+
*
102+
* @example
103+
* // Simple string
104+
* sanitizeOption("hello") // returns "hello"
105+
*
106+
* @example
107+
* // Nested object structure
108+
* sanitizeOption({
109+
* label: {label: "Documents", value: "123"},
110+
* value: {label: "Documents", value: "123"}
111+
* }) // returns {label: "Documents", value: "123"}
112+
*
113+
* @example
114+
* // __lv wrapper
115+
* sanitizeOption({
116+
* __lv: {label: "Test", value: "test-id"}
117+
* }) // returns {label: "Test", value: "test-id"}
118+
*/
119+
export function sanitizeOption(option: unknown): { label: string; value: unknown } | string {
120+
if (typeof option === "string") return option;
121+
122+
if (!option || typeof option !== "object") {
123+
return {
124+
label: "",
125+
value: "",
126+
};
127+
}
128+
129+
// If option has __lv wrapper, extract the inner option
130+
if ("__lv" in option) {
131+
const innerOption = (option as Record<string, unknown>).__lv;
132+
133+
let actualLabel = "";
134+
let actualValue = innerOption?.value;
135+
136+
// Handle nested label in __lv
137+
if (innerOption?.label && typeof innerOption.label === "object" && "label" in innerOption.label) {
138+
actualLabel = String(innerOption.label.label || "");
139+
} else {
140+
actualLabel = String(innerOption?.label || innerOption?.value || "");
141+
}
142+
143+
// Handle nested value in __lv
144+
if (innerOption?.value && typeof innerOption.value === "object" && "value" in innerOption.value) {
145+
actualValue = innerOption.value.value;
146+
}
147+
148+
return {
149+
label: actualLabel,
150+
value: actualValue,
151+
};
152+
}
153+
154+
// Handle nested label and value objects
155+
const optionObj = option as Record<string, unknown>;
156+
let actualLabel = "";
157+
let actualValue = optionObj.value;
158+
159+
// Extract nested label
160+
if (optionObj.label && typeof optionObj.label === "object" && "label" in optionObj.label) {
161+
actualLabel = String(optionObj.label.label || "");
162+
} else {
163+
actualLabel = String(optionObj.label || optionObj.value || "");
164+
}
165+
166+
// Extract nested value
167+
if (optionObj.value && typeof optionObj.value === "object" && "value" in optionObj.value) {
168+
actualValue = optionObj.value.value;
169+
}
170+
171+
return {
172+
label: actualLabel,
173+
value: actualValue,
174+
};
175+
}

0 commit comments

Comments
 (0)