diff --git a/src-tauri/yaak-models/src/db_context.rs b/src-tauri/yaak-models/src/db_context.rs index 4b24ad0e..1bf76ac1 100644 --- a/src-tauri/yaak-models/src/db_context.rs +++ b/src-tauri/yaak-models/src/db_context.rs @@ -38,14 +38,12 @@ impl<'a> DbContext<'a> { let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare query"); match stmt.query_row(&*params.as_params(), M::from_row) { Ok(result) => Ok(result), - Err(rusqlite::Error::QueryReturnedNoRows) => { - Err(ModelNotFound(format!( - r#"table "{}" {} == {}"#, - M::table_name().into_iden().to_string(), - col.into_iden().to_string(), - value_debug - ))) - } + Err(rusqlite::Error::QueryReturnedNoRows) => Err(ModelNotFound(format!( + r#"table "{}" {} == {}"#, + M::table_name().into_iden().to_string(), + col.into_iden().to_string(), + value_debug + ))), Err(e) => Err(crate::error::Error::SqlError(e)), } } @@ -69,7 +67,7 @@ impl<'a> DbContext<'a> { .expect("Failed to run find on DB") } - pub fn find_all<'s, M>(&self) -> crate::error::Result> + pub fn find_all<'s, M>(&self) -> Result> where M: Into + Clone + UpsertModelInfo, { @@ -117,7 +115,7 @@ impl<'a> DbContext<'a> { Ok(items.map(|v| v.unwrap()).collect()) } - pub fn upsert(&self, model: &M, source: &UpdateSource) -> crate::error::Result + pub fn upsert(&self, model: &M, source: &UpdateSource) -> Result where M: Into + From + UpsertModelInfo + Clone, { @@ -139,7 +137,7 @@ impl<'a> DbContext<'a> { other_values: Vec<(impl IntoIden + Eq, impl Into)>, update_columns: Vec, source: &UpdateSource, - ) -> crate::error::Result + ) -> Result where M: Into + From + UpsertModelInfo + Clone, { @@ -178,7 +176,7 @@ impl<'a> DbContext<'a> { Ok(m) } - pub(crate) fn delete<'s, M>(&self, m: &M, source: &UpdateSource) -> crate::error::Result + pub(crate) fn delete<'s, M>(&self, m: &M, source: &UpdateSource) -> Result where M: Into + Clone + UpsertModelInfo, { diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index 70dc243d..feeb9774 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -1,10 +1,13 @@ -import type { Environment, EnvironmentVariable, Workspace } from '@yaakapp-internal/models'; +import type { Environment, 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'; @@ -16,6 +19,7 @@ import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { IconTooltip } from './core/IconTooltip'; import { InlineCode } from './core/InlineCode'; +import type { PairEditorHandle } from './core/PairEditor'; import { SplitLayout } from './core/SplitLayout'; import type { TreeNode } from './core/tree/common'; import type { TreeHandle, TreeProps } from './core/tree/Tree'; @@ -26,12 +30,12 @@ import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; interface Props { initialEnvironmentId: string | null; - addOrFocusVariable?: EnvironmentVariable; + setRef?: (ref: PairEditorHandle | null) => void; } type TreeModel = Environment | Workspace; -export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable }: Props) { +export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) { const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown(); const [selectedEnvironmentId, setSelectedEnvironmentId] = useState( initialEnvironmentId ?? null, @@ -75,9 +79,9 @@ export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable ) : ( )} diff --git a/src-web/components/EnvironmentEditor.tsx b/src-web/components/EnvironmentEditor.tsx index a6dd5de4..c9799f1b 100644 --- a/src-web/components/EnvironmentEditor.tsx +++ b/src-web/components/EnvironmentEditor.tsx @@ -1,4 +1,4 @@ -import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models'; +import type { Environment } from '@yaakapp-internal/models'; import { patchModel } from '@yaakapp-internal/models'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import classNames from 'classnames'; @@ -17,7 +17,7 @@ import { BadgeButton } from './core/BadgeButton'; import { DismissibleBanner } from './core/DismissibleBanner'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import { Heading } from './core/Heading'; -import type { Pair, PairEditorHandle, PairWithId } from './core/PairEditor'; +import type { PairEditorHandle, PairWithId } from './core/PairEditor'; import { ensurePairId } from './core/PairEditor.util'; import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; @@ -27,10 +27,10 @@ interface Props { environment: Environment; hideName?: boolean; className?: string; - addOrFocusVariable?: EnvironmentVariable; + setRef?: (n: PairEditorHandle | null) => void; } -export function EnvironmentEditor({ environment, hideName, className, addOrFocusVariable }: Props) { +export function EnvironmentEditor({ environment, hideName, className, setRef }: Props) { const workspaceId = environment.workspaceId; const isEncryptionEnabled = useIsEncryptionEnabled(); const valueVisibility = useKeyValue({ @@ -96,40 +96,6 @@ export function EnvironmentEditor({ environment, hideName, className, addOrFocus }); }; - 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 (
{ - editEnvironment(v.environment, { addOrFocusVariable: v.variable }); + await editEnvironment(v.environment, { addOrFocusVariable: v.variable }); }, [], ); const onClickMissingVariable = useCallback(async (name: string) => { const activeEnvironment = jotaiStore.get(activeEnvironmentAtom); - editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } }); + await editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } }); }, []); const [, { focusParamValue }] = useRequestEditor(); diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx index 3a0d85bc..546cf9f2 100644 --- a/src-web/components/core/Input.tsx +++ b/src-web/components/core/Input.tsx @@ -131,42 +131,44 @@ function BaseInput({ const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [hasChanged, setHasChanged] = useStateWithDeps(false, [forceUpdateKey]); const editorRef = useRef(null); + const skipNextFocus = useRef(false); - 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(); - - cm.dispatch({ - selection: { anchor: 0, head: cm.state.doc.length }, - }); - }, - }; - - setRef?.(handle); - }, - [setRef], + const handle = useMemo( + () => ({ + 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) => { + // eslint-disable-next-line @typescript-eslint/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 }, + }); + }, + }), + [], + ); + + const setEditorRef = useCallback( + (h: EditorView | null) => { + editorRef.current = h; + setRef?.(handle); + }, + [handle, setRef], ); - const lastWindowFocus = useRef(0); useEffect(() => { - const fn = () => (lastWindowFocus.current = Date.now()); + const fn = () => (skipNextFocus.current = true); window.addEventListener('focus', fn); return () => { window.removeEventListener('focus', fn); @@ -176,11 +178,7 @@ function BaseInput({ const handleFocus = useCallback(() => { if (readOnly) return; - // Select all text of input when it's focused to match standard browser behavior. - // This should not, however, select when the input is focused due to a window focus event, so - // we handle that case as well. - const windowJustFocused = Date.now() - lastWindowFocus.current < 200; - if (!windowJustFocused) { + if (!skipNextFocus.current) { editorRef.current?.dispatch({ selection: { anchor: 0, head: editorRef.current.state.doc.length }, }); @@ -188,6 +186,7 @@ function BaseInput({ setFocused(true); onFocus?.(); + skipNextFocus.current = false; }, [onFocus, readOnly]); const handleBlur = useCallback(async () => { @@ -299,7 +298,7 @@ function BaseInput({ )} > { @@ -415,10 +415,12 @@ function EncryptionInput({ }, }); } else if (isEncryptionEnabled && !defaultValue) { + console.log('INIT SECOND'); // Default to encrypted field for new encrypted inputs setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null }); - setRef?.(inputRef.current); + requestAnimationFrame(() => setRef?.(inputRef.current)); } else if (isEncryptionEnabled) { + console.log('INIT THIRD'); // Don't obscure plain text when encryption is enabled setState({ fieldType: 'text', @@ -427,7 +429,9 @@ function EncryptionInput({ obscured: false, error: null, }); + requestAnimationFrame(() => setRef?.(inputRef.current)); } else { + console.log('INIT FOURTH'); // Don't obscure plain text when encryption is disabled setState({ fieldType: 'text', @@ -436,7 +440,7 @@ function EncryptionInput({ obscured: true, error: null, }); - setRef?.(inputRef.current); + requestAnimationFrame(() => setRef?.(inputRef.current)); } }, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]); @@ -467,7 +471,7 @@ function EncryptionInput({ [handleChange, state], ); - const handleSetInputRef = useCallback((h: InputHandle | null) => { + const setInputRef = useCallback((h: InputHandle | null) => { inputRef.current = h; }, []); @@ -580,7 +584,7 @@ function EncryptionInput({ return ( v.id === addOrFocusVariable.id || v.name === addOrFocusVariable.name, + ); + if (existing) { + focusId = existing.id ?? null; + } else { + const newVar = ensurePairId(addOrFocusVariable); + environment = { ...environment, variables: [...environment.variables, newVar] }; + await updateModel(environment); + environment.variables.push(newVar); + focusId = newVar.id; + } + } + toggleDialog({ id: 'environment-editor', noPadding: true, @@ -19,7 +47,11 @@ export function editEnvironment(environment: Environment | null, options: Option render: () => ( { + if (focusId) { + pairEditor?.focusValue(focusId); + } + }} /> ), });