Click env var to edit AND improve input/editor ref handling

This commit is contained in:
Gregory Schier
2025-11-01 08:39:07 -07:00
parent 2bcf67aaa6
commit 6ad4e7bbb5
19 changed files with 372 additions and 302 deletions
@@ -403,6 +403,7 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
<div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden py-2"> <div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden py-2">
<div className="px-2 w-full"> <div className="px-2 w-full">
<PlainInput <PlainInput
autoFocus
hideLabel hideLabel
leftSlot={ leftSlot={
<div className="h-md w-10 flex justify-center items-center"> <div className="h-md w-10 flex justify-center items-center">
+15 -13
View File
@@ -1,12 +1,13 @@
import { useAtomValue } from 'jotai'; 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 { dialogsAtom, hideDialog } from '../lib/dialog';
import { Dialog, type DialogProps } from './core/Dialog'; import { Dialog, type DialogProps } from './core/Dialog';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
export type DialogInstance = { export type DialogInstance = {
id: string; id: string;
render: ({ hide }: { hide: () => void }) => React.ReactNode; render: ComponentType<{ hide: () => void }>;
} & Omit<DialogProps, 'open' | 'children'>; } & Omit<DialogProps, 'open' | 'children'>;
export function Dialogs() { export function Dialogs() {
@@ -20,19 +21,20 @@ export function Dialogs() {
); );
} }
function DialogInstance({ render, onClose, id, ...props }: DialogInstance) { function DialogInstance({ render: Component, onClose, id, ...props }: DialogInstance) {
const children = render({ hide: () => hideDialog(id) }); const hide = useCallback(() => {
hideDialog(id);
}, [id]);
const handleClose = useCallback(() => {
onClose?.();
hideDialog(id);
}, [id, onClose]);
return ( return (
<ErrorBoundary name={`Dialog ${id}`}> <ErrorBoundary name={`Dialog ${id}`}>
<Dialog <Dialog open onClose={handleClose} {...props}>
open <Component hide={hide} {...props} />
onClose={() => {
onClose?.();
hideDialog(id);
}}
{...props}
>
{children}
</Dialog> </Dialog>
</ErrorBoundary> </ErrorBoundary>
); );
+13 -10
View File
@@ -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 { 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 { import { environmentsBreakdownAtom, useEnvironmentsBreakdown, } from '../hooks/useEnvironmentsBreakdown';
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';
@@ -28,15 +25,16 @@ import { EnvironmentEditor } from './EnvironmentEditor';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
interface Props { interface Props {
initialEnvironment: Environment | null; initialEnvironmentId: string | null;
addOrFocusVariable?: EnvironmentVariable;
} }
type TreeModel = Environment | Workspace; type TreeModel = Environment | Workspace;
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) { export function EnvironmentEditDialog({ initialEnvironmentId, addOrFocusVariable }: Props) {
const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown(); const { allEnvironments, baseEnvironment, baseEnvironments } = useEnvironmentsBreakdown();
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>( const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
initialEnvironment?.id ?? null, initialEnvironmentId ?? null,
); );
const selectedEnvironment = const selectedEnvironment =
@@ -76,16 +74,21 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
</Banner> </Banner>
</div> </div>
) : ( ) : (
<EnvironmentEditor className="pl-4 pt-3" environment={selectedEnvironment} /> <EnvironmentEditor
className="pl-4 pt-3"
environment={selectedEnvironment}
addOrFocusVariable={addOrFocusVariable}
/>
)} )}
</div> </div>
)} )}
/> />
); );
}; }
const sharableTooltip = ( const sharableTooltip = (
<IconTooltip <IconTooltip
tabIndex={-1}
icon="eye" icon="eye"
iconSize="sm" iconSize="sm"
content="This environment will be included in Directory Sync and data exports" content="This environment will be included in Directory Sync and data exports"
+54 -11
View File
@@ -1,4 +1,4 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment, EnvironmentVariable } 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,21 +17,20 @@ 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 { PairWithId } from './core/PairEditor'; import type { Pair, 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';
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip'; import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
export function EnvironmentEditor({ interface Props {
environment,
hideName,
className,
}: {
environment: Environment; environment: Environment;
hideName?: boolean; hideName?: boolean;
className?: string; className?: string;
}) { addOrFocusVariable?: EnvironmentVariable;
}
export function EnvironmentEditor({ environment, hideName, className, addOrFocusVariable }: Props) {
const workspaceId = environment.workspaceId; const workspaceId = environment.workspaceId;
const isEncryptionEnabled = useIsEncryptionEnabled(); const isEncryptionEnabled = useIsEncryptionEnabled();
const valueVisibility = useKeyValue<boolean>({ const valueVisibility = useKeyValue<boolean>({
@@ -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 ( return (
<div className={classNames(className, 'h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3')}> <div
className={classNames(
className,
'h-full grid grid-rows-[auto_minmax(0,1fr)] gap-2 pr-3 pb-3',
)}
>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Heading className="w-full flex items-center gap-0.5"> <Heading className="w-full flex items-center gap-0.5">
<EnvironmentColorIndicator className="mr-2" clickToEdit environment={environment ?? null} /> <EnvironmentColorIndicator
className="mr-2"
clickToEdit
environment={environment ?? null}
/>
{!hideName && <div className="mr-2">{environment?.name}</div>} {!hideName && <div className="mr-2">{environment?.name}</div>}
{isEncryptionEnabled ? ( {isEncryptionEnabled ? (
!allVariableAreEncrypted ? ( !allVariableAreEncrypted ? (
@@ -146,6 +188,7 @@ export function EnvironmentEditor({
)} )}
</div> </div>
<PairOrBulkEditor <PairOrBulkEditor
setRef={initPairEditor}
className="h-full" className="h-full"
allowMultilineValues allowMultilineValues
preferenceName="environment" preferenceName="environment"
@@ -156,7 +199,7 @@ export function EnvironmentEditor({
valueAutocompleteVariables="environment" valueAutocompleteVariables="environment"
valueAutocompleteFunctions valueAutocompleteFunctions
forceUpdateKey={`${environment.id}::${forceUpdateKey}`} forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
pairs={environment.variables} pairs={pairs}
onChange={handleChange} onChange={handleChange}
stateKey={`environment.${environment.id}`} stateKey={`environment.${environment.id}`}
forcedEnvironmentId={environment.id} forcedEnvironmentId={environment.id}
+5 -2
View File
@@ -10,7 +10,7 @@ import {
stateExtensions, stateExtensions,
updateSchema, updateSchema,
} from 'codemirror-json-schema'; } from 'codemirror-json-schema';
import { useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { showAlert } from '../lib/alert'; import { showAlert } from '../lib/alert';
import { showDialog } from '../lib/dialog'; import { showDialog } from '../lib/dialog';
@@ -40,6 +40,9 @@ export function GrpcEditor({
...extraEditorProps ...extraEditorProps
}: Props) { }: Props) {
const editorViewRef = useRef<EditorView>(null); const editorViewRef = useRef<EditorView>(null);
const handleInitEditorViewRef = useCallback((h: EditorView | null) => {
editorViewRef.current = h;
}, []);
// Find the schema for the selected service and method and update the editor // Find the schema for the selected service and method and update the editor
useEffect(() => { useEffect(() => {
@@ -167,6 +170,7 @@ export function GrpcEditor({
return ( return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]"> <div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor <Editor
setRef={handleInitEditorViewRef}
language="json" language="json"
autocompleteFunctions autocompleteFunctions
autocompleteVariables autocompleteVariables
@@ -174,7 +178,6 @@ export function GrpcEditor({
defaultValue={request.message} defaultValue={request.message}
heightMode="auto" heightMode="auto"
placeholder="..." placeholder="..."
ref={editorViewRef}
extraExtensions={extraExtensions} extraExtensions={extraExtensions}
actions={actions} actions={actions}
stateKey={`grpc_message.${request.id}`} stateKey={`grpc_message.${request.id}`}
-14
View File
@@ -53,20 +53,6 @@ export function Overlay({
<FocusTrap <FocusTrap
focusTrapOptions={{ focusTrapOptions={{
allowOutsideClick: true, // So we can still click toasts and things allowOutsideClick: true, // So we can still click toasts and things
delayInitialFocus: true,
initialFocus: () =>
// Doing this explicitly seems to work better than the default behavior for some reason
containerRef.current?.querySelector<HTMLElement>(
[
'a[href]',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'button:not([disabled])',
'[tabindex]:not([tabindex="-1"])',
'[contenteditable]:not([contenteditable="false"])',
].join(', '),
) ?? false,
}} }}
> >
<m.div <m.div
+4 -1
View File
@@ -77,6 +77,9 @@ function Sidebar({ className }: { className?: string }) {
const wrapperRef = useRef<HTMLElement>(null); const wrapperRef = useRef<HTMLElement>(null);
const treeRef = useRef<TreeHandle>(null); const treeRef = useRef<TreeHandle>(null);
const filterRef = useRef<InputHandle>(null); const filterRef = useRef<InputHandle>(null);
const setFilterRef = useCallback((h: InputHandle | null) => {
filterRef.current = h;
}, []);
const allHidden = useMemo(() => { const allHidden = useMemo(() => {
if (tree?.children?.length === 0) return false; if (tree?.children?.length === 0) return false;
else if (filterText) return tree?.children?.every((c) => c.hidden); else if (filterText) return tree?.children?.every((c) => c.hidden);
@@ -434,7 +437,7 @@ function Sidebar({ className }: { className?: string }) {
<> <>
<Input <Input
hideLabel hideLabel
ref={filterRef} setRef={setFilterRef}
size="sm" size="sm"
label="filter" label="filter"
language={null} // Explicitly disable language={null} // Explicitly disable
+6 -2
View File
@@ -1,7 +1,7 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames'; import classNames from 'classnames';
import type { FormEvent, ReactNode } from 'react'; import type { FormEvent, ReactNode } from 'react';
import { memo, useRef, useState } from 'react'; import { useCallback, memo, useRef, useState } from 'react';
import { useHotKey } from '../hooks/useHotKey'; import { useHotKey } from '../hooks/useHotKey';
import type { IconProps } from './core/Icon'; import type { IconProps } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -46,6 +46,10 @@ export const UrlBar = memo(function UrlBar({
const inputRef = useRef<InputHandle>(null); const inputRef = useRef<InputHandle>(null);
const [isFocused, setIsFocused] = useState<boolean>(false); const [isFocused, setIsFocused] = useState<boolean>(false);
const handleInitInputRef = useCallback((h: InputHandle | null) => {
inputRef.current = h;
}, []);
useHotKey('url_bar.focus', () => { useHotKey('url_bar.focus', () => {
inputRef.current?.selectAll(); inputRef.current?.selectAll();
}); });
@@ -59,7 +63,7 @@ export const UrlBar = memo(function UrlBar({
return ( return (
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}> <form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
<Input <Input
ref={inputRef} setRef={handleInitInputRef}
autocompleteFunctions autocompleteFunctions
autocompleteVariables autocompleteVariables
stateKey={stateKey} stateKey={stateKey}
+11 -7
View File
@@ -1,7 +1,7 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { useRef } from 'react'; import { useCallback, useRef } from 'react';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor'; import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import type { PairEditorProps, PairEditorRef } from './core/PairEditor'; import type { PairEditorHandle, PairEditorProps } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor'; import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
@@ -13,15 +13,19 @@ type Props = {
}; };
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey }: Props) { export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey }: Props) {
const pairEditor = useRef<PairEditorRef>(null); const pairEditorRef = useRef<PairEditorHandle>(null);
const handleInitPairEditorRef = useCallback((ref: PairEditorHandle) => {
return (pairEditorRef.current = ref);
}, []);
const [{ urlParametersKey }] = useRequestEditor(); const [{ urlParametersKey }] = useRequestEditor();
useRequestEditorEvent( useRequestEditorEvent(
'request_params.focus_value', 'request_params.focus_value',
(name) => { (name) => {
const pairIndex = pairs.findIndex((p) => p.name === name); const pair = pairs.find((p) => p.name === name);
if (pairIndex >= 0) { if (pair?.id != null) {
pairEditor.current?.focusValue(pairIndex); pairEditorRef.current?.focusValue(pair.id);
} else { } else {
console.log(`Couldn't find pair to focus`, { name, pairs }); console.log(`Couldn't find pair to focus`, { name, pairs });
} }
@@ -32,7 +36,7 @@ export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey
return ( return (
<VStack className="h-full"> <VStack className="h-full">
<PairOrBulkEditor <PairOrBulkEditor
ref={pairEditor} setRef={handleInitPairEditorRef}
allowMultilineValues allowMultilineValues
forceUpdateKey={forceUpdateKey + urlParametersKey} forceUpdateKey={forceUpdateKey + urlParametersKey}
nameAutocompleteFunctions nameAutocompleteFunctions
+43 -64
View File
@@ -17,17 +17,16 @@ import { useAtomValue } from 'jotai';
import { md5 } from 'js-md5'; import { md5 } from 'js-md5';
import type { ReactNode, RefObject } from 'react'; import type { ReactNode, RefObject } from 'react';
import { import {
useEffect,
Children, Children,
cloneElement, cloneElement,
forwardRef,
isValidElement, isValidElement,
useCallback, useCallback,
useImperativeHandle, useEffect,
useLayoutEffect, useLayoutEffect,
useMemo, useMemo,
useRef, useRef,
} from 'react'; } from 'react';
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
import { activeWorkspaceAtom } from '../../../hooks/useActiveWorkspace'; import { activeWorkspaceAtom } from '../../../hooks/useActiveWorkspace';
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables'; import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables'; import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
@@ -40,7 +39,6 @@ import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
import { jotaiStore } from '../../../lib/jotai'; import { jotaiStore } from '../../../lib/jotai';
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption'; import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog'; import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton'; import { IconButton } from '../IconButton';
import { InlineCode } from '../InlineCode'; import { InlineCode } from '../InlineCode';
import { HStack } from '../Stacks'; import { HStack } from '../Stacks';
@@ -97,6 +95,7 @@ export interface EditorProps {
tooltipContainer?: HTMLElement; tooltipContainer?: HTMLElement;
type?: 'text' | 'password'; type?: 'text' | 'password';
wrapLines?: boolean; wrapLines?: boolean;
setRef?: (view: EditorView | null) => void;
} }
const stateFields = { history: historyField, folds: foldState }; const stateFields = { history: historyField, folds: foldState };
@@ -104,41 +103,39 @@ const stateFields = { history: historyField, folds: foldState };
const emptyVariables: WrappedEnvironmentVariable[] = []; const emptyVariables: WrappedEnvironmentVariable[] = [];
const emptyExtension: Extension = []; const emptyExtension: Extension = [];
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor( export function Editor({
{ actions,
actions, autoFocus,
autoFocus, autoSelect,
autoSelect, autocomplete,
autocomplete, autocompleteFunctions,
autocompleteFunctions, autocompleteVariables,
autocompleteVariables, className,
className, defaultValue,
defaultValue, disableTabIndent,
disableTabIndent, disabled,
disabled, extraExtensions,
extraExtensions, forcedEnvironmentId,
forcedEnvironmentId, forceUpdateKey: forceUpdateKeyFromAbove,
forceUpdateKey: forceUpdateKeyFromAbove, format,
format, heightMode,
heightMode, hideGutter,
hideGutter, graphQLSchema,
graphQLSchema, language,
language, onBlur,
onBlur, onChange,
onChange, onFocus,
onFocus, onKeyDown,
onKeyDown, onPaste,
onPaste, onPasteOverwrite,
onPasteOverwrite, placeholder,
placeholder, readOnly,
readOnly, singleLine,
singleLine, stateKey,
stateKey, type,
type, wrapLines,
wrapLines, setRef,
}: EditorProps, }: EditorProps) {
ref,
) {
const settings = useAtomValue(settingsAtom); const settings = useAtomValue(settingsAtom);
const allEnvironmentVariables = useEnvironmentVariables(forcedEnvironmentId ?? null); const allEnvironmentVariables = useEnvironmentVariables(forcedEnvironmentId ?? null);
@@ -182,7 +179,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
} }
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); 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 // Use ref so we can update the handler without re-initializing the editor
const handleChange = useRef<EditorProps['onChange']>(onChange); const handleChange = useRef<EditorProps['onChange']>(onChange);
@@ -324,33 +320,15 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
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); editEnvironment(v.environment, { addOrFocusVariable: v.variable });
}, },
[], [],
); );
const onClickMissingVariable = useCallback( const onClickMissingVariable = useCallback(async (name: string) => {
async (_name: string, tagValue: string, startPos: number) => { const activeEnvironment = jotaiStore.get(activeEnvironmentAtom);
const initialTokens = parseTemplate(tagValue); editEnvironment(activeEnvironment, { addOrFocusVariable: { name, value: '', enabled: true } });
showDialog({ }, []);
size: 'dynamic',
id: 'template-variable',
title: 'Configure Variable',
render: ({ hide }) => (
<TemplateVariableDialog
hide={hide}
initialTokens={initialTokens}
onChange={(insert) => {
cm.current?.view.dispatch({
changes: [{ from: startPos, to: startPos + tagValue.length, insert }],
});
}}
/>
),
});
},
[],
);
const [, { focusParamValue }] = useRequestEditor(); const [, { focusParamValue }] = useRequestEditor();
const onClickPathParameter = useCallback( const onClickPathParameter = useCallback(
@@ -469,6 +447,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
if (autoSelect) { if (autoSelect) {
view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } }); view.dispatch({ selection: { anchor: 0, head: view.state.doc.length } });
} }
setRef?.(view);
} catch (e) { } catch (e) {
console.log('Failed to initialize Codemirror', e); console.log('Failed to initialize Codemirror', e);
} }
@@ -588,7 +567,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
)} )}
</div> </div>
); );
}); }
function getExtensions({ function getExtensions({
stateKey, stateKey,
@@ -1,13 +1,12 @@
import type { EditorView } from '@codemirror/view'; import { lazy, Suspense } from 'react';
import { forwardRef, lazy, Suspense } from 'react';
import type { EditorProps } from './Editor'; import type { EditorProps } from './Editor';
const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor }))); const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor })));
export const Editor = forwardRef<EditorView, EditorProps>(function LazyEditor(props, ref) { export function Editor(props: EditorProps) {
return ( return (
<Suspense> <Suspense>
<Editor_ ref={ref} {...props} /> <Editor_ {...props} />
</Suspense> </Suspense>
); );
}); }
+95 -77
View File
@@ -2,15 +2,7 @@ import type { EditorView } from '@codemirror/view';
import type { Color } from '@yaakapp-internal/plugins'; import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import { createFastMutation } from '../../hooks/useFastMutation'; import { createFastMutation } from '../../hooks/useFastMutation';
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled'; import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
@@ -80,6 +72,7 @@ export type InputProps = Pick<
type?: 'text' | 'password'; type?: 'text' | 'password';
validate?: boolean | ((v: string) => boolean); validate?: boolean | ((v: string) => boolean);
wrapLines?: boolean; wrapLines?: boolean;
setRef?: (h: InputHandle | null) => void;
}; };
export interface InputHandle { export interface InputHandle {
@@ -90,80 +83,86 @@ export interface InputHandle {
dispatch: EditorView['dispatch']; 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 // If it's a password and template functions are supported (ie. secure(...)) then
// use the encrypted input component. // use the encrypted input component.
if (type === 'password' && props.autocompleteFunctions) { if (type === 'password' && props.autocompleteFunctions) {
return <EncryptionInput {...props} />; return <EncryptionInput {...props} />;
} else { } else {
return <BaseInput ref={ref} type={type} {...props} />; return <BaseInput type={type} {...props} />;
} }
}); }
const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase( function BaseInput({
{ className,
className, containerClassName,
containerClassName, defaultValue,
defaultValue, disableObscureToggle,
disableObscureToggle, disabled,
disabled, forceUpdateKey,
forceUpdateKey, fullHeight,
fullHeight, help,
help, hideLabel,
hideLabel, inputWrapperClassName,
inputWrapperClassName, label,
label, labelClassName,
labelClassName, labelPosition = 'top',
labelPosition = 'top', leftSlot,
leftSlot, multiLine,
multiLine, onBlur,
onBlur, onChange,
onChange, onFocus,
onFocus, onPaste,
onPaste, onPasteOverwrite,
onPasteOverwrite, placeholder,
placeholder, readOnly,
readOnly, required,
required, rightSlot,
rightSlot, size = 'md',
size = 'md', stateKey,
stateKey, tint,
tint, type = 'text',
type = 'text', validate,
validate, wrapLines,
wrapLines, setRef,
...props ...props
}: InputProps, }: InputProps) {
ref,
) {
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
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 inputHandle = useMemo<InputHandle>( const initEditorRef = useCallback(
() => ({ (cm: EditorView | null) => {
focus: () => { editorRef.current = cm;
editorRef.current?.focus(); if (cm == null) {
}, setRef?.(null);
isFocused: () => editorRef.current?.hasFocus ?? false, return;
value: () => editorRef.current?.state.doc.toString() ?? '', }
dispatch: (...args) => { const handle: InputHandle = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any focus: () => {
editorRef.current?.dispatch(...(args as any)); cm.focus();
}, cm.dispatch({ selection: { anchor: cm.state.doc.length, head: cm.state.doc.length } });
selectAll() { },
const head = editorRef.current?.state.doc.length ?? 0; isFocused: () => cm.hasFocus ?? false,
editorRef.current?.dispatch({ value: () => cm.state.doc.toString() ?? '',
selection: { anchor: 0, head }, dispatch: (...args) => {
}); // eslint-disable-next-line @typescript-eslint/no-explicit-any
editorRef.current?.focus(); 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); const lastWindowFocus = useRef<number>(0);
useEffect(() => { useEffect(() => {
@@ -300,7 +299,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
)} )}
> >
<Editor <Editor
ref={editorRef} setRef={initEditorRef}
id={id.current} id={id.current}
hideGutter hideGutter
singleLine={!multiLine} singleLine={!multiLine}
@@ -351,7 +350,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
</HStack> </HStack>
</div> </div>
); );
}); }
function validateRequire(v: string) { function validateRequire(v: string) {
return v.length > 0; return v.length > 0;
@@ -365,8 +364,9 @@ function EncryptionInput({
autocompleteFunctions, autocompleteFunctions,
autocompleteVariables, autocompleteVariables,
forceUpdateKey: ogForceUpdateKey, forceUpdateKey: ogForceUpdateKey,
setRef,
...props ...props
}: Omit<InputProps, 'type'>) { }: InputProps) {
const isEncryptionEnabled = useIsEncryptionEnabled(); const isEncryptionEnabled = useIsEncryptionEnabled();
const [state, setState] = useStateWithDeps<{ const [state, setState] = useStateWithDeps<{
fieldType: PasswordFieldType; fieldType: PasswordFieldType;
@@ -374,11 +374,19 @@ function EncryptionInput({
security: ReturnType<typeof analyzeTemplate> | null; security: ReturnType<typeof analyzeTemplate> | null;
obscured: boolean; obscured: boolean;
error: string | null; 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 forceUpdateKey = `${ogForceUpdateKey}::${state.fieldType}::${state.value === null}`;
const inputRef = useRef<InputHandle>(null);
useEffect(() => { useEffect(() => {
if (state.value != null) { if (state.value != null) {
@@ -392,6 +400,9 @@ function EncryptionInput({
templateToInsecure.mutate(defaultValue ?? '', { templateToInsecure.mutate(defaultValue ?? '', {
onSuccess: (value) => { onSuccess: (value) => {
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
// can do stuff like change the selection.
setRef?.(inputRef.current);
}, },
onError: (value) => { onError: (value) => {
setState({ setState({
@@ -406,6 +417,7 @@ function EncryptionInput({
} else if (isEncryptionEnabled && !defaultValue) { } else if (isEncryptionEnabled && !defaultValue) {
// 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);
} else if (isEncryptionEnabled) { } else if (isEncryptionEnabled) {
// Don't obscure plain text when encryption is enabled // Don't obscure plain text when encryption is enabled
setState({ setState({
@@ -424,8 +436,9 @@ function EncryptionInput({
obscured: true, obscured: true,
error: null, error: null,
}); });
setRef?.(inputRef.current);
} }
}, [defaultValue, isEncryptionEnabled, setState, state.value]); }, [defaultValue, isEncryptionEnabled, setRef, setState, state.value]);
const handleChange = useCallback( const handleChange = useCallback(
(value: string, fieldType: PasswordFieldType) => { (value: string, fieldType: PasswordFieldType) => {
@@ -454,6 +467,10 @@ function EncryptionInput({
[handleChange, state], [handleChange, state],
); );
const handleSetInputRef = useCallback((h: InputHandle | null) => {
inputRef.current = h;
}, []);
const handleFieldTypeChange = useCallback( const handleFieldTypeChange = useCallback(
(newFieldType: PasswordFieldType) => { (newFieldType: PasswordFieldType) => {
const { value, fieldType } = state; const { value, fieldType } = state;
@@ -563,6 +580,7 @@ function EncryptionInput({
return ( return (
<BaseInput <BaseInput
setRef={handleSetInputRef}
disableObscureToggle disableObscureToggle
autocompleteFunctions={autocompleteFunctions} autocompleteFunctions={autocompleteFunctions}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}
+90 -70
View File
@@ -10,16 +10,7 @@ import {
useSensors, useSensors,
} from '@dnd-kit/core'; } from '@dnd-kit/core';
import classNames from 'classnames'; import classNames from 'classnames';
import { import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
forwardRef,
Fragment,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables'; import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../hooks/useRandomKey'; import { useRandomKey } from '../../hooks/useRandomKey';
import { useToggle } from '../../hooks/useToggle'; import { useToggle } from '../../hooks/useToggle';
@@ -45,8 +36,9 @@ import { PlainInput } from './PlainInput';
import type { RadioDropdownItem } from './RadioDropdown'; import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown'; import { RadioDropdown } from './RadioDropdown';
export interface PairEditorRef { export interface PairEditorHandle {
focusValue(index: number): void; focusName(id: string): void;
focusValue(id: string): void;
} }
export type PairEditorProps = { export type PairEditorProps = {
@@ -64,6 +56,7 @@ export type PairEditorProps = {
onChange: (pairs: PairWithId[]) => void; onChange: (pairs: PairWithId[]) => void;
pairs: Pair[]; pairs: Pair[];
stateKey: InputProps['stateKey']; stateKey: InputProps['stateKey'];
setRef?: (n: PairEditorHandle) => void;
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined; valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
valueAutocompleteFunctions?: boolean; valueAutocompleteFunctions?: boolean;
valueAutocompleteVariables?: boolean | 'environment'; 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 */ /** Max number of pairs to show before prompting the user to reveal the rest */
const MAX_INITIAL_PAIRS = 50; const MAX_INITIAL_PAIRS = 50;
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor( export function PairEditor({
{ allowFileValues,
allowFileValues, allowMultilineValues,
allowMultilineValues, className,
className, forcedEnvironmentId,
forcedEnvironmentId, forceUpdateKey,
forceUpdateKey, nameAutocomplete,
nameAutocomplete, nameAutocompleteFunctions,
nameAutocompleteFunctions, nameAutocompleteVariables,
nameAutocompleteVariables, namePlaceholder,
namePlaceholder, nameValidate,
nameValidate, noScroll,
noScroll, onChange,
onChange, pairs: originalPairs,
pairs: originalPairs, stateKey,
stateKey, valueAutocomplete,
valueAutocomplete, valueAutocompleteFunctions,
valueAutocompleteFunctions, valueAutocompleteVariables,
valueAutocompleteVariables, valuePlaceholder,
valuePlaceholder, valueType,
valueType, valueValidate,
valueValidate, setRef,
}: PairEditorProps, }: PairEditorProps) {
ref,
) {
const [forceFocusNamePairId, setForceFocusNamePairId] = useState<string | null>(null);
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const [isDragging, setIsDragging] = useState<PairWithId | null>(null); const [isDragging, setIsDragging] = useState<PairWithId | null>(null);
const [pairs, setPairs] = useState<PairWithId[]>([]); const [pairs, setPairs] = useState<PairWithId[]>([]);
@@ -124,15 +113,30 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
// we simply pass forceUpdateKey to the editor, the data set by useEffect will be stale. // we simply pass forceUpdateKey to the editor, the data set by useEffect will be stale.
const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey(); const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey();
useImperativeHandle( const rowsRef = useRef<Record<string, RowHandle | null>>({});
ref,
const handle = useMemo<PairEditorHandle>(
() => ({ () => ({
focusValue(index: number) { focusName(id: string) {
const id = pairs[index]?.id ?? 'n/a'; rowsRef.current[id]?.focusName();
setForceFocusValuePairId(id); },
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(() => { useEffect(() => {
@@ -179,21 +183,19 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
if (focusPrevious) { if (focusPrevious) {
const index = pairs.findIndex((p) => p.id === pair.id); const index = pairs.findIndex((p) => p.id === pair.id);
const id = pairs[index - 1]?.id ?? null; 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)); return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
}, },
[setPairsAndSave, setForceFocusNamePairId, pairs], [setPairsAndSave, pairs],
); );
const handleFocusName = useCallback((pair: Pair) => { const handleFocusName = useCallback((pair: Pair) => {
setForceFocusNamePairId(null); // Remove focus override when something focused
setForceFocusValuePairId(null); // Remove focus override when something focused
setPairs((pairs) => { setPairs((pairs) => {
const isLast = pair.id === pairs[pairs.length - 1]?.id; const isLast = pair.id === pairs[pairs.length - 1]?.id;
if (isLast) { if (isLast) {
const prevPair = pairs[pairs.length - 1]; const prevPair = pairs[pairs.length - 1];
setTimeout(() => setForceFocusNamePairId(prevPair?.id ?? null)); rowsRef.current[prevPair?.id ?? 'n/a']?.focusName();
return [...pairs, emptyPair()]; return [...pairs, emptyPair()];
} else { } else {
return pairs; return pairs;
@@ -202,13 +204,11 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
}, []); }, []);
const handleFocusValue = useCallback((pair: Pair) => { const handleFocusValue = useCallback((pair: Pair) => {
setForceFocusNamePairId(null); // Remove focus override when something focused
setForceFocusValuePairId(null); // Remove focus override when something focused
setPairs((pairs) => { setPairs((pairs) => {
const isLast = pair.id === pairs[pairs.length - 1]?.id; const isLast = pair.id === pairs[pairs.length - 1]?.id;
if (isLast) { if (isLast) {
const prevPair = pairs[pairs.length - 1]; const prevPair = pairs[pairs.length - 1];
setTimeout(() => setForceFocusValuePairId(prevPair?.id ?? null)); rowsRef.current[prevPair?.id ?? 'n/a']?.focusValue();
return [...pairs, emptyPair()]; return [...pairs, emptyPair()];
} else { } else {
return pairs; return pairs;
@@ -301,12 +301,11 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
<Fragment key={p.id}> <Fragment key={p.id}>
{hoveredIndex === i && <DropMarker />} {hoveredIndex === i && <DropMarker />}
<PairEditorRow <PairEditorRow
setRef={initPairEditorRow}
allowFileValues={allowFileValues} allowFileValues={allowFileValues}
allowMultilineValues={allowMultilineValues} allowMultilineValues={allowMultilineValues}
className="py-1" className="py-1"
forcedEnvironmentId={forcedEnvironmentId} forcedEnvironmentId={forcedEnvironmentId}
forceFocusNamePairId={forceFocusNamePairId}
forceFocusValuePairId={forceFocusValuePairId}
forceUpdateKey={localForceUpdateKey} forceUpdateKey={localForceUpdateKey}
index={i} index={i}
isLast={isLast} isLast={isLast}
@@ -352,7 +351,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
</DndContext> </DndContext>
</div> </div>
); );
}); }
type PairEditorRowProps = { type PairEditorRowProps = {
className?: string; className?: string;
@@ -369,6 +368,7 @@ type PairEditorRowProps = {
disableDrag?: boolean; disableDrag?: boolean;
index: number; index: number;
isDraggingGlobal?: boolean; isDraggingGlobal?: boolean;
setRef?: (id: string, n: RowHandle | null) => void;
} & Pick< } & Pick<
PairEditorProps, PairEditorProps,
| 'allowFileValues' | 'allowFileValues'
@@ -389,14 +389,17 @@ type PairEditorRowProps = {
| 'valueValidate' | 'valueValidate'
>; >;
interface RowHandle {
focusName(): void;
focusValue(): void;
}
export function PairEditorRow({ export function PairEditorRow({
allowFileValues, allowFileValues,
allowMultilineValues, allowMultilineValues,
className, className,
disableDrag, disableDrag,
disabled, disabled,
forceFocusNamePairId,
forceFocusValuePairId,
forceUpdateKey, forceUpdateKey,
forcedEnvironmentId, forcedEnvironmentId,
index, index,
@@ -419,21 +422,38 @@ export function PairEditorRow({
valuePlaceholder, valuePlaceholder,
valueType, valueType,
valueValidate, valueValidate,
setRef,
}: PairEditorRowProps) { }: PairEditorRowProps) {
const nameInputRef = useRef<InputHandle>(null); const nameInputRef = useRef<InputHandle>(null);
const valueInputRef = useRef<InputHandle>(null); const valueInputRef = useRef<InputHandle>(null);
const handle = useRef<RowHandle>({
useEffect(() => { focusName() {
if (forceFocusNamePairId === pair.id) {
nameInputRef.current?.focus(); nameInputRef.current?.focus();
} },
}, [forceFocusNamePairId, pair.id]); focusValue() {
useEffect(() => {
if (forceFocusValuePairId === pair.id) {
valueInputRef.current?.focus(); 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 handleFocusName = useCallback(() => onFocusName?.(pair), [onFocusName, pair]);
const handleFocusValue = useCallback(() => onFocusValue?.(pair), [onFocusValue, pair]); const handleFocusValue = useCallback(() => onFocusValue?.(pair), [onFocusValue, pair]);
@@ -574,7 +594,7 @@ export function PairEditorRow({
/> />
) : ( ) : (
<Input <Input
ref={nameInputRef} setRef={initNameInputRef}
hideLabel hideLabel
stateKey={`name.${pair.id}.${stateKey}`} stateKey={`name.${pair.id}.${stateKey}`}
disabled={disabled} disabled={disabled}
@@ -632,7 +652,7 @@ export function PairEditorRow({
</Button> </Button>
) : ( ) : (
<Input <Input
ref={valueInputRef} setRef={initValueInputRef}
hideLabel hideLabel
stateKey={`value.${pair.id}.${stateKey}`} stateKey={`value.${pair.id}.${stateKey}`}
wrapLines={false} wrapLines={false}
+4 -8
View File
@@ -1,9 +1,8 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { forwardRef } from 'react';
import { useKeyValue } from '../../hooks/useKeyValue'; import { useKeyValue } from '../../hooks/useKeyValue';
import { BulkPairEditor } from './BulkPairEditor'; import { BulkPairEditor } from './BulkPairEditor';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import type { PairEditorProps, PairEditorRef } from './PairEditor'; import type { PairEditorProps } from './PairEditor';
import { PairEditor } from './PairEditor'; import { PairEditor } from './PairEditor';
interface Props extends PairEditorProps { interface Props extends PairEditorProps {
@@ -11,10 +10,7 @@ interface Props extends PairEditorProps {
forcedEnvironmentId?: string; forcedEnvironmentId?: string;
} }
export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOrBulkEditor( export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
{ preferenceName, ...props }: Props,
ref,
) {
const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({ const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({
namespace: 'global', namespace: 'global',
key: ['bulk_edit', preferenceName], key: ['bulk_edit', preferenceName],
@@ -23,7 +19,7 @@ export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOr
return ( return (
<div className="relative h-full w-full group/wrapper"> <div className="relative h-full w-full group/wrapper">
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor ref={ref} {...props} />} {useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />}
<div className="absolute right-0 bottom-0"> <div className="absolute right-0 bottom-0">
<IconButton <IconButton
size="sm" size="sm"
@@ -39,4 +35,4 @@ export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOr
</div> </div>
</div> </div>
); );
}); }
+4 -4
View File
@@ -31,7 +31,7 @@ export type TreeItemProps<T extends { id: string }> = Pick<
onClick?: (item: T, e: TreeItemClickEvent) => void; onClick?: (item: T, e: TreeItemClickEvent) => void;
getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>; getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
depth: number; depth: number;
addRef?: (item: T, n: TreeItemHandle | null) => void; setRef?: (item: T, n: TreeItemHandle | null) => void;
}; };
export interface TreeItemHandle { export interface TreeItemHandle {
@@ -54,7 +54,7 @@ function TreeItem_<T extends { id: string }>({
getEditOptions, getEditOptions,
className, className,
depth, depth,
addRef, setRef,
}: TreeItemProps<T>) { }: TreeItemProps<T>) {
const listItemRef = useRef<HTMLLIElement>(null); const listItemRef = useRef<HTMLLIElement>(null);
const draggableRef = useRef<HTMLButtonElement>(null); const draggableRef = useRef<HTMLButtonElement>(null);
@@ -86,8 +86,8 @@ function TreeItem_<T extends { id: string }>({
); );
useEffect(() => { useEffect(() => {
addRef?.(node.item, handle); setRef?.(node.item, handle);
}, [addRef, handle, node.item]); }, [setRef, handle, node.item]);
const ancestorIds = useMemo(() => { const ancestorIds = useMemo(() => {
const ids: string[] = []; const ids: string[] = [];
@@ -35,7 +35,7 @@ export function TreeItemList<T extends { id: string }>({
<Fragment key={getItemKey(child.node.item)}> <Fragment key={getItemKey(child.node.item)}>
<TreeItem <TreeItem
treeId={treeId} treeId={treeId}
addRef={addTreeItemRef} setRef={addTreeItemRef}
node={child.node} node={child.node}
getItemKey={getItemKey} getItemKey={getItemKey}
depth={forceDepth == null ? child.depth : forceDepth} depth={forceDepth == null ? child.depth : forceDepth}
+1 -4
View File
@@ -1,9 +1,8 @@
import type { EditorView } from '@codemirror/view';
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import { formatSdl } from 'format-graphql'; import { formatSdl } from 'format-graphql';
import { useAtom } from 'jotai'; import { useAtom } from 'jotai';
import { useMemo, useRef } from 'react'; import { useMemo } from 'react';
import { useLocalStorage } from 'react-use'; import { useLocalStorage } from 'react-use';
import { useIntrospectGraphQL } from '../../hooks/useIntrospectGraphQL'; import { useIntrospectGraphQL } from '../../hooks/useIntrospectGraphQL';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
@@ -26,7 +25,6 @@ type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> &
}; };
export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) { export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) {
const editorViewRef = useRef<EditorView>(null);
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage< const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
Record<string, boolean> Record<string, boolean>
>('graphQLAutoIntrospectDisabled', {}); >('graphQLAutoIntrospectDisabled', {});
@@ -199,7 +197,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
defaultValue={currentBody.query} defaultValue={currentBody.query}
onChange={handleChangeQuery} onChange={handleChangeQuery}
placeholder="..." placeholder="..."
ref={editorViewRef}
actions={actions} actions={actions}
stateKey={'graphql_body.' + request.id} stateKey={'graphql_body.' + request.id}
{...extraEditorProps} {...extraEditorProps}
+9 -6
View File
@@ -4,14 +4,17 @@ import { jotaiStore } from './jotai';
export const dialogsAtom = atom<DialogInstance[]>([]); export const dialogsAtom = atom<DialogInstance[]>([]);
export function showDialog({ id, ...props }: DialogInstance) {
jotaiStore.set(dialogsAtom, (a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
}
export function toggleDialog({ id, ...props }: DialogInstance) { export function toggleDialog({ id, ...props }: DialogInstance) {
const dialogs = jotaiStore.get(dialogsAtom); const dialogs = jotaiStore.get(dialogsAtom);
if (dialogs.some((d) => d.id === id)) hideDialog(id); if (dialogs.some((d) => d.id === id)) {
else showDialog({ id, ...props }); 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) { export function hideDialog(id: string) {
+12 -3
View File
@@ -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 { openFolderSettings } from '../commands/openFolderSettings';
import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog'; import { EnvironmentEditDialog } from '../components/EnvironmentEditDialog';
import { toggleDialog } from './dialog'; 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) { if (environment?.parentModel === 'folder' && environment.parentId != null) {
openFolderSettings(environment.parentId, 'variables'); openFolderSettings(environment.parentId, 'variables');
} else { } else {
@@ -12,7 +16,12 @@ export function editEnvironment(environment: Environment | null) {
noPadding: true, noPadding: true,
size: 'lg', size: 'lg',
className: 'h-[80vh]', className: 'h-[80vh]',
render: () => <EnvironmentEditDialog initialEnvironment={environment} />, render: () => (
<EnvironmentEditDialog
initialEnvironmentId={environment?.id ?? null}
addOrFocusVariable={options.addOrFocusVariable}
/>
),
}); });
} }
} }