diff --git a/package-lock.json b/package-lock.json index e7c6d30c..832e8381 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "codemirror": "^6.0.1", "codemirror-json-schema": "^0.6.1", "date-fns": "^3.3.1", + "eventemitter3": "^5.0.1", "fast-fuzzy": "^1.12.0", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", @@ -5750,8 +5751,7 @@ "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", - "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==" }, "node_modules/execa": { "version": "7.2.0", diff --git a/package.json b/package.json index 1e9590dc..0623c3bd 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "codemirror": "^6.0.1", "codemirror-json-schema": "^0.6.1", "date-fns": "^3.3.1", + "eventemitter3": "^5.0.1", "fast-fuzzy": "^1.12.0", "focus-trap-react": "^10.1.1", "format-graphql": "^1.4.0", diff --git a/src-web/components/DefaultLayout.tsx b/src-web/components/DefaultLayout.tsx index e81341b6..9a026425 100644 --- a/src-web/components/DefaultLayout.tsx +++ b/src-web/components/DefaultLayout.tsx @@ -4,6 +4,7 @@ import { Outlet } from 'react-router-dom'; import { useOsInfo } from '../hooks/useOsInfo'; import { DialogProvider, Dialogs } from './DialogContext'; import { GlobalHooks } from './GlobalHooks'; +import { RequestEditorProvider } from './RequestEditorContext'; import { ToastProvider, Toasts } from './ToastContext'; export function DefaultLayout() { @@ -11,23 +12,25 @@ export function DefaultLayout() { return ( - <> - {/* Must be inside all the providers, so they have access to them */} - - - - - - - + + <> + {/* Must be inside all the providers, so they have access to them */} + + + + + + + + ); diff --git a/src-web/components/RequestEditorContext.tsx b/src-web/components/RequestEditorContext.tsx new file mode 100644 index 00000000..d91bf746 --- /dev/null +++ b/src-web/components/RequestEditorContext.tsx @@ -0,0 +1,58 @@ +import EventEmitter from 'eventemitter3'; +import type { DependencyList } from 'react'; +import React, { createContext, useCallback, useContext, useEffect } from 'react'; + +interface State { + focusParamValue: (name: string) => void; + focusParamsTab: () => void; +} + +export const RequestEditorContext = createContext({} as State); + +const emitter = new EventEmitter(); + +export const RequestEditorProvider = ({ children }: { children: React.ReactNode }) => { + const focusParamsTab = useCallback(() => { + emitter.emit('focus_http_request_params_tab'); + }, []); + + const focusParamValue = useCallback( + (name: string) => { + focusParamsTab(); + setTimeout(() => { + emitter.emit('focus_http_request_param_value', name); + }, 50); + }, + [focusParamsTab], + ); + + const state: State = { + focusParamValue, + focusParamsTab, + }; + + return {children}; +}; + +export function useOnFocusParamValue(cb: (name: string) => void, deps: DependencyList) { + useEffect(() => { + emitter.on('focus_http_request_param_value', cb); + return () => { + emitter.off('focus_http_request_param_value', cb); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, deps); +} + +export function useOnFocusParamsTab(cb: () => void) { + useEffect(() => { + emitter.on('focus_http_request_params_tab', cb); + return () => { + emitter.off('focus_http_request_params_tab', cb); + }; + // Only add callback once, to prevent the need for the caller to useCallback + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +} + +export const useRequestEditor = () => useContext(RequestEditorContext); diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index 6d44ecab..987e9e7a 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -42,6 +42,7 @@ import { FormMultipartEditor } from './FormMultipartEditor'; import { FormUrlencodedEditor } from './FormUrlencodedEditor'; import { GraphQLEditor } from './GraphQLEditor'; import { HeadersEditor } from './HeadersEditor'; +import { useOnFocusParamsTab } from './RequestEditorContext'; import { useToast } from './ToastContext'; import { UrlBar } from './UrlBar'; import { UrlParametersEditor } from './UrlParameterEditor'; @@ -54,6 +55,10 @@ interface Props { } const useActiveTab = createGlobalState('body'); +const TAB_BODY = 'body'; +const TAB_PARAMS = 'params'; +const TAB_HEADERS = 'headers'; +const TAB_AUTH = 'auth'; export const RequestPane = memo(function RequestPane({ style, @@ -94,7 +99,8 @@ export const RequestPane = memo(function RequestPane({ const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map( (m) => m[1] ?? '', ); - const items: Pair[] = [...activeRequest.urlParameters]; + const nonEmptyParameters = activeRequest.urlParameters.filter((p) => p.name || p.value); + const items: Pair[] = [...nonEmptyParameters]; for (const name of placeholderNames) { const index = items.findIndex((p) => p.name === name); if (index >= 0) { @@ -114,7 +120,7 @@ export const RequestPane = memo(function RequestPane({ const tabs: TabItem[] = useMemo( () => [ { - value: 'body', + value: TAB_BODY, options: { value: activeRequest.bodyType, items: [ @@ -180,16 +186,16 @@ export const RequestPane = memo(function RequestPane({ }, }, { - value: 'params', + value: TAB_PARAMS, label: (
Params - p.name).length} /> +
), }, { - value: 'headers', + value: TAB_HEADERS, label: (
Headers @@ -198,7 +204,7 @@ export const RequestPane = memo(function RequestPane({ ), }, { - value: 'auth', + value: TAB_AUTH, label: 'Auth', options: { value: activeRequest.authenticationType, @@ -292,6 +298,10 @@ export const RequestPane = memo(function RequestPane({ const { updateKey } = useRequestUpdateKey(activeRequestId ?? null); const importCurl = useImportCurl(); + useOnFocusParamsTab(() => { + setActiveTab(TAB_PARAMS); + }); + return (
(null); + + useOnFocusParamValue( + (name) => { + const pairIndex = pairs.findIndex((p) => p.name === name); + if (pairIndex >= 0) { + pairEditor.current?.focusValue(pairIndex); + } else { + console.log("Couldn't find pair to focus", { name, pairs }); + } + }, + [pairs], + ); + return ( (function E [dialog], ); - const onClickPathParameter = useCallback(async (name: string) => { - console.log('TODO: Focus', name, 'in params tab'); - }, []); + const { focusParamValue } = useRequestEditor(); + const onClickPathParameter = useCallback( + async (name: string) => { + focusParamValue(name); + }, + [focusParamValue], + ); // Update the language extension when the language changes useEffect(() => { diff --git a/src-web/components/core/Editor/twig/templateTags.ts b/src-web/components/core/Editor/twig/templateTags.ts index d5a348a0..962b441a 100644 --- a/src-web/components/core/Editor/twig/templateTags.ts +++ b/src-web/components/core/Editor/twig/templateTags.ts @@ -120,7 +120,6 @@ function templateTags( const onClick = () => onClickPathParameter(rawText); const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick); const deco = Decoration.replace({ widget, inclusive: false }); - console.log('ADDED WIDGET', globalFrom, node, rawText); widgets.push(deco.range(globalFrom, globalTo)); }, }); @@ -201,13 +200,6 @@ export function templateTagsPlugin( return view.plugin(plugin)?.decorations || Decoration.none; }); }, - eventHandlers: { - mousedown(e) { - const target = e.target as HTMLElement; - if (target.classList.contains('template-tag')) console.log('CLICKED TEMPLATE TAG'); - // return toggleBoolean(view, view.posAtDOM(target)); - }, - }, }, ); } diff --git a/src-web/components/core/Editor/url/highlight.ts b/src-web/components/core/Editor/url/highlight.ts index 8b96f6de..e908323f 100644 --- a/src-web/components/core/Editor/url/highlight.ts +++ b/src-web/components/core/Editor/url/highlight.ts @@ -3,9 +3,9 @@ import { styleTags, tags as t } from '@lezer/highlight'; export const highlight = styleTags({ Protocol: t.comment, Placeholder: t.emphasis, - PathSegment: t.tagName, - Port: t.attributeName, - Host: t.variableName, - Path: t.bool, - Query: t.string, + // PathSegment: t.tagName, + // Port: t.attributeName, + // Host: t.variableName, + // Path: t.bool, + // Query: t.string, }); diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index 91a29aff..8ff6a275 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -1,6 +1,15 @@ import classNames from 'classnames'; import type { EditorView } from 'codemirror'; -import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + Fragment, + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from 'react'; import type { XYCoord } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd'; import { v4 as uuid } from 'uuid'; @@ -16,6 +25,10 @@ import type { InputProps } from './Input'; import { Input } from './Input'; import { RadioDropdown } from './RadioDropdown'; +export interface PairEditorRef { + focusValue(index: number): void; +} + export type PairEditorProps = { pairs: Pair[]; onChange: (pairs: Pair[]) => void; @@ -49,24 +62,28 @@ type PairContainer = { id: string; }; -export function PairEditor({ - className, - forceUpdateKey, - nameAutocomplete, - nameAutocompleteVariables, - namePlaceholder, - nameValidate, - valueType, - onChange, - noScroll, - pairs: originalPairs, - valueAutocomplete, - valueAutocompleteVariables, - valuePlaceholder, - valueValidate, - allowFileValues, -}: PairEditorProps) { - const [forceFocusPairId, setForceFocusPairId] = useState(null); +export const PairEditor = forwardRef(function PairEditor( + { + className, + forceUpdateKey, + nameAutocomplete, + nameAutocompleteVariables, + namePlaceholder, + nameValidate, + valueType, + onChange, + noScroll, + pairs: originalPairs, + valueAutocomplete, + valueAutocompleteVariables, + valuePlaceholder, + valueValidate, + allowFileValues, + }: PairEditorProps, + ref, +) { + const [forceFocusNamePairId, setForceFocusNamePairId] = useState(null); + const [forceFocusValuePairId, setForceFocusValuePairId] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); const [pairs, setPairs] = useState(() => { // Remove empty headers on initial render @@ -75,6 +92,13 @@ export function PairEditor({ return [...pairs, newPairContainer()]; }); + useImperativeHandle(ref, () => ({ + focusValue(index: number) { + const id = pairs[index]?.id ?? 'n/a'; + setForceFocusValuePairId(id); + }, + })); + useEffect(() => { // Remove empty headers on initial render // TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some @@ -135,17 +159,18 @@ export function PairEditor({ if (focusPrevious) { const index = pairs.findIndex((p) => p.id === pair.id); const id = pairs[index - 1]?.id ?? null; - setForceFocusPairId(id); + setForceFocusNamePairId(id); } return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id)); }, - [setPairsAndSave, setForceFocusPairId, pairs], + [setPairsAndSave, setForceFocusNamePairId, pairs], ); const handleFocus = useCallback( (pair: PairContainer) => setPairs((pairs) => { - setForceFocusPairId(null); // Remove focus override when something focused + setForceFocusNamePairId(null); // Remove focus override when something focused + setForceFocusValuePairId(null); // Remove focus override when something focused const isLast = pair.id === pairs[pairs.length - 1]?.id; return isLast ? [...pairs, newPairContainer()] : pairs; }), @@ -185,7 +210,8 @@ export function PairEditor({ nameAutocompleteVariables={nameAutocompleteVariables} valueAutocompleteVariables={valueAutocompleteVariables} valueType={valueType} - forceFocusPairId={forceFocusPairId} + forceFocusNamePairId={forceFocusNamePairId} + forceFocusValuePairId={forceFocusValuePairId} forceUpdateKey={forceUpdateKey} nameAutocomplete={nameAutocomplete} valueAutocomplete={valueAutocomplete} @@ -204,7 +230,7 @@ export function PairEditor({ })}
); -} +}); enum ItemTypes { ROW = 'pair-row', @@ -213,7 +239,8 @@ enum ItemTypes { type PairEditorRowProps = { className?: string; pairContainer: PairContainer; - forceFocusPairId?: string | null; + forceFocusNamePairId?: string | null; + forceFocusValuePairId?: string | null; onMove: (id: string, side: 'above' | 'below') => void; onEnd: (id: string) => void; onChange: (pair: PairContainer) => void; @@ -239,7 +266,8 @@ type PairEditorRowProps = { function PairEditorRow({ allowFileValues, className, - forceFocusPairId, + forceFocusNamePairId, + forceFocusValuePairId, forceUpdateKey, isLast, nameAutocomplete, @@ -262,12 +290,19 @@ function PairEditorRow({ const ref = useRef(null); const prompt = usePrompt(); const nameInputRef = useRef(null); + const valueInputRef = useRef(null); useEffect(() => { - if (forceFocusPairId === pairContainer.id) { + if (forceFocusNamePairId === pairContainer.id) { nameInputRef.current?.focus(); } - }, [forceFocusPairId, pairContainer.id]); + }, [forceFocusNamePairId, pairContainer.id]); + + useEffect(() => { + if (forceFocusValuePairId === pairContainer.id) { + valueInputRef.current?.focus(); + } + }, [forceFocusValuePairId, pairContainer.id]); const handleChangeEnabled = useMemo( () => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }), @@ -400,6 +435,7 @@ function PairEditorRow({ /> ) : ( (function PairOrBulkEditor( + { preferenceName, ...props }: Props, + ref, +) { const { value: useBulk, set: setUseBulk } = useKeyValue({ namespace: 'global', key: ['bulk_edit', preferenceName], @@ -18,7 +22,7 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) { return (
- {useBulk ? : } + {useBulk ? : }
); -} +}); diff --git a/src-web/main.tsx b/src-web/main.tsx index 5960bc0a..90190633 100644 --- a/src-web/main.tsx +++ b/src-web/main.tsx @@ -1,5 +1,4 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -import { attachConsole } from '@tauri-apps/plugin-log'; import { type } from '@tauri-apps/plugin-os'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; @@ -18,8 +17,6 @@ if (osType !== 'macos') { await getCurrentWebviewWindow().setDecorations(false); } -await attachConsole(); - window.addEventListener('keydown', (e) => { // Hack to not go back in history on backspace. Check for document body // or else it will prevent backspace in input fields.