Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/connect-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

# Changelog

# [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
- Added basic styling to hyperlinks in prop descriptions

# [1.0.2] - 2025-04-24

- Updating README to remove note about this package being in early preview
Expand Down
2 changes: 1 addition & 1 deletion packages/connect-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pipedream/connect-react",
"version": "1.0.2",
"version": "1.1.0",
"description": "Pipedream Connect library for React",
"files": [
"dist"
Expand Down
9 changes: 9 additions & 0 deletions packages/connect-react/src/components/Control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ 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";
import { ControlSelect } from "./ControlSelect";
import { RemoteOptionsContainer } from "./RemoteOptionsContainer";

Expand Down Expand Up @@ -54,6 +56,11 @@ export function Control<T extends ConfigurableProps, U extends ConfigurableProp>
}

if (prop.type.endsWith("[]")) {
// If no options are defined, use individual inputs with "Add more" functionality
if (!("options" in prop) || !prop.options) {
return <ControlArray />;
}
// If options are defined, they would have been handled above in the options check
return <ControlSelect isCreatable={true} options={[]} components={{
IndicatorSeparator: () => null,
}} />;
Expand All @@ -73,6 +80,8 @@ export function Control<T extends ConfigurableProps, U extends ConfigurableProp>
case "integer":
// XXX split into ControlString, ControlInteger, etc? but want to share autoComplet="off", etc functionality in base one
return <ControlInput />;
case "object":
return <ControlObject />;
default:
// TODO "not supported prop type should bubble up"
throw new Error("Unsupported property type: " + prop.type);
Expand Down
137 changes: 137 additions & 0 deletions packages/connect-react/src/components/ControlArray.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {
useState, useEffect,
} from "react";
import { useFormFieldContext } from "../hooks/form-field-context";
import { useCustomize } from "../hooks/customization-context";
import {
getInputStyles, getButtonStyles, getRemoveButtonStyles, getContainerStyles, getItemStyles,
} from "../styles/control-styles";

export function ControlArray() {
const formFieldContextProps = useFormFieldContext();
const {
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<string[]>(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 as string[]);
};

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 = getContainerStyles();
const itemStyles = getItemStyles();
const inputStyles = getInputStyles(theme);
const buttonStyles = getButtonStyles(theme);
const removeButtonStyles = getRemoveButtonStyles(theme);

// 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 (
<div {...getProps("controlArray", containerStyles, formFieldContextProps)}>
{values.map((value, index) => (
<div key={index} style={itemStyles}>
<input
type="text"
value={value}
onChange={(e) => handleValueChange(index, e.target.value)}
placeholder=""
style={inputStyles}
required={!prop.optional && index === 0}
/>
{values.length > 1 && (
<button
type="button"
onClick={() => removeValue(index)}
style={removeButtonStyles}
aria-label="Remove value"
>
×
</button>
)}
</div>
))}
{shouldShowAddMoreButton && (
<button
type="button"
onClick={addValue}
style={{
...buttonStyles,
alignSelf: "flex-start",
paddingRight: `${theme.spacing.baseUnit * 2}px`,
}}
>
<span>+</span>
<span>Add more</span>
</button>
)}
</div>
);
}
Loading
Loading