Bulk editor (#45)

Bulk editor for all pair editors except multipart/form-data
This commit is contained in:
Gregory Schier
2024-06-07 13:42:08 -07:00
committed by GitHub
parent 5108bc92f3
commit 5e058af03e
19 changed files with 186 additions and 37 deletions

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo } from 'react';
import type { HttpRequest } from '../lib/models';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -27,7 +27,8 @@ export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props)
);
return (
<PairEditor
<PairOrBulkEditor
preferenceName="form_urlencoded"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="entry_name"

View File

@@ -125,7 +125,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
{...extraEditorProps}
/>
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]">
<Separator variant="primary" className="pb-1">
<Separator dashed className="pb-1">
Variables
</Separator>
<Editor

View File

@@ -6,7 +6,7 @@ import { mimeTypes } from '../lib/data/mimetypes';
import type { HttpRequest } from '../lib/models';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -16,7 +16,8 @@ type Props = {
export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) {
return (
<PairEditor
<PairOrBulkEditor
preferenceName="headers"
valueAutocompleteVariables
nameAutocompleteVariables
pairs={headers}

View File

@@ -1,5 +1,5 @@
import type { HttpRequest } from '../lib/models';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -9,7 +9,8 @@ type Props = {
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
return (
<PairEditor
<PairOrBulkEditor
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"

View File

@@ -0,0 +1,48 @@
import { useCallback, useMemo } from 'react';
import { Editor } from './Editor';
import type { PairEditorProps } from './PairEditor';
type Props = Pick<
PairEditorProps,
'onChange' | 'pairs' | 'namePlaceholder' | 'valuePlaceholder'
> & {
foo?: string;
};
export function BulkPairEditor({ pairs, onChange, namePlaceholder, valuePlaceholder }: Props) {
const pairsText = useMemo(() => {
return pairs
.filter((p) => !(p.name.trim() === '' && p.value.trim() === ''))
.map((p) => `${p.name}: ${p.value}`)
.join('\n');
}, [pairs]);
const handleChange = useCallback(
(text: string) => {
const pairs = text
.split('\n')
.filter((l: string) => l.trim())
.map(lineToPair);
onChange(pairs);
},
[onChange],
);
return (
<Editor
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
defaultValue={pairsText}
contentType="pairs"
onChange={handleChange}
/>
);
}
function lineToPair(l: string): PairEditorProps['pairs'][0] {
const [name, ...values] = l.split(':');
const pair: PairEditorProps['pairs'][0] = {
name: (name ?? '').trim(),
value: values.join(':').trim(),
};
return pair;
}

View File

@@ -102,7 +102,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
// Use ref so we can update the handler without re-initializing the editor
const handleChange = useRef<EditorProps['onChange']>(onChange);
useEffect(() => {
handleChange.current = onChange;
handleChange.current = onChange ? onChange : onChange;
}, [onChange]);
// Use ref so we can update the handler without re-initializing the editor

View File

@@ -35,6 +35,7 @@ import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import type { Environment, Workspace } from '../../../lib/models';
import type { EditorProps } from './index';
import { pairs } from './pairs/extension';
import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
@@ -71,6 +72,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
'application/xml': xml(),
'text/xml': xml(),
url: url(),
pairs: pairs(),
};
export function getLanguageExtension({

View File

@@ -0,0 +1,11 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parser } from './pairs';
const urlLanguage = LRLanguage.define({
parser,
languageData: {},
});
export function pairs() {
return new LanguageSupport(urlLanguage, []);
}

View File

@@ -0,0 +1,7 @@
import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
Sep: t.bracket,
Key: t.attributeName,
Value: t.string,
});

View File

@@ -0,0 +1,9 @@
@top pairs { (Key? Sep Value)* }
@tokens {
Sep { ":" }
Key { ![:]+ }
Value { ![\n]+ }
}
@external propSource highlight from "./highlight"

View File

@@ -0,0 +1,19 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!QQQOPOOOYOQO'#CaO_OPO'#CaQQOPOOOOOO,58{,58{OdOQO,58{OOOO-E6_-E6_OOOO1G.g1G.g",
stateData: "i~OQQORPO~OSSO~ORTO~OSVO~O",
goto: "]UPPPPPVQRORUR",
nodeNames: "⚠ pairs Key Sep Value",
maxTerm: 6,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "#oRRVOYhYZ!UZ![h![!]#[!];'Sh;'S;=`#U<%lOhRoVQPSQOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!UQ!rSSQOY!mZ;'S!m;'S;=`#O<%lO!mQ#RP;=`<%l!mR#XP;=`<%lhR#cSRPSQOY!mZ;'S!m;'S;=`#O<%lO!m",
tokenizers: [0, 1],
topRules: {"pairs":[0,1]},
tokenPrec: 0
})

