From 771ff4b579a89355d7f993d706dcff7548153b7f Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 14:23:31 -0700 Subject: [PATCH 01/11] Adding support for object prop type --- .../connect-react/src/components/Control.tsx | 3 + .../src/components/ControlObject.tsx | 216 ++++++++++++++++++ .../src/components/Description.tsx | 42 +++- .../src/hooks/customization-context.tsx | 2 + 4 files changed, 260 insertions(+), 3 deletions(-) create mode 100644 packages/connect-react/src/components/ControlObject.tsx diff --git a/packages/connect-react/src/components/Control.tsx b/packages/connect-react/src/components/Control.tsx index 23007e459a417..df4efc409e53a 100644 --- a/packages/connect-react/src/components/Control.tsx +++ b/packages/connect-react/src/components/Control.tsx @@ -8,6 +8,7 @@ import { import { ControlApp } from "./ControlApp"; import { ControlBoolean } from "./ControlBoolean"; import { ControlInput } from "./ControlInput"; +import { ControlObject } from "./ControlObject"; import { ControlSelect } from "./ControlSelect"; import { RemoteOptionsContainer } from "./RemoteOptionsContainer"; @@ -73,6 +74,8 @@ export function Control case "integer": // XXX split into ControlString, ControlInteger, etc? but want to share autoComplet="off", etc functionality in base one return ; + case "object": + return ; default: // TODO "not supported prop type should bubble up" throw new Error("Unsupported property type: " + prop.type); diff --git a/packages/connect-react/src/components/ControlObject.tsx b/packages/connect-react/src/components/ControlObject.tsx new file mode 100644 index 0000000000000..640eb16b7c684 --- /dev/null +++ b/packages/connect-react/src/components/ControlObject.tsx @@ -0,0 +1,216 @@ +import { + useState, useEffect, type CSSProperties, +} from "react"; +import { useFormFieldContext } from "../hooks/form-field-context"; +import { useCustomize } from "../hooks/customization-context"; + +type KeyValuePair = { + key: string; + value: string; +}; + +export function ControlObject() { + const formFieldContextProps = useFormFieldContext(); + const { + id, onChange, prop, value, + } = formFieldContextProps; + const { + getProps, theme, + } = useCustomize(); + + // Initialize pairs from the current value + const initializePairs = (): KeyValuePair[] => { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return [ + { + key: "", + value: "", + }, + ]; + } + + const pairs = Object.entries(value).map(([ + k, + v, + ]) => ({ + key: k, + value: typeof v === "string" + ? v + : JSON.stringify(v), + })); + + return pairs.length > 0 + ? pairs + : [ + { + key: "", + value: "", + }, + ]; + }; + + const [ + pairs, + setPairs, + ] = useState(initializePairs); + + // Update pairs when value changes externally + useEffect(() => { + setPairs(initializePairs()); + }, [ + value, + ]); + + const updateObject = (newPairs: KeyValuePair[]) => { + // Filter out empty pairs + const validPairs = newPairs.filter((p) => p.key.trim() !== ""); + + if (validPairs.length === 0) { + onChange(undefined); + return; + } + + // Convert to object + const obj: Record = {}; + validPairs.forEach((pair) => { + if (pair.key.trim()) { + // Try to parse the value as JSON, fallback to string + try { + obj[pair.key] = JSON.parse(pair.value); + } catch { + obj[pair.key] = pair.value; + } + } + }); + + onChange(obj); + }; + + const handlePairChange = (index: number, field: "key" | "value", newValue: string) => { + const newPairs = [ + ...pairs, + ]; + newPairs[index] = { + ...newPairs[index], + [field]: newValue, + }; + setPairs(newPairs); + updateObject(newPairs); + }; + + const addPair = () => { + const newPairs = [ + ...pairs, + { + key: "", + value: "", + }, + ]; + setPairs(newPairs); + }; + + const removePair = (index: number) => { + const newPairs = pairs.filter((_, i) => i !== index); + setPairs(newPairs.length > 0 + ? newPairs + : [ + { + key: "", + value: "", + }, + ]); + updateObject(newPairs); + }; + + const containerStyles: CSSProperties = { + gridArea: "control", + display: "flex", + flexDirection: "column", + gap: "0.5rem", + }; + + const pairStyles: CSSProperties = { + display: "flex", + gap: "0.5rem", + alignItems: "center", + }; + + const inputStyles: CSSProperties = { + color: theme.colors.neutral60, + border: "1px solid", + borderColor: theme.colors.neutral20, + padding: 6, + borderRadius: theme.borderRadius, + boxShadow: theme.boxShadow.input, + flex: 1, + }; + + const buttonStyles: CSSProperties = { + color: theme.colors.neutral60, + display: "inline-flex", + alignItems: "center", + padding: `${theme.spacing.baseUnit}px ${theme.spacing.baseUnit * 1.5}px ${ + theme.spacing.baseUnit + }px ${theme.spacing.baseUnit * 2.5}px`, + border: `1px solid ${theme.colors.neutral30}`, + borderRadius: theme.borderRadius, + cursor: "pointer", + fontSize: "0.8125rem", + fontWeight: 450, + gap: theme.spacing.baseUnit * 2, + textWrap: "nowrap", + backgroundColor: "white", + }; + + const removeButtonStyles: CSSProperties = { + ...buttonStyles, + flex: "0 0 auto", + padding: "6px 8px", + }; + + return ( +
+ {pairs.map((pair, index) => ( +
+ handlePairChange(index, "key", e.target.value)} + placeholder="Key" + style={inputStyles} + required={!prop.optional && index === 0} + /> + handlePairChange(index, "value", e.target.value)} + placeholder="Value" + style={inputStyles} + /> + {pairs.length > 1 && ( + + )} +
+ ))} + +
+ ); +} diff --git a/packages/connect-react/src/components/Description.tsx b/packages/connect-react/src/components/Description.tsx index abd04cdde795a..9aa811c704550 100644 --- a/packages/connect-react/src/components/Description.tsx +++ b/packages/connect-react/src/components/Description.tsx @@ -1,4 +1,6 @@ -import type { CSSProperties } from "react"; +import { + useState, type CSSProperties, +} from "react"; import Markdown from "react-markdown"; import { ConfigurableProp, ConfigurableProps, @@ -41,8 +43,42 @@ export function Description { - return ; + a: ({ ...linkProps }) => { + const [ + isHovered, + setIsHovered, + ] = useState(false); + + const linkStyles: CSSProperties = { + textDecoration: "underline", + textUnderlineOffset: "3px", + color: "inherit", + transition: "opacity 0.2s ease", + opacity: isHovered + ? 0.7 + : 1, + }; + + return ( + + setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + /> + + + ); }, }}> {markdown} diff --git a/packages/connect-react/src/hooks/customization-context.tsx b/packages/connect-react/src/hooks/customization-context.tsx index e720045941ff2..2ea2d345b2427 100644 --- a/packages/connect-react/src/hooks/customization-context.tsx +++ b/packages/connect-react/src/hooks/customization-context.tsx @@ -26,6 +26,7 @@ import { ControlAny } from "../components/ControlAny"; import { ControlApp } from "../components/ControlApp"; import { ControlBoolean } from "../components/ControlBoolean"; import { ControlInput } from "../components/ControlInput"; +import { ControlObject } from "../components/ControlObject"; import { ControlSelect } from "../components/ControlSelect"; import { ControlSubmit } from "../components/ControlSubmit"; import { Description } from "../components/Description"; @@ -69,6 +70,7 @@ export type CustomizableProps = { controlApp: ComponentProps & FormFieldContext; controlBoolean: ComponentProps & FormFieldContext; controlInput: ComponentProps & FormFieldContext; + controlObject: ComponentProps & FormFieldContext; controlSubmit: ComponentProps; description: ComponentProps; error: ComponentProps; From b1653be43cfa7c00e28d61053ab92885b410f647 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 14:24:49 -0700 Subject: [PATCH 02/11] Changelog and version rev --- packages/connect-react/CHANGELOG.md | 4 ++++ packages/connect-react/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 80d81233656f6..563f472268d71 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -2,6 +2,10 @@ # Changelog +# [1.1.0] - 2025-06-04 + +- Adding support for 'object' prop types + # [1.0.2] - 2025-04-24 - Updating README to remove note about this package being in early preview diff --git a/packages/connect-react/package.json b/packages/connect-react/package.json index c40a247770dbb..76b1bd5b2d100 100644 --- a/packages/connect-react/package.json +++ b/packages/connect-react/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/connect-react", - "version": "1.0.2", + "version": "1.1.0", "description": "Pipedream Connect library for React", "files": [ "dist" From ef1e2230e74edc6bb8515a8cb33fc021f2807113 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 14:40:55 -0700 Subject: [PATCH 03/11] Modifying string and string[] inputs if no options --- packages/connect-react/CHANGELOG.md | 1 + .../connect-react/src/components/Control.tsx | 6 + .../src/components/ControlArray.tsx | 147 ++++++++++++++++++ .../src/hooks/customization-context.tsx | 2 + 4 files changed, 156 insertions(+) create mode 100644 packages/connect-react/src/components/ControlArray.tsx diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 563f472268d71..35c3cbbf5cbdb 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -5,6 +5,7 @@ # [1.1.0] - 2025-06-04 - Adding support for 'object' prop types +- Modifying string and string[] inputs to hide the dropdown in the case of no options # [1.0.2] - 2025-04-24 diff --git a/packages/connect-react/src/components/Control.tsx b/packages/connect-react/src/components/Control.tsx index df4efc409e53a..3706a06be4a4d 100644 --- a/packages/connect-react/src/components/Control.tsx +++ b/packages/connect-react/src/components/Control.tsx @@ -6,6 +6,7 @@ import { } from "@pipedream/sdk"; // import { ControlAny } from "./ControlAny" import { ControlApp } from "./ControlApp"; +import { ControlArray } from "./ControlArray"; import { ControlBoolean } from "./ControlBoolean"; import { ControlInput } from "./ControlInput"; import { ControlObject } from "./ControlObject"; @@ -55,6 +56,11 @@ export function Control } if (prop.type.endsWith("[]")) { + // If no options are defined, use individual inputs with "Add more" functionality + if (!("options" in prop) || !prop.options) { + return ; + } + // If options are defined, they would have been handled above in the options check return null, }} />; diff --git a/packages/connect-react/src/components/ControlArray.tsx b/packages/connect-react/src/components/ControlArray.tsx new file mode 100644 index 0000000000000..7910a85c0871c --- /dev/null +++ b/packages/connect-react/src/components/ControlArray.tsx @@ -0,0 +1,147 @@ +import { useState, useEffect, type CSSProperties } from "react"; +import { useFormFieldContext } from "../hooks/form-field-context"; +import { useCustomize } from "../hooks/customization-context"; + +export function ControlArray() { + const formFieldContextProps = useFormFieldContext(); + const { + id, onChange, prop, value, + } = formFieldContextProps; + const { + getProps, theme, + } = useCustomize(); + + // Initialize values from the current value + const initializeValues = (): string[] => { + if (!value || !Array.isArray(value)) { + return [""]; + } + + const stringValues = value.map(v => typeof v === "string" ? v : JSON.stringify(v)); + return stringValues.length > 0 ? stringValues : [""]; + }; + + const [values, setValues] = useState(initializeValues); + + // Update values when value changes externally + useEffect(() => { + setValues(initializeValues()); + }, [value]); + + const updateArray = (newValues: string[]) => { + // Filter out empty values + const validValues = newValues.filter(v => v.trim() !== ""); + + if (validValues.length === 0) { + onChange(undefined); + return; + } + + onChange(validValues); + }; + + const handleValueChange = (index: number, newValue: string) => { + const newValues = [...values]; + newValues[index] = newValue; + setValues(newValues); + updateArray(newValues); + }; + + const addValue = () => { + const newValues = [...values, ""]; + setValues(newValues); + }; + + const removeValue = (index: number) => { + const newValues = values.filter((_, i) => i !== index); + setValues(newValues.length > 0 ? newValues : [""]); + updateArray(newValues); + }; + + const containerStyles: CSSProperties = { + gridArea: "control", + display: "flex", + flexDirection: "column", + gap: "0.5rem", + }; + + const itemStyles: CSSProperties = { + display: "flex", + gap: "0.5rem", + alignItems: "center", + }; + + const inputStyles: CSSProperties = { + color: theme.colors.neutral60, + border: "1px solid", + borderColor: theme.colors.neutral20, + padding: 6, + borderRadius: theme.borderRadius, + boxShadow: theme.boxShadow.input, + flex: 1, + }; + + const buttonStyles: CSSProperties = { + color: theme.colors.neutral60, + display: "inline-flex", + alignItems: "center", + padding: `${theme.spacing.baseUnit}px ${theme.spacing.baseUnit * 1.5}px ${ + theme.spacing.baseUnit + }px ${theme.spacing.baseUnit * 2.5}px`, + border: `1px solid ${theme.colors.neutral30}`, + borderRadius: theme.borderRadius, + cursor: "pointer", + fontSize: "0.8125rem", + fontWeight: 450, + gap: theme.spacing.baseUnit * 2, + textWrap: "nowrap", + backgroundColor: "white", + }; + + const removeButtonStyles: CSSProperties = { + ...buttonStyles, + flex: "0 0 auto", + padding: "6px 8px", + }; + + return ( +
+ {values.map((value, index) => ( +
+ handleValueChange(index, e.target.value)} +placeholder="" + style={inputStyles} + required={!prop.optional && index === 0} + /> + {values.length > 1 && ( + + )} +
+ ))} + {(values[values.length - 1]?.trim() || values.length > 1) && ( + + )} +
+ ); +} \ No newline at end of file diff --git a/packages/connect-react/src/hooks/customization-context.tsx b/packages/connect-react/src/hooks/customization-context.tsx index 2ea2d345b2427..7169780a8db43 100644 --- a/packages/connect-react/src/hooks/customization-context.tsx +++ b/packages/connect-react/src/hooks/customization-context.tsx @@ -24,6 +24,7 @@ import type { FormFieldContext } from "./form-field-context"; import { ComponentForm } from "../components/ComponentForm"; import { ControlAny } from "../components/ControlAny"; import { ControlApp } from "../components/ControlApp"; +import { ControlArray } from "../components/ControlArray"; import { ControlBoolean } from "../components/ControlBoolean"; import { ControlInput } from "../components/ControlInput"; import { ControlObject } from "../components/ControlObject"; @@ -68,6 +69,7 @@ export type CustomizableProps = { connectButton: ComponentProps & FormFieldContext; controlAny: ComponentProps & FormFieldContext; controlApp: ComponentProps & FormFieldContext; + controlArray: ComponentProps & FormFieldContext; controlBoolean: ComponentProps & FormFieldContext; controlInput: ComponentProps & FormFieldContext; controlObject: ComponentProps & FormFieldContext; From a40b0cf9f9d08399c8d345fffac61d002af3e703 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 14:46:05 -0700 Subject: [PATCH 04/11] Update CHANGELOG.md --- packages/connect-react/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 35c3cbbf5cbdb..33e8d28c78350 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -6,6 +6,7 @@ - Adding support for 'object' prop types - Modifying string and string[] inputs to hide the dropdown in the case of no options +- Hyperlinks in prop descriptions now open in a new tab # [1.0.2] - 2025-04-24 From 48ee93fdfa13a887effb62eb7a7f674cdbf6eae9 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 14:55:04 -0700 Subject: [PATCH 05/11] Update CHANGELOG.md --- packages/connect-react/CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/connect-react/CHANGELOG.md b/packages/connect-react/CHANGELOG.md index 33e8d28c78350..a2d8eb451213b 100644 --- a/packages/connect-react/CHANGELOG.md +++ b/packages/connect-react/CHANGELOG.md @@ -6,7 +6,7 @@ - Adding support for 'object' prop types - Modifying string and string[] inputs to hide the dropdown in the case of no options -- Hyperlinks in prop descriptions now open in a new tab +- Added basic styling to hyperlinks in prop descriptions # [1.0.2] - 2025-04-24 From 313efc6249235a9c80b453bc4d3c010b8716bf87 Mon Sep 17 00:00:00 2001 From: Danny Roosevelt Date: Wed, 4 Jun 2025 15:16:40 -0700 Subject: [PATCH 06/11] Addressing PR feedback --- .../src/components/ControlArray.tsx | 57 +++++++++---- .../src/components/ControlObject.tsx | 80 ++++++++++++++++++- 2 files changed, 120 insertions(+), 17 deletions(-) diff --git a/packages/connect-react/src/components/ControlArray.tsx b/packages/connect-react/src/components/ControlArray.tsx index 7910a85c0871c..00c8f46b3ed17 100644 --- a/packages/connect-react/src/components/ControlArray.tsx +++ b/packages/connect-react/src/components/ControlArray.tsx @@ -1,4 +1,6 @@ -import { useState, useEffect, type CSSProperties } from "react"; +import { + useState, useEffect, type CSSProperties, +} from "react"; import { useFormFieldContext } from "../hooks/form-field-context"; import { useCustomize } from "../hooks/customization-context"; @@ -14,24 +16,37 @@ export function ControlArray() { // Initialize values from the current value const initializeValues = (): string[] => { if (!value || !Array.isArray(value)) { - return [""]; + return [ + "", + ]; } - - const stringValues = value.map(v => typeof v === "string" ? v : JSON.stringify(v)); - return stringValues.length > 0 ? stringValues : [""]; + + const stringValues = value.map((v) => typeof v === "string" + ? v + : JSON.stringify(v)); + return stringValues.length > 0 + ? stringValues + : [ + "", + ]; }; - const [values, setValues] = useState(initializeValues); + const [ + values, + setValues, + ] = useState(initializeValues); // Update values when value changes externally useEffect(() => { setValues(initializeValues()); - }, [value]); + }, [ + value, + ]); const updateArray = (newValues: string[]) => { // Filter out empty values - const validValues = newValues.filter(v => v.trim() !== ""); - + const validValues = newValues.filter((v) => v.trim() !== ""); + if (validValues.length === 0) { onChange(undefined); return; @@ -41,20 +56,29 @@ export function ControlArray() { }; const handleValueChange = (index: number, newValue: string) => { - const newValues = [...values]; + const newValues = [ + ...values, + ]; newValues[index] = newValue; setValues(newValues); updateArray(newValues); }; const addValue = () => { - const newValues = [...values, ""]; + const newValues = [ + ...values, + "", + ]; setValues(newValues); }; const removeValue = (index: number) => { const newValues = values.filter((_, i) => i !== index); - setValues(newValues.length > 0 ? newValues : [""]); + setValues(newValues.length > 0 + ? newValues + : [ + "", + ]); updateArray(newValues); }; @@ -104,6 +128,9 @@ export function ControlArray() { padding: "6px 8px", }; + // Show "Add more" button if the last input has content or if there are multiple inputs + const shouldShowAddMoreButton = values[values.length - 1]?.trim() || values.length > 1; + return (
{values.map((value, index) => ( @@ -112,7 +139,7 @@ export function ControlArray() { type="text" value={value} onChange={(e) => handleValueChange(index, e.target.value)} -placeholder="" + placeholder="" style={inputStyles} required={!prop.optional && index === 0} /> @@ -128,7 +155,7 @@ placeholder="" )}
))} - {(values[values.length - 1]?.trim() || values.length > 1) && ( + {shouldShowAddMoreButton && (