Skip to content

Commit 771ff4b

Browse files
Adding support for object prop type
1 parent acc0d11 commit 771ff4b

File tree

4 files changed

+260
-3
lines changed

4 files changed

+260
-3
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
import { ControlApp } from "./ControlApp";
99
import { ControlBoolean } from "./ControlBoolean";
1010
import { ControlInput } from "./ControlInput";
11+
import { ControlObject } from "./ControlObject";
1112
import { ControlSelect } from "./ControlSelect";
1213
import { RemoteOptionsContainer } from "./RemoteOptionsContainer";
1314

@@ -73,6 +74,8 @@ export function Control<T extends ConfigurableProps, U extends ConfigurableProp>
7374
case "integer":
7475
// XXX split into ControlString, ControlInteger, etc? but want to share autoComplet="off", etc functionality in base one
7576
return <ControlInput />;
77+
case "object":
78+
return <ControlObject />;
7679
default:
7780
// TODO "not supported prop type should bubble up"
7881
throw new Error("Unsupported property type: " + prop.type);
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import {
2+
useState, useEffect, type CSSProperties,
3+
} from "react";
4+
import { useFormFieldContext } from "../hooks/form-field-context";
5+
import { useCustomize } from "../hooks/customization-context";
6+
7+
type KeyValuePair = {
8+
key: string;
9+
value: string;
10+
};
11+
12+
export function ControlObject() {
13+
const formFieldContextProps = useFormFieldContext();
14+
const {
15+
id, onChange, prop, value,
16+
} = formFieldContextProps;
17+
const {
18+
getProps, theme,
19+
} = useCustomize();
20+
21+
// Initialize pairs from the current value
22+
const initializePairs = (): KeyValuePair[] => {
23+
if (!value || typeof value !== "object" || Array.isArray(value)) {
24+
return [
25+
{
26+
key: "",
27+
value: "",
28+
},
29+
];
30+
}
31+
32+
const pairs = Object.entries(value).map(([
33+
k,
34+
v,
35+
]) => ({
36+
key: k,
37+
value: typeof v === "string"
38+
? v
39+
: JSON.stringify(v),
40+
}));
41+
42+
return pairs.length > 0
43+
? pairs
44+
: [
45+
{
46+
key: "",
47+
value: "",
48+
},
49+
];
50+
};
51+
52+
const [
53+
pairs,
54+
setPairs,
55+
] = useState<KeyValuePair[]>(initializePairs);
56+
57+
// Update pairs when value changes externally
58+
useEffect(() => {
59+
setPairs(initializePairs());
60+
}, [
61+
value,
62+
]);
63+
64+
const updateObject = (newPairs: KeyValuePair[]) => {
65+
// Filter out empty pairs
66+
const validPairs = newPairs.filter((p) => p.key.trim() !== "");
67+
68+
if (validPairs.length === 0) {
69+
onChange(undefined);
70+
return;
71+
}
72+
73+
// Convert to object
74+
const obj: Record<string, any> = {};
75+
validPairs.forEach((pair) => {
76+
if (pair.key.trim()) {
77+
// Try to parse the value as JSON, fallback to string
78+
try {
79+
obj[pair.key] = JSON.parse(pair.value);
80+
} catch {
81+
obj[pair.key] = pair.value;
82+
}
83+
}
84+
});
85+
86+
onChange(obj);
87+
};
88+
89+
const handlePairChange = (index: number, field: "key" | "value", newValue: string) => {
90+
const newPairs = [
91+
...pairs,
92+
];
93+
newPairs[index] = {
94+
...newPairs[index],
95+
[field]: newValue,
96+
};
97+
setPairs(newPairs);
98+
updateObject(newPairs);
99+
};
100+
101+
const addPair = () => {
102+
const newPairs = [
103+
...pairs,
104+
{
105+
key: "",
106+
value: "",
107+
},
108+
];
109+
setPairs(newPairs);
110+
};
111+
112+
const removePair = (index: number) => {
113+
const newPairs = pairs.filter((_, i) => i !== index);
114+
setPairs(newPairs.length > 0
115+
? newPairs
116+
: [
117+
{
118+
key: "",
119+
value: "",
120+
},
121+
]);
122+
updateObject(newPairs);
123+
};
124+
125+
const containerStyles: CSSProperties = {
126+
gridArea: "control",
127+
display: "flex",
128+
flexDirection: "column",
129+
gap: "0.5rem",
130+
};
131+
132+
const pairStyles: CSSProperties = {
133+
display: "flex",
134+
gap: "0.5rem",
135+
alignItems: "center",
136+
};
137+
138+
const inputStyles: CSSProperties = {
139+
color: theme.colors.neutral60,
140+
border: "1px solid",
141+
borderColor: theme.colors.neutral20,
142+
padding: 6,
143+
borderRadius: theme.borderRadius,
144+
boxShadow: theme.boxShadow.input,
145+
flex: 1,
146+
};
147+
148+
const buttonStyles: CSSProperties = {
149+
color: theme.colors.neutral60,
150+
display: "inline-flex",
151+
alignItems: "center",
152+
padding: `${theme.spacing.baseUnit}px ${theme.spacing.baseUnit * 1.5}px ${
153+
theme.spacing.baseUnit
154+
}px ${theme.spacing.baseUnit * 2.5}px`,
155+
border: `1px solid ${theme.colors.neutral30}`,
156+
borderRadius: theme.borderRadius,
157+
cursor: "pointer",
158+
fontSize: "0.8125rem",
159+
fontWeight: 450,
160+
gap: theme.spacing.baseUnit * 2,
161+
textWrap: "nowrap",
162+
backgroundColor: "white",
163+
};
164+
165+
const removeButtonStyles: CSSProperties = {
166+
...buttonStyles,
167+
flex: "0 0 auto",
168+
padding: "6px 8px",
169+
};
170+
171+
return (
172+
<div {...getProps("controlObject", containerStyles, formFieldContextProps)}>
173+
{pairs.map((pair, index) => (
174+
<div key={index} style={pairStyles}>
175+
<input
176+
type="text"
177+
value={pair.key}
178+
onChange={(e) => handlePairChange(index, "key", e.target.value)}
179+
placeholder="Key"
180+
style={inputStyles}
181+
required={!prop.optional && index === 0}
182+
/>
183+
<input
184+
type="text"
185+
value={pair.value}
186+
onChange={(e) => handlePairChange(index, "value", e.target.value)}
187+
placeholder="Value"
188+
style={inputStyles}
189+
/>
190+
{pairs.length > 1 && (
191+
<button
192+
type="button"
193+
onClick={() => removePair(index)}
194+
style={removeButtonStyles}
195+
aria-label="Remove pair"
196+
>
197+
×
198+
</button>
199+
)}
200+
</div>
201+
))}
202+
<button
203+
type="button"
204+
onClick={addPair}
205+
style={{
206+
...buttonStyles,
207+
alignSelf: "flex-start",
208+
paddingRight: `${theme.spacing.baseUnit * 2}px`,
209+
}}
210+
>
211+
<span>+</span>
212+
<span>Add more</span>
213+
</button>
214+
</div>
215+
);
216+
}

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

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import type { CSSProperties } from "react";
1+
import {
2+
useState, type CSSProperties,
3+
} from "react";
24
import Markdown from "react-markdown";
35
import {
46
ConfigurableProp, ConfigurableProps,
@@ -41,8 +43,42 @@ export function Description<T extends ConfigurableProps, U extends ConfigurableP
4143
return null;
4244
}
4345
return <div className={getClassNames("description", props)} style={getStyles("description", baseStyles, props)}> <Markdown components={{
44-
a: ({ ...props }) => {
45-
return <a {...props} target="_blank" rel="noopener noreferrer" />;
46+
a: ({ ...linkProps }) => {
47+
const [
48+
isHovered,
49+
setIsHovered,
50+
] = useState(false);
51+
52+
const linkStyles: CSSProperties = {
53+
textDecoration: "underline",
54+
textUnderlineOffset: "3px",
55+
color: "inherit",
56+
transition: "opacity 0.2s ease",
57+
opacity: isHovered
58+
? 0.7
59+
: 1,
60+
};
61+
62+
return (
63+
<span style={{
64+
display: "inline-flex",
65+
alignItems: "center",
66+
gap: "2px",
67+
}}>
68+
<a
69+
{...linkProps}
70+
target="_blank"
71+
rel="noopener noreferrer"
72+
style={linkStyles}
73+
onMouseEnter={() => setIsHovered(true)}
74+
onMouseLeave={() => setIsHovered(false)}
75+
/>
76+
<span style={{
77+
fontSize: "0.7em",
78+
opacity: 0.7,
79+
}}></span>
80+
</span>
81+
);
4682
},
4783
}}>
4884
{markdown}

packages/connect-react/src/hooks/customization-context.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { ControlAny } from "../components/ControlAny";
2626
import { ControlApp } from "../components/ControlApp";
2727
import { ControlBoolean } from "../components/ControlBoolean";
2828
import { ControlInput } from "../components/ControlInput";
29+
import { ControlObject } from "../components/ControlObject";
2930
import { ControlSelect } from "../components/ControlSelect";
3031
import { ControlSubmit } from "../components/ControlSubmit";
3132
import { Description } from "../components/Description";
@@ -69,6 +70,7 @@ export type CustomizableProps = {
6970
controlApp: ComponentProps<typeof ControlApp> & FormFieldContext<ConfigurableProp>;
7071
controlBoolean: ComponentProps<typeof ControlBoolean> & FormFieldContext<ConfigurableProp>;
7172
controlInput: ComponentProps<typeof ControlInput> & FormFieldContext<ConfigurableProp>;
73+
controlObject: ComponentProps<typeof ControlObject> & FormFieldContext<ConfigurableProp>;
7274
controlSubmit: ComponentProps<typeof ControlSubmit>;
7375
description: ComponentProps<typeof Description>;
7476
error: ComponentProps<typeof Errors>;

0 commit comments

Comments
 (0)