mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-26 03:11:12 +01:00
Click env var to edit AND improve input/editor ref handling
This commit is contained in:
@@ -2,15 +2,7 @@ import type { EditorView } from '@codemirror/view';
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} 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';
|
||||
@@ -80,6 +72,7 @@ export type InputProps = Pick<
|
||||
type?: 'text' | 'password';
|
||||
validate?: boolean | ((v: string) => boolean);
|
||||
wrapLines?: boolean;
|
||||
setRef?: (h: InputHandle | null) => void;
|
||||
};
|
||||
|
||||
export interface InputHandle {
|
||||
@@ -90,80 +83,86 @@ export interface InputHandle {
|
||||
dispatch: EditorView['dispatch'];
|
||||
}
|
||||
|
||||
export const Input = forwardRef<InputHandle, InputProps>(function Input({ type, ...props }, ref) {
|
||||
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} />;
|
||||
} else {
|
||||
return <BaseInput ref={ref} type={type} {...props} />;
|
||||
return <BaseInput type={type} {...props} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
|
||||
{
|
||||
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,
|
||||
size = 'md',
|
||||
stateKey,
|
||||
tint,
|
||||
type = 'text',
|
||||
validate,
|
||||
wrapLines,
|
||||
...props
|
||||
}: InputProps,
|
||||
ref,
|
||||
) {
|
||||
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,
|
||||
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 inputHandle = useMemo<InputHandle>(
|
||||
() => ({
|
||||
focus: () => {
|
||||
editorRef.current?.focus();
|
||||
},
|
||||
isFocused: () => editorRef.current?.hasFocus ?? false,
|
||||
value: () => editorRef.current?.state.doc.toString() ?? '',
|
||||
dispatch: (...args) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
editorRef.current?.dispatch(...(args as any));
|
||||
},
|
||||
selectAll() {
|
||||
const head = editorRef.current?.state.doc.length ?? 0;
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: 0, head },
|
||||
});
|
||||
editorRef.current?.focus();
|
||||
},
|
||||
}),
|
||||
[],
|
||||
);
|
||||
const initEditorRef = useCallback(
|
||||
(cm: EditorView | null) => {
|
||||
editorRef.current = cm;
|
||||
if (cm == null) {
|
||||
setRef?.(null);
|
||||
return;
|
||||
}
|
||||
const handle: InputHandle = {
|
||||
focus: () => {
|
||||
cm.focus();
|
||||
cm.dispatch({ selection: { anchor: cm.state.doc.length, head: cm.state.doc.length } });
|
||||
},
|
||||
isFocused: () => cm.hasFocus ?? false,
|
||||
value: () => cm.state.doc.toString() ?? '',
|
||||
dispatch: (...args) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
cm.dispatch(...(args as any));
|
||||
},
|
||||
selectAll() {
|
||||
cm.focus();
|
||||
|
||||
useImperativeHandle(ref, (): InputHandle => inputHandle, [inputHandle]);
|
||||
cm.dispatch({
|
||||
selection: { anchor: 0, head: cm.state.doc.length },
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
setRef?.(handle);
|
||||
},
|
||||
[setRef],
|
||||
);
|
||||
|
||||
const lastWindowFocus = useRef<number>(0);
|
||||
useEffect(() => {
|
||||
@@ -300,7 +299,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
setRef={initEditorRef}
|
||||
id={id.current}
|
||||
hideGutter
|
||||
singleLine={!multiLine}
|
||||
@@ -351,7 +350,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validateRequire(v: string) {
|
||||
return v.length > 0;
|
||||
@@ -365,8 +364,9 @@ function EncryptionInput({
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
forceUpdateKey: ogForceUpdateKey,
|
||||
setRef,
|
||||
...props
|
||||
}: Omit<InputProps, 'type'>) {
|
||||
}: InputProps) {
|
||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||
const [state, setState] = useStateWithDeps<{
|
||||
fieldType: PasswordFieldType;
|
||||
@@ -374,11 +374,19 @@ function EncryptionInput({
|
||||
security: ReturnType<typeof analyzeTemplate> | null;
|
||||
obscured: boolean;
|
||||
error: string | null;
|
||||
}>({ fieldType: 'text', value: null, security: null, obscured: true, error: null }, [
|
||||
ogForceUpdateKey,
|
||||
]);
|
||||
}>(
|
||||
{
|
||||
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) {
|
||||
@@ -392,6 +400,9 @@ function EncryptionInput({
|
||||
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.
|
||||
setRef?.(inputRef.current);
|
||||
},
|
||||
onError: (value) => {
|
||||
setState({
|
||||
@@ -406,6 +417,7 @@ function EncryptionInput({
|
||||
} else if (isEncryptionEnabled && !defaultValue) {
|
||||
// Default to encrypted field for new encrypted inputs
|
||||
setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null });
|
||||
setRef?.(inputRef.current);
|
||||
} else if (isEncryptionEnabled) {
|
||||
// Don't obscure plain text when encryption is enabled
|
||||
setState({
|
||||
@@ -424,8 +436,9 @@ function EncryptionInput({
|
||||
obscured: true,
|
||||
error: null,
|
||||
});
|
||||
setRef?.(inputRef.current);
|
||||
}
|
||||
}, [defaultValue, isEncryptionEnabled, setState, state.value]);
|
||||
}, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string, fieldType: PasswordFieldType) => {
|
||||
@@ -454,6 +467,10 @@ function EncryptionInput({
|
||||
[handleChange, state],
|
||||
);
|
||||
|
||||
const handleSetInputRef = useCallback((h: InputHandle | null) => {
|
||||
inputRef.current = h;
|
||||
}, []);
|
||||
|
||||
const handleFieldTypeChange = useCallback(
|
||||
(newFieldType: PasswordFieldType) => {
|
||||
const { value, fieldType } = state;
|
||||
@@ -563,6 +580,7 @@ function EncryptionInput({
|
||||
|
||||
return (
|
||||
<BaseInput
|
||||
setRef={handleSetInputRef}
|
||||
disableObscureToggle
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
|
||||
Reference in New Issue
Block a user