mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-19 10:08:12 +01:00
Placeholder CM tags working
This commit is contained in:
@@ -8,6 +8,7 @@ import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { useCopyHttpResponse } from './useCopyHttpResponse';
|
||||
|
||||
interface Props {
|
||||
responses: HttpResponse[];
|
||||
@@ -25,6 +26,7 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
|
||||
const latestResponseId = responses[0]?.id ?? 'n/a';
|
||||
const saveResponse = useSaveResponse(activeResponse);
|
||||
const copyResponse = useCopyHttpResponse(activeResponse);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
@@ -37,6 +39,14 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
hidden: responses.length === 0,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{
|
||||
key: 'copy',
|
||||
label: 'Copy to Clipboard',
|
||||
onSelect: copyResponse.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: responses.length === 0,
|
||||
disabled: responses.length === 0,
|
||||
},
|
||||
{
|
||||
key: 'clear-single',
|
||||
label: 'Delete',
|
||||
|
||||
@@ -34,6 +34,7 @@ import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor';
|
||||
import type { GenericCompletionOption } from './core/Editor/genericCompletion';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import type { Pair } from './core/PairEditor';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
@@ -89,6 +90,27 @@ export const RequestPane = memo(function RequestPane({
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
||||
(m) => m[1] ?? '',
|
||||
);
|
||||
const items: Pair[] = [...activeRequest.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 { urlParameterPairs: items, urlParametersKey: placeholderNames.join(',') };
|
||||
}, [activeRequest.url, activeRequest.urlParameters]);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
@@ -162,7 +184,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
label: (
|
||||
<div className="flex items-center">
|
||||
Params
|
||||
<CountBadge count={activeRequest.urlParameters.filter((p) => p.name).length} />
|
||||
<CountBadge count={urlParameterPairs.filter((p) => p.name).length} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -212,11 +234,11 @@ export const RequestPane = memo(function RequestPane({
|
||||
activeRequest.bodyType,
|
||||
activeRequest.headers,
|
||||
activeRequest.method,
|
||||
activeRequest.urlParameters,
|
||||
activeRequestId,
|
||||
handleContentTypeChange,
|
||||
toast,
|
||||
updateRequest,
|
||||
urlParameterPairs,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -342,9 +364,8 @@ export const RequestPane = memo(function RequestPane({
|
||||
</TabContent>
|
||||
<TabContent value="params">
|
||||
<UrlParametersEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
urlParameters={activeRequest.urlParameters}
|
||||
url={activeRequest.url}
|
||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||
pairs={urlParameterPairs}
|
||||
onChange={handleUrlParametersChange}
|
||||
/>
|
||||
</TabContent>
|
||||
|
||||
@@ -1,37 +1,14 @@
|
||||
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'];
|
||||
pairs: HttpRequest['headers'];
|
||||
onChange: (headers: HttpRequest['urlParameters']) => void;
|
||||
url: string;
|
||||
};
|
||||
|
||||
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]);
|
||||
|
||||
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props) {
|
||||
return (
|
||||
<VStack className="h-full">
|
||||
<PairOrBulkEditor
|
||||
@@ -42,7 +19,7 @@ export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange, u
|
||||
valuePlaceholder="Value"
|
||||
pairs={pairs}
|
||||
onChange={onChange}
|
||||
forceUpdateKey={forceUpdateKey + placeholderNames.join(':')}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
@@ -227,6 +227,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
[dialog],
|
||||
);
|
||||
|
||||
const onClickPathParameter = useCallback(async (name: string) => {
|
||||
console.log('TODO: Focus', name, 'in params tab');
|
||||
}, []);
|
||||
|
||||
// Update the language extension when the language changes
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
@@ -240,6 +244,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickFunction,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
});
|
||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||
}, [
|
||||
@@ -251,6 +256,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickFunction,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
]);
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
@@ -274,6 +280,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onClickVariable,
|
||||
onClickFunction,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
|
||||
@@ -88,12 +88,14 @@ export function getLanguageExtension({
|
||||
onClickVariable,
|
||||
onClickFunction,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
}: {
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
templateFunctions: TemplateFunction[];
|
||||
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
} & Pick<EditorProps, 'language' | 'useTemplating' | 'autocomplete'>) {
|
||||
if (language === 'graphql') {
|
||||
return graphql();
|
||||
@@ -112,6 +114,7 @@ export function getLanguageExtension({
|
||||
onClickFunction,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ export function twig({
|
||||
onClickFunction,
|
||||
onClickVariable,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
}: {
|
||||
base: LanguageSupport;
|
||||
environmentVariables: EnvironmentVariable[];
|
||||
@@ -26,6 +27,7 @@ export function twig({
|
||||
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
|
||||
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
|
||||
onClickPathParameter: (name: string) => void;
|
||||
}) {
|
||||
const language = mixLanguage(base);
|
||||
|
||||
@@ -62,11 +64,11 @@ export function twig({
|
||||
return [
|
||||
language,
|
||||
base.support,
|
||||
templateTagsPlugin(options, onClickMissingVariable),
|
||||
language.data.of({ autocomplete: completions }),
|
||||
base.language.data.of({ autocomplete: completions }),
|
||||
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||
base.language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||
templateTagsPlugin(options, onClickMissingVariable, onClickPathParameter),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,42 @@ import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import type { SyntaxNodeRef } from '@lezer/common';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
|
||||
class PathPlaceholderWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
|
||||
constructor(readonly rawText: string, readonly startPos: number, readonly onClick: () => void) {
|
||||
super();
|
||||
this.#clickListenerCallback = () => {
|
||||
this.onClick?.();
|
||||
};
|
||||
}
|
||||
|
||||
eq(other: PathPlaceholderWidget) {
|
||||
return this.startPos === other.startPos && this.rawText === other.rawText;
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
elt.className = `x-theme-templateTag x-theme-templateTag--secondary template-tag`;
|
||||
elt.textContent = this.rawText;
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
ignoreEvent() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class TemplateTagWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
|
||||
@@ -62,20 +95,41 @@ function templateTags(
|
||||
view: EditorView,
|
||||
options: TwigCompletionOption[],
|
||||
onClickMissingVariable: (name: string, rawTag: string, startPos: number) => void,
|
||||
onClickPathParameter: (name: string) => void,
|
||||
): DecorationSet {
|
||||
const widgets: Range<Decoration>[] = [];
|
||||
for (const { from, to } of view.visibleRanges) {
|
||||
syntaxTree(view.state).iterate({
|
||||
const tree = syntaxTree(view.state);
|
||||
tree.iterate({
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name == 'Tag') {
|
||||
// Don't decorate if the cursor is inside the match
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.from > node.from && r.to < node.to) {
|
||||
return;
|
||||
if (node.name === 'Text') {
|
||||
// Find the `url` node and then jump into it to find the placeholders
|
||||
for (let i = node.from; i < node.to; i++) {
|
||||
const innerTree = syntaxTree(view.state).resolveInner(i);
|
||||
if (innerTree.node.name === 'url') {
|
||||
innerTree.toTree().iterate({
|
||||
enter(node) {
|
||||
if (node.name !== 'Placeholder') return;
|
||||
if (isSelectionInsideNode(view, node)) return;
|
||||
|
||||
const globalFrom = innerTree.node.from + node.from;
|
||||
const globalTo = innerTree.node.from + node.to;
|
||||
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
||||
const onClick = () => onClickPathParameter(rawText);
|
||||
const widget = new PathPlaceholderWidget(rawText, globalFrom, onClick);
|
||||
const deco = Decoration.replace({ widget, inclusive: false });
|
||||
console.log('ADDED WIDGET', globalFrom, node, rawText);
|
||||
widgets.push(deco.range(globalFrom, globalTo));
|
||||
},
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (node.name === 'Tag') {
|
||||
// Don't decorate if the cursor is inside the match
|
||||
if (isSelectionInsideNode(view, node)) return;
|
||||
|
||||
const rawTag = view.state.doc.sliceString(node.from, node.to);
|
||||
|
||||
@@ -114,17 +168,28 @@ function templateTags(
|
||||
export function templateTagsPlugin(
|
||||
options: TwigCompletionOption[],
|
||||
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void,
|
||||
onClickPathParameter: (name: string) => void,
|
||||
) {
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
decorations: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.decorations = templateTags(view, options, onClickMissingVariable);
|
||||
this.decorations = templateTags(
|
||||
view,
|
||||
options,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
this.decorations = templateTags(update.view, options, onClickMissingVariable);
|
||||
this.decorations = templateTags(
|
||||
update.view,
|
||||
options,
|
||||
onClickMissingVariable,
|
||||
onClickPathParameter,
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -146,3 +211,10 @@ export function templateTagsPlugin(
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function isSelectionInsideNode(view: EditorView, node: SyntaxNodeRef) {
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.from > node.from && r.to < node.to) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ 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,
|
||||
// Query: t.string,
|
||||
PathSegment: t.tagName,
|
||||
Port: t.attributeName,
|
||||
Host: t.variableName,
|
||||
Path: t.bool,
|
||||
Query: t.string,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
@top Program { url }
|
||||
|
||||
url { Protocol? Host Port? Path? Query? }
|
||||
@top url { Protocol? Host Port? Path? Query? }
|
||||
|
||||
Path { ("/" (Placeholder | PathSegment))+ }
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
Program = 1,
|
||||
url = 1,
|
||||
Protocol = 2,
|
||||
Host = 3,
|
||||
Port = 4,
|
||||
|
||||
@@ -3,17 +3,17 @@ import {LRParser} from "@lezer/lr"
|
||||
import {highlight} from "./highlight"
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
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,
|
||||
states: "#SOQOPOOQYOPOOOTOPOOOeOQO'#CeOmOPO'#CaOxOSO'#CdQOOOOOQ`OPOOQ]OPOOOOOO,59P,59POOOO-E6c-E6cO}OPO,59OO!VOSO'#CfO![OPO1G.jOOOO,59Q,59QOOOO-E6d-E6d",
|
||||
stateData: "!j~OQQORPO~OSWO[RO]TO~OUXOVXO~O[ROZTX]TX~O^ZO~O_[OZWa~O^^O~O_[OZWi~OQRUVU~",
|
||||
goto: "rZPPPPP[PP`elTVPWVUPVWSSPWRYSQ]ZR_]",
|
||||
nodeNames: "⚠ url Protocol Host Port Path Placeholder PathSegment Query",
|
||||
maxTerm: 15,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
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",
|
||||
tokenData: "+U~RdOs!atv!avw#Ow}!a}!O#i!O!P#i!P!Q$o!Q![$t![!]&|!]!a!a!a!b(w!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'RVVQOs'ht!P'h!Q![(X![!a'h!b;'S'h;'S;=`(R<%lO'hQ'oUUQVQOs'ht!P'h!Q!a'h!b;'S'h;'S;=`(R<%lO'hQ(UP;=`<%l'hR(bVSPUQVQOs'ht!P'h!Q![(X![!a'h!b;'S'h;'S;=`(R<%lO'h~(|O]~V)Vb^SRPVQOs!at}!a}!O#i!O!P#i!Q![$t![!]*_!]!_!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*dVVQOs!at!P!a!P!Q*y!Q!a!a!b;'S!a;'S;=`!x<%lO!aP*|P!P!Q+PP+UOQP",
|
||||
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":"\"&\""}
|
||||
topRules: {"url":[0,1]},
|
||||
tokenPrec: 66,
|
||||
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":"\"/\"","13":"\"?\"","14":"queryPair","15":"\"&\""}
|
||||
})
|
||||
|
||||
15
src-web/components/useCopyHttpResponse.ts
Normal file
15
src-web/components/useCopyHttpResponse.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpResponse } from '@yaakapp/api';
|
||||
import { useCopy } from '../hooks/useCopy';
|
||||
import { getResponseBodyText } from '../lib/responseBody';
|
||||
|
||||
export function useCopyHttpResponse(response: HttpResponse) {
|
||||
const copy = useCopy();
|
||||
return useMutation({
|
||||
mutationKey: ['copy_http_response'],
|
||||
async mutationFn() {
|
||||
const body = await getResponseBodyText(response);
|
||||
copy(body);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import { attachConsole } from '@tauri-apps/plugin-log';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
|
||||
Reference in New Issue
Block a user