Multi-line multi-part values

This commit is contained in:
Gregory Schier
2025-01-27 07:30:06 -08:00
parent 1d37a15cfe
commit 662c38d7a0
11 changed files with 147 additions and 40 deletions

View File

@@ -29,6 +29,7 @@ import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useSettings } from '../../../hooks/useSettings';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { showDialog } from '../../../lib/dialog';
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton';
@@ -134,7 +135,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}
if (disabled) {
readOnly = true;
readOnly = true;
}
if (
@@ -147,6 +148,15 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
disableTabIndent = true;
}
if (format == null) {
format =
language === 'json'
? tryFormatJson
: language === 'xml' || language === 'html'
? tryFormatXml
: undefined;
}
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view, []);

View File

@@ -15,18 +15,16 @@ const longMethodMap = {
delete: 'DELETE',
options: 'OPTIONS',
head: 'HEAD',
grpc: 'GRPC',
} as const;
const shortMethodMap: Record<keyof typeof longMethodMap, string> = {
get: 'GET',
put: 'PUT',
post: 'POST',
patch: 'PTCH',
post: 'PST',
patch: 'PTC',
delete: 'DEL',
options: 'OPTS',
head: 'HEAD',
grpc: 'GRPC',
options: 'OPT',
head: 'HED',
};
export function HttpMethodTag({ shortNames, request, className }: Props) {
@@ -34,7 +32,7 @@ export function HttpMethodTag({ shortNames, request, className }: Props) {
request.model === 'http_request' && request.bodyType === 'graphql'
? 'GQL'
: request.model === 'grpc_request'
? 'GRPC'
? 'GRP'
: request.method;
const m = method.toLowerCase();

View File

@@ -1,3 +1,4 @@
import { formatSize } from '@yaakapp-internal/lib/formatSize';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import {
@@ -13,6 +14,8 @@ import {
import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } from 'react-dnd';
import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType';
import { showDialog } from '../../lib/dialog';
import { generateId } from '../../lib/generateId';
import { showPrompt } from '../../lib/prompt';
import { DropMarker } from '../DropMarker';
@@ -21,6 +24,8 @@ import { Button } from './Button';
import { Checkbox } from './Checkbox';
import type { DropdownItem } from './Dropdown';
import { Dropdown } from './Dropdown';
import type { EditorProps } from './Editor/Editor';
import { Editor } from './Editor/Editor';
import type { GenericCompletionConfig } from './Editor/genericCompletion';
import { Icon } from './Icon';
import { IconButton } from './IconButton';
@@ -326,6 +331,7 @@ function PairEditorRow({
const ref = useRef<HTMLDivElement>(null);
const nameInputRef = useRef<EditorView>(null);
const valueInputRef = useRef<EditorView>(null);
const valueLanguage = languageFromContentType(pair.contentType ?? null);
useEffect(() => {
if (forceFocusNamePairId === pair.id) {
@@ -380,6 +386,24 @@ function PairEditorRow({
[onChange, pair],
);
const handleEditMultiLineValue = useCallback(
() =>
showDialog({
id: 'pair-edit-multiline',
size: 'dynamic',
title: <>Edit {pair.name}</>,
render: ({ hide }) => (
<MultilineEditDialog
hide={hide}
onChange={handleChangeValueText}
defaultValue={pair.value}
language={valueLanguage}
/>
),
}),
[handleChangeValueText, pair.name, pair.value, valueLanguage],
);
const [, connectDrop] = useDrop<Pair>(
{
accept: ItemTypes.ROW,
@@ -495,6 +519,15 @@ function PairEditorRow({
onFocus={handleFocus}
placeholder={valuePlaceholder ?? 'value'}
/>
) : pair.value.includes('\n') ? (
<Button
color="secondary"
size="sm"
onClick={handleEditMultiLineValue}
title={pair.value}
>
Edit {formatSize(pair.value.length)}
</Button>
) : (
<Input
ref={valueInputRef}
@@ -505,6 +538,7 @@ function PairEditorRow({
size="sm"
containerClassName={classNames(isLast && 'border-dashed')}
validate={valueValidate}
language={valueLanguage}
forceUpdateKey={forceUpdateKey}
defaultValue={pair.value}
label="Value"
@@ -526,6 +560,7 @@ function PairEditorRow({
onChangeText={handleChangeValueText}
onChangeContentType={handleChangeValueContentType}
onDelete={handleDelete}
editMultiLine={handleEditMultiLineValue}
/>
) : (
<Dropdown items={deleteItems}>
@@ -552,12 +587,14 @@ function FileActionsDropdown({
onChangeText,
onChangeContentType,
onDelete,
editMultiLine,
}: {
pair: Pair;
onChangeFile: ({ filePath }: { filePath: string | null }) => void;
onChangeText: (text: string) => void;
onChangeContentType: (contentType: string) => void;
onDelete: () => void;
editMultiLine: () => void;
}) {
const onChange = useCallback(
(v: string) => {
@@ -569,10 +606,15 @@ function FileActionsDropdown({
const extraItems = useMemo<DropdownItem[]>(
() => [
{
label: 'Edit Multi-Line',
leftSlot: <Icon icon="file_code" />,
hidden: pair.isFile,
onSelect: editMultiLine,
},
{
label: 'Set Content-Type',
leftSlot: <Icon icon="pencil" />,
hidden: !pair.isFile,
onSelect: async () => {
const contentType = await showPrompt({
id: 'content-type',
@@ -602,7 +644,7 @@ function FileActionsDropdown({
leftSlot: <Icon icon="trash" />,
},
],
[onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
[editMultiLine, onChangeContentType, onChangeFile, onDelete, pair.contentType, pair.isFile],
);
return (
@@ -629,3 +671,40 @@ function emptyPair(): PairWithId {
function isPairEmpty(pair: Pair): boolean {
return !pair.name && !pair.value;
}
function MultilineEditDialog({
defaultValue,
language,
onChange,
hide,
}: {
defaultValue: string;
language: EditorProps['language'];
onChange: (value: string) => void;
hide: () => void;
}) {
const [value, setValue] = useState<string>(defaultValue);
return (
<div className="w-[100vw] max-w-[40rem] h-[50vh] max-h-full grid grid-rows-[minmax(0,1fr)_auto]">
<Editor
heightMode="auto"
defaultValue={defaultValue}
language={language}
onChange={setValue}
stateKey={null}
/>
<div>
<Button
color="primary"
className="ml-auto my-2"
onClick={() => {
onChange(value);
hide();
}}
>
Done
</Button>
</div>
</div>
);
}

View File

@@ -1,28 +1,13 @@
import { formatSize } from '@yaakapp-internal/lib/formatSize';
interface Props {
contentLength: number;
}
export function SizeTag({ contentLength }: Props) {
let num;
let unit;
if (contentLength > 1000 * 1000 * 1000) {
num = contentLength / 1000 / 1000 / 1000;
unit = 'GB';
} else if (contentLength > 1000 * 1000) {
num = contentLength / 1000 / 1000;
unit = 'MB';
} else if (contentLength > 1000) {
num = contentLength / 1000;
unit = 'KB';
} else {
num = contentLength;
unit = 'B';
}
return (
<span className="font-mono" title={`${contentLength} bytes`}>
{Math.round(num * 10) / 10} {unit}
{formatSize(contentLength)}
</span>
);
}