import { t } from 'i18next';
import { Calendar, SquareFunction, File } from 'lucide-react';
import React from 'react';
import { ErrorBoundary } from 'react-error-boundary';
import { ControllerRenderProps, useFormContext } from 'react-hook-form';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { FormItem, FormLabel } from '@/components/ui/form';
import { ReadMoreDescription } from '@/components/ui/read-more-description';
import { Toggle } from '@/components/ui/toggle';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { formUtils } from '@/features/pieces/lib/form-utils';
import { cn } from '@/lib/utils';
import {
PieceAuthProperty,
PieceProperty,
PropertyType,
} from '@activepieces/pieces-framework';
import {
FlowAction,
FlowTrigger,
PropertyExecutionType,
} from '@activepieces/shared';
import { ArrayPiecePropertyInInlineItemMode } from './array-property-in-inline-item-mode';
import { TextInputWithMentions } from './text-input-with-mentions';
function AutoFormFieldWrapper({
placeBeforeLabelText = false,
children,
allowDynamicValues,
propertyName,
inputName,
property,
disabled,
field,
dynamicInputModeToggled,
//we have to pass this prop, because props inside custom auth can be secret text, which means their labels will become (Connection)
isForConnectionSelect = false,
}: AutoFormFieldWrapperProps) {
const isArrayProperty =
!isPieceAuthProperty(property) && property.type === PropertyType.ARRAY;
const isAuthProperty = isForConnectionSelect || Array.isArray(property);
return (
<AutoFormFielWrapperErrorBoundary
field={field}
property={property ?? null}
dynamicInputModeToggled={dynamicInputModeToggled}
>
<FormItem className="flex flex-col gap-1">
<FormLabel className="flex items-center gap-1 ">
{placeBeforeLabelText && !dynamicInputModeToggled && children}
<div className="pt-1">
<span>
{isAuthProperty ? t('Connection') : property.displayName}
</span>{' '}
{(isAuthProperty || property.required) && (
<span className="text-destructive">*</span>
)}
</div>
{property && !isAuthProperty && (
<PropertyTypeTooltip property={property} />
)}
<span className="grow"></span>
{allowDynamicValues && (
<DynamicValueToggle
propertyName={propertyName}
inputName={inputName}
property={property}
disabled={disabled}
isToggled={dynamicInputModeToggled ?? false}
/>
)}
</FormLabel>
{dynamicInputModeToggled && !isArrayProperty && (
<TextInputWithMentions
disabled={disabled}
onChange={field.onChange}
initialValue={field.value ?? null}
/>
)}
{isArrayProperty && dynamicInputModeToggled && (
<ArrayPiecePropertyInInlineItemMode
disabled={disabled}
arrayProperties={property.properties}
inputName={inputName}
onChange={field.onChange}
value={field.value ?? null}
/>
)}
{!placeBeforeLabelText && !dynamicInputModeToggled && (
<div>{children}</div>
)}
{!isForConnectionSelect &&
!Array.isArray(property) &&
property.description && (
<ReadMoreDescription text={t(property.description)} />
)}
</FormItem>
</AutoFormFielWrapperErrorBoundary>
);
}
function AutoFormFielWrapperErrorBoundary({
children,
field,
property,
dynamicInputModeToggled,
}: AutoFormFielWrapperErrorBoundaryProps) {
return (
<ErrorBoundary
fallbackRender={() => (
<div className="text-sm flex items-center justify-between">
<div className="text-red-500">
{t('input value is invalid, please contact support')}
</div>
<Button
variant="outline"
size="sm"
onClick={() => {
navigator.clipboard.writeText(
JSON.stringify({
stringifiedValue: stringifyValue(field.value),
property,
dynamicInputModeToggled,
disabled: field.disabled,
}),
);
toast(t('Info copied to clipboard, please send it to support'), {
duration: 3000,
});
}}
>
{t('Info')}
</Button>
</div>
)}
>
{children}
</ErrorBoundary>
);
}
function getValueForInputOnDynamicToggleChange(
property: PieceProperty | PieceAuthProperty[],
newMode: PropertyExecutionType,
currentValue: unknown,
) {
const isAuthProperty = isPieceAuthProperty(property);
switch (newMode) {
case PropertyExecutionType.DYNAMIC: {
if (!isAuthProperty && property.type === PropertyType.ARRAY) {
return formUtils.getDefaultPropertyValue({
property,
dynamicInputModeToggled: true,
});
}
//to show what the selected value is for dropdowns
if (
typeof currentValue === 'string' ||
typeof currentValue === 'number'
) {
return currentValue;
}
return JSON.stringify(currentValue);
}
case PropertyExecutionType.MANUAL:
if (isAuthProperty) {
return '';
}
return formUtils.getDefaultPropertyValue({
property,
dynamicInputModeToggled: false,
});
}
}
function DynamicValueToggle({
propertyName,
inputName,
property,
disabled,
isToggled,
}: DynamicValueToggleProps) {
const form = useFormContext<FlowAction | FlowTrigger>();
function updatePropertySettings(mode: PropertyExecutionType) {
const propertySettingsForSingleProperty = {
...form.getValues().settings?.propertySettings?.[propertyName],
type: mode,
};
form.setValue(
`settings.propertySettings.${propertyName}`,
propertySettingsForSingleProperty,
);
}
function handleDynamicValueToggleChange(mode: PropertyExecutionType) {
updatePropertySettings(mode);
if (isInputNameLiteral(inputName)) {
const currentValue = form.getValues(inputName);
const newValue = getValueForInputOnDynamicToggleChange(
property,
mode,
currentValue,
);
form.setValue(inputName, newValue, {
shouldValidate: true,
});
} else {
throw new Error(
'inputName is not a member of step settings input, you might be using dynamic properties where you should not',
);
}
}
return (
<div className="flex gap-2 items-center">
<Tooltip>
<TooltipTrigger asChild>
<Toggle
pressed={isToggled}
onPressedChange={(newIsToggled) =>
handleDynamicValueToggleChange(
newIsToggled
? PropertyExecutionType.DYNAMIC
: PropertyExecutionType.MANUAL,
)
}
disabled={disabled}
>
<SquareFunction
className={cn('size-5', {
'text-foreground': isToggled,
'text-muted-foreground': !isToggled,
})}
/>
</Toggle>
</TooltipTrigger>
<TooltipContent side="top" className="bg-background">
{t('Dynamic value')}
</TooltipContent>
</Tooltip>
</div>
);
}
function PropertyTypeTooltip({ property }: { property: PieceProperty }) {
if (
property.type !== PropertyType.FILE &&
property.type !== PropertyType.DATE_TIME
) {
return null;
}
return (
<Tooltip>
<TooltipTrigger asChild>
{property.type === PropertyType.FILE ? (
<File className="w-4 h-4 stroke-foreground/55"></File>
) : (
property.type === PropertyType.DATE_TIME && (
<Calendar className="w-4 h-4 stroke-foreground/55"></Calendar>
)
)}
</TooltipTrigger>
<TooltipContent side="bottom">
<>
{property.type === PropertyType.FILE &&
t('File Input i.e a url or file passed from a previous step')}
{property.type === PropertyType.DATE_TIME &&
t('Date Input must comply with ISO 8601 format')}
</>
</TooltipContent>
</Tooltip>
);
}
function stringifyValue(value: unknown) {
try {
if (typeof value === 'string' || typeof value === 'number') {
return value;
}
return JSON.stringify(value);
} catch (e) {
return value;
}
}
AutoFormFieldWrapper.displayName = 'AutoFormFieldWrapper';
export { AutoFormFieldWrapper };
type DynamicValueToggleProps = {
propertyName: string;
inputName: string;
property: PieceProperty | PieceAuthProperty[];
disabled: boolean;
isToggled: boolean;
};
type AutoFormFieldWrapperProps = {
children: React.ReactNode;
allowDynamicValues: boolean;
propertyName: string;
hideDescription?: boolean;
placeBeforeLabelText?: boolean;
disabled: boolean;
field: ControllerRenderProps<any, string>;
inputName: string;
dynamicInputModeToggled?: boolean;
property: PieceProperty | PieceAuthProperty[];
isForConnectionSelect?: boolean;
};
type AutoFormFielWrapperErrorBoundaryProps = {
children: React.ReactNode;
field: ControllerRenderProps;
property: PieceProperty | PieceAuthProperty[] | null;
dynamicInputModeToggled?: boolean;
};
function isInputNameLiteral(
inputName: string,
): inputName is `settings.input.${string}` {
return inputName.match(/settings\.input\./) !== null;
}
function isPieceAuthProperty(
property: PieceProperty | PieceAuthProperty[],
): property is PieceAuthProperty[] {
const authPropertyTypes = [
PropertyType.SECRET_TEXT,
PropertyType.BASIC_AUTH,
PropertyType.OAUTH2,
PropertyType.CUSTOM_AUTH,
];
return (
Array.isArray(property) ||
authPropertyTypes.some((authType) => property.type === authType)
);
}