mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-15 08:02:08 +02:00
Adds a chevron on pair editor name and value inputs that opens the same options as inline autocomplete, so suggestions can be found by clicking instead of only by typing. Long values like User-Agent show a short label but insert the full string. Row action menus move to a vertical ellipsis so the chevron only ever means "pick a value into this field". Also restores the per-worktree Tauri config handling in run-dev.mjs, which a merge on the branch had reverted.
678 lines
20 KiB
TypeScript
678 lines
20 KiB
TypeScript
import type { EditorView } from "@codemirror/view";
|
|
import type { Color } from "@yaakapp-internal/plugins";
|
|
import { HStack, Icon, type IconProps } from "@yaakapp-internal/ui";
|
|
import classNames from "classnames";
|
|
import type { ReactNode } from "react";
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
import { createFastMutation } from "../../hooks/useFastMutation";
|
|
import { useIsEncryptionEnabled } from "../../hooks/useIsEncryptionEnabled";
|
|
import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
|
import { copyToClipboard } from "../../lib/copy";
|
|
import {
|
|
analyzeTemplate,
|
|
convertTemplateToInsecure,
|
|
convertTemplateToSecure,
|
|
} from "../../lib/encryption";
|
|
import { generateId } from "../../lib/generateId";
|
|
import {
|
|
setupOrConfigureEncryption,
|
|
withEncryptionEnabled,
|
|
} from "../../lib/setupOrConfigureEncryption";
|
|
import { Button } from "./Button";
|
|
import type { DropdownItem } from "./Dropdown";
|
|
import { Dropdown } from "./Dropdown";
|
|
import type { EditorProps } from "./Editor/Editor";
|
|
import type { GenericCompletion } from "./Editor/genericCompletion";
|
|
import { Editor } from "./Editor/LazyEditor";
|
|
import { IconButton } from "./IconButton";
|
|
import { IconTooltip } from "./IconTooltip";
|
|
import { Label } from "./Label";
|
|
|
|
export type InputProps = Pick<
|
|
EditorProps,
|
|
| "language"
|
|
| "autocomplete"
|
|
| "forcedEnvironmentId"
|
|
| "forceUpdateKey"
|
|
| "disabled"
|
|
| "autoFocus"
|
|
| "autoSelect"
|
|
| "autocompleteVariables"
|
|
| "autocompleteFunctions"
|
|
| "onKeyDown"
|
|
| "readOnly"
|
|
> & {
|
|
className?: string;
|
|
containerClassName?: string;
|
|
inputWrapperClassName?: string;
|
|
defaultValue?: string | null;
|
|
disableObscureToggle?: boolean;
|
|
fullHeight?: boolean;
|
|
hideLabel?: boolean;
|
|
help?: ReactNode;
|
|
label: ReactNode;
|
|
labelClassName?: string;
|
|
labelPosition?: "top" | "left";
|
|
leftSlot?: ReactNode;
|
|
multiLine?: boolean;
|
|
name?: string;
|
|
onBlur?: () => void;
|
|
onChange?: (value: string) => void;
|
|
onFocus?: () => void;
|
|
onPaste?: (value: string) => void;
|
|
onPasteOverwrite?: EditorProps["onPasteOverwrite"];
|
|
placeholder?: string;
|
|
required?: boolean;
|
|
rightSlot?: ReactNode;
|
|
/**
|
|
* Show a chevron that opens the `autocomplete` options as a menu, so a value can be picked
|
|
* without knowing what to type. Opt-in, because plenty of inputs pass `autocomplete` without
|
|
* wanting the affordance (the URL bar completes from history, and a chevron there would be
|
|
* noise). Renders nothing when there are no options to show.
|
|
*/
|
|
showOptionsPicker?: boolean;
|
|
size?: "2xs" | "xs" | "sm" | "md" | "auto";
|
|
stateKey: EditorProps["stateKey"];
|
|
extraExtensions?: EditorProps["extraExtensions"];
|
|
tint?: Color;
|
|
type?: "text" | "password";
|
|
validate?: boolean | ((v: string) => boolean);
|
|
wrapLines?: boolean;
|
|
setRef?: (h: InputHandle | null) => void;
|
|
};
|
|
|
|
export interface InputHandle {
|
|
focus: () => void;
|
|
isFocused: () => boolean;
|
|
value: () => string;
|
|
selectAll: () => void;
|
|
dispatch: EditorView["dispatch"];
|
|
}
|
|
|
|
export function Input({ type, ...props }: InputProps) {
|
|
// If it's a password and template functions are supported (ie. secure(...)) then
|
|
// use the encrypted input component.
|
|
if (type === "password" && props.autocompleteFunctions) {
|
|
return <EncryptionInput {...props} />;
|
|
}
|
|
return <BaseInput type={type} {...props} />;
|
|
}
|
|
|
|
function BaseInput({
|
|
className,
|
|
containerClassName,
|
|
defaultValue,
|
|
disableObscureToggle,
|
|
disabled,
|
|
forceUpdateKey,
|
|
fullHeight,
|
|
help,
|
|
hideLabel,
|
|
inputWrapperClassName,
|
|
label,
|
|
labelClassName,
|
|
labelPosition = "top",
|
|
leftSlot,
|
|
multiLine,
|
|
onBlur,
|
|
onChange,
|
|
onFocus,
|
|
onPaste,
|
|
onPasteOverwrite,
|
|
placeholder,
|
|
readOnly,
|
|
required,
|
|
rightSlot,
|
|
showOptionsPicker,
|
|
size = "md",
|
|
stateKey,
|
|
tint,
|
|
type = "text",
|
|
validate,
|
|
wrapLines,
|
|
setRef,
|
|
...props
|
|
}: InputProps) {
|
|
const [focused, setFocused] = useState(false);
|
|
const [obscured, setObscured] = useStateWithDeps(type === "password", [type]);
|
|
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [forceUpdateKey]);
|
|
const editorRef = useRef<EditorView | null>(null);
|
|
const skipNextFocus = useRef<boolean>(false);
|
|
|
|
const handle = useMemo<InputHandle>(
|
|
() => ({
|
|
focus: () => {
|
|
if (editorRef.current == null) return;
|
|
const anchor = editorRef.current.state.doc.length;
|
|
skipNextFocus.current = true;
|
|
editorRef.current.focus();
|
|
editorRef.current.dispatch({ selection: { anchor, head: anchor }, scrollIntoView: true });
|
|
},
|
|
isFocused: () => editorRef.current?.hasFocus ?? false,
|
|
value: () => editorRef.current?.state.doc.toString() ?? "",
|
|
dispatch: (...args) => {
|
|
// oxlint-disable-next-line no-explicit-any
|
|
editorRef.current?.dispatch(...(args as any));
|
|
},
|
|
selectAll() {
|
|
if (editorRef.current == null) return;
|
|
editorRef.current.focus();
|
|
editorRef.current.dispatch({
|
|
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
|
});
|
|
},
|
|
}),
|
|
[],
|
|
);
|
|
|
|
// Replace the whole value with a picked option. Dispatching runs it through the editor's
|
|
// onChange, same as typing, so callers see one consistent change path.
|
|
const insertOption = useCallback((value: string) => {
|
|
const view = editorRef.current;
|
|
if (view == null) return;
|
|
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } });
|
|
view.focus();
|
|
}, []);
|
|
|
|
const pickerOptions = showOptionsPicker && !disabled ? (props.autocomplete?.options ?? []) : [];
|
|
|
|
const setEditorRef = useCallback(
|
|
(h: EditorView | null) => {
|
|
editorRef.current = h;
|
|
setRef?.(handle);
|
|
},
|
|
[handle, setRef],
|
|
);
|
|
|
|
useEffect(() => {
|
|
const fn = () => {
|
|
skipNextFocus.current = true;
|
|
};
|
|
window.addEventListener("focus", fn);
|
|
return () => {
|
|
window.removeEventListener("focus", fn);
|
|
};
|
|
}, []);
|
|
|
|
const handleFocus = useCallback(() => {
|
|
if (readOnly) return;
|
|
|
|
if (!skipNextFocus.current) {
|
|
editorRef.current?.dispatch({
|
|
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
|
});
|
|
}
|
|
|
|
setFocused(true);
|
|
onFocus?.();
|
|
skipNextFocus.current = false;
|
|
}, [onFocus, readOnly]);
|
|
|
|
const handleBlur = useCallback(async () => {
|
|
setFocused(false);
|
|
// Move selection to the end on blur
|
|
const anchor = editorRef.current?.state.doc.length ?? 0;
|
|
editorRef.current?.dispatch({
|
|
selection: { anchor, head: anchor },
|
|
});
|
|
onBlur?.();
|
|
}, [onBlur]);
|
|
|
|
const id = useRef(`input-${generateId()}`);
|
|
const editorClassName = classNames(
|
|
className,
|
|
"bg-transparent! min-w-0 h-auto w-full focus:outline-hidden placeholder:text-placeholder",
|
|
);
|
|
|
|
const isValid = useMemo(() => {
|
|
if (required && !validateRequire(defaultValue ?? "")) return false;
|
|
if (typeof validate === "boolean") return validate;
|
|
if (typeof validate === "function" && !validate(defaultValue ?? "")) return false;
|
|
return true;
|
|
}, [required, defaultValue, validate]);
|
|
|
|
const handleChange = useCallback(
|
|
(value: string) => {
|
|
onChange?.(value);
|
|
setHasChanged(true);
|
|
},
|
|
[onChange, setHasChanged],
|
|
);
|
|
|
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Submit the nearest form on Enter key press
|
|
const handleKeyDown = useCallback(
|
|
(e: KeyboardEvent) => {
|
|
if (e.key !== "Enter") return;
|
|
|
|
const form = wrapperRef.current?.closest("form");
|
|
if (!isValid || form == null) return;
|
|
|
|
form?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }));
|
|
},
|
|
[isValid],
|
|
);
|
|
|
|
return (
|
|
<div
|
|
ref={wrapperRef}
|
|
className={classNames(
|
|
"pointer-events-auto", // Just in case we're placing in disabled parent
|
|
"w-full",
|
|
fullHeight && "h-full",
|
|
labelPosition === "left" && "flex items-center gap-2",
|
|
labelPosition === "top" && "flex-row gap-0.5",
|
|
)}
|
|
>
|
|
<Label
|
|
htmlFor={id.current}
|
|
help={help}
|
|
required={required}
|
|
visuallyHidden={hideLabel}
|
|
className={classNames(labelClassName)}
|
|
>
|
|
{label}
|
|
</Label>
|
|
<HStack
|
|
alignItems="stretch"
|
|
className={classNames(
|
|
containerClassName,
|
|
fullHeight && "h-full",
|
|
"x-theme-input",
|
|
"relative w-full rounded-md text overflow-hidden",
|
|
"border",
|
|
focused && !disabled ? "border-border-focus" : "border-border",
|
|
disabled && "border-dotted",
|
|
!isValid && hasChanged && "border-danger!",
|
|
size === "md" && "min-h-md",
|
|
size === "sm" && "min-h-sm",
|
|
size === "xs" && "min-h-xs",
|
|
size === "2xs" && "min-h-2xs",
|
|
)}
|
|
>
|
|
{tint != null && (
|
|
<div
|
|
aria-hidden
|
|
className={classNames(
|
|
"absolute inset-0 opacity-5 pointer-events-none",
|
|
tint === "primary" && "bg-primary",
|
|
tint === "secondary" && "bg-secondary",
|
|
tint === "info" && "bg-info",
|
|
tint === "success" && "bg-success",
|
|
tint === "notice" && "bg-notice",
|
|
tint === "warning" && "bg-warning",
|
|
tint === "danger" && "bg-danger",
|
|
)}
|
|
/>
|
|
)}
|
|
{leftSlot}
|
|
<HStack
|
|
className={classNames(
|
|
inputWrapperClassName,
|
|
"flex-1 min-w-0 px-2",
|
|
fullHeight && "h-full",
|
|
leftSlot ? "pl-0" : null,
|
|
rightSlot ? "pr-0" : null,
|
|
)}
|
|
>
|
|
<Editor
|
|
setRef={setEditorRef}
|
|
id={id.current}
|
|
hideGutter
|
|
singleLine={!multiLine}
|
|
containerOnly
|
|
stateKey={stateKey}
|
|
wrapLines={wrapLines}
|
|
heightMode="auto"
|
|
onKeyDown={handleKeyDown}
|
|
type={type === "password" && !obscured ? "text" : type}
|
|
defaultValue={defaultValue}
|
|
forceUpdateKey={forceUpdateKey}
|
|
placeholder={placeholder}
|
|
onChange={handleChange}
|
|
onPaste={onPaste}
|
|
onPasteOverwrite={onPasteOverwrite}
|
|
disabled={disabled}
|
|
className={classNames(
|
|
editorClassName,
|
|
multiLine && size === "md" && "py-1.5",
|
|
multiLine && size === "sm" && "py-1",
|
|
multiLine && (size === "xs" || size === "2xs") && "py-0.5",
|
|
)}
|
|
onFocus={handleFocus}
|
|
onBlur={handleBlur}
|
|
readOnly={readOnly}
|
|
{...props}
|
|
/>
|
|
</HStack>
|
|
{type === "password" && !disableObscureToggle && (
|
|
<IconButton
|
|
title={
|
|
obscured
|
|
? `Show ${typeof label === "string" ? label : "field"}`
|
|
: `Obscure ${typeof label === "string" ? label : "field"}`
|
|
}
|
|
size="xs"
|
|
className={classNames("mr-0.5 h-auto! my-0.5", disabled && "opacity-disabled")}
|
|
color={tint}
|
|
// iconClassName={classNames(
|
|
// tint === 'primary' && 'text-primary',
|
|
// tint === 'secondary' && 'text-secondary',
|
|
// tint === 'info' && 'text-info',
|
|
// tint === 'success' && 'text-success',
|
|
// tint === 'notice' && 'text-notice',
|
|
// tint === 'warning' && 'text-warning',
|
|
// tint === 'danger' && 'text-danger',
|
|
// )}
|
|
iconSize="sm"
|
|
icon={obscured ? "eye" : "eye_closed"}
|
|
onClick={() => setObscured((o) => !o)}
|
|
/>
|
|
)}
|
|
{pickerOptions.length > 0 && (
|
|
<OptionsPickerDropdown options={pickerOptions} onSelect={insertOption} />
|
|
)}
|
|
{rightSlot}
|
|
</HStack>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Chevron that opens the input's autocomplete options as a menu, for browsing values rather than
|
|
* typing them. Shows `label` but inserts `apply` when set, so a long value can be listed under a
|
|
* short name (e.g. a User-Agent).
|
|
*
|
|
* Skipped in the tab order: once focus is in the input, inline autocomplete offers the same
|
|
* options, so this would only add a stop between fields. Hidden when the nearest container is
|
|
* narrow and the input needs the width more — written as a `max` query so it stays visible for
|
|
* inputs that have no container ancestor at all.
|
|
*/
|
|
function OptionsPickerDropdown({
|
|
options,
|
|
onSelect,
|
|
}: {
|
|
options: GenericCompletion[];
|
|
onSelect: (value: string) => void;
|
|
}) {
|
|
const items = useMemo<DropdownItem[]>(
|
|
() =>
|
|
options.map((o) => ({
|
|
label: o.label,
|
|
onSelect: () => onSelect(typeof o.apply === "string" ? o.apply : o.label),
|
|
})),
|
|
[options, onSelect],
|
|
);
|
|
|
|
return (
|
|
<Dropdown items={items}>
|
|
<IconButton
|
|
tabIndex={-1}
|
|
iconSize="sm"
|
|
size="xs"
|
|
icon="chevron_down"
|
|
title="Show suggestions"
|
|
className="mr-0.5 h-auto! my-0.5 text-text-subtlest @max-[24rem]:hidden"
|
|
/>
|
|
</Dropdown>
|
|
);
|
|
}
|
|
|
|
function validateRequire(v: string) {
|
|
return v.length > 0;
|
|
}
|
|
|
|
type PasswordFieldType = "text" | "encrypted";
|
|
|
|
function EncryptionInput({
|
|
defaultValue,
|
|
onChange,
|
|
autocompleteFunctions,
|
|
autocompleteVariables,
|
|
forceUpdateKey: ogForceUpdateKey,
|
|
setRef,
|
|
...props
|
|
}: InputProps) {
|
|
const isEncryptionEnabled = useIsEncryptionEnabled();
|
|
const [state, setState] = useStateWithDeps<{
|
|
fieldType: PasswordFieldType;
|
|
value: string | null;
|
|
security: ReturnType<typeof analyzeTemplate> | null;
|
|
obscured: boolean;
|
|
error: string | null;
|
|
}>(
|
|
{
|
|
fieldType: isEncryptionEnabled ? "encrypted" : "text",
|
|
value: null,
|
|
security: null,
|
|
obscured: true,
|
|
error: null,
|
|
},
|
|
[ogForceUpdateKey],
|
|
);
|
|
|
|
const forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`;
|
|
const inputRef = useRef<InputHandle>(null);
|
|
|
|
useEffect(() => {
|
|
if (state.value != null) {
|
|
// We already configured it
|
|
return;
|
|
}
|
|
|
|
const security = analyzeTemplate(defaultValue ?? "");
|
|
if (analyzeTemplate(defaultValue ?? "") === "global_secured") {
|
|
// Lazily update value to decrypted representation
|
|
templateToInsecure.mutate(defaultValue ?? "", {
|
|
onSuccess: (value) => {
|
|
setState({ fieldType: "encrypted", security, value, obscured: true, error: null });
|
|
// We're calling this here because we want the input to be fully initialized so the caller
|
|
// can do stuff like change the selection.
|
|
requestAnimationFrame(() => setRef?.(inputRef.current));
|
|
},
|
|
onError: (value) => {
|
|
setState({
|
|
fieldType: "encrypted",
|
|
security,
|
|
value: null,
|
|
error: String(value),
|
|
obscured: true,
|
|
});
|
|
},
|
|
});
|
|
} else if (isEncryptionEnabled && !defaultValue) {
|
|
// Default to encrypted field for new encrypted inputs
|
|
setState({ fieldType: "encrypted", security, value: "", obscured: true, error: null });
|
|
requestAnimationFrame(() => setRef?.(inputRef.current));
|
|
} else if (isEncryptionEnabled) {
|
|
// Don't obscure plain text when encryption is enabled
|
|
setState({
|
|
fieldType: "text",
|
|
security,
|
|
value: defaultValue ?? "",
|
|
obscured: false,
|
|
error: null,
|
|
});
|
|
requestAnimationFrame(() => setRef?.(inputRef.current));
|
|
} else {
|
|
// Don't obscure plain text when encryption is disabled
|
|
setState({
|
|
fieldType: "text",
|
|
security,
|
|
value: defaultValue ?? "",
|
|
obscured: true,
|
|
error: null,
|
|
});
|
|
requestAnimationFrame(() => setRef?.(inputRef.current));
|
|
}
|
|
}, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]);
|
|
|
|
const handleChange = useCallback(
|
|
(value: string, fieldType: PasswordFieldType) => {
|
|
if (fieldType === "encrypted") {
|
|
templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) });
|
|
} else {
|
|
onChange?.(value);
|
|
}
|
|
setState((s) => {
|
|
// We can't analyze when encrypted because we don't have the raw value, so assume it's secured
|
|
const security = fieldType === "encrypted" ? "global_secured" : analyzeTemplate(value);
|
|
// Reset obscured value when the field type is being changed
|
|
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== "text";
|
|
return { fieldType, value, security, obscured, error: s.error };
|
|
});
|
|
},
|
|
[onChange, setState],
|
|
);
|
|
|
|
const handleInputChange = useCallback(
|
|
(value: string) => {
|
|
if (state.fieldType != null) {
|
|
handleChange(value, state.fieldType);
|
|
}
|
|
},
|
|
[handleChange, state],
|
|
);
|
|
|
|
const setInputRef = useCallback((h: InputHandle | null) => {
|
|
inputRef.current = h;
|
|
}, []);
|
|
|
|
const handleFieldTypeChange = useCallback(
|
|
(newFieldType: PasswordFieldType) => {
|
|
const { value, fieldType } = state;
|
|
if (value == null || fieldType === newFieldType) {
|
|
return;
|
|
}
|
|
|
|
withEncryptionEnabled(async () => {
|
|
const newValue = await convertTemplateToInsecure(value);
|
|
handleChange(newValue, newFieldType);
|
|
});
|
|
},
|
|
[handleChange, state],
|
|
);
|
|
|
|
const dropdownItems = useMemo<DropdownItem[]>(
|
|
() => [
|
|
{
|
|
label: state.obscured ? "Show" : "Hide",
|
|
disabled: isEncryptionEnabled && state.fieldType === "text",
|
|
leftSlot: <Icon icon={state.obscured ? "eye" : "eye_closed"} />,
|
|
onSelect: () => setState((s) => ({ ...s, obscured: !s.obscured })),
|
|
},
|
|
{
|
|
label: "Copy",
|
|
leftSlot: <Icon icon="copy" />,
|
|
hidden: !state.value,
|
|
onSelect: () => copyToClipboard(state.value ?? ""),
|
|
},
|
|
{ type: "separator" },
|
|
{
|
|
label: state.fieldType === "text" ? "Encrypt Field" : "Decrypt Field",
|
|
leftSlot: <Icon icon={state.fieldType === "text" ? "lock" : "lock_open"} />,
|
|
onSelect: () => handleFieldTypeChange(state.fieldType === "text" ? "encrypted" : "text"),
|
|
},
|
|
],
|
|
[
|
|
handleFieldTypeChange,
|
|
isEncryptionEnabled,
|
|
setState,
|
|
state.fieldType,
|
|
state.obscured,
|
|
state.value,
|
|
],
|
|
);
|
|
|
|
let tint: InputProps["tint"];
|
|
if (!isEncryptionEnabled) {
|
|
tint = undefined;
|
|
} else if (state.fieldType === "encrypted") {
|
|
tint = "info";
|
|
} else if (state.security === "local_secured") {
|
|
tint = "secondary";
|
|
} else if (state.security === "insecure") {
|
|
tint = "notice";
|
|
}
|
|
|
|
const rightSlot = useMemo(() => {
|
|
let icon: IconProps["icon"];
|
|
if (isEncryptionEnabled) {
|
|
icon = state.security === "insecure" ? "shield_off" : "shield_check";
|
|
} else {
|
|
icon = state.obscured ? "eye_closed" : "eye";
|
|
}
|
|
return (
|
|
<HStack className="h-auto m-0.5">
|
|
<Dropdown items={dropdownItems}>
|
|
<Button
|
|
size="sm"
|
|
variant="border"
|
|
color={tint}
|
|
aria-label="Configure encryption"
|
|
className={classNames(
|
|
"flex items-center justify-center h-full! px-1!",
|
|
"opacity-70", // Makes it a bit subtler
|
|
props.disabled && "opacity-disabled!",
|
|
)}
|
|
>
|
|
<HStack space={0.5}>
|
|
<Icon size="sm" title="Configure encryption" icon={icon} />
|
|
<Icon size="xs" title="Configure encryption" icon="chevron_down" />
|
|
</HStack>
|
|
</Button>
|
|
</Dropdown>
|
|
</HStack>
|
|
);
|
|
}, [dropdownItems, isEncryptionEnabled, props.disabled, state.obscured, state.security, tint]);
|
|
|
|
const type = state.obscured ? "password" : "text";
|
|
|
|
if (state.error) {
|
|
return (
|
|
<Button
|
|
variant="border"
|
|
color="danger"
|
|
size={props.size}
|
|
className="text-sm"
|
|
rightSlot={<IconTooltip tabIndex={-1} content={state.error} icon="alert_triangle" />}
|
|
onClick={() => {
|
|
setupOrConfigureEncryption();
|
|
}}
|
|
>
|
|
{state.error.replace(/^Render Error: /i, "")}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<BaseInput
|
|
setRef={setInputRef}
|
|
disableObscureToggle
|
|
autocompleteFunctions={autocompleteFunctions}
|
|
autocompleteVariables={autocompleteVariables}
|
|
defaultValue={state.value ?? ""}
|
|
forceUpdateKey={forceUpdateKey}
|
|
onChange={handleInputChange}
|
|
tint={tint}
|
|
type={type}
|
|
rightSlot={rightSlot}
|
|
disabled={state.error != null}
|
|
className="pr-1.5" // To account for encryption dropdown
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const templateToSecure = createFastMutation({
|
|
mutationKey: ["template-to-secure"],
|
|
mutationFn: convertTemplateToSecure,
|
|
});
|
|
|
|
const templateToInsecure = createFastMutation({
|
|
mutationKey: ["template-to-insecure"],
|
|
mutationFn: convertTemplateToInsecure,
|
|
disableToastError: true,
|
|
});
|