mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-20 00:23:58 +01:00
Multipart form UI and fixes
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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<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],
|
||||
);
|
||||
|
||||
@@ -29,6 +38,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||
<PairEditor
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
allowFileValues
|
||||
pairs={pairs}
|
||||
onChange={handleChange}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
|
||||
@@ -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,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],
|
||||
);
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<string | null>(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(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}
|
||||
/>
|
||||
<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>
|
||||
@@ -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,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 }),
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user