Fixes for last commit

This commit is contained in:
Gregory Schier
2025-11-01 09:33:57 -07:00
parent 6ad4e7bbb5
commit 0f9975339c
6 changed files with 109 additions and 105 deletions

View File

@@ -38,14 +38,12 @@ impl<'a> DbContext<'a> {
let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare query"); let mut stmt = self.conn.prepare(sql.as_str()).expect("Failed to prepare query");
match stmt.query_row(&*params.as_params(), M::from_row) { match stmt.query_row(&*params.as_params(), M::from_row) {
Ok(result) => Ok(result), Ok(result) => Ok(result),
Err(rusqlite::Error::QueryReturnedNoRows) => { Err(rusqlite::Error::QueryReturnedNoRows) => Err(ModelNotFound(format!(
Err(ModelNotFound(format!( r#"table "{}" {} == {}"#,
r#"table "{}" {} == {}"#, M::table_name().into_iden().to_string(),
M::table_name().into_iden().to_string(), col.into_iden().to_string(),
col.into_iden().to_string(), value_debug
value_debug ))),
)))
}
Err(e) => Err(crate::error::Error::SqlError(e)), Err(e) => Err(crate::error::Error::SqlError(e)),
} }
} }
@@ -69,7 +67,7 @@ impl<'a> DbContext<'a> {
.expect("Failed to run find on DB") .expect("Failed to run find on DB")
} }
pub fn find_all<'s, M>(&self) -> crate::error::Result<Vec<M>> pub fn find_all<'s, M>(&self) -> Result<Vec<M>>
where where
M: Into<AnyModel> + Clone + UpsertModelInfo, M: Into<AnyModel> + Clone + UpsertModelInfo,
{ {
@@ -117,7 +115,7 @@ impl<'a> DbContext<'a> {
Ok(items.map(|v| v.unwrap()).collect()) Ok(items.map(|v| v.unwrap()).collect())
} }
pub fn upsert<M>(&self, model: &M, source: &UpdateSource) -> crate::error::Result<M> pub fn upsert<M>(&self, model: &M, source: &UpdateSource) -> Result<M>
where where
M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone, M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone,
{ {
@@ -139,7 +137,7 @@ impl<'a> DbContext<'a> {
other_values: Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>, other_values: Vec<(impl IntoIden + Eq, impl Into<SimpleExpr>)>,
update_columns: Vec<impl IntoIden>, update_columns: Vec<impl IntoIden>,
source: &UpdateSource, source: &UpdateSource,
) -> crate::error::Result<M> ) -> Result<M>
where where
M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone, M: Into<AnyModel> + From<AnyModel> + UpsertModelInfo + Clone,
{ {
@@ -178,7 +176,7 @@ impl<'a> DbContext<'a> {
Ok(m) Ok(m)
} }
pub(crate) fn delete<'s, M>(&self, m: &M, source: &UpdateSource) -> crate::error::Result<M> pub(crate) fn delete<'s, M>(&self, m: &M, source: &UpdateSource) -> Result<M>
where where
M: Into<AnyModel> + Clone + UpsertModelInfo, M: Into<AnyModel> + Clone + UpsertModelInfo,
{ {

View File

@@ -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 { duplicateModel, patchModel } from '@yaakapp-internal/models';
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react'; import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { createSubEnvironmentAndActivate } from '../commands/createEnvironment'; import { createSubEnvironmentAndActivate } from '../commands/createEnvironment';
import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace'; import { activeWorkspaceAtom, activeWorkspaceIdAtom } from '../hooks/useActiveWorkspace';
import { environmentsBreakdownAtom, useEnvironmentsBreakdown, } from '../hooks/useEnvironmentsBreakdown'; import {
environmentsBreakdownAtom,
useEnvironmentsBreakdown,
} from '../hooks/useEnvironmentsBreakdown';
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm'; import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
import { jotaiStore } from '../lib/jotai'; import { jotaiStore } from '../lib/jotai';
import { isBaseEnvironment } from '../lib/model_util'; import { isBaseEnvironment } from '../lib/model_util';
@@ -16,6 +19,7 @@ import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { IconTooltip } from './core/IconTooltip'; import { IconTooltip } from './core/IconTooltip';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import type { PairEditorHandle } from './core/PairEditor';
import { SplitLayout } from './core/SplitLayout'; import { SplitLayout } from './core/SplitLayout';
import type { TreeNode } from './core/tree/common'; import type { TreeNode } from './core/tree/common';
import type { TreeHandle, TreeProps } from './core/tree/Tree'; import type { TreeHandle, TreeProps } from './core/tree/Tree';
@@ -26,12 +30,12 @@ import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
interface Props { interface Props {
initialEnvironmentId: string | null; initialEnvironmentId: string | null;
addOrFocusVariable?: EnvironmentVariable; setRef?: (ref: PairEditorHandle | null) => void;
} }
type TreeModel = Environment | Workspace; type TreeModel = Environment | Workspace;
export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable }: Props) { export function EnvironmentEditDialog({ initialEnvironmentId, setRef }: Props) {
const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown(); const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>( const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironmentId ?? null, initialEnvironmentId ?? null,
@@ -75,9 +79,9 @@ export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable
</div> </div>
) : ( ) : (
<EnvironmentEditor <EnvironmentEditor
setRef={setRef}
className="pl-4 pt-3" className="pl-4 pt-3"
environment={selectedEnvironment} environment={selectedEnvironment}
addOrFocusVariable={addOrFocusVariable}
/> />
)} )}
</div> </div>

View File

@@ -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 { patchModel } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
@@ -17,7 +17,7 @@ import { BadgeButton } from './core/BadgeButton';
import { DismissibleBanner } from './core/DismissibleBanner'; import { DismissibleBanner } from './core/DismissibleBanner';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion'; import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Heading } from './core/Heading'; 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 { ensurePairId } from './core/PairEditor.util';
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator'; import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
@@ -27,10 +27,10 @@ interface Props {
environment: Environment; environment: Environment;
hideName?: boolean; hideName?: boolean;
className?: string; 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 workspaceId = environment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled(); const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({ const valueVisibility = useKeyValue<boolean>({
@@ -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 ( return (
<div <div
className={classNames( className={classNames(
@@ -188,7 +154,7 @@ export function EnvironmentEditor({ environment, hideName, className, addOrFocus
)} )}
</div> </div>
<PairOrBulkEditor <PairOrBulkEditor
setRef={initPairEditor} setRef={setRef}
className="h-full" className="h-full"
allowMultilineValues allowMultilineValues
preferenceName="environment" preferenceName="environment"
@@ -199,7 +165,7 @@ export function EnvironmentEditor({ environment, hideName, className, addOrFocus
valueAutocompleteVariables="environment" valueAutocompleteVariables="environment"
valueAutocompleteFunctions valueAutocompleteFunctions
forceUpdateKey={`${environment.id}::${forceUpdateKey}`} forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
pairs={pairs} pairs={environment.variables}
onChange={handleChange} onChange={handleChange}
stateKey={`environment.${environment.id}`} stateKey={`environment.${environment.id}`}
forcedEnvironmentId={environment.id} forcedEnvironmentId={environment.id}

View File

@@ -320,14 +320,14 @@ export function Editor({
const onClickVariable = useCallback( const onClickVariable = useCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => { async (v: WrappedEnvironmentVariable, _tagValue: string, _startPos: number) => {
editEnvironment(v.environment, { addOrFocusVariable: v.variable }); await editEnvironment(v.environment, { addOrFocusVariable: v.variable });
}, },
[], [],
); );
const onClickMissingVariable = useCallback(async (name: string) => { const onClickMissingVariable = useCallback(async (name: string) => {
const activeEnvironment = jotaiStore.get(activeEnvironmentAtom); const activeEnvironment = jotaiStore.get(activeEnvironmentAtom);
editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } }); await editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } });
}, []); }, []);
const [, { focusParamValue }] = useRequestEditor(); const [, { focusParamValue }] = useRequestEditor();

View File

@@ -131,42 +131,44 @@ function BaseInput({
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [forceUpdateKey]); const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [forceUpdateKey]);
const editorRef = useRef<EditorView | null>(null); const editorRef = useRef<EditorView | null>(null);
const skipNextFocus = useRef<boolean>(false);
const initEditorRef = useCallback( const handle = useMemo<InputHandle>(
(cm: EditorView | null) => { () => ({
editorRef.current = cm; focus: () => {
if (cm == null) { if (editorRef.current == null) return;
setRef?.(null); const anchor = editorRef.current.state.doc.length;
return; skipNextFocus.current = true;
} editorRef.current.focus();
const handle: InputHandle = { editorRef.current.dispatch({ selection: { anchor, head: anchor }, scrollIntoView: true });
focus: () => { },
cm.focus(); isFocused: () => editorRef.current?.hasFocus ?? false,
cm.dispatch({ selection: { anchor: cm.state.doc.length, head: cm.state.doc.length } }); value: () => editorRef.current?.state.doc.toString() ?? '',
}, dispatch: (...args) => {
isFocused: () => cm.hasFocus ?? false, // eslint-disable-next-line @typescript-eslint/no-explicit-any
value: () => cm.state.doc.toString() ?? '', editorRef.current?.dispatch(...(args as any));
dispatch: (...args) => { },
// eslint-disable-next-line @typescript-eslint/no-explicit-any selectAll() {
cm.dispatch(...(args as any)); if (editorRef.current == null) return;
}, editorRef.current.focus();
selectAll() { editorRef.current.dispatch({
cm.focus(); selection: { anchor: 0, head: editorRef.current.state.doc.length },
});
cm.dispatch({ },
selection: { anchor: 0, head: cm.state.doc.length }, }),
}); [],
}, );
};
const setEditorRef = useCallback(
setRef?.(handle); (h: EditorView | null) => {
}, editorRef.current = h;
[setRef], setRef?.(handle);
},
[handle, setRef],
); );
const lastWindowFocus = useRef<number>(0);
useEffect(() => { useEffect(() => {
const fn = () => (lastWindowFocus.current = Date.now()); const fn = () => (skipNextFocus.current = true);
window.addEventListener('focus', fn); window.addEventListener('focus', fn);
return () => { return () => {
window.removeEventListener('focus', fn); window.removeEventListener('focus', fn);
@@ -176,11 +178,7 @@ function BaseInput({
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
if (readOnly) return; if (readOnly) return;
// Select all text of input when it's focused to match standard browser behavior. if (!skipNextFocus.current) {
// 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) {
editorRef.current?.dispatch({ editorRef.current?.dispatch({
selection: { anchor: 0, head: editorRef.current.state.doc.length }, selection: { anchor: 0, head: editorRef.current.state.doc.length },
}); });
@@ -188,6 +186,7 @@ function BaseInput({
setFocused(true); setFocused(true);
onFocus?.(); onFocus?.();
skipNextFocus.current = false;
}, [onFocus, readOnly]); }, [onFocus, readOnly]);
const handleBlur = useCallback(async () => { const handleBlur = useCallback(async () => {
@@ -299,7 +298,7 @@ function BaseInput({
)} )}
> >
<Editor <Editor
setRef={initEditorRef} setRef={setEditorRef}
id={id.current} id={id.current}
hideGutter hideGutter
singleLine={!multiLine} singleLine={!multiLine}
@@ -402,6 +401,7 @@ function EncryptionInput({
setState({ fieldType: 'encrypted', security, value, obscured: true, error: null }); 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 // We're calling this here because we want the input to be fully initialized so the caller
// can do stuff like change the selection. // can do stuff like change the selection.
console.log('INIT FIRST');
setRef?.(inputRef.current); setRef?.(inputRef.current);
}, },
onError: (value) => { onError: (value) => {
@@ -415,10 +415,12 @@ function EncryptionInput({
}, },
}); });
} else if (isEncryptionEnabled && !defaultValue) { } else if (isEncryptionEnabled && !defaultValue) {
console.log('INIT SECOND');
// Default to encrypted field for new encrypted inputs // Default to encrypted field for new encrypted inputs
setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null }); setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null });
setRef?.(inputRef.current); requestAnimationFrame(() => setRef?.(inputRef.current));
} else if (isEncryptionEnabled) { } else if (isEncryptionEnabled) {
console.log('INIT THIRD');
// Don't obscure plain text when encryption is enabled // Don't obscure plain text when encryption is enabled
setState({ setState({
fieldType: 'text', fieldType: 'text',
@@ -427,7 +429,9 @@ function EncryptionInput({
obscured: false, obscured: false,
error: null, error: null,
}); });
requestAnimationFrame(() => setRef?.(inputRef.current));
} else { } else {
console.log('INIT FOURTH');
// Don't obscure plain text when encryption is disabled // Don't obscure plain text when encryption is disabled
setState({ setState({
fieldType: 'text', fieldType: 'text',
@@ -436,7 +440,7 @@ function EncryptionInput({
obscured: true, obscured: true,
error: null, error: null,
}); });
setRef?.(inputRef.current); requestAnimationFrame(() => setRef?.(inputRef.current));
} }
}, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]); }, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]);
@@ -467,7 +471,7 @@ function EncryptionInput({
[handleChange, state], [handleChange, state],
); );
const handleSetInputRef = useCallback((h: InputHandle | null) => { const setInputRef = useCallback((h: InputHandle | null) => {
inputRef.current = h; inputRef.current = h;
}, []); }, []);
@@ -580,7 +584,7 @@ function EncryptionInput({
return ( return (
<BaseInput <BaseInput
setRef={handleSetInputRef} setRef={setInputRef}
disableObscureToggle disableObscureToggle
autocompleteFunctions={autocompleteFunctions} autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}

View File

@@ -1,16 +1,44 @@
import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models'; import type { Environment, EnvironmentVariable } from '@yaakapp-internal/models';
import { updateModel } from '@yaakapp-internal/models';
import { openFolderSettings } from '../commands/openFolderSettings'; import { openFolderSettings } from '../commands/openFolderSettings';
import type { PairEditorHandle } from '../components/core/PairEditor';
import { ensurePairId } from '../components/core/PairEditor.util';
import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog'; import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog';
import { environmentsBreakdownAtom } from '../hooks/useEnvironmentsBreakdown';
import { toggleDialog } from './dialog'; import { toggleDialog } from './dialog';
import { jotaiStore } from './jotai';
interface Options { interface Options {
addOrFocusVariable?: EnvironmentVariable; addOrFocusVariable?: EnvironmentVariable;
} }
export function editEnvironment(environment: Environment | null, options: Options = {}) { export async function editEnvironment(
if (environment?.parentModel === 'folder' && environment.parentId != null) { initialEnvironment: Environment | null,
openFolderSettings(environment.parentId, 'variables'); options: Options = {},
) {
if (initialEnvironment?.parentModel === 'folder' && initialEnvironment.parentId != null) {
openFolderSettings(initialEnvironment.parentId, 'variables');
} else { } else {
const { addOrFocusVariable } = options;
const { baseEnvironment } = jotaiStore.get(environmentsBreakdownAtom);
let environment = initialEnvironment ?? baseEnvironment;
let focusId: string | null = null;
if (addOrFocusVariable && environment != null) {
const existing = environment.variables.find(
(v) => 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({ toggleDialog({
id: 'environment-editor', id: 'environment-editor',
noPadding: true, noPadding: true,
@@ -19,7 +47,11 @@ export function editEnvironment(environment: Environment | null, options: Option
render: () => ( render: () => (
<EnvironmentEditDialog <EnvironmentEditDialog
initialEnvironmentId={environment?.id ?? null} initialEnvironmentId={environment?.id ?? null}
addOrFocusVariable={options.addOrFocusVariable} setRef={(pairEditor: PairEditorHandle | null) => {
if (focusId) {
pairEditor?.focusValue(focusId);
}
}}
/> />
), ),
}); });