mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-11 20:00:29 +01:00
Multi-line multi-part values
This commit is contained in:
20
packages/common-lib/formatSize.ts
Normal file
20
packages/common-lib/formatSize.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
export function formatSize(bytes: number): string {
|
||||
let num;
|
||||
let unit;
|
||||
|
||||
if (bytes > 1000 * 1000 * 1000) {
|
||||
num = bytes / 1000 / 1000 / 1000;
|
||||
unit = 'GB';
|
||||
} else if (bytes > 1000 * 1000) {
|
||||
num = bytes / 1000 / 1000;
|
||||
unit = 'MB';
|
||||
} else if (bytes > 1000) {
|
||||
num = bytes / 1000;
|
||||
unit = 'KB';
|
||||
} else {
|
||||
num = bytes;
|
||||
unit = 'B';
|
||||
}
|
||||
|
||||
return `${Math.round(num * 10) / 10} ${unit}`;
|
||||
}
|
||||
@@ -326,14 +326,33 @@ pub async fn send_http_request<R: Runtime>(
|
||||
|
||||
// Set or guess mimetype
|
||||
if !content_type.is_empty() {
|
||||
part = part.mime_str(content_type).map_err(|e| e.to_string())?;
|
||||
part = match part.mime_str(content_type) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&*response.lock().await,
|
||||
format!("Invalid mime for multi-part entry {e:?}"),
|
||||
window,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
};
|
||||
} else if !file_path.is_empty() {
|
||||
let default_mime =
|
||||
Mime::from_str("application/octet-stream").unwrap();
|
||||
let mime =
|
||||
mime_guess::from_path(file_path.clone()).first_or(default_mime);
|
||||
part =
|
||||
part.mime_str(mime.essence_str()).map_err(|e| e.to_string())?;
|
||||
part = match part.mime_str(mime.essence_str()) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
return Ok(response_err(
|
||||
&*response.lock().await,
|
||||
format!("Invalid mime for multi-part entry {e:?}"),
|
||||
window,
|
||||
)
|
||||
.await);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Set file path if not empty
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_
|
||||
use log::{info, warn};
|
||||
use std::process::exit;
|
||||
use tauri::{
|
||||
AppHandle, Emitter, LogicalSize, Manager, Runtime, TitleBarStyle, WebviewUrl, WebviewWindow,
|
||||
AppHandle, Emitter, LogicalSize, Manager, Runtime, WebviewUrl, WebviewWindow,
|
||||
};
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
use tokio::sync::mpsc;
|
||||
@@ -67,6 +67,7 @@ pub(crate) fn create_window<R: Runtime>(
|
||||
if config.hide_titlebar {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use tauri::TitleBarStyle;
|
||||
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@@ -7,7 +7,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { Button } from './core/Button';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import type { EditorProps } from './core/Editor/Editor';
|
||||
@@ -178,7 +177,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
Variables
|
||||
</Separator>
|
||||
<Editor
|
||||
format={tryFormatJson}
|
||||
language="json"
|
||||
heightMode="auto"
|
||||
defaultValue={currentBody.variables}
|
||||
|
||||
@@ -14,7 +14,6 @@ import { useEffect, useMemo, useRef } from 'react';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { showAlert } from '../lib/alert';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { pluralizeCount } from '../lib/pluralize';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor/Editor';
|
||||
@@ -186,7 +185,6 @@ export function GrpcEditor({
|
||||
useTemplating
|
||||
forceUpdateKey={request.id}
|
||||
defaultValue={request.message}
|
||||
format={tryFormatJson}
|
||||
heightMode="auto"
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||
import type {GenericCompletionOption} from "@yaakapp-internal/plugins";
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
@@ -21,7 +21,6 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import { generateId } from '../lib/generateId';
|
||||
import {
|
||||
BODY_TYPE_BINARY,
|
||||
@@ -407,7 +406,6 @@ export const RequestPane = memo(function RequestPane({
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
language="json"
|
||||
onChange={handleBodyTextChange}
|
||||
format={tryFormatJson}
|
||||
stateKey={`json.${activeRequest.id}`}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
|
||||
@@ -217,6 +217,7 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
|
||||
const itemPrefix = (item.model === 'http_request' || item.model === 'grpc_request') && (
|
||||
<HttpMethodTag
|
||||
shortNames
|
||||
request={item}
|
||||
className={classNames(!(active || selected) && 'text-text-subtlest')}
|
||||
/>
|
||||
@@ -271,7 +272,7 @@ export const SidebarItem = memo(function SidebarItem({
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<span className="truncate">{itemName}</span>
|
||||
<span className="truncate w-full">{itemName}</span>
|
||||
)}
|
||||
</div>
|
||||
{latestGrpcConnection ? (
|
||||
|
||||
@@ -29,6 +29,7 @@ import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||
import { useSettings } from '../../../hooks/useSettings';
|
||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||
import { showDialog } from '../../../lib/dialog';
|
||||
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
@@ -134,7 +135,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
readOnly = true;
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
if (
|
||||
@@ -147,6 +148,15 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
disableTabIndent = true;
|
||||
}
|
||||
|
||||
if (format == null) {
|
||||
format =
|
||||
language === 'json'
|
||||
? tryFormatJson
|
||||
: language === 'xml' || language === 'html'
|
||||
? tryFormatXml
|
||||
: undefined;
|
||||
}
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view, []);
|
||||
|
||||
|
||||
@@ -15,18 +15,16 @@ const longMethodMap = {
|
||||
delete: 'DELETE',
|
||||
options: 'OPTIONS',
|
||||
head: 'HEAD',
|
||||
grpc: 'GRPC',
|
||||
} as const;
|
||||
|
||||
const shortMethodMap: Record<keyof typeof longMethodMap, string> = {
|
||||
get: 'GET',
|
||||
put: 'PUT',
|
||||
post: 'POST',
|
||||
patch: 'PTCH',
|
||||
post: 'PST',
|
||||
patch: 'PTC',
|
||||
delete: 'DEL',
|
||||
options: 'OPTS',
|
||||
head: 'HEAD',
|
||||
grpc: 'GRPC',
|
||||
options: 'OPT',
|
||||
head: 'HED',
|
||||
};
|
||||
|
||||
export function HttpMethodTag({ shortNames, request, className }: Props) {
|
||||
@@ -34,7 +32,7 @@ export function HttpMethodTag({ shortNames, request, className }: Props) {
|
||||
request.model === 'http_request' && request.bodyType === 'graphql'
|
||||
? 'GQL'
|
||||
: request.model === 'grpc_request'
|
||||
? 'GRPC'
|
||||
? 'GRP'
|
||||
: request.method;
|
||||
|
||||
const m = method.toLowerCase();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { formatSize } from '@yaakapp-internal/lib/formatSize';
|
||||
import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import {
|
||||
@@ -13,6 +14,8 @@ import {
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { languageFromContentType } from '../../lib/contentType';
|
||||
import { showDialog } from '../../lib/dialog';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { showPrompt } from '../../lib/prompt';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
@@ -21,6 +24,8 @@ import { Button } from './Button';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import type { DropdownItem } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import { Editor } from './Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -326,6 +331,7 @@ function PairEditorRow({
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const nameInputRef = useRef<EditorView>(null);
|
||||
const valueInputRef = useRef<EditorView>(null);
|
||||
const valueLanguage = languageFromContentType(pair.contentType ?? null);
|
||||
|
||||
useEffect(() => {
|
||||
if (forceFocusNamePairId === pair.id) {
|
||||
@@ -380,6 +386,24 @@ function PairEditorRow({
|
||||
[onChange, pair],
|
||||
);
|
||||
|
||||
const handleEditMultiLineValue = useCallback(
|
||||
() =>
|
||||
showDialog({
|
||||
id: 'pair-edit-multiline',
|
||||
size: 'dynamic',
|
||||
title: <>Edit {pair.name}</>,
|
||||
render: ({ hide }) => (
|
||||
<MultilineEditDialog
|
||||
hide={hide}
|
||||
onChange={handleChangeValueText}
|
||||
defaultValue={pair.value}
|
||||
language={valueLanguage}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[handleChangeValueText, pair.name, pair.value, valueLanguage],
|
||||
);
|
||||
|
||||
const [, connectDrop] = useDrop<Pair>(
|
||||
{
|
||||
accept: ItemTypes.ROW,
|
||||
@@ -495,6 +519,15 @@ function PairEditorRow({
|
||||
onFocus={handleFocus}
|
||||
placeholder={valuePlaceholder ?? 'value'}
|
||||
/>
|
||||
) : pair.value.includes('\n') ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
onClick={handleEditMultiLineValue}
|
||||
title={pair.value}
|
||||
>
|
||||
Edit {formatSize(pair.value.length)}
|
||||
</Button>
|
||||
) : (
|
||||
<Input
|
||||
ref={valueInputRef}
|
||||
@@ -505,6 +538,7 @@ function PairEditorRow({
|
||||
size="sm"
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
validate={valueValidate}
|
||||
language={valueLanguage}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
defaultValue={pair.value}
|
||||
label="Value"
|
||||
@@ -526,6 +560,7 @@ function PairEditorRow({
|
||||
onChangeText={handleChangeValueText}
|
||||
onChangeContentType={handleChangeValueContentType}
|
||||
onDelete={handleDelete}
|
||||
editMultiLine={handleEditMultiLineValue}
|
||||
/>
|
||||
) : (
|
||||
<Dropdown items={deleteItems}>
|
||||
@@ -552,12 +587,14 @@ function FileActionsDropdown({
|
||||
onChangeText,
|
||||
onChangeContentType,
|
||||
onDelete,
|
||||
editMultiLine,
|
||||
}: {
|
||||
pair: Pair;
|
||||
onChangeFile: ({ filePath }: { filePath: string | null }) => void;
|
||||
onChangeText: (text: string) => void;
|
||||
onChangeContentType: (contentType: string) => void;
|
||||
onDelete: () => void;
|
||||
editMultiLine: () => void;
|
||||
}) {
|
||||
const onChange = useCallback(
|
||||
(v: string) => {
|
||||
@@ -569,10 +606,15 @@ function FileActionsDropdown({
|
||||
|
||||
const extraItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: 'Edit Multi-Line',
|
||||
leftSlot: <Icon icon="file_code" />,
|
||||
hidden: pair.isFile,
|
||||
onSelect: editMultiLine,
|
||||
},
|
||||
{
|
||||
label: 'Set Content-Type',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: !pair.isFile,
|
||||
onSelect: async () => {
|
||||
const contentType = await showPrompt({
|
||||
id: 'content-type',
|
||||
@@ -602,7 +644,7 @@ function FileActionsDropdown({
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
],
|
||||
[onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
|
||||
[editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -629,3 +671,40 @@ function emptyPair(): PairWithId {
|
||||
function isPairEmpty(pair: Pair): boolean {
|
||||
return !pair.name && !pair.value;
|
||||
}
|
||||
|
||||
function MultilineEditDialog({
|
||||
defaultValue,
|
||||
language,
|
||||
onChange,
|
||||
hide,
|
||||
}: {
|
||||
defaultValue: string;
|
||||
language: EditorProps['language'];
|
||||
onChange: (value: string) => void;
|
||||
hide: () => void;
|
||||
}) {
|
||||
const [value, setValue] = useState<string>(defaultValue);
|
||||
return (
|
||||
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
|
||||
<Editor
|
||||
heightMode="auto"
|
||||
defaultValue={defaultValue}
|
||||
language={language}
|
||||
onChange={setValue}
|
||||
stateKey={null}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
color="primary"
|
||||
className="ml-auto my-2"
|
||||
onClick={() => {
|
||||
onChange(value);
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,28 +1,13 @@
|
||||
import { formatSize } from '@yaakapp-internal/lib/formatSize';
|
||||
|
||||
interface Props {
|
||||
contentLength: number;
|
||||
}
|
||||
|
||||
export function SizeTag({ contentLength }: Props) {
|
||||
let num;
|
||||
let unit;
|
||||
|
||||
if (contentLength > 1000 * 1000 * 1000) {
|
||||
num = contentLength / 1000 / 1000 / 1000;
|
||||
unit = 'GB';
|
||||
} else if (contentLength > 1000 * 1000) {
|
||||
num = contentLength / 1000 / 1000;
|
||||
unit = 'MB';
|
||||
} else if (contentLength > 1000) {
|
||||
num = contentLength / 1000;
|
||||
unit = 'KB';
|
||||
} else {
|
||||
num = contentLength;
|
||||
unit = 'B';
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="font-mono" title={`${contentLength} bytes`}>
|
||||
{Math.round(num * 10) / 10} {unit}
|
||||
{formatSize(contentLength)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user