mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-18 06:49:50 +02:00
Multipart form UI and fixes
This commit is contained in:
@@ -196,9 +196,9 @@ async fn send_request(
|
|||||||
let pool2 = pool.clone();
|
let pool2 = pool.clone();
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2)
|
if let Err(e) = actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2).await {
|
||||||
.await
|
response_err(&response2, e, &app_handle2, &pool2).await.expect("Failed to update response");
|
||||||
.expect("Failed to send request");
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
emit_and_return(&window, "created_model", response)
|
emit_and_return(&window, "created_model", response)
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ pub async fn actually_send_request(
|
|||||||
multipart_form = multipart_form.part(
|
multipart_form = multipart_form.part(
|
||||||
render::render(name, &workspace, environment_ref),
|
render::render(name, &workspace, environment_ref),
|
||||||
match !file.is_empty() {
|
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)),
|
false => multipart::Part::text(render::render(value, &workspace, environment_ref)),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import type { PairEditorProps } from './core/PairEditor';
|
import type { Pair, PairEditorProps } from './core/PairEditor';
|
||||||
import { PairEditor } from './core/PairEditor';
|
import { PairEditor } from './core/PairEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,18 +10,27 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||||
const pairs = useMemo(
|
const pairs = useMemo<Pair[]>(
|
||||||
() =>
|
() =>
|
||||||
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
value: p.value,
|
value: p.file ?? p.value,
|
||||||
|
isFile: !!p.file,
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[body.form],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
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],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -29,6 +38,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
<PairEditor
|
<PairEditor
|
||||||
valueAutocompleteVariables
|
valueAutocompleteVariables
|
||||||
nameAutocompleteVariables
|
nameAutocompleteVariables
|
||||||
|
allowFileValues
|
||||||
pairs={pairs}
|
pairs={pairs}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import type { PairEditorProps } from './core/PairEditor';
|
import type { Pair, PairEditorProps } from './core/PairEditor';
|
||||||
import { PairEditor } from './core/PairEditor';
|
import { PairEditor } from './core/PairEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -10,18 +10,19 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) {
|
export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||||
const pairs = useMemo(
|
const pairs = useMemo<Pair[]>(
|
||||||
() =>
|
() =>
|
||||||
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
||||||
enabled: p.enabled,
|
enabled: !!p.enabled,
|
||||||
name: p.name,
|
name: p.name || '',
|
||||||
value: p.value,
|
value: p.value || '',
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[body.form],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
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],
|
[onChange],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -222,8 +222,9 @@ export function Sidebar({ className }: Props) {
|
|||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
'ArrowUp',
|
'ArrowUp',
|
||||||
() => {
|
(e) => {
|
||||||
if (!hasFocus) return;
|
if (!hasFocus) return;
|
||||||
|
e.preventDefault();
|
||||||
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
||||||
const newSelectable = selectableRequests[i - 1];
|
const newSelectable = selectableRequests[i - 1];
|
||||||
if (newSelectable == null) {
|
if (newSelectable == null) {
|
||||||
@@ -239,8 +240,9 @@ export function Sidebar({ className }: Props) {
|
|||||||
|
|
||||||
useKey(
|
useKey(
|
||||||
'ArrowDown',
|
'ArrowDown',
|
||||||
() => {
|
(e) => {
|
||||||
if (!hasFocus) return;
|
if (!hasFocus) return;
|
||||||
|
e.preventDefault();
|
||||||
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
||||||
const newSelectable = selectableRequests[i + 1];
|
const newSelectable = selectableRequests[i + 1];
|
||||||
if (newSelectable == null) {
|
if (newSelectable == null) {
|
||||||
|
|||||||
@@ -1,16 +1,19 @@
|
|||||||
|
import { open } from '@tauri-apps/api/dialog';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import type { EditorView } from 'codemirror';
|
||||||
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
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 { DropMarker } from '../DropMarker';
|
import { DropMarker } from '../DropMarker';
|
||||||
|
import { Button } from './Button';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
|
import { Dropdown } from './Dropdown';
|
||||||
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
||||||
import { Icon } from './Icon';
|
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 type { EditorView } from 'codemirror';
|
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
@@ -23,6 +26,7 @@ export type PairEditorProps = {
|
|||||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||||
nameAutocompleteVariables?: boolean;
|
nameAutocompleteVariables?: boolean;
|
||||||
valueAutocompleteVariables?: boolean;
|
valueAutocompleteVariables?: boolean;
|
||||||
|
allowFileValues?: boolean;
|
||||||
nameValidate?: InputProps['validate'];
|
nameValidate?: InputProps['validate'];
|
||||||
valueValidate?: InputProps['validate'];
|
valueValidate?: InputProps['validate'];
|
||||||
};
|
};
|
||||||
@@ -32,6 +36,7 @@ export type Pair = {
|
|||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
isFile?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PairContainer = {
|
type PairContainer = {
|
||||||
@@ -52,6 +57,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
valueAutocompleteVariables,
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
valueValidate,
|
valueValidate,
|
||||||
|
allowFileValues,
|
||||||
}: PairEditorProps) {
|
}: PairEditorProps) {
|
||||||
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
|
const [forceFocusPairId, setForceFocusPairId] = useState<string | null>(null);
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
@@ -167,6 +173,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
pairContainer={p}
|
pairContainer={p}
|
||||||
className="py-1"
|
className="py-1"
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
|
allowFileValues={allowFileValues}
|
||||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||||
forceFocusPairId={forceFocusPairId}
|
forceFocusPairId={forceFocusPairId}
|
||||||
@@ -177,7 +184,6 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
valuePlaceholder={isLast ? valuePlaceholder : ''}
|
valuePlaceholder={isLast ? valuePlaceholder : ''}
|
||||||
nameValidate={nameValidate}
|
nameValidate={nameValidate}
|
||||||
valueValidate={valueValidate}
|
valueValidate={valueValidate}
|
||||||
showDelete={!isLast}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
@@ -199,7 +205,6 @@ type FormRowProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
pairContainer: PairContainer;
|
pairContainer: PairContainer;
|
||||||
forceFocusPairId?: string | null;
|
forceFocusPairId?: string | null;
|
||||||
showDelete?: boolean;
|
|
||||||
onMove: (id: string, side: 'above' | 'below') => void;
|
onMove: (id: string, side: 'above' | 'below') => void;
|
||||||
onEnd: (id: string) => void;
|
onEnd: (id: string) => void;
|
||||||
onChange: (pair: PairContainer) => void;
|
onChange: (pair: PairContainer) => void;
|
||||||
@@ -218,17 +223,18 @@ type FormRowProps = {
|
|||||||
| 'nameValidate'
|
| 'nameValidate'
|
||||||
| 'valueValidate'
|
| 'valueValidate'
|
||||||
| 'forceUpdateKey'
|
| 'forceUpdateKey'
|
||||||
|
| 'allowFileValues'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const FormRow = memo(function FormRow({
|
const FormRow = memo(function FormRow({
|
||||||
|
allowFileValues,
|
||||||
className,
|
className,
|
||||||
forceFocusPairId,
|
forceFocusPairId,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
isLast,
|
isLast,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
namePlaceholder,
|
|
||||||
nameAutocompleteVariables,
|
nameAutocompleteVariables,
|
||||||
valueAutocompleteVariables,
|
namePlaceholder,
|
||||||
nameValidate,
|
nameValidate,
|
||||||
onChange,
|
onChange,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -236,8 +242,8 @@ const FormRow = memo(function FormRow({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onMove,
|
onMove,
|
||||||
pairContainer,
|
pairContainer,
|
||||||
showDelete,
|
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
valueValidate,
|
valueValidate,
|
||||||
}: FormRowProps) {
|
}: FormRowProps) {
|
||||||
@@ -261,8 +267,14 @@ const FormRow = memo(function FormRow({
|
|||||||
[onChange, id, pairContainer.pair],
|
[onChange, id, pairContainer.pair],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChangeValue = useMemo(
|
const handleChangeValueText = useMemo(
|
||||||
() => (value: string) => onChange({ id, pair: { ...pairContainer.pair, value } }),
|
() => (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],
|
[onChange, id, pairContainer.pair],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -354,31 +366,66 @@ const FormRow = memo(function FormRow({
|
|||||||
autocomplete={nameAutocomplete}
|
autocomplete={nameAutocomplete}
|
||||||
autocompleteVariables={nameAutocompleteVariables}
|
autocompleteVariables={nameAutocompleteVariables}
|
||||||
/>
|
/>
|
||||||
<Input
|
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
|
||||||
hideLabel
|
{pairContainer.pair.isFile ? (
|
||||||
useTemplating
|
<Button
|
||||||
size="sm"
|
size="xs"
|
||||||
containerClassName={classNames(isLast && 'border-dashed')}
|
color="gray"
|
||||||
validate={valueValidate}
|
className="font-mono text-xs"
|
||||||
forceUpdateKey={forceUpdateKey}
|
onClick={async (e) => {
|
||||||
defaultValue={pairContainer.pair.value}
|
e.preventDefault();
|
||||||
label="Value"
|
const file = await open({
|
||||||
name="value"
|
title: 'Select file',
|
||||||
onChange={handleChangeValue}
|
multiple: false,
|
||||||
onFocus={handleFocus}
|
});
|
||||||
placeholder={valuePlaceholder ?? 'value'}
|
handleChangeValueFile((Array.isArray(file) ? file[0] : file) ?? '');
|
||||||
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
|
}}
|
||||||
autocompleteVariables={valueAutocompleteVariables}
|
>
|
||||||
/>
|
{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>
|
</div>
|
||||||
<IconButton
|
<IconButton
|
||||||
aria-hidden={!showDelete}
|
aria-hidden={isLast}
|
||||||
disabled={!showDelete}
|
disabled={isLast}
|
||||||
color="custom"
|
color="custom"
|
||||||
icon={showDelete ? 'trash' : 'empty'}
|
icon={!isLast ? 'trash' : 'empty'}
|
||||||
size="sm"
|
size="sm"
|
||||||
title="Delete header"
|
title="Delete header"
|
||||||
onClick={showDelete ? handleDelete : undefined}
|
onClick={!isLast ? handleDelete : undefined}
|
||||||
className="ml-0.5 group-hover:!opacity-100 focus-visible:!opacity-100"
|
className="ml-0.5 group-hover:!opacity-100 focus-visible:!opacity-100"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -387,6 +434,11 @@ const FormRow = memo(function FormRow({
|
|||||||
|
|
||||||
const newPairContainer = (initialPair?: Pair): PairContainer => {
|
const newPairContainer = (initialPair?: Pair): PairContainer => {
|
||||||
const id = initialPair?.id ?? uuid();
|
const id = initialPair?.id ?? uuid();
|
||||||
const pair = initialPair ?? { name: '', value: '', enabled: true };
|
const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
|
||||||
return { id, pair };
|
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 { trackEvent } from '../lib/analytics';
|
||||||
import type { HttpResponse } from '../lib/models';
|
import type { HttpResponse } from '../lib/models';
|
||||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||||
|
import { useAlert } from './useAlert';
|
||||||
|
|
||||||
export function useSendAnyRequest() {
|
export function useSendAnyRequest() {
|
||||||
const environmentId = useActiveEnvironmentId();
|
const environmentId = useActiveEnvironmentId();
|
||||||
|
const alert = useAlert();
|
||||||
return useMutation<HttpResponse, string, string | null>({
|
return useMutation<HttpResponse, string, string | null>({
|
||||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||||
onSettled: () => trackEvent('http_request', 'send'),
|
onSettled: () => trackEvent('http_request', 'send'),
|
||||||
|
onError: (err) => alert({ title: 'Export Failed', body: err }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user