From 6ad4e7bbb5f2046fc70b688325fa655a842b8bd3 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Sat, 1 Nov 2025 08:39:07 -0700 Subject: [PATCH] Click env var to edit AND improve input/editor ref handling --- src-web/components/CommandPaletteDialog.tsx | 1 + src-web/components/Dialogs.tsx | 28 +-- src-web/components/EnvironmentEditDialog.tsx | 23 ++- src-web/components/EnvironmentEditor.tsx | 65 +++++-- src-web/components/GrpcEditor.tsx | 7 +- src-web/components/Overlay.tsx | 14 -- src-web/components/Sidebar.tsx | 5 +- src-web/components/UrlBar.tsx | 8 +- src-web/components/UrlParameterEditor.tsx | 18 +- src-web/components/core/Editor/Editor.tsx | 107 +++++------ src-web/components/core/Editor/LazyEditor.tsx | 9 +- src-web/components/core/Input.tsx | 172 ++++++++++-------- src-web/components/core/PairEditor.tsx | 160 +++++++++------- src-web/components/core/PairOrBulkEditor.tsx | 12 +- src-web/components/core/tree/TreeItem.tsx | 8 +- src-web/components/core/tree/TreeItemList.tsx | 2 +- src-web/components/graphql/GraphQLEditor.tsx | 5 +- src-web/lib/dialog.ts | 15 +- src-web/lib/editEnvironment.tsx | 15 +- 19 files changed, 372 insertions(+), 302 deletions(-) diff --git a/src-web/components/CommandPaletteDialog.tsx b/src-web/components/CommandPaletteDialog.tsx index 48237530..806d278d 100644 --- a/src-web/components/CommandPaletteDialog.tsx +++ b/src-web/components/CommandPaletteDialog.tsx @@ -403,6 +403,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
diff --git a/src-web/components/Dialogs.tsx b/src-web/components/Dialogs.tsx index b96e0f83..8c2a99ec 100644 --- a/src-web/components/Dialogs.tsx +++ b/src-web/components/Dialogs.tsx @@ -1,12 +1,13 @@ import { useAtomValue } from 'jotai'; -import React from 'react'; +import type { ComponentType } from 'react'; +import React, { useCallback } from 'react'; import { dialogsAtom, hideDialog } from '../lib/dialog'; import { Dialog, type DialogProps } from './core/Dialog'; import { ErrorBoundary } from './ErrorBoundary'; export type DialogInstance = { id: string; - render: ({ hide }: { hide: () => void }) => React.ReactNode; + render: ComponentType<{ hide: () => void }>; } & Omit; export function Dialogs() { @@ -20,19 +21,20 @@ export function Dialogs() { ); } -function DialogInstance({ render, onClose, id, ...props }: DialogInstance) { - const children = render({ hide: () => hideDialog(id) }); +function DialogInstance({ render: Component, onClose, id, ...props }: DialogInstance) { + const hide = useCallback(() => { + hideDialog(id); + }, [id]); + + const handleClose = useCallback(() => { + onClose?.(); + hideDialog(id); + }, [id, onClose]); + return ( - { - onClose?.(); - hideDialog(id); - }} - {...props} - > - {children} + + ); diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index c417b6fc..70dc243d 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -1,13 +1,10 @@ -import type { Environment, Workspace } from '@yaakapp-internal/models'; +import type { Environment, EnvironmentVariable, Workspace } from '@yaakapp-internal/models'; import { duplicateModel, patchModel } from '@yaakapp-internal/models'; import { atom, useAtomValue } from 'jotai'; import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import { createSubEnvironmentAndActivate } from '../commands/createEnvironment'; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; -import { - environmentsBreakdownAtom, - useEnvironmentsBreakdown, -} from '../hooks/useEnvironmentsBreakdown'; +import { environmentsBreakdownAtom, useEnvironmentsBreakdown, } from '../hooks/useEnvironmentsBreakdown'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { jotaiStore } from '../lib/jotai'; import { isBaseEnvironment } from '../lib/model_util'; @@ -28,15 +25,16 @@ import { EnvironmentEditor } from './EnvironmentEditor'; import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; interface Props { - initialEnvironment: Environment | null; + initialEnvironmentId: string | null; + addOrFocusVariable?: EnvironmentVariable; } type TreeModel = Environment | Workspace; -export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { +export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable }: Props) { const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown(); const [selectedEnvironmentId, setSelectedEnvironmentId] = useState( - initialEnvironment?.id ?? null, + initialEnvironmentId ?? null, ); const selectedEnvironment = @@ -76,16 +74,21 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
) : ( - + )}
)} /> ); -}; +} const sharableTooltip = ( ({ @@ -97,11 +96,54 @@ export function EnvironmentEditor({ }); }; + const { pairs, autoFocusValue } = useMemo<{ + pairs: Pair[]; + autoFocusValue?: string; + }>(() => { + if (addOrFocusVariable != null) { + const existing = environment.variables.find( + (v) => v.id === addOrFocusVariable.id || v.name === addOrFocusVariable.name, + ); + if (existing) { + return { + pairs: environment.variables, + autoFocusValue: existing.id, + }; + } else { + const newPair = ensurePairId(addOrFocusVariable); + return { + pairs: [...environment.variables, newPair], + autoFocusValue: newPair.id, + }; + } + } else { + return { pairs: environment.variables }; + } + }, [addOrFocusVariable, environment.variables]); + + const initPairEditor = useCallback( + (n: PairEditorHandle | null) => { + if (n && autoFocusValue) { + n.focusValue(autoFocusValue); + } + }, + [autoFocusValue], + ); + return ( -
+
- + {!hideName &&
{environment?.name}
} {isEncryptionEnabled ? ( !allVariableAreEncrypted ? ( @@ -146,6 +188,7 @@ export function EnvironmentEditor({ )}
(null); + const handleInitEditorViewRef = useCallback((h: EditorView | null) => { + editorViewRef.current = h; + }, []); // Find the schema for the selected service and method and update the editor useEffect(() => { @@ -167,6 +170,7 @@ export function GrpcEditor({ return (
- // Doing this explicitly seems to work better than the default behavior for some reason - containerRef.current?.querySelector( - [ - 'a[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'button:not([disabled])', - '[tabindex]:not([tabindex="-1"])', - '[contenteditable]:not([contenteditable="false"])', - ].join(', '), - ) ?? false, }} > (null); const treeRef = useRef(null); const filterRef = useRef(null); + const setFilterRef = useCallback((h: InputHandle | null) => { + filterRef.current = h; + }, []); const allHidden = useMemo(() => { if (tree?.children?.length === 0) return false; else if (filterText) return tree?.children?.every((c) => c.hidden); @@ -434,7 +437,7 @@ function Sidebar({ className }: { className?: string }) { <> (null); const [isFocused, setIsFocused] = useState(false); + const handleInitInputRef = useCallback((h: InputHandle | null) => { + inputRef.current = h; + }, []); + useHotKey('url_bar.focus', () => { inputRef.current?.selectAll(); }); @@ -59,7 +63,7 @@ export const UrlBar = memo(function UrlBar({ return (
(null); + const pairEditorRef = useRef(null); + const handleInitPairEditorRef = useCallback((ref: PairEditorHandle) => { + return (pairEditorRef.current = ref); + }, []); + const [{ urlParametersKey }] = useRequestEditor(); useRequestEditorEvent( 'request_params.focus_value', (name) => { - const pairIndex = pairs.findIndex((p) => p.name === name); - if (pairIndex >= 0) { - pairEditor.current?.focusValue(pairIndex); + const pair = pairs.find((p) => p.name === name); + if (pair?.id != null) { + pairEditorRef.current?.focusValue(pair.id); } else { console.log(`Couldn't find pair to focus`, { name, pairs }); } @@ -32,7 +36,7 @@ export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey return ( void; } const stateFields = { history: historyField, folds: foldState }; @@ -104,41 +103,39 @@ const stateFields = { history: historyField, folds: foldState }; const emptyVariables: WrappedEnvironmentVariable[] = []; const emptyExtension: Extension = []; -export const Editor = forwardRef(function Editor( - { - actions, - autoFocus, - autoSelect, - autocomplete, - autocompleteFunctions, - autocompleteVariables, - className, - defaultValue, - disableTabIndent, - disabled, - extraExtensions, - forcedEnvironmentId, - forceUpdateKey: forceUpdateKeyFromAbove, - format, - heightMode, - hideGutter, - graphQLSchema, - language, - onBlur, - onChange, - onFocus, - onKeyDown, - onPaste, - onPasteOverwrite, - placeholder, - readOnly, - singleLine, - stateKey, - type, - wrapLines, - }: EditorProps, - ref, -) { +export function Editor({ + actions, + autoFocus, + autoSelect, + autocomplete, + autocompleteFunctions, + autocompleteVariables, + className, + defaultValue, + disableTabIndent, + disabled, + extraExtensions, + forcedEnvironmentId, + forceUpdateKey: forceUpdateKeyFromAbove, + format, + heightMode, + hideGutter, + graphQLSchema, + language, + onBlur, + onChange, + onFocus, + onKeyDown, + onPaste, + onPasteOverwrite, + placeholder, + readOnly, + singleLine, + stateKey, + type, + wrapLines, + setRef, +}: EditorProps) { const settings = useAtomValue(settingsAtom); const allEnvironmentVariables = useEnvironmentVariables(forcedEnvironmentId ?? null); @@ -182,7 +179,6 @@ export const Editor = forwardRef(function E } const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); - useImperativeHandle(ref, () => cm.current?.view, []); // Use ref so we can update the handler without re-initializing the editor const handleChange = useRef(onChange); @@ -324,33 +320,15 @@ export const Editor = forwardRef(function E const onClickVariable = useCallback( // eslint-disable-next-line @typescript-eslint/no-unused-vars async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => { - editEnvironment(v.environment); + editEnvironment(v.environment, { addOrFocusVariable: v.variable }); }, [], ); - const onClickMissingVariable = useCallback( - async (_name: string, tagValue: string, startPos: number) => { - const initialTokens = parseTemplate(tagValue); - showDialog({ - size: 'dynamic', - id: 'template-variable', - title: 'Configure Variable', - render: ({ hide }) => ( - { - cm.current?.view.dispatch({ - changes: [{ from: startPos, to: startPos + tagValue.length, insert }], - }); - }} - /> - ), - }); - }, - [], - ); + const onClickMissingVariable = useCallback(async (name: string) => { + const activeEnvironment = jotaiStore.get(activeEnvironmentAtom); + editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } }); + }, []); const [, { focusParamValue }] = useRequestEditor(); const onClickPathParameter = useCallback( @@ -469,6 +447,7 @@ export const Editor = forwardRef(function E if (autoSelect) { view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); } + setRef?.(view); } catch (e) { console.log('Failed to initialize Codemirror', e); } @@ -588,7 +567,7 @@ export const Editor = forwardRef(function E )}
); -}); +} function getExtensions({ stateKey, diff --git a/src-web/components/core/Editor/LazyEditor.tsx b/src-web/components/core/Editor/LazyEditor.tsx index 5b6adf31..c2ae1ab1 100644 --- a/src-web/components/core/Editor/LazyEditor.tsx +++ b/src-web/components/core/Editor/LazyEditor.tsx @@ -1,13 +1,12 @@ -import type { EditorView } from '@codemirror/view'; -import { forwardRef, lazy, Suspense } from 'react'; +import { lazy, Suspense } from 'react'; import type { EditorProps } from './Editor'; const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor }))); -export const Editor = forwardRef(function LazyEditor(props, ref) { +export function Editor(props: EditorProps) { return ( - + ); -}); +} diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 67e6e81e..3a0d85bc 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -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(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 ; } else { - return ; + return ; } -}); +} -const BaseInput = forwardRef(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(false, [forceUpdateKey]); const editorRef = useRef(null); - const inputHandle = useMemo( - () => ({ - 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(0); useEffect(() => { @@ -300,7 +299,7 @@ const BaseInput = forwardRef(function InputBase( )} > (function InputBase(
); -}); +} function validateRequire(v: string) { return v.length > 0; @@ -365,8 +364,9 @@ function EncryptionInput({ autocompleteFunctions, autocompleteVariables, forceUpdateKey: ogForceUpdateKey, + setRef, ...props -}: Omit) { +}: InputProps) { const isEncryptionEnabled = useIsEncryptionEnabled(); const [state, setState] = useStateWithDeps<{ fieldType: PasswordFieldType; @@ -374,11 +374,19 @@ function EncryptionInput({ security: ReturnType | 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(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 ( void; pairs: Pair[]; stateKey: InputProps['stateKey']; + setRef?: (n: PairEditorHandle) => void; valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined; valueAutocompleteFunctions?: boolean; valueAutocompleteVariables?: boolean | 'environment'; @@ -89,33 +82,29 @@ export type PairWithId = Pair & { /** Max number of pairs to show before prompting the user to reveal the rest */ const MAX_INITIAL_PAIRS = 50; -export const PairEditor = forwardRef(function PairEditor( - { - allowFileValues, - allowMultilineValues, - className, - forcedEnvironmentId, - forceUpdateKey, - nameAutocomplete, - nameAutocompleteFunctions, - nameAutocompleteVariables, - namePlaceholder, - nameValidate, - noScroll, - onChange, - pairs: originalPairs, - stateKey, - valueAutocomplete, - valueAutocompleteFunctions, - valueAutocompleteVariables, - valuePlaceholder, - valueType, - valueValidate, - }: PairEditorProps, - ref, -) { - const [forceFocusNamePairId, setForceFocusNamePairId] = useState(null); - const [forceFocusValuePairId, setForceFocusValuePairId] = useState(null); +export function PairEditor({ + allowFileValues, + allowMultilineValues, + className, + forcedEnvironmentId, + forceUpdateKey, + nameAutocomplete, + nameAutocompleteFunctions, + nameAutocompleteVariables, + namePlaceholder, + nameValidate, + noScroll, + onChange, + pairs: originalPairs, + stateKey, + valueAutocomplete, + valueAutocompleteFunctions, + valueAutocompleteVariables, + valuePlaceholder, + valueType, + valueValidate, + setRef, +}: PairEditorProps) { const [hoveredIndex, setHoveredIndex] = useState(null); const [isDragging, setIsDragging] = useState(null); const [pairs, setPairs] = useState([]); @@ -124,15 +113,30 @@ export const PairEditor = forwardRef(function Pa // we simply pass forceUpdateKey to the editor, the data set by useEffect will be stale. const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey(); - useImperativeHandle( - ref, + const rowsRef = useRef>({}); + + const handle = useMemo( () => ({ - focusValue(index: number) { - const id = pairs[index]?.id ?? 'n/a'; - setForceFocusValuePairId(id); + focusName(id: string) { + rowsRef.current[id]?.focusName(); + }, + focusValue(id: string) { + rowsRef.current[id]?.focusValue(); }, }), - [pairs], + [], + ); + + const initPairEditorRow = useCallback( + (id: string, n: RowHandle | null) => { + rowsRef.current[id] = n; + const ready = + Object.values(rowsRef.current).filter((v) => v != null).length === pairs.length - 1; // Ignore the last placeholder pair + if (ready) { + setRef?.(handle); + } + }, + [handle, pairs.length, setRef], ); useEffect(() => { @@ -179,21 +183,19 @@ export const PairEditor = forwardRef(function Pa if (focusPrevious) { const index = pairs.findIndex((p) => p.id === pair.id); const id = pairs[index - 1]?.id ?? null; - setForceFocusNamePairId(id); + rowsRef.current[id ?? 'n/a']?.focusName(); } return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id)); }, - [setPairsAndSave, setForceFocusNamePairId, pairs], + [setPairsAndSave, pairs], ); const handleFocusName = useCallback((pair: Pair) => { - setForceFocusNamePairId(null); // Remove focus override when something focused - setForceFocusValuePairId(null); // Remove focus override when something focused setPairs((pairs) => { const isLast = pair.id === pairs[pairs.length - 1]?.id; if (isLast) { const prevPair = pairs[pairs.length - 1]; - setTimeout(() => setForceFocusNamePairId(prevPair?.id ?? null)); + rowsRef.current[prevPair?.id ?? 'n/a']?.focusName(); return [...pairs, emptyPair()]; } else { return pairs; @@ -202,13 +204,11 @@ export const PairEditor = forwardRef(function Pa }, []); const handleFocusValue = useCallback((pair: Pair) => { - setForceFocusNamePairId(null); // Remove focus override when something focused - setForceFocusValuePairId(null); // Remove focus override when something focused setPairs((pairs) => { const isLast = pair.id === pairs[pairs.length - 1]?.id; if (isLast) { const prevPair = pairs[pairs.length - 1]; - setTimeout(() => setForceFocusValuePairId(prevPair?.id ?? null)); + rowsRef.current[prevPair?.id ?? 'n/a']?.focusValue(); return [...pairs, emptyPair()]; } else { return pairs; @@ -301,12 +301,11 @@ export const PairEditor = forwardRef(function Pa {hoveredIndex === i && } (function Pa
); -}); +} type PairEditorRowProps = { className?: string; @@ -369,6 +368,7 @@ type PairEditorRowProps = { disableDrag?: boolean; index: number; isDraggingGlobal?: boolean; + setRef?: (id: string, n: RowHandle | null) => void; } & Pick< PairEditorProps, | 'allowFileValues' @@ -389,14 +389,17 @@ type PairEditorRowProps = { | 'valueValidate' >; +interface RowHandle { + focusName(): void; + focusValue(): void; +} + export function PairEditorRow({ allowFileValues, allowMultilineValues, className, disableDrag, disabled, - forceFocusNamePairId, - forceFocusValuePairId, forceUpdateKey, forcedEnvironmentId, index, @@ -419,21 +422,38 @@ export function PairEditorRow({ valuePlaceholder, valueType, valueValidate, + setRef, }: PairEditorRowProps) { const nameInputRef = useRef(null); const valueInputRef = useRef(null); - - useEffect(() => { - if (forceFocusNamePairId === pair.id) { + const handle = useRef({ + focusName() { nameInputRef.current?.focus(); - } - }, [forceFocusNamePairId, pair.id]); - - useEffect(() => { - if (forceFocusValuePairId === pair.id) { + }, + focusValue() { valueInputRef.current?.focus(); - } - }, [forceFocusValuePairId, pair.id]); + }, + }); + + const initNameInputRef = useCallback( + (n: InputHandle | null) => { + nameInputRef.current = n; + if (nameInputRef.current && valueInputRef.current) { + setRef?.(pair.id, handle.current); + } + }, + [pair.id, setRef], + ); + + const initValueInputRef = useCallback( + (n: InputHandle | null) => { + valueInputRef.current = n; + if (nameInputRef.current && valueInputRef.current) { + setRef?.(pair.id, handle.current); + } + }, + [pair.id, setRef], + ); const handleFocusName = useCallback(() => onFocusName?.(pair), [onFocusName, pair]); const handleFocusValue = useCallback(() => onFocusValue?.(pair), [onFocusValue, pair]); @@ -574,7 +594,7 @@ export function PairEditorRow({ /> ) : ( ) : ( (function PairOrBulkEditor( - { preferenceName, ...props }: Props, - ref, -) { +export function PairOrBulkEditor({ preferenceName, ...props }: Props) { const { value: useBulk, set: setUseBulk } = useKeyValue({ namespace: 'global', key: ['bulk_edit', preferenceName], @@ -23,7 +19,7 @@ export const PairOrBulkEditor = forwardRef(function PairOr return (
- {useBulk ? : } + {useBulk ? : }
(function PairOr
); -}); +} diff --git a/src-web/components/core/tree/TreeItem.tsx b/src-web/components/core/tree/TreeItem.tsx index 043350c9..8b41e2c3 100644 --- a/src-web/components/core/tree/TreeItem.tsx +++ b/src-web/components/core/tree/TreeItem.tsx @@ -31,7 +31,7 @@ export type TreeItemProps = Pick< onClick?: (item: T, e: TreeItemClickEvent) => void; getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise; depth: number; - addRef?: (item: T, n: TreeItemHandle | null) => void; + setRef?: (item: T, n: TreeItemHandle | null) => void; }; export interface TreeItemHandle { @@ -54,7 +54,7 @@ function TreeItem_({ getEditOptions, className, depth, - addRef, + setRef, }: TreeItemProps) { const listItemRef = useRef(null); const draggableRef = useRef(null); @@ -86,8 +86,8 @@ function TreeItem_({ ); useEffect(() => { - addRef?.(node.item, handle); - }, [addRef, handle, node.item]); + setRef?.(node.item, handle); + }, [setRef, handle, node.item]); const ancestorIds = useMemo(() => { const ids: string[] = []; diff --git a/src-web/components/core/tree/TreeItemList.tsx b/src-web/components/core/tree/TreeItemList.tsx index d2db7257..92c5f3d0 100644 --- a/src-web/components/core/tree/TreeItemList.tsx +++ b/src-web/components/core/tree/TreeItemList.tsx @@ -35,7 +35,7 @@ export function TreeItemList({ & }; export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) { - const editorViewRef = useRef(null); const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage< Record >('graphQLAutoIntrospectDisabled', {}); @@ -199,7 +197,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr defaultValue={currentBody.query} onChange={handleChangeQuery} placeholder="..." - ref={editorViewRef} actions={actions} stateKey={'graphql_body.' + request.id} {...extraEditorProps} diff --git a/src-web/lib/dialog.ts b/src-web/lib/dialog.ts index 3158b210..f90e38e8 100644 --- a/src-web/lib/dialog.ts +++ b/src-web/lib/dialog.ts @@ -4,14 +4,17 @@ import { jotaiStore } from './jotai'; export const dialogsAtom = atom([]); -export function showDialog({ id, ...props }: DialogInstance) { - jotaiStore.set(dialogsAtom, (a) => [...a.filter((d) => d.id !== id), { id, ...props }]); -} - export function toggleDialog({ id, ...props }: DialogInstance) { const dialogs = jotaiStore.get(dialogsAtom); - if (dialogs.some((d) => d.id === id)) hideDialog(id); - else showDialog({ id, ...props }); + if (dialogs.some((d) => d.id === id)) { + hideDialog(id); + } else { + showDialog({ id, ...props }); + } +} + +export function showDialog({ id, ...props }: DialogInstance) { + jotaiStore.set(dialogsAtom, (a) => [...a.filter((d) => d.id !== id), { id, ...props }]); } export function hideDialog(id: string) { diff --git a/src-web/lib/editEnvironment.tsx b/src-web/lib/editEnvironment.tsx index 4be717cb..81c5e4fb 100644 --- a/src-web/lib/editEnvironment.tsx +++ b/src-web/lib/editEnvironment.tsx @@ -1,9 +1,13 @@ -import type { Environment } from '@yaakapp-internal/models'; +import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models'; import { openFolderSettings } from '../commands/openFolderSettings'; import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog'; import { toggleDialog } from './dialog'; -export function editEnvironment(environment: Environment | null) { +interface Options { + addOrFocusVariable?: EnvironmentVariable; +} + +export function editEnvironment(environment: Environment | null, options: Options = {}) { if (environment?.parentModel === 'folder' && environment.parentId != null) { openFolderSettings(environment.parentId, 'variables'); } else { @@ -12,7 +16,12 @@ export function editEnvironment(environment: Environment | null) { noPadding: true, size: 'lg', className: 'h-[80vh]', - render: () => , + render: () => ( + + ), }); } }