mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-20 15:51:23 +02:00
Request pane context (#69)
This commit is contained in:
4
package-lock.json
generated
4
package-lock.json
generated
@@ -34,6 +34,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-json-schema": "^0.6.1",
|
"codemirror-json-schema": "^0.6.1",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
"fast-fuzzy": "^1.12.0",
|
"fast-fuzzy": "^1.12.0",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
@@ -5750,8 +5751,7 @@
|
|||||||
"node_modules/eventemitter3": {
|
"node_modules/eventemitter3": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/execa": {
|
"node_modules/execa": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
|
|||||||
@@ -49,6 +49,7 @@
|
|||||||
"codemirror": "^6.0.1",
|
"codemirror": "^6.0.1",
|
||||||
"codemirror-json-schema": "^0.6.1",
|
"codemirror-json-schema": "^0.6.1",
|
||||||
"date-fns": "^3.3.1",
|
"date-fns": "^3.3.1",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
"fast-fuzzy": "^1.12.0",
|
"fast-fuzzy": "^1.12.0",
|
||||||
"focus-trap-react": "^10.1.1",
|
"focus-trap-react": "^10.1.1",
|
||||||
"format-graphql": "^1.4.0",
|
"format-graphql": "^1.4.0",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { Outlet } from 'react-router-dom';
|
|||||||
import { useOsInfo } from '../hooks/useOsInfo';
|
import { useOsInfo } from '../hooks/useOsInfo';
|
||||||
import { DialogProvider, Dialogs } from './DialogContext';
|
import { DialogProvider, Dialogs } from './DialogContext';
|
||||||
import { GlobalHooks } from './GlobalHooks';
|
import { GlobalHooks } from './GlobalHooks';
|
||||||
|
import { RequestEditorProvider } from './RequestEditorContext';
|
||||||
import { ToastProvider, Toasts } from './ToastContext';
|
import { ToastProvider, Toasts } from './ToastContext';
|
||||||
|
|
||||||
export function DefaultLayout() {
|
export function DefaultLayout() {
|
||||||
@@ -11,23 +12,25 @@ export function DefaultLayout() {
|
|||||||
return (
|
return (
|
||||||
<DialogProvider>
|
<DialogProvider>
|
||||||
<ToastProvider>
|
<ToastProvider>
|
||||||
<>
|
<RequestEditorProvider>
|
||||||
{/* Must be inside all the providers, so they have access to them */}
|
<>
|
||||||
<Toasts />
|
{/* Must be inside all the providers, so they have access to them */}
|
||||||
<Dialogs />
|
<Toasts />
|
||||||
</>
|
<Dialogs />
|
||||||
<motion.div
|
</>
|
||||||
initial={{ opacity: 0 }}
|
<motion.div
|
||||||
animate={{ opacity: 1 }}
|
initial={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.1, delay: 0.1 }}
|
animate={{ opacity: 1 }}
|
||||||
className={classNames(
|
transition={{ duration: 0.1, delay: 0.1 }}
|
||||||
'w-full h-full',
|
className={classNames(
|
||||||
osInfo?.osType === 'linux' && 'border border-border-subtle',
|
'w-full h-full',
|
||||||
)}
|
osInfo?.osType === 'linux' && 'border border-border-subtle',
|
||||||
>
|
)}
|
||||||
<Outlet />
|
>
|
||||||
</motion.div>
|
<Outlet />
|
||||||
<GlobalHooks />
|
</motion.div>
|
||||||
|
<GlobalHooks />
|
||||||
|
</RequestEditorProvider>
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
</DialogProvider>
|
</DialogProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
58
src-web/components/RequestEditorContext.tsx
Normal file
58
src-web/components/RequestEditorContext.tsx
Normal file
@@ -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<State>({} 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 <RequestEditorContext.Provider value={state}>{children}</RequestEditorContext.Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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);
|
||||||
@@ -42,6 +42,7 @@ import { FormMultipartEditor } from './FormMultipartEditor';
|
|||||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||||
import { GraphQLEditor } from './GraphQLEditor';
|
import { GraphQLEditor } from './GraphQLEditor';
|
||||||
import { HeadersEditor } from './HeadersEditor';
|
import { HeadersEditor } from './HeadersEditor';
|
||||||
|
import { useOnFocusParamsTab } from './RequestEditorContext';
|
||||||
import { useToast } from './ToastContext';
|
import { useToast } from './ToastContext';
|
||||||
import { UrlBar } from './UrlBar';
|
import { UrlBar } from './UrlBar';
|
||||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||||
@@ -54,6 +55,10 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const useActiveTab = createGlobalState<string>('body');
|
const useActiveTab = createGlobalState<string>('body');
|
||||||
|
const TAB_BODY = 'body';
|
||||||
|
const TAB_PARAMS = 'params';
|
||||||
|
const TAB_HEADERS = 'headers';
|
||||||
|
const TAB_AUTH = 'auth';
|
||||||
|
|
||||||
export const RequestPane = memo(function RequestPane({
|
export const RequestPane = memo(function RequestPane({
|
||||||
style,
|
style,
|
||||||
@@ -94,7 +99,8 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
||||||
(m) => m[1] ?? '',
|
(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) {
|
for (const name of placeholderNames) {
|
||||||
const index = items.findIndex((p) => p.name === name);
|
const index = items.findIndex((p) => p.name === name);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -114,7 +120,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
const tabs: TabItem[] = useMemo(
|
const tabs: TabItem[] = useMemo(
|
||||||
() => [
|
() => [
|
||||||
{
|
{
|
||||||
value: 'body',
|
value: TAB_BODY,
|
||||||
options: {
|
options: {
|
||||||
value: activeRequest.bodyType,
|
value: activeRequest.bodyType,
|
||||||
items: [
|
items: [
|
||||||
@@ -180,16 +186,16 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'params',
|
value: TAB_PARAMS,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Params
|
Params
|
||||||
<CountBadge count={urlParameterPairs.filter((p) => p.name).length} />
|
<CountBadge count={urlParameterPairs.length} />
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'headers',
|
value: TAB_HEADERS,
|
||||||
label: (
|
label: (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
Headers
|
Headers
|
||||||
@@ -198,7 +204,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'auth',
|
value: TAB_AUTH,
|
||||||
label: 'Auth',
|
label: 'Auth',
|
||||||
options: {
|
options: {
|
||||||
value: activeRequest.authenticationType,
|
value: activeRequest.authenticationType,
|
||||||
@@ -292,6 +298,10 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
|
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
|
||||||
const importCurl = useImportCurl();
|
const importCurl = useImportCurl();
|
||||||
|
|
||||||
|
useOnFocusParamsTab(() => {
|
||||||
|
setActiveTab(TAB_PARAMS);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={style}
|
style={style}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import type { HttpRequest } from '@yaakapp/api';
|
import type { HttpRequest } from '@yaakapp/api';
|
||||||
|
import type { PairEditorRef } from './core/PairEditor';
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
|
import { useOnFocusParamValue } from './RequestEditorContext';
|
||||||
|
import { useRef } from 'react';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
@@ -9,9 +12,24 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props) {
|
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props) {
|
||||||
|
const pairEditor = useRef<PairEditorRef>(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 (
|
return (
|
||||||
<VStack className="h-full">
|
<VStack className="h-full">
|
||||||
<PairOrBulkEditor
|
<PairOrBulkEditor
|
||||||
|
ref={pairEditor}
|
||||||
preferenceName="url_parameters"
|
preferenceName="url_parameters"
|
||||||
valueAutocompleteVariables
|
valueAutocompleteVariables
|
||||||
nameAutocompleteVariables
|
nameAutocompleteVariables
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import { parseTemplate } from '../../../hooks/useParseTemplate';
|
|||||||
import { useSettings } from '../../../hooks/useSettings';
|
import { useSettings } from '../../../hooks/useSettings';
|
||||||
import { useTemplateFunctions } from '../../../hooks/useTemplateFunctions';
|
import { useTemplateFunctions } from '../../../hooks/useTemplateFunctions';
|
||||||
import { useDialog } from '../../DialogContext';
|
import { useDialog } from '../../DialogContext';
|
||||||
|
import { useRequestEditor } from '../../RequestEditorContext';
|
||||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||||
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||||
import { IconButton } from '../IconButton';
|
import { IconButton } from '../IconButton';
|
||||||
@@ -227,9 +228,13 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
[dialog],
|
[dialog],
|
||||||
);
|
);
|
||||||
|
|
||||||
const onClickPathParameter = useCallback(async (name: string) => {
|
const { focusParamValue } = useRequestEditor();
|
||||||
console.log('TODO: Focus', name, 'in params tab');
|
const onClickPathParameter = useCallback(
|
||||||
}, []);
|
async (name: string) => {
|
||||||
|
focusParamValue(name);
|
||||||
|
},
|
||||||
|
[focusParamValue],
|
||||||
|
);
|
||||||
|
|
||||||
// Update the language extension when the language changes
|
// Update the language extension when the language changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -120,7 +120,6 @@ function templateTags(
|
|||||||
const onClick = () => onClickPathParameter(rawText);
|
const onClick = () => onClickPathParameter(rawText);
|
||||||
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
|
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
|
||||||
const deco = Decoration.replace({ widget, inclusive: false });
|
const deco = Decoration.replace({ widget, inclusive: false });
|
||||||
console.log('ADDED WIDGET', globalFrom, node, rawText);
|
|
||||||
widgets.push(deco.range(globalFrom, globalTo));
|
widgets.push(deco.range(globalFrom, globalTo));
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -201,13 +200,6 @@ export function templateTagsPlugin(
|
|||||||
return view.plugin(plugin)?.decorations || Decoration.none;
|
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));
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { styleTags, tags as t } from '@lezer/highlight';
|
|||||||
export const highlight = styleTags({
|
export const highlight = styleTags({
|
||||||
Protocol: t.comment,
|
Protocol: t.comment,
|
||||||
Placeholder: t.emphasis,
|
Placeholder: t.emphasis,
|
||||||
PathSegment: t.tagName,
|
// PathSegment: t.tagName,
|
||||||
Port: t.attributeName,
|
// Port: t.attributeName,
|
||||||
Host: t.variableName,
|
// Host: t.variableName,
|
||||||
Path: t.bool,
|
// Path: t.bool,
|
||||||
Query: t.string,
|
// Query: t.string,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,15 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { EditorView } from 'codemirror';
|
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 type { XYCoord } from 'react-dnd';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
@@ -16,6 +25,10 @@ import type { InputProps } from './Input';
|
|||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
import { RadioDropdown } from './RadioDropdown';
|
import { RadioDropdown } from './RadioDropdown';
|
||||||
|
|
||||||
|
export interface PairEditorRef {
|
||||||
|
focusValue(index: number): void;
|
||||||
|
}
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
onChange: (pairs: Pair[]) => void;
|
onChange: (pairs: Pair[]) => void;
|
||||||
@@ -49,24 +62,28 @@ type PairContainer = {
|
|||||||
id: string;
|
id: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function PairEditor({
|
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
|
||||||
className,
|
{
|
||||||
forceUpdateKey,
|
className,
|
||||||
nameAutocomplete,
|
forceUpdateKey,
|
||||||
nameAutocompleteVariables,
|
nameAutocomplete,
|
||||||
namePlaceholder,
|
nameAutocompleteVariables,
|
||||||
nameValidate,
|
namePlaceholder,
|
||||||
valueType,
|
nameValidate,
|
||||||
onChange,
|
valueType,
|
||||||
noScroll,
|
onChange,
|
||||||
pairs: originalPairs,
|
noScroll,
|
||||||
valueAutocomplete,
|
pairs: originalPairs,
|
||||||
valueAutocompleteVariables,
|
valueAutocomplete,
|
||||||
valuePlaceholder,
|
valueAutocompleteVariables,
|
||||||
valueValidate,
|
valuePlaceholder,
|
||||||
allowFileValues,
|
valueValidate,
|
||||||
}: PairEditorProps) {
|
allowFileValues,
|
||||||
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
|
}: 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 [pairs, setPairs] = useState<PairContainer[]>(() => {
|
const [pairs, setPairs] = useState<PairContainer[]>(() => {
|
||||||
// Remove empty headers on initial render
|
// Remove empty headers on initial render
|
||||||
@@ -75,6 +92,13 @@ export function PairEditor({
|
|||||||
return [...pairs, newPairContainer()];
|
return [...pairs, newPairContainer()];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
focusValue(index: number) {
|
||||||
|
const id = pairs[index]?.id ?? 'n/a';
|
||||||
|
setForceFocusValuePairId(id);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Remove empty headers on initial render
|
// Remove empty headers on initial render
|
||||||
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
||||||
@@ -135,17 +159,18 @@ export function PairEditor({
|
|||||||
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;
|
||||||
setForceFocusPairId(id);
|
setForceFocusNamePairId(id);
|
||||||
}
|
}
|
||||||
return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
|
return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
|
||||||
},
|
},
|
||||||
[setPairsAndSave, setForceFocusPairId, pairs],
|
[setPairsAndSave, setForceFocusNamePairId, pairs],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleFocus = useCallback(
|
const handleFocus = useCallback(
|
||||||
(pair: PairContainer) =>
|
(pair: PairContainer) =>
|
||||||
setPairs((pairs) => {
|
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;
|
const isLast = pair.id === pairs[pairs.length - 1]?.id;
|
||||||
return isLast ? [...pairs, newPairContainer()] : pairs;
|
return isLast ? [...pairs, newPairContainer()] : pairs;
|
||||||
}),
|
}),
|
||||||
@@ -185,7 +210,8 @@ export function PairEditor({
|
|||||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||||
valueType={valueType}
|
valueType={valueType}
|
||||||
forceFocusPairId={forceFocusPairId}
|
forceFocusNamePairId={forceFocusNamePairId}
|
||||||
|
forceFocusValuePairId={forceFocusValuePairId}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
valueAutocomplete={valueAutocomplete}
|
valueAutocomplete={valueAutocomplete}
|
||||||
@@ -204,7 +230,7 @@ export function PairEditor({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
enum ItemTypes {
|
enum ItemTypes {
|
||||||
ROW = 'pair-row',
|
ROW = 'pair-row',
|
||||||
@@ -213,7 +239,8 @@ enum ItemTypes {
|
|||||||
type PairEditorRowProps = {
|
type PairEditorRowProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
pairContainer: PairContainer;
|
pairContainer: PairContainer;
|
||||||
forceFocusPairId?: string | null;
|
forceFocusNamePairId?: string | null;
|
||||||
|
forceFocusValuePairId?: string | null;
|
||||||
onMove: (id: string, side: 'above' | 'below') => void;
|
onMove: (id: string, side: 'above' | 'below') => void;
|
||||||
onEnd: (id: string) => void;
|
onEnd: (id: string) => void;
|
||||||
onChange: (pair: PairContainer) => void;
|
onChange: (pair: PairContainer) => void;
|
||||||
@@ -239,7 +266,8 @@ type PairEditorRowProps = {
|
|||||||
function PairEditorRow({
|
function PairEditorRow({
|
||||||
allowFileValues,
|
allowFileValues,
|
||||||
className,
|
className,
|
||||||
forceFocusPairId,
|
forceFocusNamePairId,
|
||||||
|
forceFocusValuePairId,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
isLast,
|
isLast,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
@@ -262,12 +290,19 @@ function PairEditorRow({
|
|||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const nameInputRef = useRef<EditorView>(null);
|
const nameInputRef = useRef<EditorView>(null);
|
||||||
|
const valueInputRef = useRef<EditorView>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (forceFocusPairId === pairContainer.id) {
|
if (forceFocusNamePairId === pairContainer.id) {
|
||||||
nameInputRef.current?.focus();
|
nameInputRef.current?.focus();
|
||||||
}
|
}
|
||||||
}, [forceFocusPairId, pairContainer.id]);
|
}, [forceFocusNamePairId, pairContainer.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (forceFocusValuePairId === pairContainer.id) {
|
||||||
|
valueInputRef.current?.focus();
|
||||||
|
}
|
||||||
|
}, [forceFocusValuePairId, pairContainer.id]);
|
||||||
|
|
||||||
const handleChangeEnabled = useMemo(
|
const handleChangeEnabled = useMemo(
|
||||||
() => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }),
|
() => (enabled: boolean) => onChange({ id, pair: { ...pairContainer.pair, enabled } }),
|
||||||
@@ -400,6 +435,7 @@ function PairEditorRow({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Input
|
<Input
|
||||||
|
ref={valueInputRef}
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
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 } from './PairEditor';
|
import type { PairEditorProps, PairEditorRef } from './PairEditor';
|
||||||
import { PairEditor } from './PairEditor';
|
import { PairEditor } from './PairEditor';
|
||||||
|
|
||||||
interface Props extends PairEditorProps {
|
interface Props extends PairEditorProps {
|
||||||
preferenceName: string;
|
preferenceName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOrBulkEditor(
|
||||||
|
{ 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],
|
||||||
@@ -18,7 +22,7 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full group/wrapper">
|
<div className="relative h-full w-full group/wrapper">
|
||||||
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />}
|
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor ref={ref} {...props} />}
|
||||||
<div className="absolute right-0 bottom-0">
|
<div className="absolute right-0 bottom-0">
|
||||||
<IconButton
|
<IconButton
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -34,4 +38,4 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
import { attachConsole } from '@tauri-apps/plugin-log';
|
|
||||||
import { type } from '@tauri-apps/plugin-os';
|
import { type } from '@tauri-apps/plugin-os';
|
||||||
import { StrictMode } from 'react';
|
import { StrictMode } from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
@@ -18,8 +17,6 @@ if (osType !== 'macos') {
|
|||||||
await getCurrentWebviewWindow().setDecorations(false);
|
await getCurrentWebviewWindow().setDecorations(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
await attachConsole();
|
|
||||||
|
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
// Hack to not go back in history on backspace. Check for document body
|
// Hack to not go back in history on backspace. Check for document body
|
||||||
// or else it will prevent backspace in input fields.
|
// or else it will prevent backspace in input fields.
|
||||||
|
|||||||
Reference in New Issue
Block a user