mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-25 02:08:28 +02:00
Autocomplete, and more CM stuff!
This commit is contained in:
@@ -21,5 +21,10 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"@typescript-eslint/consistent-type-imports": ["error", {
|
||||||
|
prefer: "type-imports",
|
||||||
|
disallowTypeAnnotations: true,
|
||||||
|
fixStyle: "separate-type-imports",
|
||||||
|
}]
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use reqwest::redirect::Policy;
|
|||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use sqlx::sqlite::SqlitePoolOptions;
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
|
use tauri::regex::Regex;
|
||||||
use tauri::{AppHandle, State, Wry};
|
use tauri::{AppHandle, State, Wry};
|
||||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
@@ -61,15 +62,33 @@ async fn send_request(
|
|||||||
.expect("Failed to get request");
|
.expect("Failed to get request");
|
||||||
let start = std::time::Instant::now();
|
let start = std::time::Instant::now();
|
||||||
|
|
||||||
let mut abs_url = req.url.to_string();
|
let mut url_string = req.url.to_string();
|
||||||
if !abs_url.starts_with("http://") && !abs_url.starts_with("https://") {
|
|
||||||
abs_url = format!("http://{}", req.url);
|
let mut variables = HashMap::new();
|
||||||
|
variables.insert("PROJECT_ID", "project_123");
|
||||||
|
variables.insert("TOKEN", "s3cret");
|
||||||
|
variables.insert("DOMAIN", "schier.co");
|
||||||
|
variables.insert("BASE_URL", "https://schier.co");
|
||||||
|
|
||||||
|
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex");
|
||||||
|
url_string = re
|
||||||
|
.replace(&url_string, |caps: &tauri::regex::Captures| {
|
||||||
|
let key = caps.get(1).unwrap().as_str();
|
||||||
|
match variables.get(key) {
|
||||||
|
Some(v) => v,
|
||||||
|
None => "",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
|
||||||
|
url_string = format!("http://{}", url_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.redirect(Policy::none())
|
.redirect(Policy::none())
|
||||||
.build()
|
.build()
|
||||||
.unwrap();
|
.expect("Failed to build client");
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(USER_AGENT, HeaderValue::from_static("reqwest"));
|
headers.insert(USER_AGENT, HeaderValue::from_static("reqwest"));
|
||||||
@@ -79,14 +98,21 @@ async fn send_request(
|
|||||||
HeaderValue::from_str(models::generate_id("x").as_str()).expect("Failed to create header"),
|
HeaderValue::from_str(models::generate_id("x").as_str()).expect("Failed to create header"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let m = Method::from_bytes(req.method.to_uppercase().as_bytes()).unwrap();
|
let m =
|
||||||
let builder = client.request(m, abs_url.to_string()).headers(headers);
|
Method::from_bytes(req.method.to_uppercase().as_bytes()).expect("Failed to create method");
|
||||||
|
let builder = client.request(m, url_string.to_string()).headers(headers);
|
||||||
|
|
||||||
let sendable_req = match req.body {
|
let sendable_req_result = match req.body {
|
||||||
Some(b) => builder.body(b).build(),
|
Some(b) => builder.body(b).build(),
|
||||||
None => builder.build(),
|
None => builder.build(),
|
||||||
}
|
};
|
||||||
.expect("Failed to build request");
|
|
||||||
|
let sendable_req = match sendable_req_result {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => {
|
||||||
|
return Err(e.to_string());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let resp = client.execute(sendable_req).await;
|
let resp = client.execute(sendable_req).await;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import Editor from './components/Editor/Editor';
|
import Editor from './components/Editor/Editor';
|
||||||
import { HStack, VStack } from './components/Stacks';
|
import { HStack, VStack } from './components/Stacks';
|
||||||
import { WindowDragRegion } from './components/WindowDragRegion';
|
import { WindowDragRegion } from './components/WindowDragRegion';
|
||||||
@@ -40,11 +40,17 @@ function App() {
|
|||||||
return () => document.documentElement.removeEventListener('keypress', listener);
|
return () => document.documentElement.removeEventListener('keypress', listener);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const [screenWidth, setScreenWidth] = useState(window.innerWidth);
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('SCREEN WIDTH', document.documentElement.clientWidth);
|
||||||
|
window.addEventListener('resize', () => setScreenWidth(window.innerWidth));
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-[auto_1fr] h-full text-gray-900">
|
<div className="grid grid-cols-[auto_1fr] h-full text-gray-900">
|
||||||
<Sidebar requests={requests ?? []} workspaceId={workspaceId} activeRequestId={request?.id} />
|
<Sidebar requests={requests ?? []} workspaceId={workspaceId} activeRequestId={request?.id} />
|
||||||
{request && (
|
{request && (
|
||||||
<Grid cols={2}>
|
<Grid cols={screenWidth > 700 ? 2 : 1} rows={screenWidth > 700 ? 1 : 2}>
|
||||||
<VStack className="w-full">
|
<VStack className="w-full">
|
||||||
<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">
|
<HStack as={WindowDragRegion} items="center" className="pl-3 pr-1.5">
|
||||||
Test Request
|
Test Request
|
||||||
@@ -61,7 +67,7 @@ function App() {
|
|||||||
sendRequest={sendRequest.mutate}
|
sendRequest={sendRequest.mutate}
|
||||||
/>
|
/>
|
||||||
<Editor
|
<Editor
|
||||||
key={request.id}
|
valueKey={request.id}
|
||||||
useTemplating
|
useTemplating
|
||||||
defaultValue={request.body ?? undefined}
|
defaultValue={request.body ?? undefined}
|
||||||
contentType="application/json"
|
contentType="application/json"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { LinkProps } from 'react-router-dom';
|
import type { LinkProps } from 'react-router-dom';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { Button, ButtonProps } from './Button';
|
import type { ButtonProps } from './Button';
|
||||||
|
import { Button } from './Button';
|
||||||
|
|
||||||
type Props = ButtonProps<typeof Link> & LinkProps;
|
type Props = ButtonProps<typeof Link> & LinkProps;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import './Editor.css';
|
|
||||||
import { HTMLAttributes, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { EditorView } from 'codemirror';
|
|
||||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
|
||||||
import type { TransactionSpec } from '@codemirror/state';
|
|
||||||
import { Compartment, EditorSelection, EditorState, Transaction } from '@codemirror/state';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { autocompletion } from '@codemirror/autocomplete';
|
import { autocompletion } from '@codemirror/autocomplete';
|
||||||
|
import type { Transaction, TransactionSpec } from '@codemirror/state';
|
||||||
|
import { Compartment, EditorSelection, EditorState, Prec } from '@codemirror/state';
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { EditorView } from 'codemirror';
|
||||||
|
import type { HTMLAttributes } from 'react';
|
||||||
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
import './Editor.css';
|
||||||
|
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||||
|
|
||||||
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onChange'> {
|
||||||
contentType: string;
|
contentType: string;
|
||||||
@@ -96,18 +97,19 @@ function getExtensions({
|
|||||||
}: Pick<Props, 'singleLine' | 'onChange' | 'onSubmit' | 'contentType' | 'useTemplating'>) {
|
}: Pick<Props, 'singleLine' | 'onChange' | 'onSubmit' | 'contentType' | 'useTemplating'>) {
|
||||||
const ext = getLanguageExtension({ contentType, useTemplating });
|
const ext = getLanguageExtension({ contentType, useTemplating });
|
||||||
return [
|
return [
|
||||||
autocompletion(),
|
|
||||||
...(singleLine
|
...(singleLine
|
||||||
? [
|
? [
|
||||||
EditorView.domEventHandlers({
|
Prec.high(
|
||||||
keydown: (e) => {
|
EditorView.domEventHandlers({
|
||||||
// TODO: Figure out how to not have this mess up autocomplete
|
keydown: (e) => {
|
||||||
if (e.key === 'Enter') {
|
// TODO: Figure out how to not have this not trigger on autocomplete selection
|
||||||
e.preventDefault();
|
if (e.key === 'Enter') {
|
||||||
onSubmit?.();
|
e.preventDefault();
|
||||||
}
|
onSubmit?.();
|
||||||
},
|
}
|
||||||
}),
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
EditorState.transactionFilter.of(
|
EditorState.transactionFilter.of(
|
||||||
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
(tr: Transaction): TransactionSpec | TransactionSpec[] => {
|
||||||
if (!tr.isUserEvent('input.paste')) {
|
if (!tr.isUserEvent('input.paste')) {
|
||||||
@@ -117,7 +119,6 @@ function getExtensions({
|
|||||||
// let addedNewline = false;
|
// let addedNewline = false;
|
||||||
const trs: TransactionSpec[] = [];
|
const trs: TransactionSpec[] = [];
|
||||||
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
tr.changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||||
// console.log('CHANGE', { fromA, toA }, { fromB, toB }, inserted);
|
|
||||||
let insert = '';
|
let insert = '';
|
||||||
for (const line of inserted) {
|
for (const line of inserted) {
|
||||||
insert += line.replace('\n', '');
|
insert += line.replace('\n', '');
|
||||||
|
|||||||
34
src-web/components/Editor/completion/completion.ts
Normal file
34
src-web/components/Editor/completion/completion.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||||
|
|
||||||
|
const openTag = '${[ ';
|
||||||
|
const closeTag = ' ]}';
|
||||||
|
|
||||||
|
const variables = [
|
||||||
|
{ name: 'DOMAIN' },
|
||||||
|
{ name: 'BASE_URL' },
|
||||||
|
{ name: 'TOKEN' },
|
||||||
|
{ name: 'PROJECT_ID' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function myCompletions(context: CompletionContext) {
|
||||||
|
// console.log('COMPLETE', context);
|
||||||
|
const toStartOfName = context.matchBefore(/\w*/);
|
||||||
|
const toStartOfVariable = context.matchBefore(/\$\{.*/);
|
||||||
|
const toMatch = toStartOfVariable ?? toStartOfName ?? null;
|
||||||
|
|
||||||
|
if (toMatch === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (toMatch.from === toMatch.to && !context.explicit) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
from: toMatch.from,
|
||||||
|
options: variables.map((v) => ({
|
||||||
|
label: `${openTag}${v.name}${closeTag}`,
|
||||||
|
type: 'variable',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,18 +1,27 @@
|
|||||||
import { parser as twigParser } from './twig/twig';
|
import {
|
||||||
|
autocompletion,
|
||||||
|
closeBrackets,
|
||||||
|
closeBracketsKeymap,
|
||||||
|
completionKeymap,
|
||||||
|
} from '@codemirror/autocomplete';
|
||||||
|
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
||||||
|
import { html } from '@codemirror/lang-html';
|
||||||
|
import { javascript } from '@codemirror/lang-javascript';
|
||||||
|
import { json } from '@codemirror/lang-json';
|
||||||
|
import { xml } from '@codemirror/lang-xml';
|
||||||
import {
|
import {
|
||||||
bracketMatching,
|
bracketMatching,
|
||||||
foldGutter,
|
foldGutter,
|
||||||
foldInside,
|
|
||||||
foldKeymap,
|
foldKeymap,
|
||||||
foldNodeProp,
|
|
||||||
HighlightStyle,
|
HighlightStyle,
|
||||||
indentNodeProp,
|
|
||||||
indentOnInput,
|
indentOnInput,
|
||||||
LanguageSupport,
|
LanguageSupport,
|
||||||
LRLanguage,
|
LRLanguage,
|
||||||
syntaxHighlighting,
|
syntaxHighlighting,
|
||||||
} from '@codemirror/language';
|
} from '@codemirror/language';
|
||||||
import { lintKeymap } from '@codemirror/lint';
|
import { lintKeymap } from '@codemirror/lint';
|
||||||
|
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
import {
|
import {
|
||||||
crosshairCursor,
|
crosshairCursor,
|
||||||
drawSelection,
|
drawSelection,
|
||||||
@@ -24,23 +33,12 @@ import {
|
|||||||
lineNumbers,
|
lineNumbers,
|
||||||
rectangularSelection,
|
rectangularSelection,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { html } from '@codemirror/lang-html';
|
|
||||||
import { xml } from '@codemirror/lang-xml';
|
|
||||||
import { parseMixed } from '@lezer/common';
|
import { parseMixed } from '@lezer/common';
|
||||||
import { EditorState } from '@codemirror/state';
|
|
||||||
import { json } from '@codemirror/lang-json';
|
|
||||||
import { javascript } from '@codemirror/lang-javascript';
|
|
||||||
import { tags as t } from '@lezer/highlight';
|
import { tags as t } from '@lezer/highlight';
|
||||||
import { defaultKeymap, history, historyKeymap } from '@codemirror/commands';
|
import { myCompletions } from './completion/completion';
|
||||||
import { highlightSelectionMatches, searchKeymap } from '@codemirror/search';
|
import { parser as twigParser } from './twig/twig';
|
||||||
import {
|
|
||||||
autocompletion,
|
|
||||||
closeBrackets,
|
|
||||||
closeBracketsKeymap,
|
|
||||||
completionKeymap,
|
|
||||||
} from '@codemirror/autocomplete';
|
|
||||||
import { placeholders } from './widgets';
|
|
||||||
import { url } from './url/extension';
|
import { url } from './url/extension';
|
||||||
|
import { placeholders } from './widgets';
|
||||||
|
|
||||||
export const myHighlightStyle = HighlightStyle.define([
|
export const myHighlightStyle = HighlightStyle.define([
|
||||||
{
|
{
|
||||||
@@ -82,13 +80,13 @@ export const myHighlightStyle = HighlightStyle.define([
|
|||||||
// { tag: t.invalid, color: '#f00' },
|
// { tag: t.invalid, color: '#f00' },
|
||||||
// ]);
|
// ]);
|
||||||
|
|
||||||
const syntaxExtensions: Record<string, { base: LanguageSupport; ext: any[] }> = {
|
const syntaxExtensions: Record<string, LanguageSupport> = {
|
||||||
'application/json': { base: json(), ext: [] },
|
'application/json': json(),
|
||||||
'application/javascript': { base: javascript(), ext: [] },
|
'application/javascript': javascript(),
|
||||||
'text/html': { base: html(), ext: [] },
|
'text/html': html(),
|
||||||
'application/xml': { base: xml(), ext: [] },
|
'application/xml': xml(),
|
||||||
'text/xml': { base: xml(), ext: [] },
|
'text/xml': xml(),
|
||||||
url: { base: url(), ext: [] },
|
url: url(),
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getLanguageExtension({
|
export function getLanguageExtension({
|
||||||
@@ -99,7 +97,7 @@ export function getLanguageExtension({
|
|||||||
useTemplating?: boolean;
|
useTemplating?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const justContentType = contentType.split(';')[0] ?? contentType;
|
const justContentType = contentType.split(';')[0] ?? contentType;
|
||||||
const { base, ext } = syntaxExtensions[justContentType] ?? { base: json(), ext: [] };
|
const base = syntaxExtensions[justContentType] ?? json();
|
||||||
if (!useTemplating) {
|
if (!useTemplating) {
|
||||||
return [base];
|
return [base];
|
||||||
}
|
}
|
||||||
@@ -107,35 +105,44 @@ export function getLanguageExtension({
|
|||||||
const mixedTwigParser = twigParser.configure({
|
const mixedTwigParser = twigParser.configure({
|
||||||
props: [
|
props: [
|
||||||
// Add basic folding/indent metadata
|
// Add basic folding/indent metadata
|
||||||
foldNodeProp.add({ Conditional: foldInside }),
|
// foldNodeProp.add({ Conditional: foldInside }),
|
||||||
indentNodeProp.add({
|
// indentNodeProp.add({
|
||||||
Conditional: (cx) => {
|
// Conditional: (cx) => {
|
||||||
const closed = /^\s*\{% endif/.test(cx.textAfter);
|
// const closed = /^\s*\{% endif/.test(cx.textAfter);
|
||||||
return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit);
|
// return cx.lineIndent(cx.node.from) + (closed ? 0 : cx.unit);
|
||||||
},
|
// },
|
||||||
}),
|
// }),
|
||||||
],
|
],
|
||||||
wrap: parseMixed((node) => {
|
wrap: parseMixed((node) => {
|
||||||
return node.type.isTop
|
return node.type.isTop
|
||||||
? {
|
? {
|
||||||
parser: base.language.parser,
|
parser: base.language.parser,
|
||||||
overlay: (node) => node.type.name == 'Text',
|
overlay: (node) => node.type.name === 'Text',
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const twigLanguage = LRLanguage.define({ parser: mixedTwigParser });
|
const twigLanguage = LRLanguage.define({ parser: mixedTwigParser, languageData: {} });
|
||||||
return [twigLanguage, placeholders, base.support, ...ext];
|
const completion = twigLanguage.data.of({
|
||||||
|
autocomplete: myCompletions,
|
||||||
|
});
|
||||||
|
const languageSupport = new LanguageSupport(twigLanguage, [completion]);
|
||||||
|
const completion2 = base.language.data.of({ autocomplete: myCompletions });
|
||||||
|
const languageSupport2 = new LanguageSupport(base.language, [completion2]);
|
||||||
|
return [languageSupport, languageSupport2, placeholders, base.support];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const baseExtensions = [
|
export const baseExtensions = [
|
||||||
|
keymap.of([...defaultKeymap]),
|
||||||
highlightSpecialChars(),
|
highlightSpecialChars(),
|
||||||
history(),
|
history(),
|
||||||
drawSelection(),
|
drawSelection(),
|
||||||
dropCursor(),
|
dropCursor(),
|
||||||
EditorState.allowMultipleSelections.of(true),
|
bracketMatching(),
|
||||||
|
autocompletion(),
|
||||||
syntaxHighlighting(myHighlightStyle),
|
syntaxHighlighting(myHighlightStyle),
|
||||||
|
EditorState.allowMultipleSelections.of(true),
|
||||||
];
|
];
|
||||||
|
|
||||||
export const multiLineExtensions = [
|
export const multiLineExtensions = [
|
||||||
@@ -152,12 +159,10 @@ export const multiLineExtensions = [
|
|||||||
return el;
|
return el;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
drawSelection(),
|
|
||||||
EditorState.allowMultipleSelections.of(true),
|
EditorState.allowMultipleSelections.of(true),
|
||||||
indentOnInput(),
|
indentOnInput(),
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
closeBrackets(),
|
closeBrackets(),
|
||||||
autocompletion(),
|
|
||||||
rectangularSelection(),
|
rectangularSelection(),
|
||||||
crosshairCursor(),
|
crosshairCursor(),
|
||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { styleTags, tags as t } from '@lezer/highlight';
|
import { styleTags, tags as t } from '@lezer/highlight';
|
||||||
|
|
||||||
export const twigHighlight = styleTags({
|
export const highlight = styleTags({
|
||||||
'if endif': t.controlKeyword,
|
'if endif': t.controlKeyword,
|
||||||
'{{ }} {% %}': t.meta,
|
'${[ ]}': t.meta,
|
||||||
DirectiveContent: t.variableName,
|
DirectiveContent: t.variableName,
|
||||||
});
|
});
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
// Very crude grammar for a subset of Twig templating syntax
|
|
||||||
|
|
||||||
@top Template { (directive | Text)* }
|
@top Template { (directive | Text)* }
|
||||||
|
|
||||||
directive {
|
directive {
|
||||||
@@ -7,15 +5,15 @@ directive {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@skip {space} {
|
@skip {space} {
|
||||||
Insert { "{{" DirectiveContent "}}" }
|
Insert { "${[" DirectiveContent "]}" }
|
||||||
}
|
}
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
Text { ![{] Text? | "{" (@eof | ![%{] Text?) }
|
Text { ![${[] Text? }
|
||||||
space { @whitespace+ }
|
space { @whitespace+ }
|
||||||
DirectiveContent { ![%}] DirectiveContent? | $[%}] (@eof | ![}] DirectiveContent?) }
|
DirectiveContent { ![\]}$] DirectiveContent? }
|
||||||
@precedence { space DirectiveContent }
|
@precedence { space DirectiveContent }
|
||||||
"{{" "}}" // "{%" "%}"
|
"${[" "]}"
|
||||||
}
|
}
|
||||||
|
|
||||||
@external propSource twigHighlight from "./twig-highlight"
|
@external propSource highlight from "./highlight"
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "@lezer/lr"
|
import {LRParser} from "@lezer/lr"
|
||||||
import {twigHighlight} from "./twig-highlight"
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "zQVOPOOO_QQO'#C^OOOO'#Cc'#CcQVOPOOOdQQO,58xOOOO-E6a-E6aOOOO1G.d1G.d",
|
states: "zQVOPOOO_QQO'#C^OOOO'#Cc'#CcQVOPOOOdQQO,58xOOOO-E6a-E6aOOOO1G.d1G.d",
|
||||||
stateData: "l~OYOS~ORPOUQO~OSSO~OTUO~OYS~",
|
stateData: "l~OYOS~ORPOUQO~OSSO~OTUO~OYS~",
|
||||||
goto: "cWPPXPPPP]TQORQRORTR",
|
goto: "cWPPXPPPP]TQORQRORTR",
|
||||||
nodeNames: "⚠ Template Insert {{ DirectiveContent }} Text",
|
nodeNames: "⚠ Template Insert ${[ DirectiveContent ]} Text",
|
||||||
maxTerm: 10,
|
maxTerm: 10,
|
||||||
propSources: [twigHighlight],
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 1,
|
repeatNodeCount: 1,
|
||||||
tokenData: ",gRRmOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p)y#p#q!|#q#r+_#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R#TXUPSQOu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r;'S!|;'S;=`&f<%lO!|R#uXUPO#o!|#o#p$b#p#q!|#q#r&q#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR$gZSQOu!|uv%Yv#o!|#o#p%o#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lQ%]UO#q%o#r;'S%o;'S;=`&Z<%l~%o~O%o~~&aQ%tVSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oQ&^P;=`<%l%oQ&fOSQR&iP;=`<%l!|P&qOUPP&vTUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&qP'YVOu&qv#o&q#p;'S&q;'S;=`'o<%l~&q~O&q~~&lP'rP;=`<%l&qR(OmUPYQSQOX!|X^'u^p!|pq'uqu!|uv#pv#o!|#o#p$b#p#q!|#q#r#p#r#y!|#y#z'u#z$f!|$f$g'u$g#BY!|#BY#BZ'u#BZ$IS!|$IS$I_'u$I_$I|!|$I|$JO'u$JO$JT!|$JT$JU'u$JU$KV!|$KV$KW'u$KW&FU!|&FU&FV'u&FV;'S!|;'S;=`&f<%lO!|R*OZSQOu!|uv%Yv#o!|#o#p*q#p#q!|#q#r#p#r;'S!|;'S;=`&f<%l~!|~O!|~~&lR*xVRPSQOu%ouv%Yv#q%o#q#r%Y#r;'S%o;'S;=`&Z<%lO%oR+dXUPO#o!|#o#p$b#p#q!|#q#r,P#r;'S!|;'S;=`&f<%l~!|~O!|~~&aR,WTTQUPO#o&q#o#p'V#p;'S&q;'S;=`'o<%lO&q",
|
tokenData: ")a~RqOX#YX^%i^p#Ypq%iqt#Ytu'vu!}#Y!}#O$V#O#P#Y#P#Q(X#Q#o#Y#o#p$V#p#q#Y#q#r$t#r#y#Y#y#z%i#z$f#Y$f$g%i$g#BY#Y#BY#BZ%i#BZ$IS#Y$IS$I_%i$I_$I|#Y$I|$JO%i$JO$JT#Y$JT$JU%i$JU$KV#Y$KV$KW%i$KW&FU#Y&FU&FV%i&FV;'S#Y;'S;=`%c<%lO#YR#a[UPSQOt#Yu!}#Y!}#O$V#O#P#Y#P#Q$t#Q#o#Y#o#p$V#p#q#Y#q#r$t#r;'S#Y;'S;=`%c<%lO#YQ$[USQOt$Vu#P$V#Q#q$V#r;'S$V;'S;=`$n<%lO$VQ$qP;=`<%l$VP$yUUPOt$tu!}$t#O#o$t#p;'S$t;'S;=`%]<%lO$tP%`P;=`<%l$tR%fP;=`<%l#YR%rpUPYQSQOX#YX^%i^p#Ypq%iqt#Yu!}#Y!}#O$V#O#P#Y#P#Q$t#Q#o#Y#o#p$V#p#q#Y#q#r$t#r#y#Y#y#z%i#z$f#Y$f$g%i$g#BY#Y#BY#BZ%i#BZ$IS#Y$IS$I_%i$I_$I|#Y$I|$JO%i$JO$JT#Y$JT$JU%i$JU$KV#Y$KV$KW%i$KW&FU#Y&FU&FV%i&FV;'S#Y;'S;=`%c<%lO#Y~'yP#o#p'|~(PP!}#O(S~(XOR~R(^WUPOt$tu!}$t#O#o$t#p#q$t#q#r(v#r;'S$t;'S;=`%]<%lO$tR(}UTQUPOt$tu!}$t#O#o$t#p;'S$t;'S;=`%]<%lO$t",
|
||||||
tokenizers: [0, 1],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Template":[0,1]},
|
topRules: {"Template":[0,1]},
|
||||||
tokenPrec: 25
|
tokenPrec: 25
|
||||||
|
|||||||
@@ -1,43 +1,16 @@
|
|||||||
import { parser } from './url';
|
|
||||||
// import { foldNodeProp, foldInside, indentNodeProp } from '@codemirror/language';
|
|
||||||
import { styleTags, tags as t } from '@lezer/highlight';
|
|
||||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
|
||||||
import { completeFromList } from '@codemirror/autocomplete';
|
import { completeFromList } from '@codemirror/autocomplete';
|
||||||
|
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||||
const parserWithMetadata = parser.configure({
|
import { parser } from './url';
|
||||||
props: [
|
|
||||||
styleTags({
|
|
||||||
Protocol: t.comment,
|
|
||||||
Port: t.attributeName,
|
|
||||||
Host: t.variableName,
|
|
||||||
PathSegment: t.bool,
|
|
||||||
Slash: t.bool,
|
|
||||||
Question: t.attributeName,
|
|
||||||
QueryName: t.attributeName,
|
|
||||||
QueryValue: t.attributeName,
|
|
||||||
Amp: t.comment,
|
|
||||||
Equal: t.comment,
|
|
||||||
}),
|
|
||||||
// indentNodeProp.add({
|
|
||||||
// Application: (context) => context.column(context.node.from) + context.unit,
|
|
||||||
// }),
|
|
||||||
// foldNodeProp.add({
|
|
||||||
// Application: foldInside,
|
|
||||||
// }),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const urlLanguage = LRLanguage.define({
|
const urlLanguage = LRLanguage.define({
|
||||||
parser: parserWithMetadata,
|
parser,
|
||||||
languageData: {
|
languageData: {},
|
||||||
// commentTokens: {line: ";"}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const exampleCompletion = urlLanguage.data.of({
|
const exampleCompletion = urlLanguage.data.of({
|
||||||
autocomplete: completeFromList([
|
autocomplete: completeFromList([
|
||||||
{ label: 'http://', type: 'keyword' },
|
{ label: 'http://', type: 'constant' },
|
||||||
{ label: 'https://', type: 'keyword' },
|
{ label: 'https://', type: 'constant' },
|
||||||
]),
|
]),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
9
src-web/components/Editor/url/highlight.ts
Normal file
9
src-web/components/Editor/url/highlight.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { styleTags, tags as t } from '@lezer/highlight';
|
||||||
|
|
||||||
|
export const highlight = styleTags({
|
||||||
|
Protocol: t.comment,
|
||||||
|
Port: t.attributeName,
|
||||||
|
Host: t.variableName,
|
||||||
|
Path: t.bool,
|
||||||
|
Query: t.string,
|
||||||
|
});
|
||||||
@@ -1,29 +1,18 @@
|
|||||||
@top url { Protocol Host Port? Path Query }
|
@top url { Protocol? Host Port? Path? Query? }
|
||||||
|
|
||||||
Protocol {
|
|
||||||
"http://" | "https://"
|
|
||||||
}
|
|
||||||
|
|
||||||
Path {
|
|
||||||
(Slash PathSegment)*
|
|
||||||
}
|
|
||||||
|
|
||||||
Query {
|
Query {
|
||||||
Question (QueryPair)*
|
"?" queryPair ("&" queryPair)*
|
||||||
}
|
|
||||||
|
|
||||||
QueryPair {
|
|
||||||
Amp? QueryName Equal QueryValue
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
|
Protocol { $[a-zA-Z]+ "://" }
|
||||||
|
Path { ("/" $[a-zA-Z0-9\-_.]*)+ }
|
||||||
|
queryPair { ($[a-zA-Z0-9]+ ("=" $[a-zA-Z0-9]*)?) }
|
||||||
Port { ":" $[0-9]+ }
|
Port { ":" $[0-9]+ }
|
||||||
Host { $[a-zA-Z0-9-_.]+ }
|
Host { $[a-zA-Z0-9-_.]+ }
|
||||||
QueryName { $[a-zA-Z0-9-_.]+ }
|
|
||||||
QueryValue { $[a-zA-Z0-9-_./]+ }
|
// Protocol/host overlaps, so give proto explicit precedence
|
||||||
PathSegment { $[a-zA-Z0-9-_.]+ }
|
@precedence { Protocol, Host }
|
||||||
Slash { "/" }
|
|
||||||
Question { "?" }
|
|
||||||
Equal { "=" }
|
|
||||||
Amp { "&" }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@external propSource highlight from "./highlight"
|
||||||
|
|||||||
@@ -5,12 +5,4 @@ export const
|
|||||||
Host = 3,
|
Host = 3,
|
||||||
Port = 4,
|
Port = 4,
|
||||||
Path = 5,
|
Path = 5,
|
||||||
Slash = 6,
|
Query = 6
|
||||||
PathSegment = 7,
|
|
||||||
Query = 8,
|
|
||||||
Question = 9,
|
|
||||||
QueryPair = 10,
|
|
||||||
Amp = 11,
|
|
||||||
QueryName = 12,
|
|
||||||
Equal = 13,
|
|
||||||
QueryValue = 14
|
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "@lezer/lr"
|
import {LRParser} from "@lezer/lr"
|
||||||
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "#xOQOPOOOOOO'#C^'#C^OYOQOOO_OPOOOjOSO'#CkOoOPO'#CaOwOPOOObOPOOOOOO,59V,59VOOOO-E6i-E6iO|OWO'#CdQOOOOOO!XOPO'#CfO!^OWO'#CfOOOO'#Cl'#ClO!cOWO,59OO!nO`O,59QO!sOPO,59QOOOO-E6j-E6jOOOO1G.l1G.lO!xO`O1G.lOOOO7+$W7+$W",
|
states: "!jOQOPOOQYOPOOOTOPOOOeOQO'#CbQOOOOOQ`OPOOQ]OPOOOjOPO,58|OrOQO'#CcOwOPO1G.hOOOO,58},58}OOOO-E6a-E6a",
|
||||||
stateData: "!}~ObPOcPO~ORRO~OSVOUSOXTP~OVWO~OUSOXTX~OXYO~OZ]O[[OaWX~O]`O~O[aO~OZ]O[[OaWa~O^cO~O]dO~O^eO~O",
|
stateData: "!S~OQQORPO~OSUOTTOXRO~OYVO~OZWOWUa~OYYO~OZWOWUi~OQR~",
|
||||||
goto: "}aPPbPPePPiPlPPPPpwRQOTURVRZUT^Y_STRVRXTQ_YRb_",
|
goto: "dWPPPPPPX^VSPTUQXVRZX",
|
||||||
nodeNames: "⚠ url Protocol Host Port Path Slash PathSegment Query Question QueryPair Amp QueryName Equal QueryValue",
|
nodeNames: "⚠ url Protocol Host Port Path Query",
|
||||||
maxTerm: 19,
|
maxTerm: 11,
|
||||||
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 2,
|
repeatNodeCount: 1,
|
||||||
tokenData: ")]~R]vwz}!O!P!O!P!P!P!Q#]!Q![!P![!]#y!_!`$X!a!b$^!c!}!P#R#S!P#T#[!P#[#]$c#]#o!P~!POZ~n![VRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#o!P`!vV^`}!O!q!O!P!q!P!Q!q!Q![!q!c!}!q#R#S!q#T#o!qa#dV^`UP}!O!q!O!P!q!P!Q!q!Q![!q!c!}!q#R#S!q#T#o!q~#|P!Q![$P~$UPS~!Q![$P~$^O]~~$cOX~o$nXRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#h!P#h#i%Z#i#o!Po%fXRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#h!P#h#i&R#i#o!Po&^XRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P!c!}!P#R#S!P#T#d!P#d#e&y#e#o!Po'UYRQVS[W^`}!O!P!O!P!P!P!Q!q!Q![!P![!]'t!c!}!P#R#S!P#T#g!P#g#h(V#h#o!PP'wP!P!Q'zP'}P!P!Q(QP(VObPo(bWRQVS[W^`}!O!P!O!P!P!P!Q!q!QQP)TP!P!Q)WP)]OcP",
|
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, 2, 3, 4],
|
tokenizers: [0, 1],
|
||||||
topRules: {"url":[0,1]},
|
topRules: {"url":[0,1]},
|
||||||
tokenPrec: 0
|
tokenPrec: 47
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
import {
|
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||||
Decoration,
|
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||||
DecorationSet,
|
|
||||||
EditorView,
|
|
||||||
MatchDecorator,
|
|
||||||
ViewPlugin,
|
|
||||||
ViewUpdate,
|
|
||||||
WidgetType,
|
|
||||||
} from '@codemirror/view';
|
|
||||||
|
|
||||||
class PlaceholderWidget extends WidgetType {
|
class PlaceholderWidget extends WidgetType {
|
||||||
constructor(readonly name: string) {
|
constructor(readonly name: string) {
|
||||||
@@ -40,7 +33,7 @@ class BetterMatchDecorator extends MatchDecorator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const placeholderMatcher = new BetterMatchDecorator({
|
const placeholderMatcher = new BetterMatchDecorator({
|
||||||
regexp: /\{\{\s*([^}\s]+)\s*}}/g,
|
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
||||||
decoration(match, view, matchStartPos) {
|
decoration(match, view, matchStartPos) {
|
||||||
const matchEndPos = matchStartPos + match[0].length - 1;
|
const matchEndPos = matchStartPos + match[0].length - 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InputHTMLAttributes, ReactNode } from 'react';
|
import type { InputHTMLAttributes, ReactNode } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { HStack, VStack } from './Stacks';
|
import { HStack, VStack } from './Stacks';
|
||||||
import Editor from './Editor/Editor';
|
import Editor from './Editor/Editor';
|
||||||
@@ -11,6 +11,7 @@ interface Props extends Omit<InputHTMLAttributes<HTMLInputElement>, 'size' | 'on
|
|||||||
containerClassName?: string;
|
containerClassName?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
onSubmit?: () => void;
|
onSubmit?: () => void;
|
||||||
|
useTemplating?: boolean;
|
||||||
useEditor?: boolean;
|
useEditor?: boolean;
|
||||||
leftSlot?: ReactNode;
|
leftSlot?: ReactNode;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
@@ -24,6 +25,7 @@ export function Input({
|
|||||||
containerClassName,
|
containerClassName,
|
||||||
labelClassName,
|
labelClassName,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
useTemplating,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
useEditor,
|
useEditor,
|
||||||
onChange,
|
onChange,
|
||||||
@@ -61,6 +63,7 @@ export function Input({
|
|||||||
<Editor
|
<Editor
|
||||||
id={id}
|
id={id}
|
||||||
singleLine
|
singleLine
|
||||||
|
useTemplating
|
||||||
contentType="url"
|
contentType="url"
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
|||||||
@@ -76,21 +76,24 @@ export function ResponsePane({ requestId, error }: Props) {
|
|||||||
items="center"
|
items="center"
|
||||||
className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
|
className="italic text-gray-500 text-sm w-full h-10 mb-3 flex-shrink-0"
|
||||||
>
|
>
|
||||||
<div>
|
<div className="w-full">
|
||||||
{response.status}
|
{response.status}
|
||||||
{response.statusReason && ` ${response.statusReason}`}
|
{response.statusReason && ` ${response.statusReason}`}
|
||||||
•
|
•
|
||||||
{response.elapsed}ms •
|
{response.elapsed}ms •
|
||||||
{Math.round(response.body.length / 1000)} KB
|
{Math.round(response.body.length / 1000)} KB
|
||||||
</div>
|
</div>
|
||||||
{contentType.includes('html') && (
|
<HStack items="center" className="ml-auto">
|
||||||
<IconButton
|
<div className="font-mono">{response.url}</div>
|
||||||
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
{contentType.includes('html') && (
|
||||||
size="sm"
|
<IconButton
|
||||||
className="ml-auto"
|
icon={viewMode === 'pretty' ? 'eye' : 'code'}
|
||||||
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
|
size="sm"
|
||||||
/>
|
className="ml-1"
|
||||||
)}
|
onClick={() => setViewMode((m) => (m === 'pretty' ? 'raw' : 'pretty'))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
{viewMode === 'pretty' && contentType.includes('html') ? (
|
{viewMode === 'pretty' && contentType.includes('html') ? (
|
||||||
<iframe
|
<iframe
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { Children, Fragment, HTMLAttributes, ReactNode } from 'react';
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
import React, { Children, Fragment } from 'react';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
|
|
||||||
const spaceClassesX = {
|
const spaceClassesX = {
|
||||||
@@ -24,7 +25,7 @@ interface HStackProps extends BaseStackProps {
|
|||||||
|
|
||||||
export function HStack({ className, space, children, ...props }: HStackProps) {
|
export function HStack({ className, space, children, ...props }: HStackProps) {
|
||||||
return (
|
return (
|
||||||
<BaseStack className={classnames(className, 'w-full flex-row')} {...props}>
|
<BaseStack className={classnames(className, 'flex-row')} {...props}>
|
||||||
{space
|
{space
|
||||||
? Children.toArray(children)
|
? Children.toArray(children)
|
||||||
.filter(Boolean) // Remove null/false/undefined children
|
.filter(Boolean) // Remove null/false/undefined children
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { DropdownMenuRadio } from './Dropdown';
|
import { DropdownMenuRadio } from './Dropdown';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
import { Input } from './Input';
|
import { Input } from './Input';
|
||||||
import { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -24,6 +24,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
|
|||||||
<Input
|
<Input
|
||||||
hideLabel
|
hideLabel
|
||||||
useEditor
|
useEditor
|
||||||
|
useTemplating
|
||||||
onSubmit={sendRequest}
|
onSubmit={sendRequest}
|
||||||
name="url"
|
name="url"
|
||||||
label="Enter URL"
|
label="Enter URL"
|
||||||
|
|||||||
Reference in New Issue
Block a user