diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index bf1c241d..c4016aca 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -196,9 +196,9 @@ async fn send_request( let pool2 = pool.clone(); tokio::spawn(async move { - actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2) - .await - .expect("Failed to send request"); + if let Err(e) = actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2).await { + response_err(&response2, e, &app_handle2, &pool2).await.expect("Failed to update response"); + } }); emit_and_return(&window, "created_model", response) diff --git a/src-tauri/src/send.rs b/src-tauri/src/send.rs index 7addf34e..8c3f7db3 100644 --- a/src-tauri/src/send.rs +++ b/src-tauri/src/send.rs @@ -160,7 +160,7 @@ pub async fn actually_send_request( multipart_form = multipart_form.part( render::render(name, &workspace, environment_ref), match !file.is_empty() { - true => multipart::Part::bytes(fs::read(file).expect("Failed to read file")), + true => multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?), false => multipart::Part::text(render::render(value, &workspace, environment_ref)), }, ); diff --git a/src-web/components/FormMultipartEditor.tsx b/src-web/components/FormMultipartEditor.tsx index 5a66d354..31225c88 100644 --- a/src-web/components/FormMultipartEditor.tsx +++ b/src-web/components/FormMultipartEditor.tsx @@ -1,6 +1,6 @@ import { useCallback, useMemo } from 'react'; import type { HttpRequest } from '../lib/models'; -import type { PairEditorProps } from './core/PairEditor'; +import type { Pair, PairEditorProps } from './core/PairEditor'; import { PairEditor } from './core/PairEditor'; type Props = { @@ -10,18 +10,27 @@ type Props = { }; export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) { - const pairs = useMemo( + const pairs = useMemo( () => (Array.isArray(body.form) ? body.form : []).map((p) => ({ enabled: p.enabled, name: p.name, - value: p.value, + value: p.file ?? p.value, + isFile: !!p.file, })), [body.form], ); const handleChange = useCallback( - (pairs) => onChange({ form: pairs }), + (pairs) => + onChange({ + form: pairs.map((p) => ({ + enabled: p.enabled, + name: p.name, + file: p.isFile ? p.value : undefined, + value: p.isFile ? undefined : p.value, + })), + }), [onChange], ); @@ -29,6 +38,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) { ( () => (Array.isArray(body.form) ? body.form : []).map((p) => ({ - enabled: p.enabled, - name: p.name, - value: p.value, + enabled: !!p.enabled, + name: p.name || '', + value: p.value || '', })), [body.form], ); const handleChange = useCallback( - (pairs) => onChange({ form: pairs }), + (pairs) => + onChange({ form: pairs.map((p) => ({ enabled: p.enabled, name: p.name, value: p.value })) }), [onChange], ); diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 4b29891f..a9d9ea19 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -222,8 +222,9 @@ export function Sidebar({ className }: Props) { useKey( 'ArrowUp', - () => { + (e) => { if (!hasFocus) return; + e.preventDefault(); const i = selectableRequests.findIndex((r) => r.id === selectedId); const newSelectable = selectableRequests[i - 1]; if (newSelectable == null) { @@ -239,8 +240,9 @@ export function Sidebar({ className }: Props) { useKey( 'ArrowDown', - () => { + (e) => { if (!hasFocus) return; + e.preventDefault(); const i = selectableRequests.findIndex((r) => r.id === selectedId); const newSelectable = selectableRequests[i + 1]; if (newSelectable == null) { diff --git a/src-web/components/core/PairEditor.tsx b/src-web/components/core/PairEditor.tsx index 6197c5bc..d44b2515 100644 --- a/src-web/components/core/PairEditor.tsx +++ b/src-web/components/core/PairEditor.tsx @@ -1,16 +1,19 @@ +import { open } from '@tauri-apps/api/dialog'; import classNames from 'classnames'; +import type { EditorView } from 'codemirror'; import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type { XYCoord } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd'; import { v4 as uuid } from 'uuid'; import { DropMarker } from '../DropMarker'; +import { Button } from './Button'; import { Checkbox } from './Checkbox'; +import { Dropdown } from './Dropdown'; import type { GenericCompletionConfig } from './Editor/genericCompletion'; import { Icon } from './Icon'; import { IconButton } from './IconButton'; import type { InputProps } from './Input'; import { Input } from './Input'; -import type { EditorView } from 'codemirror'; export type PairEditorProps = { pairs: Pair[]; @@ -23,6 +26,7 @@ export type PairEditorProps = { valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined; nameAutocompleteVariables?: boolean; valueAutocompleteVariables?: boolean; + allowFileValues?: boolean; nameValidate?: InputProps['validate']; valueValidate?: InputProps['validate']; }; @@ -32,6 +36,7 @@ export type Pair = { enabled?: boolean; name: string; value: string; + isFile?: boolean; }; type PairContainer = { @@ -52,6 +57,7 @@ export const PairEditor = memo(function PairEditor({ valueAutocompleteVariables, valuePlaceholder, valueValidate, + allowFileValues, }: PairEditorProps) { const [forceFocusPairId, setForceFocusPairId] = useState(null); const [hoveredIndex, setHoveredIndex] = useState(null); @@ -167,6 +173,7 @@ export const PairEditor = memo(function PairEditor({ pairContainer={p} className="py-1" isLast={isLast} + allowFileValues={allowFileValues} nameAutocompleteVariables={nameAutocompleteVariables} valueAutocompleteVariables={valueAutocompleteVariables} forceFocusPairId={forceFocusPairId} @@ -177,7 +184,6 @@ export const PairEditor = memo(function PairEditor({ valuePlaceholder={isLast ? valuePlaceholder : ''} nameValidate={nameValidate} valueValidate={valueValidate} - showDelete={!isLast} onChange={handleChange} onFocus={handleFocus} onDelete={handleDelete} @@ -199,7 +205,6 @@ type FormRowProps = { className?: string; pairContainer: PairContainer; forceFocusPairId?: string | null; - showDelete?: boolean; onMove: (id: string, side: 'above' | 'below') => void; onEnd: (id: string) => void; onChange: (pair: PairContainer) => void; @@ -218,17 +223,18 @@ type FormRowProps = { | 'nameValidate' | 'valueValidate' | 'forceUpdateKey' + | 'allowFileValues' >; const FormRow = memo(function FormRow({ + allowFileValues, className, forceFocusPairId, forceUpdateKey, isLast, nameAutocomplete, - namePlaceholder, nameAutocompleteVariables, - valueAutocompleteVariables, + namePlaceholder, nameValidate, onChange, onDelete, @@ -236,8 +242,8 @@ const FormRow = memo(function FormRow({ onFocus, onMove, pairContainer, - showDelete, valueAutocomplete, + valueAutocompleteVariables, valuePlaceholder, valueValidate, }: FormRowProps) { @@ -261,8 +267,14 @@ const FormRow = memo(function FormRow({ [onChange, id, pairContainer.pair], ); - const handleChangeValue = useMemo( - () => (value: string) => onChange({ id, pair: { ...pairContainer.pair, value } }), + const handleChangeValueText = useMemo( + () => (value: string) => + onChange({ id, pair: { ...pairContainer.pair, value, isFile: false } }), + [onChange, id, pairContainer.pair], + ); + + const handleChangeValueFile = useMemo( + () => (value: string) => onChange({ id, pair: { ...pairContainer.pair, value, isFile: true } }), [onChange, id, pairContainer.pair], ); @@ -354,31 +366,66 @@ const FormRow = memo(function FormRow({ autocomplete={nameAutocomplete} autocompleteVariables={nameAutocompleteVariables} /> - +
+ {pairContainer.pair.isFile ? ( + + ) : ( + + )} + {allowFileValues && ( + handleChangeValueText('') }, + { key: 'file', label: 'File', onSelect: () => handleChangeValueFile('') }, + ]} + > + + + )} +
@@ -387,6 +434,11 @@ const FormRow = memo(function FormRow({ const newPairContainer = (initialPair?: Pair): PairContainer => { const id = initialPair?.id ?? uuid(); - const pair = initialPair ?? { name: '', value: '', enabled: true }; + const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false }; return { id, pair }; }; + +const getFileName = (path: string): string => { + const parts = path.split(/[\\/]/); + return parts[parts.length - 1] ?? ''; +}; diff --git a/src-web/hooks/useSendAnyRequest.ts b/src-web/hooks/useSendAnyRequest.ts index 012ab412..0f95b838 100644 --- a/src-web/hooks/useSendAnyRequest.ts +++ b/src-web/hooks/useSendAnyRequest.ts @@ -3,11 +3,14 @@ import { invoke } from '@tauri-apps/api'; import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '../lib/models'; import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useAlert } from './useAlert'; export function useSendAnyRequest() { const environmentId = useActiveEnvironmentId(); + const alert = useAlert(); return useMutation({ mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }), onSettled: () => trackEvent('http_request', 'send'), + onError: (err) => alert({ title: 'Export Failed', body: err }), }); }