feat(blocks): Add support for mutually exclusive input fields (#8856)
- resolves part of #8731 ### Changes - Introduced `mutually_exclusive` parameter in `SchemaField` to manage input exclusivity. - Implemented logic in `NodeGenericInputField` to disable inputs based on mutual exclusivity. - Updated related components to support the new `disabled` state for inputs. - Enhanced `BlockIOSubSchemaMeta` to include `mutually_exclusive` property. > Currently, I’m disabling the input from the same group (I haven’t added any frontend validation to prevent users from bypassing it). https://github.com/user-attachments/assets/71fb9fe4-943b-4724-8acb-6aed2232ed6b --------- Co-authored-by: Nicholas Tindle <nicholas.tindle@agpt.co>
This commit is contained in:
parent
f588b69484
commit
2fe6eb1df1
|
@ -90,6 +90,7 @@ class BlockSchema(BaseModel):
|
||||||
}
|
}
|
||||||
elif isinstance(obj, list):
|
elif isinstance(obj, list):
|
||||||
return [ref_to_dict(item) for item in obj]
|
return [ref_to_dict(item) for item in obj]
|
||||||
|
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
cls.cached_jsonschema = cast(dict[str, Any], ref_to_dict(model))
|
cls.cached_jsonschema = cast(dict[str, Any], ref_to_dict(model))
|
||||||
|
|
|
@ -18,7 +18,7 @@ import {
|
||||||
BlockIONumberSubSchema,
|
BlockIONumberSubSchema,
|
||||||
BlockIOBooleanSubSchema,
|
BlockIOBooleanSubSchema,
|
||||||
} from "@/lib/autogpt-server-api/types";
|
} from "@/lib/autogpt-server-api/types";
|
||||||
import React, { FC, useCallback, useEffect, useState } from "react";
|
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
import { Switch } from "./ui/switch";
|
import { Switch } from "./ui/switch";
|
||||||
import {
|
import {
|
||||||
|
@ -326,13 +326,26 @@ export const NodeGenericInputField: FC<{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("oneOf" in propSchema) {
|
if (
|
||||||
// At the time of writing, this isn't used in the backend -> no impl. needed
|
"oneOf" in propSchema &&
|
||||||
console.error(
|
propSchema.oneOf &&
|
||||||
`Unsupported 'oneOf' in schema for '${propKey}'!`,
|
"discriminator" in propSchema &&
|
||||||
propSchema,
|
propSchema.discriminator
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<NodeOneOfDiscriminatorField
|
||||||
|
nodeId={nodeId}
|
||||||
|
propKey={propKey}
|
||||||
|
propSchema={propSchema}
|
||||||
|
currentValue={currentValue}
|
||||||
|
errors={errors}
|
||||||
|
connections={connections}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
handleInputClick={handleInputClick}
|
||||||
|
className={className}
|
||||||
|
displayName={displayName}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!("type" in propSchema)) {
|
if (!("type" in propSchema)) {
|
||||||
|
@ -451,6 +464,132 @@ export const NodeGenericInputField: FC<{
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const NodeOneOfDiscriminatorField: FC<{
|
||||||
|
nodeId: string;
|
||||||
|
propKey: string;
|
||||||
|
propSchema: any;
|
||||||
|
currentValue?: any;
|
||||||
|
errors: { [key: string]: string | undefined };
|
||||||
|
connections: any;
|
||||||
|
handleInputChange: (key: string, value: any) => void;
|
||||||
|
handleInputClick: (key: string) => void;
|
||||||
|
className?: string;
|
||||||
|
displayName?: string;
|
||||||
|
}> = ({
|
||||||
|
nodeId,
|
||||||
|
propKey,
|
||||||
|
propSchema,
|
||||||
|
currentValue,
|
||||||
|
errors,
|
||||||
|
connections,
|
||||||
|
handleInputChange,
|
||||||
|
handleInputClick,
|
||||||
|
className,
|
||||||
|
displayName,
|
||||||
|
}) => {
|
||||||
|
const discriminator = propSchema.discriminator;
|
||||||
|
|
||||||
|
const discriminatorProperty = discriminator.propertyName;
|
||||||
|
|
||||||
|
const variantOptions = useMemo(() => {
|
||||||
|
const oneOfVariants = propSchema.oneOf || [];
|
||||||
|
|
||||||
|
return oneOfVariants
|
||||||
|
.map((variant: any) => {
|
||||||
|
const variantDiscValue =
|
||||||
|
variant.properties?.[discriminatorProperty]?.const;
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: variantDiscValue,
|
||||||
|
schema: variant,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((v: any) => v.value != null);
|
||||||
|
}, [discriminatorProperty, propSchema.oneOf]);
|
||||||
|
|
||||||
|
const currentVariant = variantOptions.find(
|
||||||
|
(opt: any) => currentValue?.[discriminatorProperty] === opt.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
const [chosenType, setChosenType] = useState<string>(
|
||||||
|
currentVariant?.value || "",
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleVariantChange = (newType: string) => {
|
||||||
|
setChosenType(newType);
|
||||||
|
const chosenVariant = variantOptions.find(
|
||||||
|
(opt: any) => opt.value === newType,
|
||||||
|
);
|
||||||
|
if (chosenVariant) {
|
||||||
|
const initialValue = {
|
||||||
|
[discriminatorProperty]: newType,
|
||||||
|
};
|
||||||
|
handleInputChange(propKey, initialValue);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const chosenVariantSchema = variantOptions.find(
|
||||||
|
(opt: any) => opt.value === chosenType,
|
||||||
|
)?.schema;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={cn("flex flex-col space-y-2", className)}>
|
||||||
|
<Select value={chosenType || ""} onValueChange={handleVariantChange}>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a type..." />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{variantOptions.map((opt: any) => (
|
||||||
|
<SelectItem key={opt.value} value={opt.value}>
|
||||||
|
{beautifyString(opt.value)}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{chosenVariantSchema && (
|
||||||
|
<div className={cn(className, "w-full flex-col")}>
|
||||||
|
{Object.entries(chosenVariantSchema.properties).map(
|
||||||
|
([someKey, childSchema]) => {
|
||||||
|
if (someKey === "discriminator") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const childKey = propKey ? `${propKey}.${someKey}` : someKey;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={childKey}
|
||||||
|
className="flex w-full flex-row justify-between space-y-2"
|
||||||
|
>
|
||||||
|
<span className="mr-2 mt-3 dark:text-gray-300">
|
||||||
|
{(childSchema as BlockIOSubSchema).title ||
|
||||||
|
beautifyString(someKey)}
|
||||||
|
</span>
|
||||||
|
<NodeGenericInputField
|
||||||
|
nodeId={nodeId}
|
||||||
|
key={propKey}
|
||||||
|
propKey={childKey}
|
||||||
|
propSchema={childSchema as BlockIOSubSchema}
|
||||||
|
currentValue={
|
||||||
|
currentValue ? currentValue[someKey] : undefined
|
||||||
|
}
|
||||||
|
errors={errors}
|
||||||
|
connections={connections}
|
||||||
|
handleInputChange={handleInputChange}
|
||||||
|
handleInputClick={handleInputClick}
|
||||||
|
displayName={
|
||||||
|
chosenVariantSchema.title || beautifyString(someKey)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const NodeCredentialsInput: FC<{
|
const NodeCredentialsInput: FC<{
|
||||||
selfKey: string;
|
selfKey: string;
|
||||||
value: any;
|
value: any;
|
||||||
|
@ -849,7 +988,7 @@ const NodeStringInput: FC<{
|
||||||
placeholder={
|
placeholder={
|
||||||
schema?.placeholder || `Enter ${beautifyString(displayName)}`
|
schema?.placeholder || `Enter ${beautifyString(displayName)}`
|
||||||
}
|
}
|
||||||
className="pr-8 read-only:cursor-pointer read-only:text-gray-500 dark:text-white"
|
className="pr-8 read-only:cursor-pointer read-only:text-gray-500"
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
Loading…
Reference in New Issue