Skip to content

Commit

Permalink
Multipart form UI and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
gschier committed Nov 14, 2023
1 parent 1bc155d commit 11f5541
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 45 deletions.
6 changes: 3 additions & 3 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/send.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
},
);
Expand Down
18 changes: 14 additions & 4 deletions src-web/components/FormMultipartEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -10,25 +10,35 @@ type Props = {
};

export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
const pairs = useMemo(
const pairs = useMemo<Pair[]>(
() =>
(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<PairEditorProps['onChange']>(
(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],
);

return (
<PairEditor
valueAutocompleteVariables
nameAutocompleteVariables
allowFileValues
pairs={pairs}
onChange={handleChange}
forceUpdateKey={forceUpdateKey}
Expand Down
13 changes: 7 additions & 6 deletions src-web/components/FormUrlencodedEditor.tsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -10,18 +10,19 @@ type Props = {
};

export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) {
const pairs = useMemo(
const pairs = useMemo<Pair[]>(
() =>
(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<PairEditorProps['onChange']>(
(pairs) => onChange({ form: pairs }),
(pairs) =>
onChange({ form: pairs.map((p) => ({ enabled: p.enabled, name: p.name, value: p.value })) }),
[onChange],
);

Expand Down
6 changes: 4 additions & 2 deletions src-web/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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) {
Expand Down
110 changes: 81 additions & 29 deletions src-web/components/core/PairEditor.tsx
Original file line number Diff line number Diff line change
@@ -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[];
Expand All @@ -23,6 +26,7 @@ export type PairEditorProps = {
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
nameAutocompleteVariables?: boolean;
valueAutocompleteVariables?: boolean;
allowFileValues?: boolean;
nameValidate?: InputProps['validate'];
valueValidate?: InputProps['validate'];
};
Expand All @@ -32,6 +36,7 @@ export type Pair = {
enabled?: boolean;
name: string;
value: string;
isFile?: boolean;
};

type PairContainer = {
Expand All @@ -52,6 +57,7 @@ export const PairEditor = memo(function PairEditor({
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
allowFileValues,
}: PairEditorProps) {
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
Expand Down Expand Up @@ -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}
Expand All @@ -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}
Expand All @@ -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;
Expand All @@ -218,26 +223,27 @@ type FormRowProps = {
| 'nameValidate'
| 'valueValidate'
| 'forceUpdateKey'
| 'allowFileValues'
>;

const FormRow = memo(function FormRow({
allowFileValues,
className,
forceFocusPairId,
forceUpdateKey,
isLast,
nameAutocomplete,
namePlaceholder,
nameAutocompleteVariables,
valueAutocompleteVariables,
namePlaceholder,
nameValidate,
onChange,
onDelete,
onEnd,
onFocus,
onMove,
pairContainer,
showDelete,
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
}: FormRowProps) {
Expand All @@ -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],
);

Expand Down Expand Up @@ -354,31 +366,66 @@ const FormRow = memo(function FormRow({
autocomplete={nameAutocomplete}
autocompleteVariables={nameAutocompleteVariables}
/>
<Input
hideLabel
useTemplating
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
forceUpdateKey={forceUpdateKey}
defaultValue={pairContainer.pair.value}
label="Value"
name="value"
onChange={handleChangeValue}
onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'}
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
autocompleteVariables={valueAutocompleteVariables}
/>
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
{pairContainer.pair.isFile ? (
<Button
size="xs"
color="gray"
className="font-mono text-xs"
onClick={async (e) => {
e.preventDefault();
const file = await open({
title: 'Select file',
multiple: false,
});
handleChangeValueFile((Array.isArray(file) ? file[0] : file) ?? '');
}}
>
{getFileName(pairContainer.pair.value) || 'Select File'}
</Button>
) : (
<Input
hideLabel
useTemplating
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
forceUpdateKey={forceUpdateKey}
defaultValue={pairContainer.pair.value}
label="Value"
name="value"
onChange={handleChangeValueText}
onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'}
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
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={!showDelete}
disabled={!showDelete}
aria-hidden={isLast}
disabled={isLast}
color="custom"
icon={showDelete ? 'trash' : 'empty'}
icon={!isLast ? 'trash' : 'empty'}
size="sm"
title="Delete header"
onClick={showDelete ? handleDelete : undefined}
onClick={!isLast ? handleDelete : undefined}
className="ml-0.5 group-hover:!opacity-100 focus-visible:!opacity-100"
/>
</div>
Expand All @@ -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] ?? '';
};
3 changes: 3 additions & 0 deletions src-web/hooks/useSendAnyRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpResponse, string, string | null>({
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
onSettled: () => trackEvent('http_request', 'send'),
onError: (err) => alert({ title: 'Export Failed', body: err }),
});
}

0 comments on commit 11f5541

Please sign in to comment.