Custom content-type for multipart items

This commit is contained in:
Gregory Schier
2024-03-16 12:49:17 -07:00
parent bc33244549
commit 6fd1b35a50
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 // 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;
} }

View File

@@ -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

View File

@@ -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,
})), })),

View File

@@ -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',

View File

@@ -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>
); );
}); });

View File

@@ -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],
); );

View File

@@ -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}

View File

@@ -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,
}),
}); });
}); });
} }