mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 14:30:24 +01:00
Click env var to edit AND improve input/editor ref handling
This commit is contained in:
@@ -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="px-2 w-full">
|
||||
<PlainInput
|
||||
autoFocus
|
||||
hideLabel
|
||||
leftSlot={
|
||||
<div className="h-md w-10 flex justify-center items-center">
|
||||
|
||||
@@ -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<DialogProps, 'open' | 'children'>;
|
||||
|
||||
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 (
|
||||
<ErrorBoundary name={`Dialog ${id}`}>
|
||||
<Dialog
|
||||
open
|
||||
onClose={() => {
|
||||
onClose?.();
|
||||
hideDialog(id);
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<Dialog open onClose={handleClose} {...props}>
|
||||
<Component hide={hide} {...props} />
|
||||
</Dialog>
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
@@ -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<string | null>(
|
||||
initialEnvironment?.id ?? null,
|
||||
initialEnvironmentId ?? null,
|
||||
);
|
||||
|
||||
const selectedEnvironment =
|
||||
@@ -76,16 +74,21 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
</Banner>
|
||||
</div>
|
||||
) : (
|
||||
<EnvironmentEditor className="pl-4 pt-3" environment={selectedEnvironment} />
|
||||
<EnvironmentEditor
|
||||
className="pl-4 pt-3"
|
||||
environment={selectedEnvironment}
|
||||
addOrFocusVariable={addOrFocusVariable}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const sharableTooltip = (
|
||||
<IconTooltip
|
||||
tabIndex={-1}
|
||||
icon="eye"
|
||||
iconSize="sm"
|
||||
content="This environment will be included in Directory Sync and data exports"
|
||||
|
||||
@@ -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 type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
@@ -17,21 +17,20 @@ import { BadgeButton } from './core/BadgeButton';
|
||||
import { DismissibleBanner } from './core/DismissibleBanner';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
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 { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
import { EnvironmentColorIndicator } from './EnvironmentColorIndicator';
|
||||
import { EnvironmentSharableTooltip } from './EnvironmentSharableTooltip';
|
||||
|
||||
export function EnvironmentEditor({
|
||||
environment,
|
||||
hideName,
|
||||
className,
|
||||
}: {
|
||||
interface Props {
|
||||
environment: Environment;
|
||||
hideName?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
addOrFocusVariable?: EnvironmentVariable;
|
||||
}
|
||||
|
||||
export function EnvironmentEditor({ environment, hideName, className, addOrFocusVariable }: Props) {
|
||||
const workspaceId = environment.workspaceId;
|
||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||
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 (
|
||||
<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">
|
||||
<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>}
|
||||
{isEncryptionEnabled ? (
|
||||
!allVariableAreEncrypted ? (
|
||||
@@ -146,6 +188,7 @@ export function EnvironmentEditor({
|
||||
)}
|
||||
</div>
|
||||
<PairOrBulkEditor
|
||||
setRef={initPairEditor}
|
||||
className="h-full"
|
||||
allowMultilineValues
|
||||
preferenceName="environment"
|
||||
@@ -156,7 +199,7 @@ export function EnvironmentEditor({
|
||||
valueAutocompleteVariables="environment"
|
||||
valueAutocompleteFunctions
|
||||
forceUpdateKey={`${environment.id}::${forceUpdateKey}`}
|
||||
pairs={environment.variables}
|
||||
pairs={pairs}
|
||||
onChange={handleChange}
|
||||
stateKey={`environment.${environment.id}`}
|
||||
forcedEnvironmentId={environment.id}
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
stateExtensions,
|
||||
updateSchema,
|
||||
} from 'codemirror-json-schema';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { showAlert } from '../lib/alert';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
@@ -40,6 +40,9 @@ export function GrpcEditor({
|
||||
...extraEditorProps
|
||||
}: Props) {
|
||||
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
|
||||
useEffect(() => {
|
||||
@@ -167,6 +170,7 @@ export function GrpcEditor({
|
||||
return (
|
||||
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||
<Editor
|
||||
setRef={handleInitEditorViewRef}
|
||||
language="json"
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
@@ -174,7 +178,6 @@ export function GrpcEditor({
|
||||
defaultValue={request.message}
|
||||
heightMode="auto"
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
extraExtensions={extraExtensions}
|
||||
actions={actions}
|
||||
stateKey={`grpc_message.${request.id}`}
|
||||
|
||||
@@ -53,20 +53,6 @@ export function Overlay({
|
||||
<FocusTrap
|
||||
focusTrapOptions={{
|
||||
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
|
||||
|
||||
@@ -77,6 +77,9 @@ function Sidebar({ className }: { className?: string }) {
|
||||
const wrapperRef = useRef<HTMLElement>(null);
|
||||
const treeRef = useRef<TreeHandle>(null);
|
||||
const filterRef = useRef<InputHandle>(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 }) {
|
||||
<>
|
||||
<Input
|
||||
hideLabel
|
||||
ref={filterRef}
|
||||
setRef={setFilterRef}
|
||||
size="sm"
|
||||
label="filter"
|
||||
language={null} // Explicitly disable
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
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 type { IconProps } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -46,6 +46,10 @@ export const UrlBar = memo(function UrlBar({
|
||||
const inputRef = useRef<InputHandle>(null);
|
||||
const [isFocused, setIsFocused] = useState<boolean>(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 (
|
||||
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
setRef={handleInitInputRef}
|
||||
autocompleteFunctions
|
||||
autocompleteVariables
|
||||
stateKey={stateKey}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import { useRef } from 'react';
|
||||
import { useCallback, useRef } from 'react';
|
||||
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 { VStack } from './core/Stacks';
|
||||
|
||||
@@ -13,15 +13,19 @@ type 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();
|
||||
|
||||
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 (
|
||||
<VStack className="h-full">
|
||||
<PairOrBulkEditor
|
||||
ref={pairEditor}
|
||||
setRef={handleInitPairEditorRef}
|
||||
allowMultilineValues
|
||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||
nameAutocompleteFunctions
|
||||
|
||||
@@ -17,17 +17,16 @@ import { useAtomValue } from 'jotai';
|
||||
import { md5 } from 'js-md5';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import {
|
||||
useEffect,
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
|
||||
import { activeWorkspaceAtom } from '../../../hooks/useActiveWorkspace';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||
@@ -40,7 +39,6 @@ import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { InlineCode } from '../InlineCode';
|
||||
import { HStack } from '../Stacks';
|
||||
@@ -97,6 +95,7 @@ export interface EditorProps {
|
||||
tooltipContainer?: HTMLElement;
|
||||
type?: 'text' | 'password';
|
||||
wrapLines?: boolean;
|
||||
setRef?: (view: EditorView | null) => 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<EditorView | undefined, EditorProps>(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<EditorView | undefined, EditorProps>(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<EditorProps['onChange']>(onChange);
|
||||
@@ -324,33 +320,15 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(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 }) => (
|
||||
<TemplateVariableDialog
|
||||
hide={hide}
|
||||
initialTokens={initialTokens}
|
||||
onChange={(insert) => {
|
||||
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<EditorView | undefined, EditorProps>(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<EditorView | undefined, EditorProps>(function E
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function getExtensions({
|
||||
stateKey,
|
||||
|
||||
@@ -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<EditorView, EditorProps>(function LazyEditor(props, ref) {
|
||||
export function Editor(props: EditorProps) {
|
||||
return (
|
||||
<Suspense>
|
||||
<Editor_ ref={ref} {...props} />
|
||||
<Editor_ {...props} />
|
||||
</Suspense>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<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
|
||||
// use the encrypted input component.
|
||||
if (type === 'password' && props.autocompleteFunctions) {
|
||||
return <EncryptionInput {...props} />;
|
||||
} else {
|
||||
return <BaseInput ref={ref} type={type} {...props} />;
|
||||
return <BaseInput type={type} {...props} />;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const BaseInput = forwardRef<InputHandle, InputProps>(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<boolean>(false, [forceUpdateKey]);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
|
||||
const inputHandle = useMemo<InputHandle>(
|
||||
() => ({
|
||||
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<number>(0);
|
||||
useEffect(() => {
|
||||
@@ -300,7 +299,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
ref={editorRef}
|
||||
setRef={initEditorRef}
|
||||
id={id.current}
|
||||
hideGutter
|
||||
singleLine={!multiLine}
|
||||
@@ -351,7 +350,7 @@ const BaseInput = forwardRef<InputHandle, InputProps>(function InputBase(
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function validateRequire(v: string) {
|
||||
return v.length > 0;
|
||||
@@ -365,8 +364,9 @@ function EncryptionInput({
|
||||
autocompleteFunctions,
|
||||
autocompleteVariables,
|
||||
forceUpdateKey: ogForceUpdateKey,
|
||||
setRef,
|
||||
...props
|
||||
}: Omit<InputProps, 'type'>) {
|
||||
}: InputProps) {
|
||||
const isEncryptionEnabled = useIsEncryptionEnabled();
|
||||
const [state, setState] = useStateWithDeps<{
|
||||
fieldType: PasswordFieldType;
|
||||
@@ -374,11 +374,19 @@ function EncryptionInput({
|
||||
security: ReturnType<typeof analyzeTemplate> | 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<InputHandle>(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 (
|
||||
<BaseInput
|
||||
setRef={handleSetInputRef}
|
||||
disableObscureToggle
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
|
||||
@@ -10,16 +10,7 @@ import {
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
import {
|
||||
forwardRef,
|
||||
Fragment,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
|
||||
import { useRandomKey } from '../../hooks/useRandomKey';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
@@ -45,8 +36,9 @@ import { PlainInput } from './PlainInput';
|
||||
import type { RadioDropdownItem } from './RadioDropdown';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
|
||||
export interface PairEditorRef {
|
||||
focusValue(index: number): void;
|
||||
export interface PairEditorHandle {
|
||||
focusName(id: string): void;
|
||||
focusValue(id: string): void;
|
||||
}
|
||||
|
||||
export type PairEditorProps = {
|
||||
@@ -64,6 +56,7 @@ export type PairEditorProps = {
|
||||
onChange: (pairs: PairWithId[]) => 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<PairEditorRef, PairEditorProps>(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<string | null>(null);
|
||||
const [forceFocusValuePairId, setForceFocusValuePairId] = useState<string | null>(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<number | null>(null);
|
||||
const [isDragging, setIsDragging] = useState<PairWithId | null>(null);
|
||||
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.
|
||||
const [localForceUpdateKey, regenerateLocalForceUpdateKey] = useRandomKey();
|
||||
|
||||
useImperativeHandle(
|
||||
ref,
|
||||
const rowsRef = useRef<Record<string, RowHandle | null>>({});
|
||||
|
||||
const handle = useMemo<PairEditorHandle>(
|
||||
() => ({
|
||||
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<PairEditorRef, PairEditorProps>(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<PairEditorRef, PairEditorProps>(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<PairEditorRef, PairEditorProps>(function Pa
|
||||
<Fragment key={p.id}>
|
||||
{hoveredIndex === i && <DropMarker />}
|
||||
<PairEditorRow
|
||||
setRef={initPairEditorRow}
|
||||
allowFileValues={allowFileValues}
|
||||
allowMultilineValues={allowMultilineValues}
|
||||
className="py-1"
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceFocusNamePairId={forceFocusNamePairId}
|
||||
forceFocusValuePairId={forceFocusValuePairId}
|
||||
forceUpdateKey={localForceUpdateKey}
|
||||
index={i}
|
||||
isLast={isLast}
|
||||
@@ -352,7 +351,7 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
</DndContext>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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<InputHandle>(null);
|
||||
const valueInputRef = useRef<InputHandle>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceFocusNamePairId === pair.id) {
|
||||
const handle = useRef<RowHandle>({
|
||||
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({
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={nameInputRef}
|
||||
setRef={initNameInputRef}
|
||||
hideLabel
|
||||
stateKey={`name.${pair.id}.${stateKey}`}
|
||||
disabled={disabled}
|
||||
@@ -632,7 +652,7 @@ export function PairEditorRow({
|
||||
</Button>
|
||||
) : (
|
||||
<Input
|
||||
ref={valueInputRef}
|
||||
setRef={initValueInputRef}
|
||||
hideLabel
|
||||
stateKey={`value.${pair.id}.${stateKey}`}
|
||||
wrapLines={false}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef } from 'react';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import { BulkPairEditor } from './BulkPairEditor';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { PairEditorProps, PairEditorRef } from './PairEditor';
|
||||
import type { PairEditorProps } from './PairEditor';
|
||||
import { PairEditor } from './PairEditor';
|
||||
|
||||
interface Props extends PairEditorProps {
|
||||
@@ -11,10 +10,7 @@ interface Props extends PairEditorProps {
|
||||
forcedEnvironmentId?: string;
|
||||
}
|
||||
|
||||
export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOrBulkEditor(
|
||||
{ preferenceName, ...props }: Props,
|
||||
ref,
|
||||
) {
|
||||
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
||||
const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['bulk_edit', preferenceName],
|
||||
@@ -23,7 +19,7 @@ export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOr
|
||||
|
||||
return (
|
||||
<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">
|
||||
<IconButton
|
||||
size="sm"
|
||||
@@ -39,4 +35,4 @@ export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOr
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ export type TreeItemProps<T extends { id: string }> = Pick<
|
||||
onClick?: (item: T, e: TreeItemClickEvent) => void;
|
||||
getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||
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_<T extends { id: string }>({
|
||||
getEditOptions,
|
||||
className,
|
||||
depth,
|
||||
addRef,
|
||||
setRef,
|
||||
}: TreeItemProps<T>) {
|
||||
const listItemRef = useRef<HTMLLIElement>(null);
|
||||
const draggableRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -86,8 +86,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
addRef?.(node.item, handle);
|
||||
}, [addRef, handle, node.item]);
|
||||
setRef?.(node.item, handle);
|
||||
}, [setRef, handle, node.item]);
|
||||
|
||||
const ancestorIds = useMemo(() => {
|
||||
const ids: string[] = [];
|
||||
|
||||
@@ -35,7 +35,7 @@ export function TreeItemList<T extends { id: string }>({
|
||||
<Fragment key={getItemKey(child.node.item)}>
|
||||
<TreeItem
|
||||
treeId={treeId}
|
||||
addRef={addTreeItemRef}
|
||||
setRef={addTreeItemRef}
|
||||
node={child.node}
|
||||
getItemKey={getItemKey}
|
||||
depth={forceDepth == null ? child.depth : forceDepth}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
|
||||
import { formatSdl } from 'format-graphql';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useIntrospectGraphQL } from '../../hooks/useIntrospectGraphQL';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
@@ -26,7 +25,6 @@ type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> &
|
||||
};
|
||||
|
||||
export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||
const editorViewRef = useRef<EditorView>(null);
|
||||
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
||||
Record<string, boolean>
|
||||
>('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}
|
||||
|
||||
@@ -4,14 +4,17 @@ import { jotaiStore } from './jotai';
|
||||
|
||||
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) {
|
||||
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) {
|
||||
|
||||
@@ -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: () => <EnvironmentEditDialog initialEnvironment={environment} />,
|
||||
render: () => (
|
||||
<EnvironmentEditDialog
|
||||
initialEnvironmentId={environment?.id ?? null}
|
||||
addOrFocusVariable={options.addOrFocusVariable}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user