mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 17:18:32 +02:00
Custom content-type for multipart items
This commit is contained in:
@@ -182,7 +182,7 @@ pub async fn track_event(
|
|||||||
|
|
||||||
// Disable analytics actual sending in dev
|
// Disable analytics actual sending in dev
|
||||||
if is_dev() {
|
if is_dev() {
|
||||||
debug!("track: {} {} {:?}", event, attributes_json, params);
|
// debug!("track: {} {} {:?}", event, attributes_json, params);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -259,7 +259,6 @@ pub async fn send_http_request(
|
|||||||
return response_err(response, e, window).await;
|
return response_err(response, e, window).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||||
let mut multipart_form = multipart::Form::new();
|
let mut multipart_form = multipart::Form::new();
|
||||||
if let Some(form_definition) = request_body.get("form") {
|
if let Some(form_definition) = request_body.get("form") {
|
||||||
@@ -269,12 +268,13 @@ pub async fn send_http_request(
|
|||||||
.unwrap_or(empty_bool)
|
.unwrap_or(empty_bool)
|
||||||
.as_bool()
|
.as_bool()
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let name = p
|
let name_raw = p
|
||||||
.get("name")
|
.get("name")
|
||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
if !enabled || name.is_empty() {
|
|
||||||
|
if !enabled || name_raw.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,24 +283,41 @@ pub async fn send_http_request(
|
|||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let value = p
|
let value_raw = p
|
||||||
.get("value")
|
.get("value")
|
||||||
.unwrap_or(empty_string)
|
.unwrap_or(empty_string)
|
||||||
.as_str()
|
.as_str()
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
multipart_form = multipart_form.part(
|
|
||||||
render::render(name, &workspace, environment_ref),
|
let name = render::render(name_raw, &workspace, environment_ref);
|
||||||
match !file.is_empty() {
|
let part = if file.is_empty() {
|
||||||
true => {
|
multipart::Part::text(render::render(
|
||||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
value_raw,
|
||||||
|
&workspace,
|
||||||
|
environment_ref,
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
match fs::read(file) {
|
||||||
|
Ok(f) => multipart::Part::bytes(f),
|
||||||
|
Err(e) => {
|
||||||
|
return response_err(response, e.to_string(), window).await;
|
||||||
}
|
}
|
||||||
false => multipart::Part::text(render::render(
|
}
|
||||||
value,
|
};
|
||||||
&workspace,
|
|
||||||
environment_ref,
|
let ct_raw = p
|
||||||
)),
|
.get("contentType")
|
||||||
},
|
.unwrap_or(empty_string)
|
||||||
);
|
.as_str()
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
multipart_form = multipart_form.part(name, if ct_raw.is_empty() {
|
||||||
|
part
|
||||||
|
} else {
|
||||||
|
let ct = render::render(ct_raw, &workspace, environment_ref);
|
||||||
|
println!("CT: {}", ct);
|
||||||
|
part.mime_str(ct.as_str()).map_err(|e| e.to_string())?
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers.remove("Content-Type"); // reqwest will add this automatically
|
headers.remove("Content-Type"); // reqwest will add this automatically
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
value: p.file ?? p.value,
|
value: p.file ?? p.value,
|
||||||
|
contentType: p.contentType,
|
||||||
isFile: !!p.file,
|
isFile: !!p.file,
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[body.form],
|
||||||
@@ -27,6 +28,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
form: pairs.map((p) => ({
|
form: pairs.map((p) => ({
|
||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
|
contentType: p.contentType,
|
||||||
file: p.isFile ? p.value : undefined,
|
file: p.isFile ? p.value : undefined,
|
||||||
value: p.isFile ? undefined : p.value,
|
value: p.isFile ? undefined : p.value,
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -195,7 +195,6 @@ export function GrpcConnectionSetupPane({
|
|||||||
shortLabel: o.label,
|
shortLabel: o.label,
|
||||||
}))}
|
}))}
|
||||||
extraItems={[
|
extraItems={[
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
{
|
||||||
label: 'Refresh',
|
label: 'Refresh',
|
||||||
type: 'default',
|
type: 'default',
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } fro
|
|||||||
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';
|
||||||
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
import { DropMarker } from '../DropMarker';
|
import { DropMarker } from '../DropMarker';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
@@ -14,6 +15,7 @@ import { Icon } from './Icon';
|
|||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import type { InputProps } from './Input';
|
import type { InputProps } from './Input';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
|
import { RadioDropdown } from './RadioDropdown';
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
@@ -37,6 +39,7 @@ export type Pair = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
contentType?: string;
|
||||||
isFile?: boolean;
|
isFile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,6 +257,7 @@ const FormRow = memo(function FormRow({
|
|||||||
}: FormRowProps) {
|
}: FormRowProps) {
|
||||||
const { id } = pairContainer;
|
const { id } = pairContainer;
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const prompt = usePrompt();
|
||||||
const nameInputRef = useRef<EditorView>(null);
|
const nameInputRef = useRef<EditorView>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -283,6 +287,11 @@ const FormRow = memo(function FormRow({
|
|||||||
[onChange, id, pairContainer.pair],
|
[onChange, id, pairContainer.pair],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const handleChangeValueContentType = useMemo(
|
||||||
|
() => (contentType: string) => onChange({ id, pair: { ...pairContainer.pair, contentType } }),
|
||||||
|
[onChange, id, pairContainer.pair],
|
||||||
|
);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
||||||
const handleDelete = useCallback(
|
const handleDelete = useCallback(
|
||||||
() => onDelete?.(pairContainer, false),
|
() => onDelete?.(pairContainer, false),
|
||||||
@@ -408,34 +417,67 @@ const FormRow = memo(function FormRow({
|
|||||||
autocompleteVariables={valueAutocompleteVariables}
|
autocompleteVariables={valueAutocompleteVariables}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{allowFileValues && (
|
|
||||||
<Dropdown
|
|
||||||
items={[
|
|
||||||
{ key: 'text', label: 'Text', onSelect: () => handleChangeValueText('') },
|
|
||||||
{ key: 'file', label: 'File', onSelect: () => handleChangeValueFile('') },
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<IconButton
|
|
||||||
iconSize="sm"
|
|
||||||
size="xs"
|
|
||||||
icon={isLast ? 'empty' : 'chevronDown'}
|
|
||||||
title="Select form data type"
|
|
||||||
/>
|
|
||||||
</Dropdown>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<IconButton
|
{allowFileValues ? (
|
||||||
aria-hidden={isLast}
|
<RadioDropdown
|
||||||
disabled={isLast}
|
value={pairContainer.pair.isFile ? 'file' : 'text'}
|
||||||
color="custom"
|
onChange={(v) => {
|
||||||
icon={!isLast ? 'trash' : 'empty'}
|
if (v === 'file') handleChangeValueFile('');
|
||||||
size="sm"
|
else handleChangeValueText('');
|
||||||
iconSize="sm"
|
}}
|
||||||
title="Delete header"
|
items={[
|
||||||
onClick={!isLast ? handleDelete : undefined}
|
{ label: 'Text', value: 'text' },
|
||||||
className="ml-0.5 opacity-0 group-hover:!opacity-100 focus-visible:!opacity-100"
|
{ label: 'File', value: 'file' },
|
||||||
/>
|
]}
|
||||||
|
extraItems={[
|
||||||
|
{
|
||||||
|
key: 'mime',
|
||||||
|
label: 'Set Content-Type',
|
||||||
|
leftSlot: <Icon icon="pencil" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const v = await prompt({
|
||||||
|
id: 'content-type',
|
||||||
|
require: false,
|
||||||
|
title: 'Override Content-Type',
|
||||||
|
label: 'Content-Type',
|
||||||
|
placeholder: 'text/plain',
|
||||||
|
defaultValue: pairContainer.pair.contentType ?? '',
|
||||||
|
name: 'content-type',
|
||||||
|
confirmLabel: 'Set',
|
||||||
|
description: 'Leave blank to auto-detect',
|
||||||
|
});
|
||||||
|
handleChangeValueContentType(v);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
onSelect: handleDelete,
|
||||||
|
variant: 'danger',
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
iconSize="sm"
|
||||||
|
size="xs"
|
||||||
|
icon={isLast ? 'empty' : 'chevronDown'}
|
||||||
|
title="Select form data type"
|
||||||
|
/>
|
||||||
|
</RadioDropdown>
|
||||||
|
) : (
|
||||||
|
<Dropdown
|
||||||
|
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
iconSize="sm"
|
||||||
|
size="xs"
|
||||||
|
icon={isLast ? 'empty' : 'chevronDown'}
|
||||||
|
title="Select form data type"
|
||||||
|
/>
|
||||||
|
</Dropdown>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
|
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||||
import { Dropdown } from './Dropdown';
|
import { Dropdown } from './Dropdown';
|
||||||
import { Icon } from './Icon';
|
import { Icon } from './Icon';
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ export function RadioDropdown<T = string | null>({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
...(extraItems ?? []),
|
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),
|
||||||
],
|
],
|
||||||
[items, extraItems, value, onChange],
|
[items, extraItems, value, onChange],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface PromptProps {
|
|||||||
name: InputProps['name'];
|
name: InputProps['name'];
|
||||||
defaultValue: InputProps['defaultValue'];
|
defaultValue: InputProps['defaultValue'];
|
||||||
placeholder: InputProps['placeholder'];
|
placeholder: InputProps['placeholder'];
|
||||||
|
require?: InputProps['require'];
|
||||||
confirmLabel?: string;
|
confirmLabel?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export function Prompt({
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
onResult,
|
onResult,
|
||||||
|
require = true,
|
||||||
confirmLabel = 'Save',
|
confirmLabel = 'Save',
|
||||||
}: PromptProps) {
|
}: PromptProps) {
|
||||||
const [value, setValue] = useState<string>(defaultValue ?? '');
|
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||||
@@ -41,7 +43,7 @@ export function Prompt({
|
|||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
hideLabel
|
hideLabel
|
||||||
require
|
require={require}
|
||||||
autoSelect
|
autoSelect
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
label={label}
|
label={label}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export function usePrompt() {
|
|||||||
defaultValue,
|
defaultValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
confirmLabel,
|
confirmLabel,
|
||||||
|
require,
|
||||||
}: Pick<DialogProps, 'title' | 'description'> &
|
}: Pick<DialogProps, 'title' | 'description'> &
|
||||||
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
||||||
new Promise((onResult: PromptProps['onResult']) => {
|
new Promise((onResult: PromptProps['onResult']) => {
|
||||||
@@ -24,7 +25,16 @@ export function usePrompt() {
|
|||||||
hideX: true,
|
hideX: true,
|
||||||
size: 'sm',
|
size: 'sm',
|
||||||
render: ({ hide }) =>
|
render: ({ hide }) =>
|
||||||
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
|
Prompt({
|
||||||
|
onHide: hide,
|
||||||
|
onResult,
|
||||||
|
name,
|
||||||
|
label,
|
||||||
|
defaultValue,
|
||||||
|
placeholder,
|
||||||
|
confirmLabel,
|
||||||
|
require,
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user