URL path placeholders

This commit is contained in:
Gregory Schier
2024-08-30 05:24:07 -07:00
parent f8936e7b76
commit c73262b037
20 changed files with 267 additions and 73 deletions

View File

@@ -344,6 +344,7 @@ export const RequestPane = memo(function RequestPane({
<UrlParametersEditor
forceUpdateKey={forceUpdateKey}
urlParameters={activeRequest.urlParameters}
url={activeRequest.url}
onChange={handleUrlParametersChange}
/>
</TabContent>

View File

@@ -1,23 +1,49 @@
import type { HttpRequest } from '@yaakapp/api';
import { useMemo } from 'react';
import type { Pair } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { VStack } from './core/Stacks';
type Props = {
forceUpdateKey: string;
urlParameters: HttpRequest['headers'];
onChange: (headers: HttpRequest['urlParameters']) => void;
url: string;
};
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange, url }: Props) {
const placeholderNames = Array.from(url.matchAll(/\/(:[^/]+)/g)).map((m) => m[1] ?? '');
const pairs = useMemo(() => {
const items: Pair[] = [...urlParameters];
for (const name of placeholderNames) {
const index = items.findIndex((p) => p.name === name);
if (index >= 0) {
items[index]!.readOnlyName = true;
} else {
items.push({
name,
value: '',
enabled: true,
readOnlyName: true,
});
}
}
return items;
}, [placeholderNames, urlParameters]);
return (
<PairOrBulkEditor
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"
valuePlaceholder="Value"
pairs={urlParameters}
onChange={onChange}
forceUpdateKey={forceUpdateKey}
/>
<VStack className="h-full">
<PairOrBulkEditor
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"
valuePlaceholder="Value"
pairs={pairs}
onChange={onChange}
forceUpdateKey={forceUpdateKey + placeholderNames.join(':')}
/>
</VStack>
);
}

View File

@@ -42,12 +42,13 @@ export function BulkPairEditor({
);
}
function lineToPair(l: string): PairEditorProps['pairs'][0] {
const [name, ...values] = l.split(':');
function lineToPair(line: string): PairEditorProps['pairs'][0] {
const [, name, value] = line.match(/^(:?[^:]+):\s+([^$]*)/) ?? [];
const pair: PairEditorProps['pairs'][0] = {
enabled: true,
name: (name ?? '').trim(),
value: values.join(':').trim(),
value: (value ?? '').trim(),
};
return pair;
}

View File

@@ -19,7 +19,8 @@
}
.cm-line {
@apply w-full; /* Important! Ensure it spans the entire width */
@apply w-full;
/* Important! Ensure it spans the entire width */
@apply w-full text-text pl-1 pr-1.5;
}
@@ -169,9 +170,14 @@
}
}
.cm-wrapper.cm-readonly .cm-editor {
.cm-cursor {
@apply border-danger !important;
/* Cursor and mouse cursor for readonly mode */
.cm-wrapper.cm-readonly {
.cm-editor .cm-cursor {
@apply hidden !important;
}
&.cm-singleline .cm-line {
@apply cursor-default;
}
}

View File

@@ -39,6 +39,7 @@ export { formatSdl } from 'format-graphql';
export interface EditorProps {
id?: string;
readOnly?: boolean;
disabled?: boolean;
type?: 'text' | 'password';
className?: string;
heightMode?: 'auto' | 'full';

View File

@@ -46,6 +46,10 @@ export const syntaxHighlightStyle = HighlightStyle.define([
color: 'var(--textSubtlest)',
fontStyle: 'italic',
},
{
tag: [t.emphasis],
textDecoration: 'underline',
},
{
tag: [t.paren, t.bracket, t.brace],
color: 'var(--textSubtle)',

View File

@@ -1,8 +1,8 @@
@top pairs { (Key? Sep Value)* }
@top pairs { (Key Sep Value "\n")* }
@tokens {
Sep { ":" }
Key { ![:]+ }
Key { ":"? ![:]+ }
Value { ![\n]+ }
}

View File

@@ -0,0 +1,6 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
pairs = 1,
Key = 2,
Sep = 3,
Value = 4

View File

@@ -3,17 +3,17 @@ 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",
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
goto: "]UPPPPPVQQORSQ",
nodeNames: "⚠ pairs Key Sep Value",
maxTerm: 6,
maxTerm: 7,
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],
tokenData: "$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenizers: [0, 1, 2],
topRules: {"pairs":[0,1]},
tokenPrec: 0
tokenPrec: 0,
termNames: {"0":"⚠","1":"@top","2":"Key","3":"Sep","4":"Value","5":"(Key Sep Value \"\\n\")+","6":"␄","7":"\"\\n\""}
})

View File

@@ -1,7 +1,8 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const Template = 1,
export const
Template = 1,
Tag = 2,
TagOpen = 3,
TagContent = 4,
Open = 3,
Close = 5,
Text = 6;
TagClose = 5,
Text = 6

View File

@@ -16,4 +16,3 @@ export const parser = LRParser.deserialize({
topRules: {"Template":[0,1]},
tokenPrec: 0
})

View File

@@ -2,6 +2,8 @@ import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
Protocol: t.comment,
Placeholder: t.emphasis,
// PathSegment: t.tagName,
// Port: t.attributeName,
// Host: t.variableName,
// Path: t.bool,

View File

@@ -1,18 +1,22 @@
@top url { Protocol? Host Port? Path? Query? }
@top Program { url }
Query {
"?" queryPair ("&" queryPair)*
}
url { Protocol? Host Port? Path? Query? }
Path { ("/" (Placeholder | PathSegment))+ }
Query { "?" queryPair ("&" queryPair)* }
@tokens {
Protocol { $[a-zA-Z]+ "://" }
Path { ("/" $[a-zA-Z0-9\-_.]*)+ }
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
Port { ":" $[0-9]+ }
Host { $[a-zA-Z0-9-_.]+ }
Port { ":" $[0-9]+ }
Placeholder { ":" ![/?#]+ }
PathSegment { ![?#/]+ }
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
// Protocol/host overlaps, so give proto explicit precedence
@precedence { Protocol, Host }
@precedence { Placeholder, PathSegment }
}
@external propSource highlight from "./highlight"

View File

@@ -1,8 +1,10 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
url = 1,
Program = 1,
Protocol = 2,
Host = 3,
Port = 4,
Path = 5,
Query = 6
Placeholder = 6,
PathSegment = 7,
Query = 8

View File

@@ -3,16 +3,17 @@ import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!jOQOPOOQYOPOOOTOPOOOeOQO'#CbQOOOOOQ`OPOOQ]OPOOOjOPO,58|OrOQO'#CcOwOPO1G.hOOOO,58},58}OOOO-E6a-E6a",
stateData: "!S~OQQORPO~OSUOTTOXRO~OYVO~OZWOWUa~OYYO~OZWOWUi~OQR~",
goto: "dWPPPPPPX^VSPTUQXVRZX",
nodeNames: "⚠ url Protocol Host Port Path Query",
maxTerm: 11,
states: "$UOQOPOOOYOPO'#ChOhOPO'#ChQOOOOOOmOQO'#CeOuOPO'#CaO!QOSO'#CdOOOO,59S,59SO!VOPO,59SO!_OPO,59SO!jOPO,59SOOOO,59P,59POOOO-E6c-E6cO!xOPO,59OOOOO1G.n1G.nO#QOPO1G.nO#YOPO1G.nO#eOSO'#CfO#jOPO1G.jOOOO7+$Y7+$YO#rOPO7+$YOOOO,59Q,59QOOOO-E6d-E6dOOOO<<Gt<<Gt",
stateData: "$Q~OQQORPO~OSXO]SO^UOZ[X~ORYO~OUZOVZO~O]SOZTX^TX~O_]O~O^UOZ[a~O]SO^UOZ[a~OS`O]SO^UOZ[a~O`aOZWa~O^UOZ[i~O]SO^UOZ[i~O_eO~O`aOZWi~O^UOZ[q~OQRUVU~",
goto: "!Z]PPPPP^PPhw!QP!WQWPS_XYRd`QVPU^WXYSc_`RgdWTPXY`R[TQb]RfbRRO",
nodeNames: "⚠ Program Protocol Host Port Path Placeholder PathSegment Query",
maxTerm: 16,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "%[~RYvwq}!Ov!O!Pv!P!Q!_!Q![!y![!]#u!a!b$T!c!}$Y#R#Sv#T#o$Y~vOZ~P{URP}!Ov!O!Pv!Q![v!c!}v#R#Sv#T#ov~!dVT~}!O!_!O!P!_!P!Q!_!Q![!_!c!}!_#R#S!_#T#o!_R#QVYQRP}!Ov!O!Pv!Q![!y!_!`#g!c!}!y#R#Sv#T#o!yQ#lRYQ!Q![#g!c!}#g#T#o#g~#xP!Q![#{~$QPS~!Q![#{~$YOX~R$aWYQRP}!Ov!O!Pv!Q![!y![!]$y!_!`#g!c!}$Y#R#Sv#T#o$YP$|P!P!Q%PP%SP!P!Q%VP%[OQP",
tokenizers: [0, 1],
topRules: {"url":[0,1]},
tokenPrec: 47
repeatNodeCount: 2,
tokenData: "+h~RdOs!atv!avw#Ow}!a}!O#i!O!P#i!P!Q$o!Q![$t![!]&|!]!a!a!a!b)Z!b!c!a!c!})`!}#R!a#R#S#i#S#T!a#T#o)`#o;'S!a;'S;=`!x<%lO!aQ!fUVQOs!at!P!a!Q!a!a!b;'S!a;'S;=`!x<%lO!aQ!{P;=`<%l!aR#VU`PVQOs!at!P!a!Q!a!a!b;'S!a;'S;=`!x<%lO!aR#p_RPVQOs!at}!a}!O#i!O!P#i!Q![#i![!a!a!b!c!a!c!}#i!}#R!a#R#S#i#S#T!a#T#o#i#o;'S!a;'S;=`!x<%lO!a~$tO]~V$}a_SRPVQOs!at}!a}!O#i!O!P#i!Q![$t![!_!a!_!`&S!`!a!a!b!c!a!c!}$t!}#R!a#R#S#i#S#T!a#T#o$t#o;'S!a;'S;=`!x<%lO!aU&ZZ_SVQOs!at!P!a!Q![&S![!a!a!b!c!a!c!}&S!}#T!a#T#o&S#o;'S!a;'S;=`!x<%lO!aR'RXVQOs'ntv'nvw!aw!P'n!Q![(e![!a'n!b;'S'n;'S;=`(_<%lO'nQ'uWUQVQOs'ntv'nvw!aw!P'n!Q!a'n!b;'S'n;'S;=`(_<%lO'nQ(bP;=`<%l'nR(nXSPUQVQOs'ntv'nvw!aw!P'n!Q![(e![!a'n!b;'S'n;'S;=`(_<%lO'n~)`O^~V)ib_SRPVQOs!at}!a}!O#i!O!P#i!Q![$t![!]*q!]!_!a!_!`&S!`!a!a!b!c!a!c!})`!}#R!a#R#S#i#S#T!a#T#o)`#o;'S!a;'S;=`!x<%lO!aR*vVVQOs!at!P!a!P!Q+]!Q!a!a!b;'S!a;'S;=`!x<%lO!aP+`P!P!Q+cP+hOQP",
tokenizers: [0, 1, 2],
topRules: {"Program":[0,1]},
tokenPrec: 134,
termNames: {"0":"⚠","1":"@top","2":"Protocol","3":"Host","4":"Port","5":"Path","6":"Placeholder","7":"PathSegment","8":"Query","9":"(\"/\" (Placeholder | PathSegment))+","10":"(\"&\" queryPair)+","11":"␄","12":"url","13":"\"/\"","14":"\"?\"","15":"queryPair","16":"\"&\""}
})

View File

@@ -22,6 +22,7 @@ export type InputProps = Omit<
| 'autoSelect'
| 'autocompleteVariables'
| 'onKeyDown'
| 'readOnly'
> & {
name: string;
type?: 'text' | 'password';
@@ -68,6 +69,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
size = 'md',
type = 'text',
validate,
readOnly,
...props
}: InputProps,
ref,
@@ -77,9 +79,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const [focused, setFocused] = useState(false);
const handleFocus = useCallback(() => {
if (readOnly) return;
setFocused(true);
onFocus?.();
}, [onFocus]);
}, [onFocus, readOnly]);
const handleBlur = useCallback(() => {
setFocused(false);
@@ -179,6 +182,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
className={editorClassName}
onFocus={handleFocus}
onBlur={handleBlur}
readOnly={readOnly}
{...props}
/>
</HStack>

View File

@@ -41,6 +41,7 @@ export type Pair = {
value: string;
contentType?: string;
isFile?: boolean;
readOnlyName?: boolean;
};
type PairContainer = {
@@ -254,8 +255,8 @@ function PairEditorRow({
valueAutocomplete,
valueAutocompleteVariables,
valuePlaceholder,
valueValidate,
valueType,
valueValidate,
}: PairEditorRowProps) {
const { id } = pairContainer;
const ref = useRef<HTMLDivElement>(null);
@@ -374,6 +375,7 @@ function PairEditorRow({
ref={nameInputRef}
hideLabel
useTemplating
readOnly={pairContainer.pair.readOnlyName}
size="sm"
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
validate={nameValidate}
@@ -476,7 +478,14 @@ function PairEditorRow({
</RadioDropdown>
) : (
<Dropdown
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
items={[
{
key: 'delete',
label: 'Delete',
onSelect: handleDelete,
variant: 'danger',
},
]}
>
<IconButton
iconSize="sm"