View File

@@ -5,6 +5,9 @@ import { memo } from 'react';
const icons = {
alert: lucide.AlertTriangleIcon,
text: lucide.FileTextIcon,
table: lucide.TableIcon,
fileCode: lucide.FileCodeIcon,
archive: lucide.ArchiveIcon,
arrowBigDownDash: lucide.ArrowBigDownDashIcon,
arrowBigLeftDash: lucide.ArrowBigLeftDashIcon,

View File

@@ -1,7 +1,7 @@
import { open } from '@tauri-apps/plugin-dialog';
import classNames from 'classnames';
import type { EditorView } from 'codemirror';
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Fragment, 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';
@@ -49,7 +49,7 @@ type PairContainer = {
id: string;
};
export const PairEditor = memo(function PairEditor({
export function PairEditor({
className,
forceUpdateKey,
nameAutocomplete,
@@ -163,8 +163,8 @@ export const PairEditor = memo(function PairEditor({
<div
className={classNames(
className,
'@container',
'pb-2 mb-auto',
'@container relative',
'pb-2 mb-auto h-full',
!noScroll && 'overflow-y-auto max-h-full',
// Move over the width of the drag handle
'-ml-3',
@@ -204,7 +204,7 @@ export const PairEditor = memo(function PairEditor({
})}
</div>
);
});
}
enum ItemTypes {
ROW = 'pair-row',

View File

@@ -0,0 +1,37 @@
import classNames from 'classnames';
import { useKeyValue } from '../../hooks/useKeyValue';
import { BulkPairEditor } from './BulkPairEditor';
import { IconButton } from './IconButton';
import type { PairEditorProps } from './PairEditor';
import { PairEditor } from './PairEditor';
interface Props extends PairEditorProps {
preferenceName: string;
}
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({
namespace: 'global',
key: ['bulk_edit', preferenceName],
fallback: false,
});
return (
<div className="relative h-full w-full group/wrapper">
{useBulk ? <BulkPairEditor {...props} /> : <PairEditor {...props} />}
<div className="absolute right-0 bottom-0">
<IconButton
size="sm"
variant="border"
title={useBulk ? 'Bulk edit' : 'Regular Edit'}
className={classNames(
'transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow',
'bg-background text-fg-subtle hover:text-fg group-hover/wrapper:opacity-100',
)}
onClick={() => setUseBulk((b) => !b)}
icon={useBulk ? 'table' : 'fileCode'}
/>
</div>
</div>
);
}

View File

@@ -3,18 +3,19 @@ import type { ReactNode } from 'react';
interface Props {
orientation?: 'horizontal' | 'vertical';
variant?: 'primary' | 'secondary';
dashed?: boolean;
className?: string;
children?: ReactNode;
}
export function Separator({ className, orientation = 'horizontal', children }: Props) {
export function Separator({ className, dashed, orientation = 'horizontal', children }: Props) {
return (
<div role="separator" className={classNames(className, 'flex items-center')}>
{children && <div className="text-sm text-fg-subtler mr-2 whitespace-nowrap">{children}</div>}
<div
className={classNames(
'bg-background-highlight',
'h-0 border-t border-t-background-highlight',
dashed && 'border-dashed',
orientation === 'horizontal' && 'w-full h-[1px]',
orientation === 'vertical' && 'h-full w-[1px]',
)}

View File

@@ -102,7 +102,7 @@ export function TextViewer({ response, pretty, className }: Props) {
);
return result;
}, [canFilter, filterText, isJson, isSearching, setFilterText, toggleSearch]);
}, [canFilter, filterText, isJson, isSearching, response.id, setFilterText, toggleSearch]);
return (
<Editor