diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 37cd71eb..c3f18486 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -37,6 +37,7 @@ export interface EditorProps { onFocus?: () => void; onBlur?: () => void; onEnter?: () => void; + onKeyDown?: (e: KeyboardEvent) => void; singleLine?: boolean; wrapLines?: boolean; format?: (v: string) => string; @@ -60,6 +61,7 @@ const _Editor = forwardRef(function Editor( onChange, onFocus, onBlur, + onKeyDown, onEnter, className, singleLine, @@ -101,6 +103,12 @@ const _Editor = forwardRef(function Editor( handleBlur.current = onBlur; }, [onBlur]); + // Use ref so we can update the onChange handler without re-initializing the editor + const handleKeyDown = useRef(onKeyDown); + useEffect(() => { + handleKeyDown.current = onKeyDown; + }, [onKeyDown]); + // Update placeholder const placeholderCompartment = useRef(new Compartment()); useEffect(() => { @@ -168,6 +176,7 @@ const _Editor = forwardRef(function Editor( onChange: handleChange, onFocus: handleFocus, onBlur: handleBlur, + onKeyDown: handleKeyDown, }), ], }); @@ -242,6 +251,7 @@ function getExtensions({ onChange, onFocus, onBlur, + onKeyDown, onEnter, }: Pick & { container: HTMLDivElement | null; @@ -249,6 +259,7 @@ function getExtensions({ onFocus: MutableRefObject; onBlur: MutableRefObject; onEnter: MutableRefObject; + onKeyDown: MutableRefObject; }) { // TODO: Ensure tooltips render inside the dialog if we are in one. const parent = @@ -282,6 +293,7 @@ function getExtensions({ EditorView.domEventHandlers({ focus: onFocus.current, blur: onBlur.current, + keydown: onKeyDown.current, }), // Handle onChange diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 8afda66e..dc726cb0 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -7,8 +7,21 @@ import { Editor } from './Editor'; import { IconButton } from './IconButton'; import { HStack, VStack } from './Stacks'; -export type InputProps = Omit, 'onChange' | 'onFocus'> & - Pick & { +export type InputProps = Omit< + HTMLAttributes, + 'onChange' | 'onFocus' | 'onKeyDown' +> & + Pick< + EditorProps, + | 'contentType' + | 'useTemplating' + | 'autocomplete' + | 'forceUpdateKey' + | 'autoFocus' + | 'autoSelect' + | 'autocompleteVariables' + | 'onKeyDown' + > & { name: string; type?: 'text' | 'password'; label: string; diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index b59b8b01..c91dd9b1 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -132,14 +132,13 @@ export const PairEditor = memo(function PairEditor({ [setPairsAndSave], ); - const handleFocus = useCallback( - (pair: PairContainer) => - setPairs((pairs) => { - const isLast = pair.id === pairs[pairs.length - 1]?.id; - return isLast ? [...pairs, newPairContainer()] : pairs; - }), - [], - ); + const handleFocus = useCallback((pair: PairContainer) => { + return setPairs((pairs) => { + setForceFocusPairId(null); + const isLast = pair.id === pairs[pairs.length - 1]?.id; + return isLast ? [...pairs, newPairContainer()] : pairs; + }); + }, []); // Ensure there's always at least one pair useEffect(() => { @@ -177,10 +176,11 @@ export const PairEditor = memo(function PairEditor({ valuePlaceholder={valuePlaceholder} nameValidate={nameValidate} valueValidate={valueValidate} + showDelete={!isLast} onSubmit={handleSubmitRow} onChange={handleChange} onFocus={handleFocus} - onDelete={isLast ? undefined : handleDelete} + onDelete={handleDelete} onEnd={handleEnd} onMove={handleMove} /> @@ -199,6 +199,7 @@ type FormRowProps = { className?: string; pairContainer: PairContainer; forceFocusPairId?: string | null; + showDelete?: boolean; onMove: (id: string, side: 'above' | 'below') => void; onEnd: (id: string) => void; onChange: (pair: PairContainer) => void; @@ -236,6 +237,7 @@ const FormRow = memo(function FormRow({ onMove, onSubmit, pairContainer, + showDelete, valueAutocomplete, valuePlaceholder, valueValidate, @@ -268,6 +270,20 @@ const FormRow = memo(function FormRow({ const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]); const handleDelete = useCallback(() => onDelete?.(pairContainer), [onDelete, pairContainer]); + const handleKeyDownName = useMemo( + () => (e: KeyboardEvent) => { + if ( + e.key === 'Backspace' && + pairContainer.pair.name === '' && + pairContainer.pair.value === '' + ) { + e.preventDefault(); + onDelete?.(pairContainer); + } + }, + [pairContainer, onDelete], + ); + const [, connectDrop] = useDrop( { accept: ItemTypes.ROW, @@ -349,6 +365,7 @@ const FormRow = memo(function FormRow({ defaultValue={pairContainer.pair.name} label="Name" name="name" + onKeyDown={handleKeyDownName} onChange={handleChangeName} onFocus={handleFocus} placeholder={namePlaceholder ?? 'name'} @@ -373,13 +390,13 @@ const FormRow = memo(function FormRow({ /> diff --git a/src-web/hooks/useUpdateRequest.ts b/src-web/hooks/useUpdateRequest.ts index d6d8c38f..808e9859 100644 --- a/src-web/hooks/useUpdateRequest.ts +++ b/src-web/hooks/useUpdateRequest.ts @@ -20,9 +20,9 @@ export function useUpdateRequest(id: string | null) { const request = await getRequest(id); if (request === null) return; - const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v }; + const patchedRequest = typeof v === 'function' ? v(request) : { ...request, ...v }; queryClient.setQueryData(requestsQueryKey(request), (requests) => - (requests ?? []).map((r) => (r.id === newRequest.id ? newRequest : r)), + (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), ); }, });