mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
URL path placeholders
This commit is contained in:
7
src-tauri/Cargo.lock
generated
7
src-tauri/Cargo.lock
generated
@@ -6738,6 +6738,12 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -7568,6 +7574,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
"urlencoding",
|
||||||
"uuid",
|
"uuid",
|
||||||
"yaak_grpc",
|
"yaak_grpc",
|
||||||
"yaak_models",
|
"yaak_models",
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ tokio-stream = "0.1.15"
|
|||||||
uuid = "1.7.0"
|
uuid = "1.7.0"
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
mime_guess = "2.0.5"
|
mime_guess = "2.0.5"
|
||||||
|
urlencoding = "2.1.3"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
yaak_models = { path = "yaak_models" }
|
yaak_models = { path = "yaak_models" }
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ use tauri::{Manager, Runtime, WebviewWindow};
|
|||||||
use tokio::sync::oneshot;
|
use tokio::sync::oneshot;
|
||||||
use tokio::sync::watch::Receiver;
|
use tokio::sync::watch::Receiver;
|
||||||
use yaak_models::models::{
|
use yaak_models::models::{
|
||||||
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader,
|
Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, HttpUrlParameter,
|
||||||
};
|
};
|
||||||
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
|
use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar};
|
||||||
|
|
||||||
@@ -40,13 +40,8 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
.expect("Failed to get Workspace");
|
.expect("Failed to get Workspace");
|
||||||
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
|
let cb = &*window.app_handle().state::<PluginTemplateCallback>();
|
||||||
let cb = cb.for_send();
|
let cb = cb.for_send();
|
||||||
let rendered_request = render_http_request(
|
let rendered_request =
|
||||||
&request,
|
render_http_request(&request, &workspace, environment.as_ref(), &cb).await;
|
||||||
&workspace,
|
|
||||||
environment.as_ref(),
|
|
||||||
&cb,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let mut url_string = rendered_request.url;
|
let mut url_string = rendered_request.url;
|
||||||
|
|
||||||
@@ -101,6 +96,23 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
|
|
||||||
let client = client_builder.build().expect("Failed to build client");
|
let client = client_builder.build().expect("Failed to build client");
|
||||||
|
|
||||||
|
// Render query parameters
|
||||||
|
let mut query_params = Vec::new();
|
||||||
|
for p in rendered_request.url_parameters {
|
||||||
|
if !p.enabled || p.name.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace path parameters with values from URL parameters
|
||||||
|
let old_url_string = url_string.clone();
|
||||||
|
url_string = replace_path_placeholder(&p, url_string.as_str());
|
||||||
|
|
||||||
|
// Treat as regular param if wasn't used as path param
|
||||||
|
if old_url_string == url_string {
|
||||||
|
query_params.push((p.name, p.value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let uri = match http::Uri::from_str(url_string.as_str()) {
|
let uri = match http::Uri::from_str(url_string.as_str()) {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -127,7 +139,7 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
|
|
||||||
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
|
let m = Method::from_bytes(rendered_request.method.to_uppercase().as_bytes())
|
||||||
.expect("Failed to create method");
|
.expect("Failed to create method");
|
||||||
let mut request_builder = client.request(m, url);
|
let mut request_builder = client.request(m, url).query(&query_params);
|
||||||
|
|
||||||
let mut headers = HeaderMap::new();
|
let mut headers = HeaderMap::new();
|
||||||
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
headers.insert(USER_AGENT, HeaderValue::from_static("yaak"));
|
||||||
@@ -210,15 +222,6 @@ pub async fn send_http_request<R: Runtime>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut query_params = Vec::new();
|
|
||||||
for p in rendered_request.url_parameters {
|
|
||||||
if !p.enabled || p.name.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
query_params.push((p.name, p.value));
|
|
||||||
}
|
|
||||||
request_builder = request_builder.query(&query_params);
|
|
||||||
|
|
||||||
let request_body = rendered_request.body;
|
let request_body = rendered_request.body;
|
||||||
if let Some(body_type) = &rendered_request.body_type {
|
if let Some(body_type) = &rendered_request.body_type {
|
||||||
if request_body.contains_key("text") {
|
if request_body.contains_key("text") {
|
||||||
@@ -489,3 +492,119 @@ fn get_str_h<'a>(v: &'a HashMap<String, Value>, key: &str) -> &'a str {
|
|||||||
Some(v) => v.as_str().unwrap_or_default(),
|
Some(v) => v.as_str().unwrap_or_default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn replace_path_placeholder(p: &HttpUrlParameter, url: &str) -> String {
|
||||||
|
if !p.enabled {
|
||||||
|
return url.to_string();
|
||||||
|
}
|
||||||
|
|
||||||
|
let re = regex::Regex::new(format!("(/){}([/?#]|$)", p.name).as_str()).unwrap();
|
||||||
|
let result = re
|
||||||
|
.replace_all(url, |cap: ®ex::Captures| {
|
||||||
|
format!(
|
||||||
|
"{}{}{}",
|
||||||
|
cap[1].to_string(),
|
||||||
|
urlencoding::encode(p.value.as_str()),
|
||||||
|
cap[2].to_string()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.into_owned();
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::http_request::replace_path_placeholder;
|
||||||
|
use yaak_models::models::HttpUrlParameter;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_middle() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
||||||
|
"https://example.com/xxx/bar",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_end() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/xxx",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_query() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
||||||
|
"https://example.com/xxx?:foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_missing() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
enabled: true,
|
||||||
|
name: "".to_string(),
|
||||||
|
value: "".to_string(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:missing"),
|
||||||
|
"https://example.com/:missing",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_disabled() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
enabled: false,
|
||||||
|
name: ":foo".to_string(),
|
||||||
|
value: "xxx".to_string(),
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/:foo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_prefix() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "xxx".into(),
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
||||||
|
"https://example.com/:foooo",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn placeholder_encode() {
|
||||||
|
let p = HttpUrlParameter {
|
||||||
|
name: ":foo".into(),
|
||||||
|
value: "Hello World".into(),
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
|
"https://example.com/Hello%20World",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -344,6 +344,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
<UrlParametersEditor
|
<UrlParametersEditor
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
urlParameters={activeRequest.urlParameters}
|
urlParameters={activeRequest.urlParameters}
|
||||||
|
url={activeRequest.url}
|
||||||
onChange={handleUrlParametersChange}
|
onChange={handleUrlParametersChange}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|||||||
@@ -1,23 +1,49 @@
|
|||||||
import type { HttpRequest } from '@yaakapp/api';
|
import type { HttpRequest } from '@yaakapp/api';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import type { Pair } from './core/PairEditor';
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
urlParameters: HttpRequest['headers'];
|
urlParameters: HttpRequest['headers'];
|
||||||
onChange: (headers: HttpRequest['urlParameters']) => void;
|
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 (
|
return (
|
||||||
<PairOrBulkEditor
|
<VStack className="h-full">
|
||||||
preferenceName="url_parameters"
|
<PairOrBulkEditor
|
||||||
valueAutocompleteVariables
|
preferenceName="url_parameters"
|
||||||
nameAutocompleteVariables
|
valueAutocompleteVariables
|
||||||
namePlaceholder="param_name"
|
nameAutocompleteVariables
|
||||||
valuePlaceholder="Value"
|
namePlaceholder="param_name"
|
||||||
pairs={urlParameters}
|
valuePlaceholder="Value"
|
||||||
onChange={onChange}
|
pairs={pairs}
|
||||||
forceUpdateKey={forceUpdateKey}
|
onChange={onChange}
|
||||||
/>
|
forceUpdateKey={forceUpdateKey + placeholderNames.join(':')}
|
||||||
|
/>
|
||||||
|
</VStack>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,12 +42,13 @@ export function BulkPairEditor({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function lineToPair(l: string): PairEditorProps['pairs'][0] {
|
function lineToPair(line: string): PairEditorProps['pairs'][0] {
|
||||||
const [name, ...values] = l.split(':');
|
const [, name, value] = line.match(/^(:?[^:]+):\s+([^$]*)/) ?? [];
|
||||||
|
|
||||||
const pair: PairEditorProps['pairs'][0] = {
|
const pair: PairEditorProps['pairs'][0] = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
name: (name ?? '').trim(),
|
name: (name ?? '').trim(),
|
||||||
value: values.join(':').trim(),
|
value: (value ?? '').trim(),
|
||||||
};
|
};
|
||||||
return pair;
|
return pair;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cm-line {
|
.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;
|
@apply w-full text-text pl-1 pr-1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,9 +170,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cm-wrapper.cm-readonly .cm-editor {
|
/* Cursor and mouse cursor for readonly mode */
|
||||||
.cm-cursor {
|
.cm-wrapper.cm-readonly {
|
||||||
@apply border-danger !important;
|
.cm-editor .cm-cursor {
|
||||||
|
@apply hidden !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.cm-singleline .cm-line {
|
||||||
|
@apply cursor-default;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ export { formatSdl } from 'format-graphql';
|
|||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
className?: string;
|
className?: string;
|
||||||
heightMode?: 'auto' | 'full';
|
heightMode?: 'auto' | 'full';
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ export const syntaxHighlightStyle = HighlightStyle.define([
|
|||||||
color: 'var(--textSubtlest)',
|
color: 'var(--textSubtlest)',
|
||||||
fontStyle: 'italic',
|
fontStyle: 'italic',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
tag: [t.emphasis],
|
||||||
|
textDecoration: 'underline',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
tag: [t.paren, t.bracket, t.brace],
|
tag: [t.paren, t.bracket, t.brace],
|
||||||
color: 'var(--textSubtle)',
|
color: 'var(--textSubtle)',
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@top pairs { (Key? Sep Value)* }
|
@top pairs { (Key Sep Value "\n")* }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
Sep { ":" }
|
Sep { ":" }
|
||||||
Key { ![:]+ }
|
Key { ":"? ![:]+ }
|
||||||
Value { ![\n]+ }
|
Value { ![\n]+ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
src-web/components/core/Editor/pairs/pairs.terms.ts
Normal file
6
src-web/components/core/Editor/pairs/pairs.terms.ts
Normal 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
|
||||||
@@ -3,17 +3,17 @@ import {LRParser} from "@lezer/lr"
|
|||||||
import {highlight} from "./highlight"
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "!QQQOPOOOYOQO'#CaO_OPO'#CaQQOPOOOOOO,58{,58{OdOQO,58{OOOO-E6_-E6_OOOO1G.g1G.g",
|
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
|
||||||
stateData: "i~OQQORPO~OSSO~ORTO~OSVO~O",
|
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
|
||||||
goto: "]UPPPPPVQRORUR",
|
goto: "]UPPPPPVQQORSQ",
|
||||||
nodeNames: "⚠ pairs Key Sep Value",
|
nodeNames: "⚠ pairs Key Sep Value",
|
||||||
maxTerm: 6,
|
maxTerm: 7,
|
||||||
propSources: [highlight],
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 1,
|
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",
|
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],
|
tokenizers: [0, 1, 2],
|
||||||
topRules: {"pairs":[0,1]},
|
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\""}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
// 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.
|
||||||
export const Template = 1,
|
export const
|
||||||
|
Template = 1,
|
||||||
Tag = 2,
|
Tag = 2,
|
||||||
|
TagOpen = 3,
|
||||||
TagContent = 4,
|
TagContent = 4,
|
||||||
Open = 3,
|
TagClose = 5,
|
||||||
Close = 5,
|
Text = 6
|
||||||
Text = 6;
|
|
||||||
|
|||||||
@@ -16,4 +16,3 @@ export const parser = LRParser.deserialize({
|
|||||||
topRules: {"Template":[0,1]},
|
topRules: {"Template":[0,1]},
|
||||||
tokenPrec: 0
|
tokenPrec: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { styleTags, tags as t } from '@lezer/highlight';
|
|||||||
|
|
||||||
export const highlight = styleTags({
|
export const highlight = styleTags({
|
||||||
Protocol: t.comment,
|
Protocol: t.comment,
|
||||||
|
Placeholder: t.emphasis,
|
||||||
|
// PathSegment: t.tagName,
|
||||||
// Port: t.attributeName,
|
// Port: t.attributeName,
|
||||||
// Host: t.variableName,
|
// Host: t.variableName,
|
||||||
// Path: t.bool,
|
// Path: t.bool,
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
@top url { Protocol? Host Port? Path? Query? }
|
@top Program { url }
|
||||||
|
|
||||||
Query {
|
url { Protocol? Host Port? Path? Query? }
|
||||||
"?" queryPair ("&" queryPair)*
|
|
||||||
}
|
Path { ("/" (Placeholder | PathSegment))+ }
|
||||||
|
|
||||||
|
Query { "?" queryPair ("&" queryPair)* }
|
||||||
|
|
||||||
@tokens {
|
@tokens {
|
||||||
Protocol { $[a-zA-Z]+ "://" }
|
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-_.]+ }
|
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
|
// Protocol/host overlaps, so give proto explicit precedence
|
||||||
@precedence { Protocol, Host }
|
@precedence { Protocol, Host }
|
||||||
|
@precedence { Placeholder, PathSegment }
|
||||||
}
|
}
|
||||||
|
|
||||||
@external propSource highlight from "./highlight"
|
@external propSource highlight from "./highlight"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// 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.
|
||||||
export const
|
export const
|
||||||
url = 1,
|
Program = 1,
|
||||||
Protocol = 2,
|
Protocol = 2,
|
||||||
Host = 3,
|
Host = 3,
|
||||||
Port = 4,
|
Port = 4,
|
||||||
Path = 5,
|
Path = 5,
|
||||||
Query = 6
|
Placeholder = 6,
|
||||||
|
PathSegment = 7,
|
||||||
|
Query = 8
|
||||||
|
|||||||
@@ -3,16 +3,17 @@ import {LRParser} from "@lezer/lr"
|
|||||||
import {highlight} from "./highlight"
|
import {highlight} from "./highlight"
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "!jOQOPOOQYOPOOOTOPOOOeOQO'#CbQOOOOOQ`OPOOQ]OPOOOjOPO,58|OrOQO'#CcOwOPO1G.hOOOO,58},58}OOOO-E6a-E6a",
|
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: "!S~OQQORPO~OSUOTTOXRO~OYVO~OZWOWUa~OYYO~OZWOWUi~OQR~",
|
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: "dWPPPPPPX^VSPTUQXVRZX",
|
goto: "!Z]PPPPP^PPhw!QP!WQWPS_XYRd`QVPU^WXYSc_`RgdWTPXY`R[TQb]RfbRRO",
|
||||||
nodeNames: "⚠ url Protocol Host Port Path Query",
|
nodeNames: "⚠ Program Protocol Host Port Path Placeholder PathSegment Query",
|
||||||
maxTerm: 11,
|
maxTerm: 16,
|
||||||
propSources: [highlight],
|
propSources: [highlight],
|
||||||
skippedNodes: [0],
|
skippedNodes: [0],
|
||||||
repeatNodeCount: 1,
|
repeatNodeCount: 2,
|
||||||
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",
|
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],
|
tokenizers: [0, 1, 2],
|
||||||
topRules: {"url":[0,1]},
|
topRules: {"Program":[0,1]},
|
||||||
tokenPrec: 47
|
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":"\"&\""}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ export type InputProps = Omit<
|
|||||||
| 'autoSelect'
|
| 'autoSelect'
|
||||||
| 'autocompleteVariables'
|
| 'autocompleteVariables'
|
||||||
| 'onKeyDown'
|
| 'onKeyDown'
|
||||||
|
| 'readOnly'
|
||||||
> & {
|
> & {
|
||||||
name: string;
|
name: string;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
@@ -68,6 +69,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
size = 'md',
|
size = 'md',
|
||||||
type = 'text',
|
type = 'text',
|
||||||
validate,
|
validate,
|
||||||
|
readOnly,
|
||||||
...props
|
...props
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref,
|
ref,
|
||||||
@@ -77,9 +79,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
const [focused, setFocused] = useState(false);
|
const [focused, setFocused] = useState(false);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback(() => {
|
||||||
|
if (readOnly) return;
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
}, [onFocus]);
|
}, [onFocus, readOnly]);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
@@ -179,6 +182,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
className={editorClassName}
|
className={editorClassName}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
|
readOnly={readOnly}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ export type Pair = {
|
|||||||
value: string;
|
value: string;
|
||||||
contentType?: string;
|
contentType?: string;
|
||||||
isFile?: boolean;
|
isFile?: boolean;
|
||||||
|
readOnlyName?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type PairContainer = {
|
type PairContainer = {
|
||||||
@@ -254,8 +255,8 @@ function PairEditorRow({
|
|||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
valueAutocompleteVariables,
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
valueValidate,
|
|
||||||
valueType,
|
valueType,
|
||||||
|
valueValidate,
|
||||||
}: PairEditorRowProps) {
|
}: PairEditorRowProps) {
|
||||||
const { id } = pairContainer;
|
const { id } = pairContainer;
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
@@ -374,6 +375,7 @@ function PairEditorRow({
|
|||||||
ref={nameInputRef}
|
ref={nameInputRef}
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
|
readOnly={pairContainer.pair.readOnlyName}
|
||||||
size="sm"
|
size="sm"
|
||||||
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
|
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
|
||||||
validate={nameValidate}
|
validate={nameValidate}
|
||||||
@@ -476,7 +478,14 @@ function PairEditorRow({
|
|||||||
</RadioDropdown>
|
</RadioDropdown>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown
|
<Dropdown
|
||||||
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
|
items={[
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
onSelect: handleDelete,
|
||||||
|
variant: 'danger',
|
||||||
|
},
|
||||||
|
]}
|
||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
iconSize="sm"
|
iconSize="sm"
|
||||||
|
|||||||
Reference in New Issue
Block a user