mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-06-11 17:12:47 +02: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="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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}`}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
});
|
}
|
||||||
|
|||||||
@@ -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,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}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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}
|
||||||
|
/>
|
||||||
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user