From ee6c7b6b1ab8d82ea72d64900d4d7a7c89616443 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Fri, 31 May 2024 10:37:21 -0700 Subject: [PATCH 1/2] Also release on beta branch --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a66e057a..2e996410 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,6 +3,7 @@ on: push: branches: - release + - beta jobs: build-artifacts: permissions: From 3135f9c187fb7e908deeae5d997ee34a856c9c06 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Mon, 3 Jun 2024 14:03:25 -0700 Subject: [PATCH 2/2] 2024.5.0-beta.2 (#38) --- plugins/importer-postman/src/index.ts | 2 + src-tauri/plugins/importer-postman/index.mjs | 1 + src-tauri/src/http_request.rs | 14 ++- src-tauri/src/lib.rs | 8 +- src-tauri/tauri.conf.json | 2 +- src-web/components/EnvironmentEditDialog.tsx | 2 +- src-web/components/FormUrlencodedEditor.tsx | 2 + src-web/components/GlobalHooks.tsx | 7 +- src-web/components/RequestPane.tsx | 14 ++- .../Settings/SettingsAppearance.tsx | 15 +-- .../components/Settings/SettingsGeneral.tsx | 5 +- src-web/components/UrlParameterEditor.tsx | 2 + src-web/components/core/Button.tsx | 2 +- src-web/components/core/Dropdown.tsx | 20 +-- src-web/components/core/Editor/Editor.css | 4 +- src-web/components/core/Editor/Editor.tsx | 38 +++--- src-web/components/core/Editor/extensions.ts | 8 +- .../core/Editor/genericCompletion.ts | 6 +- .../components/core/Editor/twig/extension.ts | 24 ++-- src-web/components/core/FormattedError.tsx | 2 +- src-web/components/core/Input.tsx | 6 +- src-web/components/core/PairEditor.tsx | 12 +- src-web/components/core/PlainInput.tsx | 6 +- src-web/components/core/RadioDropdown.tsx | 6 +- src-web/components/core/Select.tsx | 117 ++++++++++-------- .../components/responseViewers/TextViewer.tsx | 53 +++++--- src-web/hooks/useFilterResponse.ts | 2 +- src-web/hooks/useIsFullscreen.ts | 32 +++-- src-web/lib/theme/themes/moonlight.ts | 14 +-- 29 files changed, 242 insertions(+), 184 deletions(-) diff --git a/plugins/importer-postman/src/index.ts b/plugins/importer-postman/src/index.ts index 43af714c..b0ddbffa 100644 --- a/plugins/importer-postman/src/index.ts +++ b/plugins/importer-postman/src/index.ts @@ -180,6 +180,7 @@ function importBody(rawBody: any): Pick(obj: T): T { } const idCount: Partial> = {}; + function generateId(model: Model['model']): string { idCount[model] = (idCount[model] ?? -1) + 1; return `GENERATE_ID::${model.toUpperCase()}_${idCount[model]}`; diff --git a/src-tauri/plugins/importer-postman/index.mjs b/src-tauri/plugins/importer-postman/index.mjs index fd6d58c9..0e813e99 100644 --- a/src-tauri/plugins/importer-postman/index.mjs +++ b/src-tauri/plugins/importer-postman/index.mjs @@ -131,6 +131,7 @@ function j(e) { form: b(t.formdata).map( (n) => n.src != null ? { enabled: !n.disabled, + contentType: n.contentType ?? null, name: n.key ?? "", file: n.src ?? "" } : { diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index d260eb26..23d25fb3 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -89,14 +89,24 @@ pub async fn send_http_request( let uri = match http::Uri::from_str(url_string.as_str()) { Ok(u) => u, Err(e) => { - return response_err(response, e.to_string(), window).await; + return response_err( + response, + format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()), + window, + ) + .await; } }; // Yes, we're parsing both URI and URL because they could return different errors let url = match Url::from_str(uri.to_string().as_str()) { Ok(u) => u, Err(e) => { - return response_err(response, e.to_string(), window).await; + return response_err( + response, + format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()), + window, + ) + .await; } }; diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6d0ddf3b..1bc5203e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -752,16 +752,16 @@ async fn cmd_import_data( ) -> Result { let mut result: Option = None; let plugins = vec![ - "importer-yaak", - "importer-insomnia", "importer-postman", + "importer-insomnia", + "importer-yaak", "importer-curl", ]; - let file = fs::read_to_string(file_path) + let file = read_to_string(file_path) .unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); let file_contents = file.as_str(); for plugin_name in plugins { - let v = plugin::run_plugin_import(&w.app_handle(), plugin_name, file_contents) + let v = run_plugin_import(&w.app_handle(), plugin_name, file_contents) .await .map_err(|e| e.to_string())?; if let Some(r) = v { diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 6434052c..7cac88fb 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -6,7 +6,7 @@ "frontendDist": "../dist" }, "productName": "Yaak", - "version": "2024.5.0-beta.1", + "version": "2024.5.0", "identifier": "app.yaak.desktop", "app": { "withGlobalTauri": false, diff --git a/src-web/components/EnvironmentEditDialog.tsx b/src-web/components/EnvironmentEditDialog.tsx index 525e89b3..93969962 100644 --- a/src-web/components/EnvironmentEditDialog.tsx +++ b/src-web/components/EnvironmentEditDialog.tsx @@ -281,7 +281,7 @@ function SidebarButton({ }, }, { - key: 'delete', + key: 'delete-environment', variant: 'danger', label: 'Delete', leftSlot: , diff --git a/src-web/components/FormUrlencodedEditor.tsx b/src-web/components/FormUrlencodedEditor.tsx index 7780895b..80ca6816 100644 --- a/src-web/components/FormUrlencodedEditor.tsx +++ b/src-web/components/FormUrlencodedEditor.tsx @@ -30,6 +30,8 @@ export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) (queryKey, (values = []) => { const index = values.findIndex((v) => modelsEq(v, model)) ?? -1; if (index >= 0) { - // console.log('UPDATED', payload); return [...values.slice(0, index), model, ...values.slice(index + 1)]; } else { - // console.log('CREATED', payload); return pushToFront ? [model, ...(values ?? [])] : [...(values ?? []), model]; } }); @@ -117,6 +118,8 @@ export function GlobalHooks() { queryClient.setQueryData(httpResponsesQueryKey(model), removeById(model)); } else if (model.model === 'folder') { queryClient.setQueryData(foldersQueryKey(model), removeById(model)); + } else if (model.model === 'environment') { + queryClient.setQueryData(environmentsQueryKey(model), removeById(model)); } else if (model.model === 'grpc_request') { queryClient.setQueryData(grpcRequestsQueryKey(model), removeById(model)); } else if (model.model === 'grpc_connection') { diff --git a/src-web/components/RequestPane.tsx b/src-web/components/RequestPane.tsx index b35b6a68..ea57214d 100644 --- a/src-web/components/RequestPane.tsx +++ b/src-web/components/RequestPane.tsx @@ -109,11 +109,14 @@ export const RequestPane = memo(function RequestPane({ if (bodyType === BODY_TYPE_NONE) { newContentType = null; } else if ( - bodyType === BODY_TYPE_FORM_URLENCODED || - bodyType === BODY_TYPE_FORM_MULTIPART || - bodyType === BODY_TYPE_JSON || - bodyType === BODY_TYPE_OTHER || - bodyType === BODY_TYPE_XML + activeRequest.method.toLowerCase() !== 'put' && + activeRequest.method.toLowerCase() !== 'patch' && + activeRequest.method.toLowerCase() !== 'post' && + (bodyType === BODY_TYPE_FORM_URLENCODED || + bodyType === BODY_TYPE_FORM_MULTIPART || + bodyType === BODY_TYPE_JSON || + bodyType === BODY_TYPE_OTHER || + bodyType === BODY_TYPE_XML) ) { patch.method = 'POST'; newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType; @@ -181,6 +184,7 @@ export const RequestPane = memo(function RequestPane({ activeRequest.authenticationType, activeRequest.bodyType, activeRequest.headers, + activeRequest.method, activeRequest.urlParameters, handleContentTypeChange, updateRequest, diff --git a/src-web/components/Settings/SettingsAppearance.tsx b/src-web/components/Settings/SettingsAppearance.tsx index c63b3a6f..e80c147c 100644 --- a/src-web/components/Settings/SettingsAppearance.tsx +++ b/src-web/components/Settings/SettingsAppearance.tsx @@ -14,7 +14,7 @@ import { Editor } from '../core/Editor'; import type { IconProps } from '../core/Icon'; import { Icon } from '../core/Icon'; import { IconButton } from '../core/IconButton'; -import type { SelectOption } from '../core/Select'; +import type { SelectProps } from '../core/Select'; import { Select } from '../core/Select'; import { Separator } from '../core/Separator'; import { HStack, VStack } from '../core/Stacks'; @@ -64,14 +64,14 @@ export function SettingsAppearance() { return null; } - const lightThemes: SelectOption[] = themes + const lightThemes: SelectProps['options'] = themes .filter((theme) => !isThemeDark(theme)) .map((theme) => ({ label: theme.name, value: theme.id, })); - const darkThemes: SelectOption[] = themes + const darkThemes: SelectProps['options'] = themes .filter((theme) => isThemeDark(theme)) .map((theme) => ({ label: theme.name, @@ -109,7 +109,7 @@ export function SettingsAppearance() { { trackEvent('theme', 'update', { theme: themeLight, appearance: 'light' }); updateSettings.mutateAsync({ ...settings, themeLight }); @@ -143,11 +143,12 @@ export function SettingsAppearance() { parseInt(value) >= 0} onChange={(v) => updateWorkspace.mutate({ settingRequestTimeout: parseInt(v) || 0 })} + type="number" /> (function Button {isLoading ? ( ) : leftSlot ? ( -
{leftSlot}
+
{leftSlot}
) : null}
void; onClose?: () => void; + fullWidth?: boolean; hotKeyAction?: HotkeyAction; } @@ -73,7 +74,7 @@ export interface DropdownRef { } export const Dropdown = forwardRef(function Dropdown( - { children, items, onOpen, onClose, hotKeyAction }: DropdownProps, + { children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps, ref, ) { const [isOpen, _setIsOpen] = useState(false); @@ -153,6 +154,7 @@ export const Dropdown = forwardRef(function Dropdown | null; onClose: () => void; showTriangle?: boolean; + fullWidth?: boolean; isOpen: boolean; } @@ -211,6 +214,7 @@ const Menu = forwardRef, MenuPro className, isOpen, items, + fullWidth, onClose, triggerShape, defaultSelectedIndex, @@ -359,21 +363,23 @@ const Menu = forwardRef, MenuPro const heightAbove = triggerShape.top; const heightBelow = docRect.height - triggerShape.bottom; const hSpaceRemaining = docRect.width - triggerShape.left; - const top = triggerShape?.bottom + 5; + const top = triggerShape.bottom + 5; const onRight = hSpaceRemaining < 200; const upsideDown = heightAbove > heightBelow && heightBelow < 200; + const triggerWidth = triggerShape.right - triggerShape.left; const containerStyles = { top: !upsideDown ? top : undefined, bottom: upsideDown ? docRect.height - top : undefined, - right: onRight ? docRect.width - triggerShape?.right : undefined, - left: !onRight ? triggerShape?.left : undefined, + right: onRight ? docRect.width - triggerShape.right : undefined, + left: !onRight ? triggerShape.left : undefined, + minWidth: fullWidth ? triggerWidth : undefined, }; const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' }; const triangleStyles = onRight ? { right: width / 2, marginRight: '-0.2rem', ...size } : { left: width / 2, marginLeft: '-0.2rem', ...size }; return { containerStyles, triangleStyles }; - }, [triggerShape]); + }, [fullWidth, triggerShape]); const filteredItems = useMemo( () => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())), @@ -521,9 +527,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men onClick={handleClick} justify="start" leftSlot={ - item.leftSlot && ( -
{item.leftSlot}
- ) + item.leftSlot &&
{item.leftSlot}
} rightSlot={rightSlot &&
{rightSlot}
} innerClassName="!text-left" diff --git a/src-web/components/core/Editor/Editor.css b/src-web/components/core/Editor/Editor.css index 7d78ab04..af6e0ee6 100644 --- a/src-web/components/core/Editor/Editor.css +++ b/src-web/components/core/Editor/Editor.css @@ -266,7 +266,7 @@ } &.cm-completionInfo-right { - @apply ml-1 -mt-0.5; + @apply ml-1 -mt-0.5 font-sans; } &.cm-completionInfo-right-narrow { @@ -278,7 +278,7 @@ } &.cm-tooltip-autocomplete { - @apply font-mono text-editor; + @apply font-mono; & > ul { @apply p-1 max-h-[40vh]; diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 79ac8d34..61110b7b 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -195,7 +195,7 @@ export const Editor = forwardRef(function E placeholderCompartment.current.of( placeholderExt(placeholderElFromText(placeholder ?? '')), ), - wrapLinesCompartment.current.of([]), + wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []), ...getExtensions({ container, readOnly, @@ -331,7 +331,25 @@ function getExtensions({ undefined; return [ - // NOTE: These *must* be anonymous functions so the references update properly + ...baseExtensions, // Must be first + tooltips({ parent }), + keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), + ...(singleLine ? [singleLineExt()] : []), + ...(!singleLine ? [multiLineExtensions] : []), + ...(readOnly + ? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })] + : []), + + // ------------------------ // + // Things that must be last // + // ------------------------ // + + EditorView.updateListener.of((update) => { + if (onChange && update.docChanged) { + onChange.current?.(update.state.doc.toString()); + } + }), + EditorView.domEventHandlers({ focus: () => { onFocus.current?.(); @@ -346,22 +364,6 @@ function getExtensions({ onPaste.current?.(e.clipboardData?.getData('text/plain') ?? ''); }, }), - tooltips({ parent }), - keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), - ...(singleLine ? [singleLineExt()] : []), - ...(!singleLine ? [multiLineExtensions] : []), - ...(readOnly - ? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })] - : []), - - // Handle onChange - EditorView.updateListener.of((update) => { - if (onChange && update.docChanged) { - onChange.current?.(update.state.doc.toString()); - } - }), - - ...baseExtensions, ]; } diff --git a/src-web/components/core/Editor/extensions.ts b/src-web/components/core/Editor/extensions.ts index 47c13e4b..7cab4610 100644 --- a/src-web/components/core/Editor/extensions.ts +++ b/src-web/components/core/Editor/extensions.ts @@ -39,7 +39,7 @@ import { text } from './text/extension'; import { twig } from './twig/extension'; import { url } from './url/extension'; -export const myHighlightStyle = HighlightStyle.define([ +export const syntaxHighlightStyle = HighlightStyle.define([ { tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment], color: 'var(--fg-subtler)', @@ -61,7 +61,7 @@ export const myHighlightStyle = HighlightStyle.define([ { tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' }, ]); -const myTheme = EditorView.theme({}, { dark: true }); +const syntaxTheme = EditorView.theme({}, { dark: true }); const syntaxExtensions: Record = { 'application/graphql': graphqlLanguageSupport(), @@ -108,8 +108,8 @@ export const baseExtensions = [ return (a.boost ?? 0) - (b.boost ?? 0); }, }), - syntaxHighlighting(myHighlightStyle), - myTheme, + syntaxHighlighting(syntaxHighlightStyle), + syntaxTheme, EditorState.allowMultipleSelections.of(true), ]; diff --git a/src-web/components/core/Editor/genericCompletion.ts b/src-web/components/core/Editor/genericCompletion.ts index fa6b4056..1ba136b5 100644 --- a/src-web/components/core/Editor/genericCompletion.ts +++ b/src-web/components/core/Editor/genericCompletion.ts @@ -20,7 +20,11 @@ export interface GenericCompletionConfig { /** * Complete options, always matching until the start of the line */ -export function genericCompletion({ options, minMatch = 1 }: GenericCompletionConfig) { +export function genericCompletion(config?: GenericCompletionConfig) { + if (config == null) return []; + + const { minMatch = 1, options } = config; + return function completions(context: CompletionContext) { const toMatch = context.matchBefore(/.*/); diff --git a/src-web/components/core/Editor/twig/extension.ts b/src-web/components/core/Editor/twig/extension.ts index 95ed4c19..2c68c6fa 100644 --- a/src-web/components/core/Editor/twig/extension.ts +++ b/src-web/components/core/Editor/twig/extension.ts @@ -1,13 +1,13 @@ import type { LanguageSupport } from '@codemirror/language'; import { LRLanguage } from '@codemirror/language'; import { parseMixed } from '@lezer/common'; +import type { Environment, Workspace } from '../../../../lib/models'; import type { GenericCompletionConfig } from '../genericCompletion'; import { genericCompletion } from '../genericCompletion'; -import { placeholders } from './placeholder'; import { textLanguageName } from '../text/extension'; import { twigCompletion } from './completion'; +import { placeholders } from './placeholder'; import { parser as twigParser } from './twig'; -import type { Environment, Workspace } from '../../../../lib/models'; export function twig( base: LanguageSupport, @@ -15,25 +15,19 @@ export function twig( workspace: Workspace | null, autocomplete?: GenericCompletionConfig, ) { - const variables = - [...(workspace?.variables ?? []), ...(environment?.variables ?? [])].filter((v) => v.enabled) ?? - []; - const completions = twigCompletion({ options: variables }); - const language = mixLanguage(base); - const completion = language.data.of({ autocomplete: completions }); - const completionBase = base.language.data.of({ autocomplete: completions }); - const additionalCompletion = autocomplete - ? [base.language.data.of({ autocomplete: genericCompletion(autocomplete) })] - : []; + const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])]; + const variables = allVariables.filter((v) => v.enabled) ?? []; + const completions = twigCompletion({ options: variables }); return [ language, - completion, - completionBase, base.support, placeholders(variables), - ...additionalCompletion, + language.data.of({ autocomplete: completions }), + base.language.data.of({ autocomplete: completions }), + language.data.of({ autocomplete: genericCompletion(autocomplete) }), + base.language.data.of({ autocomplete: genericCompletion(autocomplete) }), ]; } diff --git a/src-web/components/core/FormattedError.tsx b/src-web/components/core/FormattedError.tsx index 87a159db..8539b4a3 100644 --- a/src-web/components/core/FormattedError.tsx +++ b/src-web/components/core/FormattedError.tsx @@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) { return (
diff --git a/src-web/components/core/Input.tsx b/src-web/components/core/Input.tsx
index 50d18acb..7f8a734f 100644
--- a/src-web/components/core/Input.tsx
+++ b/src-web/components/core/Input.tsx
@@ -133,7 +133,11 @@ export const Input = forwardRef(function Inp
     >
       
diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx
index bd8dea68..8bf7f8b6 100644
--- a/src-web/components/core/PairEditor.tsx
+++ b/src-web/components/core/PairEditor.tsx
@@ -389,7 +389,7 @@ function PairEditorRow({
             
           ) : (
              {
   const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
   return { id, pair };
 };
-
-const getFileName = (path: string | null | undefined): string => {
-  if (typeof path !== 'string') return '';
-  const parts = path.split(/[\\/]/);
-  return parts[parts.length - 1] ?? '';
-};
diff --git a/src-web/components/core/PlainInput.tsx b/src-web/components/core/PlainInput.tsx
index bb058b0d..9995f378 100644
--- a/src-web/components/core/PlainInput.tsx
+++ b/src-web/components/core/PlainInput.tsx
@@ -85,7 +85,11 @@ export const PlainInput = forwardRef(function
     >
       
diff --git a/src-web/components/core/RadioDropdown.tsx b/src-web/components/core/RadioDropdown.tsx
index 2ed68567..2f689d93 100644
--- a/src-web/components/core/RadioDropdown.tsx
+++ b/src-web/components/core/RadioDropdown.tsx
@@ -50,5 +50,9 @@ export function RadioDropdown({
     [items, extraItems, value, onChange],
   );
 
-  return {children};
+  return (
+    
+      {children}
+    
+  );
 }
diff --git a/src-web/components/core/Select.tsx b/src-web/components/core/Select.tsx
index 79b15dfb..5a086d98 100644
--- a/src-web/components/core/Select.tsx
+++ b/src-web/components/core/Select.tsx
@@ -1,9 +1,14 @@
 import classNames from 'classnames';
 import type { CSSProperties, ReactNode } from 'react';
 import { useState } from 'react';
+import { useOsInfo } from '../../hooks/useOsInfo';
+import type { ButtonProps } from './Button';
+import { Button } from './Button';
+import type { RadioDropdownItem } from './RadioDropdown';
+import { RadioDropdown } from './RadioDropdown';
 import { HStack } from './Stacks';
 
-interface Props {
+export interface SelectProps {
   name: string;
   label: string;
   labelPosition?: 'top' | 'left';
@@ -11,22 +16,12 @@ interface Props {
   hideLabel?: boolean;
   value: T;
   leftSlot?: ReactNode;
-  options: SelectOption[] | SelectOptionGroup[];
+  options: RadioDropdownItem[];
   onChange: (value: T) => void;
-  size?: 'xs' | 'sm' | 'md' | 'lg';
+  size?: ButtonProps['size'];
   className?: string;
 }
 
-export interface SelectOption {
-  label: string;
-  value: T;
-}
-
-export interface SelectOptionGroup {
-  label: string;
-  options: SelectOption[];
-}
-
 export function Select({
   labelPosition = 'top',
   name,
@@ -39,7 +34,8 @@ export function Select({
   onChange,
   className,
   size = 'md',
-}: Props) {
+}: SelectProps) {
+  const osInfo = useOsInfo();
   const [focused, setFocused] = useState(false);
   const id = `input-${name}`;
   return (
@@ -49,55 +45,68 @@ export function Select({
         'x-theme-input',
         'w-full',
         'pointer-events-auto', // Just in case we're placing in disabled parent
-        labelPosition === 'left' && 'flex items-center gap-2',
+        labelPosition === 'left' && 'grid grid-cols-[auto_1fr] items-center gap-2',
         labelPosition === 'top' && 'flex-row gap-0.5',
       )}
     >
       
-      
-        {leftSlot && 
{leftSlot}
} - onChange(e.target.value as T)} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} + className={classNames('pr-7 w-full outline-none bg-transparent')} + > + {options.map((o) => { + if (o.type === 'separator') return null; + return ( + + ); + })} + +
+ ) : ( + // Use custom "select" component until Tauri can be configured to have select menus not always appear in + // light mode + + + + )}
); } diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index 241faa1d..a396c3e5 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -1,11 +1,11 @@ import classNames from 'classnames'; import type { ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; +import { createGlobalState } from 'react-use'; import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders'; -import { useDebouncedState } from '../../hooks/useDebouncedState'; +import { useDebouncedValue } from '../../hooks/useDebouncedValue'; import { useFilterResponse } from '../../hooks/useFilterResponse'; import { useResponseBodyText } from '../../hooks/useResponseBodyText'; -import { useToggle } from '../../hooks/useToggle'; import { tryFormatJson, tryFormatXml } from '../../lib/formatters'; import type { HttpResponse } from '../../lib/models'; import { Editor } from '../core/Editor'; @@ -21,25 +21,42 @@ interface Props { className?: string; } +const useFilterText = createGlobalState>({}); + export function TextViewer({ response, pretty, className }: Props) { - const [isSearching, toggleIsSearching] = useToggle(); - const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState('', 400); + const [filterTextMap, setFilterTextMap] = useFilterText(); + const filterText = filterTextMap[response.id] ?? null; + const debouncedFilterText = useDebouncedValue(filterText, 300); + const setFilterText = useCallback( + (v: string | null) => { + setFilterTextMap((m) => ({ ...m, [response.id]: v })); + }, + [setFilterTextMap, response], + ); const contentType = useContentTypeFromHeaders(response.headers); const rawBody = useResponseBodyText(response) ?? ''; + const isSearching = filterText != null; const formattedBody = pretty && contentType?.includes('json') ? tryFormatJson(rawBody) : pretty && contentType?.includes('xml') ? tryFormatXml(rawBody) : rawBody; - const filteredResponse = useFilterResponse({ filter: filterText, responseId: response.id }); - const body = filteredResponse ?? formattedBody; - const clearSearch = useCallback(() => { - toggleIsSearching(); - setFilterText(''); - }, [setFilterText, toggleIsSearching]); + const filteredResponse = useFilterResponse({ + filter: debouncedFilterText ?? '', + responseId: response.id, + }); + + const body = isSearching && filterText?.length > 0 ? filteredResponse : formattedBody; + const toggleSearch = useCallback(() => { + if (isSearching) { + setFilterText(null); + } else { + setFilterText(''); + } + }, [isSearching, setFilterText]); const isJson = contentType?.includes('json'); const isXml = contentType?.includes('xml') || contentType?.includes('html'); @@ -54,16 +71,17 @@ export function TextViewer({ response, pretty, className }: Props) { result.push(
e.key === 'Escape' && clearSearch()} - onChange={setDebouncedFilterText} + onKeyDown={(e) => e.key === 'Escape' && toggleSearch()} + onChange={setFilterText} />
, ); @@ -75,13 +93,16 @@ export function TextViewer({ response, pretty, className }: Props) { size="sm" icon={isSearching ? 'x' : 'filter'} title={isSearching ? 'Close filter' : 'Filter response'} - onClick={clearSearch} - className={classNames(isSearching && '!opacity-100')} + onClick={toggleSearch} + className={classNames( + 'bg-background border !border-background-highlight', + isSearching && '!opacity-100', + )} />, ); return result; - }, [canFilter, clearSearch, filterText, isJson, isSearching, setDebouncedFilterText]); + }, [canFilter, filterText, isJson, isSearching, setFilterText, toggleSearch]); return ( (false); const windowSize = useWindowSize(); + const debouncedWindowWidth = useDebouncedValue(windowSize.width); - useEffect(() => { - (async function () { - // Fullscreen state isn't updated right after resize event on Mac (needs to wait for animation) so - // we'll poll for 10 seconds to see if it changes. Hopefully Tauri eventually adds a way to listen - // for this. - for (let i = 0; i < 100; i++) { - await new Promise((resolve) => setTimeout(resolve, 100)); - const newIsFullscreen = await getCurrent().isFullscreen(); - if (newIsFullscreen !== isFullscreen) { - setIsFullscreen(newIsFullscreen); - break; - } - } - })(); - }, [windowSize, isFullscreen]); + // NOTE: Fullscreen state isn't updated right after resize event on Mac (needs to wait for animation) so + // we'll wait for a bit using the debounced window size. Hopefully Tauri eventually adds a way to listen + // for fullscreen change events. - return isFullscreen; + return ( + useQuery({ + queryKey: ['is_fullscreen', debouncedWindowWidth], + queryFn: async () => { + return getCurrent().isFullscreen(); + }, + }).data ?? false + ); } diff --git a/src-web/lib/theme/themes/moonlight.ts b/src-web/lib/theme/themes/moonlight.ts index eaa44f5e..a24a0cae 100644 --- a/src-web/lib/theme/themes/moonlight.ts +++ b/src-web/lib/theme/themes/moonlight.ts @@ -40,12 +40,10 @@ export const colors = { const moonlightDefault: YaakTheme = { id: 'moonlight', name: 'Moonlight', - background: new Color(colors.gray4, 'dark'), - backgroundHighlight: new Color(colors.gray5, 'dark'), - backgroundHighlightSecondary: new Color(colors.gray5, 'dark'), - foreground: new Color(colors.gray11, 'dark'), - foregroundSubtle: new Color(colors.gray7, 'dark'), - foregroundSubtler: new Color(colors.gray6, 'dark'), + background: new Color('#222436', 'dark'), + foreground: new Color('#d5def8', 'dark'), + foregroundSubtle: new Color('#828bb8', 'dark'), + foregroundSubtler: new Color('hsl(232,26%,43%)', 'dark'), colors: { primary: new Color(colors.purple, 'dark'), secondary: new Color(colors.desaturatedGray, 'dark'), @@ -58,13 +56,9 @@ const moonlightDefault: YaakTheme = { components: { appHeader: { background: new Color(colors.gray3, 'dark'), - backgroundHighlight: new Color(colors.gray5, 'dark'), - backgroundHighlightSecondary: new Color(colors.gray4, 'dark'), }, sidebar: { background: new Color(colors.gray3, 'dark'), - backgroundHighlight: new Color(colors.gray5, 'dark'), - backgroundHighlightSecondary: new Color(colors.gray4, 'dark'), }, }, };