Custom content-type for multipart items

This commit is contained in:
Gregory Schier
2024-03-16 12:49:17 -07:00
parent 4b33a696ac
commit 901cf53cd2
8 changed files with 120 additions and 48 deletions

View File

@@ -182,7 +182,7 @@ pub async fn track_event(
// Disable analytics actual sending in dev
if is_dev() {
debug!("track: {} {} {:?}", event, attributes_json, params);
// debug!("track: {} {} {:?}", event, attributes_json, params);
return;
}

View File

@@ -259,7 +259,6 @@ pub async fn send_http_request(
return response_err(response, e, window).await;
}
}
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
let mut multipart_form = multipart::Form::new();
if let Some(form_definition) = request_body.get("form") {
@@ -269,12 +268,13 @@ pub async fn send_http_request(
.unwrap_or(empty_bool)
.as_bool()
.unwrap_or(false);
let name = p
let name_raw = p
.get("name")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
if !enabled || name.is_empty() {
if !enabled || name_raw.is_empty() {
continue;
}
@@ -283,24 +283,41 @@ pub async fn send_http_request(
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
let value = p
let value_raw = p
.get("value")
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
multipart_form = multipart_form.part(
render::render(name, &workspace, environment_ref),
match !file.is_empty() {
true => {
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
let name = render::render(name_raw, &workspace, environment_ref);
let part = if file.is_empty() {
multipart::Part::text(render::render(
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

View File

@@ -16,6 +16,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
enabled: p.enabled,
name: p.name,
value: p.file ?? p.value,
contentType: p.contentType,
isFile: !!p.file,
})),
[body.form],
@@ -27,6 +28,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
form: pairs.map((p) => ({
enabled: p.enabled,
name: p.name,
contentType: p.contentType,
file: p.isFile ? p.value : undefined,
value: p.isFile ? undefined : p.value,
})),

View File

@@ -195,7 +195,6 @@ export function GrpcConnectionSetupPane({
shortLabel: o.label,
}))}
extraItems={[
{ type: 'separator' },
{
label: 'Refresh',
type: 'default',

View File

@@ -5,6 +5,7 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } fro
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { v4 as uuid } from 'uuid';
import { usePrompt } from '../../hooks/usePrompt';
import { DropMarker } from '../DropMarker';
import { Button } from './Button';
import { Checkbox } from './Checkbox';
@@ -14,6 +15,7 @@ import { Icon } from './Icon';
import { IconButton } from './IconButton';
import type { InputProps } from './Input';
import { Input } from './Input';
import { RadioDropdown } from './RadioDropdown';
export type PairEditorProps = {
pairs: Pair[];
@@ -37,6 +39,7 @@ export type Pair = {
enabled?: boolean;
name: string;
value: string;
contentType?: string;
isFile?: boolean;
};
@@ -254,6 +257,7 @@ const FormRow = memo(function FormRow({
}: FormRowProps) {
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
const prompt = usePrompt();
const nameInputRef = useRef<EditorView>(null);
useEffect(() => {
@@ -283,6 +287,11 @@ const FormRow = memo(function FormRow({
[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 handleDelete = useCallback(
() => onDelete?.(pairContainer, false),
@@ -408,34 +417,67 @@ const FormRow = memo(function FormRow({
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>
<IconButton
aria-hidden={isLast}
disabled={isLast}
color="custom"
icon={!isLast ? 'trash' : 'empty'}
size="sm"
iconSize="sm"
title="Delete header"
onClick={!isLast ? handleDelete : undefined}
className="ml-0.5 opacity-0 group-hover:!opacity-100 focus-visible:!opacity-100"
/>
{allowFileValues ? (
<RadioDropdown
value={pairContainer.pair.isFile ? 'file' : 'text'}
onChange={(v) => {
if (v === 'file') handleChangeValueFile('');
else handleChangeValueText('');
}}
items={[
{ label: 'Text', value: 'text' },
{ 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>
);
});

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
import { Dropdown } from './Dropdown';
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],
);

View File

@@ -12,6 +12,7 @@ export interface PromptProps {
name: InputProps['name'];
defaultValue: InputProps['defaultValue'];
placeholder: InputProps['placeholder'];
require?: InputProps['require'];
confirmLabel?: string;
}
@@ -22,6 +23,7 @@ export function Prompt({
defaultValue,
placeholder,
onResult,
require = true,
confirmLabel = 'Save',
}: PromptProps) {
const [value, setValue] = useState<string>(defaultValue ?? '');
@@ -41,7 +43,7 @@ export function Prompt({
>
<Input
hideLabel
require
require={require}
autoSelect
placeholder={placeholder}
label={label}

View File

@@ -14,6 +14,7 @@ export function usePrompt() {
defaultValue,
placeholder,
confirmLabel,
require,
}: Pick<DialogProps, 'title' | 'description'> &
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
new Promise((onResult: PromptProps['onResult']) => {
@@ -24,7 +25,16 @@ export function usePrompt() {
hideX: true,
size: 'sm',
render: ({ hide }) =>
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
Prompt({
onHide: hide,
onResult,
name,
label,
defaultValue,
placeholder,
confirmLabel,
require,
}),
});
});
}