mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-09 06:15:08 +02:00
Preserve Editor State (#151)
This commit is contained in:
Generated
+19
-10
@@ -6529,16 +6529,6 @@
|
|||||||
"eslint": ">=8.40"
|
"eslint": ">=8.40"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-react-refresh": {
|
|
||||||
"version": "0.4.16",
|
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.16.tgz",
|
|
||||||
"integrity": "sha512-slterMlxAhov/DZO8NScf6mEeMBBXodFUolijDvrtTxyezyLoTQaa73FyYus/VbTdftd8wBgBxPMRk3poleXNQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"peerDependencies": {
|
|
||||||
"eslint": ">=8.40"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
"node_modules/eslint-plugin-react/node_modules/brace-expansion": {
|
||||||
"version": "1.1.11",
|
"version": "1.1.11",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
@@ -15876,6 +15866,7 @@
|
|||||||
"jotai": "^2.9.3",
|
"jotai": "^2.9.3",
|
||||||
"lucide-react": "^0.439.0",
|
"lucide-react": "^0.439.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
|
"nanoid": "^5.0.9",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -15920,6 +15911,24 @@
|
|||||||
"vite-plugin-svgr": "^4.2.0",
|
"vite-plugin-svgr": "^4.2.0",
|
||||||
"vite-plugin-top-level-await": "^1.4.4"
|
"vite-plugin-top-level-await": "^1.4.4"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"src-web/node_modules/nanoid": {
|
||||||
|
"version": "5.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.9.tgz",
|
||||||
|
"integrity": "sha512-Aooyr6MXU6HpvvWXKoVoXwKMs/KyVakWwg7xQfv5/S/RIgJMy0Ifa45H9qqYy7pTCszrHzP21Uk4PZq2HpEM8Q==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ai"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"nanoid": "bin/nanoid.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^18 || >=20"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,11 @@ use crate::render::{render_grpc_request, render_http_request, render_json_value,
|
|||||||
use crate::template_callback::PluginTemplateCallback;
|
use crate::template_callback::PluginTemplateCallback;
|
||||||
use crate::updates::{UpdateMode, YaakUpdater};
|
use crate::updates::{UpdateMode, YaakUpdater};
|
||||||
use crate::window_menu::app_menu;
|
use crate::window_menu::app_menu;
|
||||||
use yaak_models::models::{CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, Settings, Workspace};
|
use yaak_models::models::{
|
||||||
|
CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState,
|
||||||
|
GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue,
|
||||||
|
ModelType, Plugin, Settings, Workspace,
|
||||||
|
};
|
||||||
use yaak_models::queries::{
|
use yaak_models::queries::{
|
||||||
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
|
cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response,
|
||||||
delete_all_grpc_connections, delete_all_grpc_connections_for_workspace,
|
delete_all_grpc_connections, delete_all_grpc_connections_for_workspace,
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ pub async fn render_grpc_request<T: TemplateCallback>(
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: render(p.name.as_str(), vars, cb).await,
|
name: render(p.name.as_str(), vars, cb).await,
|
||||||
value: render(p.value.as_str(), vars, cb).await,
|
value: render(p.value.as_str(), vars, cb).await,
|
||||||
|
id: p.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +74,7 @@ pub async fn render_http_request(
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: render(p.name.as_str(), vars, cb).await,
|
name: render(p.name.as_str(), vars, cb).await,
|
||||||
value: render(p.value.as_str(), vars, cb).await,
|
value: render(p.value.as_str(), vars, cb).await,
|
||||||
|
id: p.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +84,7 @@ pub async fn render_http_request(
|
|||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: render(p.name.as_str(), vars, cb).await,
|
name: render(p.name.as_str(), vars, cb).await,
|
||||||
value: render(p.value.as_str(), vars, cb).await,
|
value: render(p.value.as_str(), vars, cb).await,
|
||||||
|
id: p.id,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,6 +312,7 @@ mod placeholder_tests {
|
|||||||
name: ":foo".into(),
|
name: ":foo".into(),
|
||||||
value: "xxx".into(),
|
value: "xxx".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
replace_path_placeholder(&p, "https://example.com/:foo/bar"),
|
||||||
@@ -322,6 +326,7 @@ mod placeholder_tests {
|
|||||||
name: ":foo".into(),
|
name: ":foo".into(),
|
||||||
value: "xxx".into(),
|
value: "xxx".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
@@ -335,6 +340,7 @@ mod placeholder_tests {
|
|||||||
name: ":foo".into(),
|
name: ":foo".into(),
|
||||||
value: "xxx".into(),
|
value: "xxx".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
replace_path_placeholder(&p, "https://example.com/:foo?:foo"),
|
||||||
@@ -348,6 +354,7 @@ mod placeholder_tests {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
name: "".to_string(),
|
name: "".to_string(),
|
||||||
value: "".to_string(),
|
value: "".to_string(),
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:missing"),
|
replace_path_placeholder(&p, "https://example.com/:missing"),
|
||||||
@@ -361,6 +368,7 @@ mod placeholder_tests {
|
|||||||
enabled: false,
|
enabled: false,
|
||||||
name: ":foo".to_string(),
|
name: ":foo".to_string(),
|
||||||
value: "xxx".to_string(),
|
value: "xxx".to_string(),
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
@@ -374,6 +382,7 @@ mod placeholder_tests {
|
|||||||
name: ":foo".into(),
|
name: ":foo".into(),
|
||||||
value: "xxx".into(),
|
value: "xxx".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
replace_path_placeholder(&p, "https://example.com/:foooo"),
|
||||||
@@ -387,6 +396,7 @@ mod placeholder_tests {
|
|||||||
name: ":foo".into(),
|
name: ":foo".into(),
|
||||||
value: "Hello World".into(),
|
value: "Hello World".into(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
};
|
};
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
replace_path_placeholder(&p, "https://example.com/:foo"),
|
replace_path_placeholder(&p, "https://example.com/:foo"),
|
||||||
@@ -403,11 +413,13 @@ mod placeholder_tests {
|
|||||||
name: "b".to_string(),
|
name: "b".to_string(),
|
||||||
value: "bbb".to_string(),
|
value: "bbb".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p1".into(),
|
||||||
},
|
},
|
||||||
HttpUrlParameter {
|
HttpUrlParameter {
|
||||||
name: ":a".to_string(),
|
name: ":a".to_string(),
|
||||||
value: "aaa".to_string(),
|
value: "aaa".to_string(),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
id: "p2".into(),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
|||||||
@@ -294,6 +294,7 @@ pub struct EnvironmentVariable {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
@@ -356,6 +357,7 @@ pub struct HttpRequestHeader {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
@@ -367,6 +369,7 @@ pub struct HttpUrlParameter {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
@@ -572,6 +575,7 @@ pub struct GrpcMetadataEntry {
|
|||||||
pub enabled: bool,
|
pub enabled: bool,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
|
pub id: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Prop
|
|||||||
<Input
|
<Input
|
||||||
useTemplating
|
useTemplating
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
|
stateKey={`basic.username.${request.id}`}
|
||||||
forceUpdateKey={request.id}
|
forceUpdateKey={request.id}
|
||||||
placeholder="username"
|
placeholder="username"
|
||||||
label="Username"
|
label="Username"
|
||||||
@@ -47,6 +48,7 @@ export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Prop
|
|||||||
useTemplating
|
useTemplating
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
forceUpdateKey={request?.id}
|
forceUpdateKey={request?.id}
|
||||||
|
stateKey={`basic.password.${request.id}`}
|
||||||
placeholder="password"
|
placeholder="password"
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ export function BearerAuth<T extends HttpRequest | GrpcRequest>({ request }: Pro
|
|||||||
useTemplating
|
useTemplating
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
placeholder="token"
|
placeholder="token"
|
||||||
|
stateKey={`bearer.${request.id}`}
|
||||||
type="password"
|
type="password"
|
||||||
label="Token"
|
label="Token"
|
||||||
name="token"
|
name="token"
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ const EnvironmentEditor = function ({
|
|||||||
forceUpdateKey={environment.id}
|
forceUpdateKey={environment.id}
|
||||||
pairs={environment.variables}
|
pairs={environment.variables}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
stateKey={`environment.${environment.id}`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function FolderSettingsDialog({ folderId }: Props) {
|
|||||||
placeholder="Folder description"
|
placeholder="Folder description"
|
||||||
className="min-h-[10rem] border border-border px-2"
|
className="min-h-[10rem] border border-border px-2"
|
||||||
defaultValue={folder.description}
|
defaultValue={folder.description}
|
||||||
|
stateKey={`description.${folder.id}`}
|
||||||
onChange={(description) => {
|
onChange={(description) => {
|
||||||
if (folderId == null) return;
|
if (folderId == null) return;
|
||||||
updateFolder({
|
updateFolder({
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
|
||||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
|
import { useCallback, useMemo } from 'react';
|
||||||
import type { Pair, PairEditorProps } from './core/PairEditor';
|
import type { Pair, PairEditorProps } from './core/PairEditor';
|
||||||
import { PairEditor } from './core/PairEditor';
|
import { PairEditor } from './core/PairEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
body: HttpRequest['body'];
|
request: HttpRequest;
|
||||||
onChange: (body: HttpRequest['body']) => void;
|
onChange: (body: HttpRequest['body']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
export function FormMultipartEditor({ request, forceUpdateKey, onChange }: Props) {
|
||||||
const pairs = useMemo<Pair[]>(
|
const pairs = useMemo<Pair[]>(
|
||||||
() =>
|
() =>
|
||||||
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
(Array.isArray(request.body.form) ? request.body.form : []).map((p) => ({
|
||||||
enabled: p.enabled,
|
enabled: p.enabled,
|
||||||
name: p.name,
|
name: p.name,
|
||||||
value: p.file ?? p.value,
|
value: p.file ?? p.value,
|
||||||
contentType: p.contentType,
|
contentType: p.contentType,
|
||||||
isFile: !!p.file,
|
isFile: !!p.file,
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[request.body.form],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||||
@@ -44,6 +44,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
|||||||
pairs={pairs}
|
pairs={pairs}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
stateKey={'multipart.' + request.id}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,19 +5,19 @@ import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
body: HttpRequest['body'];
|
request: HttpRequest;
|
||||||
onChange: (headers: HttpRequest['body']) => void;
|
onChange: (headers: HttpRequest['body']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props) {
|
export function FormUrlencodedEditor({ request, forceUpdateKey, onChange }: Props) {
|
||||||
const pairs = useMemo<Pair[]>(
|
const pairs = useMemo<Pair[]>(
|
||||||
() =>
|
() =>
|
||||||
(Array.isArray(body.form) ? body.form : []).map((p) => ({
|
(Array.isArray(request.body.form) ? request.body.form : []).map((p) => ({
|
||||||
enabled: !!p.enabled,
|
enabled: !!p.enabled,
|
||||||
name: p.name || '',
|
name: p.name || '',
|
||||||
value: p.value || '',
|
value: p.value || '',
|
||||||
})),
|
})),
|
||||||
[body.form],
|
[request.body.form],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||||
@@ -36,6 +36,7 @@ export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props)
|
|||||||
pairs={pairs}
|
pairs={pairs}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
stateKey={`urlencoded.${request.id}`}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { EditorView } from 'codemirror';
|
|||||||
import { formatSdl } from 'format-graphql';
|
import { formatSdl } from 'format-graphql';
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useLocalStorage } from 'react-use';
|
import { useLocalStorage } from 'react-use';
|
||||||
|
import { useDialog } from '../hooks/useDialog';
|
||||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
@@ -14,15 +15,14 @@ import { Editor } from './core/Editor/Editor';
|
|||||||
import { FormattedError } from './core/FormattedError';
|
import { FormattedError } from './core/FormattedError';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { Separator } from './core/Separator';
|
import { Separator } from './core/Separator';
|
||||||
import { useDialog } from '../hooks/useDialog';
|
|
||||||
|
|
||||||
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
|
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
|
||||||
baseRequest: HttpRequest;
|
baseRequest: HttpRequest;
|
||||||
onChange: (body: HttpRequest['body']) => void;
|
onChange: (body: HttpRequest['body']) => void;
|
||||||
body: HttpRequest['body'];
|
request: HttpRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps }: Props) {
|
export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||||
const editorViewRef = useRef<EditorView>(null);
|
const editorViewRef = useRef<EditorView>(null);
|
||||||
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
const [autoIntrospectDisabled, setAutoIntrospectDisabled] = useLocalStorage<
|
||||||
Record<string, boolean>
|
Record<string, boolean>
|
||||||
@@ -34,13 +34,13 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
|
|||||||
() => {
|
() => {
|
||||||
// Migrate text bodies to GraphQL format
|
// Migrate text bodies to GraphQL format
|
||||||
// NOTE: This is how GraphQL used to be stored
|
// NOTE: This is how GraphQL used to be stored
|
||||||
if ('text' in body) {
|
if ('text' in request.body) {
|
||||||
const b = tryParseJson(body.text, {});
|
const b = tryParseJson(request.body.text, {});
|
||||||
const variables = JSON.stringify(b.variables || undefined, null, 2);
|
const variables = JSON.stringify(b.variables || undefined, null, 2);
|
||||||
return { query: b.query ?? '', variables };
|
return { query: b.query ?? '', variables };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { query: body.query ?? '', variables: body.variables ?? '' };
|
return { query: request.body.query ?? '', variables: request.body.variables ?? '' };
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -176,6 +176,7 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
|
|||||||
placeholder="..."
|
placeholder="..."
|
||||||
ref={editorViewRef}
|
ref={editorViewRef}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
stateKey={'graphql_body.' + request.id}
|
||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]">
|
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]">
|
||||||
@@ -189,6 +190,7 @@ export function GraphQLEditor({ body, onChange, baseRequest, ...extraEditorProps
|
|||||||
defaultValue={currentBody.variables}
|
defaultValue={currentBody.variables}
|
||||||
onChange={handleChangeVariables}
|
onChange={handleChangeVariables}
|
||||||
placeholder="{}"
|
placeholder="{}"
|
||||||
|
stateKey={'graphql_vars.' + request.id}
|
||||||
useTemplating
|
useTemplating
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
|
|||||||
@@ -224,6 +224,7 @@ export function GrpcConnectionSetupPane({
|
|||||||
onUrlChange={handleChangeUrl}
|
onUrlChange={handleChangeUrl}
|
||||||
onCancel={onCancel}
|
onCancel={onCancel}
|
||||||
isLoading={isStreaming}
|
isLoading={isStreaming}
|
||||||
|
stateKey={'grpc_url.'+activeRequest.id}
|
||||||
/>
|
/>
|
||||||
<HStack space={1.5}>
|
<HStack space={1.5}>
|
||||||
<RadioDropdown
|
<RadioDropdown
|
||||||
@@ -339,6 +340,7 @@ export function GrpcConnectionSetupPane({
|
|||||||
pairs={activeRequest.metadata}
|
pairs={activeRequest.metadata}
|
||||||
onChange={handleMetadataChange}
|
onChange={handleMetadataChange}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
stateKey={`grpc_metadata.${activeRequest.id}`}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_DESCRIPTION}>
|
<TabContent value={TAB_DESCRIPTION}>
|
||||||
@@ -346,6 +348,7 @@ export function GrpcConnectionSetupPane({
|
|||||||
name="request-description"
|
name="request-description"
|
||||||
placeholder="Request description"
|
placeholder="Request description"
|
||||||
defaultValue={activeRequest.description}
|
defaultValue={activeRequest.description}
|
||||||
|
stateKey={`description.${activeRequest.id}`}
|
||||||
onChange={handleDescriptionChange}
|
onChange={handleDescriptionChange}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ export function GrpcEditor({
|
|||||||
ref={editorViewRef}
|
ref={editorViewRef}
|
||||||
extraExtensions={extraExtensions}
|
extraExtensions={extraExtensions}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
|
stateKey={`grpc_message.${request.id}`}
|
||||||
{...extraEditorProps}
|
{...extraEditorProps}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,26 +1,27 @@
|
|||||||
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
import { charsets } from '../lib/data/charsets';
|
import { charsets } from '../lib/data/charsets';
|
||||||
import { connections } from '../lib/data/connections';
|
import { connections } from '../lib/data/connections';
|
||||||
import { encodings } from '../lib/data/encodings';
|
import { encodings } from '../lib/data/encodings';
|
||||||
import { headerNames } from '../lib/data/headerNames';
|
import { headerNames } from '../lib/data/headerNames';
|
||||||
import { mimeTypes } from '../lib/data/mimetypes';
|
import { mimeTypes } from '../lib/data/mimetypes';
|
||||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
|
||||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||||
import type { PairEditorProps } from './core/PairEditor';
|
import type { PairEditorProps } from './core/PairEditor';
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
headers: HttpRequest['headers'];
|
request: HttpRequest;
|
||||||
onChange: (headers: HttpRequest['headers']) => void;
|
onChange: (headers: HttpRequest['headers']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) {
|
export function HeadersEditor({ request, onChange, forceUpdateKey }: Props) {
|
||||||
return (
|
return (
|
||||||
<PairOrBulkEditor
|
<PairOrBulkEditor
|
||||||
preferenceName="headers"
|
preferenceName="headers"
|
||||||
|
stateKey={`headers.${request.id}`}
|
||||||
valueAutocompleteVariables
|
valueAutocompleteVariables
|
||||||
nameAutocompleteVariables
|
nameAutocompleteVariables
|
||||||
pairs={headers}
|
pairs={request.headers}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
nameValidate={validateHttpHeader}
|
nameValidate={validateHttpHeader}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { SplitLayout } from './core/SplitLayout';
|
|||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { Prose } from './Prose';
|
import { Prose } from './Prose';
|
||||||
|
|
||||||
interface Props extends Pick<EditorProps, 'heightMode'> {
|
interface Props extends Pick<EditorProps, 'heightMode' | 'stateKey'> {
|
||||||
placeholder: string;
|
placeholder: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
@@ -26,6 +26,7 @@ export function MarkdownEditor({
|
|||||||
name,
|
name,
|
||||||
placeholder,
|
placeholder,
|
||||||
heightMode,
|
heightMode,
|
||||||
|
stateKey,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ export function MarkdownEditor({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
heightMode={heightMode}
|
heightMode={heightMode}
|
||||||
|
stateKey={stateKey}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
|||||||
import { useToast } from '../hooks/useToast';
|
import { useToast } from '../hooks/useToast';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import {
|
import {
|
||||||
AUTH_TYPE_BASIC,
|
AUTH_TYPE_BASIC,
|
||||||
@@ -338,6 +337,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
{activeRequest && (
|
{activeRequest && (
|
||||||
<>
|
<>
|
||||||
<UrlBar
|
<UrlBar
|
||||||
|
stateKey={`url.${activeRequest.id}`}
|
||||||
key={forceUpdateKey + urlKey}
|
key={forceUpdateKey + urlKey}
|
||||||
url={activeRequest.url}
|
url={activeRequest.url}
|
||||||
method={activeRequest.method}
|
method={activeRequest.method}
|
||||||
@@ -384,7 +384,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
<TabContent value={TAB_HEADERS}>
|
<TabContent value={TAB_HEADERS}>
|
||||||
<HeadersEditor
|
<HeadersEditor
|
||||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||||
headers={activeRequest.headers}
|
request={activeRequest}
|
||||||
onChange={(headers) =>
|
onChange={(headers) =>
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { headers } })
|
updateRequest.mutate({ id: activeRequestId, update: { headers } })
|
||||||
}
|
}
|
||||||
@@ -392,6 +392,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PARAMS}>
|
<TabContent value={TAB_PARAMS}>
|
||||||
<UrlParametersEditor
|
<UrlParametersEditor
|
||||||
|
stateKey={`params.${activeRequest.id}`}
|
||||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||||
pairs={urlParameterPairs}
|
pairs={urlParameterPairs}
|
||||||
onChange={(urlParameters) =>
|
onChange={(urlParameters) =>
|
||||||
@@ -411,6 +412,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
language="json"
|
language="json"
|
||||||
onChange={handleBodyTextChange}
|
onChange={handleBodyTextChange}
|
||||||
format={tryFormatJson}
|
format={tryFormatJson}
|
||||||
|
stateKey={`json.${activeRequest.id}`}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||||
<Editor
|
<Editor
|
||||||
@@ -422,24 +424,25 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||||
language="xml"
|
language="xml"
|
||||||
onChange={handleBodyTextChange}
|
onChange={handleBodyTextChange}
|
||||||
|
stateKey={`xml.${activeRequest.id}`}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||||
<GraphQLEditor
|
<GraphQLEditor
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
baseRequest={activeRequest}
|
baseRequest={activeRequest}
|
||||||
body={activeRequest.body}
|
request={activeRequest}
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
) : activeRequest.bodyType === BODY_TYPE_FORM_URLENCODED ? (
|
||||||
<FormUrlencodedEditor
|
<FormUrlencodedEditor
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
body={activeRequest.body}
|
request={activeRequest}
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART ? (
|
) : activeRequest.bodyType === BODY_TYPE_FORM_MULTIPART ? (
|
||||||
<FormMultipartEditor
|
<FormMultipartEditor
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
body={activeRequest.body}
|
request={activeRequest}
|
||||||
onChange={handleBodyChange}
|
onChange={handleBodyChange}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
||||||
@@ -462,6 +465,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
heightMode={fullHeight ? 'full' : 'auto'}
|
heightMode={fullHeight ? 'full' : 'auto'}
|
||||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||||
onChange={handleBodyTextChange}
|
onChange={handleBodyTextChange}
|
||||||
|
stateKey={`other.${activeRequest.id}`}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<EmptyStateText>Empty Body</EmptyStateText>
|
<EmptyStateText>Empty Body</EmptyStateText>
|
||||||
@@ -475,7 +479,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
defaultValue={activeRequest.name}
|
defaultValue={activeRequest.name}
|
||||||
className="font-sans !text-xl !px-0"
|
className="font-sans !text-xl !px-0"
|
||||||
containerClassName="border-0"
|
containerClassName="border-0"
|
||||||
placeholder={fallbackRequestName(activeRequest)}
|
placeholder={activeRequest.id}
|
||||||
onChange={(name) =>
|
onChange={(name) =>
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { name } })
|
updateRequest.mutate({ id: activeRequestId, update: { name } })
|
||||||
}
|
}
|
||||||
@@ -484,6 +488,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
name="request-description"
|
name="request-description"
|
||||||
placeholder="Request description"
|
placeholder="Request description"
|
||||||
defaultValue={activeRequest.description}
|
defaultValue={activeRequest.description}
|
||||||
|
stateKey={`description.${activeRequest.id}`}
|
||||||
onChange={(description) =>
|
onChange={(description) =>
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { description } })
|
updateRequest.mutate({ id: activeRequestId, update: { description } })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,7 @@ export function SettingsAppearance() {
|
|||||||
].join('\n')}
|
].join('\n')}
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
language="javascript"
|
language="javascript"
|
||||||
|
stateKey={null}
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|||||||
@@ -107,6 +107,7 @@ export function SettingsDesign() {
|
|||||||
placeholder="Placeholder"
|
placeholder="Placeholder"
|
||||||
size="sm"
|
size="sm"
|
||||||
rightSlot={<IconButton title="search" size="xs" className="w-8 m-0.5" icon="search" />}
|
rightSlot={<IconButton title="search" size="xs" className="w-8 m-0.5" icon="search" />}
|
||||||
|
stateKey={null}
|
||||||
/>
|
/>
|
||||||
<Editor
|
<Editor
|
||||||
defaultValue={[
|
defaultValue={[
|
||||||
@@ -118,6 +119,7 @@ export function SettingsDesign() {
|
|||||||
].join('\n')}
|
].join('\n')}
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
language="javascript"
|
language="javascript"
|
||||||
|
stateKey={null}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import type { FormEvent, ReactNode } from 'react';
|
import type { FormEvent, ReactNode } from 'react';
|
||||||
import { memo, useRef, useState } from 'react';
|
import { memo, useRef, useState } from 'react';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
|
||||||
import type { IconProps } from './core/Icon';
|
import type { IconProps } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import type { InputProps } from './core/Input';
|
import type { InputProps } from './core/Input';
|
||||||
@@ -25,6 +25,7 @@ type Props = Pick<HttpRequest, 'url'> & {
|
|||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
autocomplete?: InputProps['autocomplete'];
|
autocomplete?: InputProps['autocomplete'];
|
||||||
|
stateKey: InputProps['stateKey'];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UrlBar = memo(function UrlBar({
|
export const UrlBar = memo(function UrlBar({
|
||||||
@@ -43,6 +44,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
autocomplete,
|
autocomplete,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
isLoading,
|
isLoading,
|
||||||
|
stateKey,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const inputRef = useRef<EditorView>(null);
|
const inputRef = useRef<EditorView>(null);
|
||||||
const [isFocused, setIsFocused] = useState<boolean>(false);
|
const [isFocused, setIsFocused] = useState<boolean>(false);
|
||||||
@@ -65,7 +67,7 @@ export const UrlBar = memo(function UrlBar({
|
|||||||
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
|
<form onSubmit={handleSubmit} className={classNames('x-theme-urlBar', className)}>
|
||||||
<Input
|
<Input
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
ref={inputRef}
|
stateKey={stateKey}
|
||||||
size="md"
|
size="md"
|
||||||
wrapLines={isFocused}
|
wrapLines={isFocused}
|
||||||
hideLabel
|
hideLabel
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
|
import { useRef } from 'react';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
import type { PairEditorRef } from './core/PairEditor';
|
import type { PairEditorProps, PairEditorRef } from './core/PairEditor';
|
||||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
import { useRef } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
forceUpdateKey: string;
|
forceUpdateKey: string;
|
||||||
pairs: HttpRequest['headers'];
|
pairs: HttpRequest['headers'];
|
||||||
|
stateKey: PairEditorProps['stateKey'];
|
||||||
onChange: (headers: HttpRequest['urlParameters']) => void;
|
onChange: (headers: HttpRequest['urlParameters']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props) {
|
export function UrlParametersEditor({ pairs, forceUpdateKey, onChange, stateKey }: Props) {
|
||||||
const pairEditor = useRef<PairEditorRef>(null);
|
const pairEditor = useRef<PairEditorRef>(null);
|
||||||
const [{ urlParametersKey }] = useRequestEditor();
|
const [{ urlParametersKey }] = useRequestEditor();
|
||||||
|
|
||||||
@@ -32,14 +33,15 @@ export function UrlParametersEditor({ pairs, forceUpdateKey, onChange }: Props)
|
|||||||
<VStack className="h-full">
|
<VStack className="h-full">
|
||||||
<PairOrBulkEditor
|
<PairOrBulkEditor
|
||||||
ref={pairEditor}
|
ref={pairEditor}
|
||||||
preferenceName="url_parameters"
|
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||||
valueAutocompleteVariables
|
|
||||||
nameAutocompleteVariables
|
nameAutocompleteVariables
|
||||||
namePlaceholder="param_name"
|
namePlaceholder="param_name"
|
||||||
valuePlaceholder="Value"
|
|
||||||
pairs={pairs}
|
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
pairs={pairs}
|
||||||
|
preferenceName="url_parameters"
|
||||||
|
stateKey={stateKey}
|
||||||
|
valueAutocompleteVariables
|
||||||
|
valuePlaceholder="Value"
|
||||||
/>
|
/>
|
||||||
</VStack>
|
</VStack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function WorkspaceSettingsDialog({ workspaceId }: Props) {
|
|||||||
placeholder="Workspace description"
|
placeholder="Workspace description"
|
||||||
className="min-h-[10rem] border border-border px-2"
|
className="min-h-[10rem] border border-border px-2"
|
||||||
defaultValue={workspace.description}
|
defaultValue={workspace.description}
|
||||||
|
stateKey={`description.${workspace.id}`}
|
||||||
onChange={(description) => updateWorkspace.mutate({ description })}
|
onChange={(description) => updateWorkspace.mutate({ description })}
|
||||||
heightMode='auto'
|
heightMode='auto'
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import {Editor} from "./Editor/Editor";
|
import { Editor } from './Editor/Editor';
|
||||||
import type { PairEditorProps } from './PairEditor';
|
import type { PairEditorProps } from './PairEditor';
|
||||||
|
|
||||||
type Props = PairEditorProps;
|
type Props = PairEditorProps;
|
||||||
@@ -10,6 +10,7 @@ export function BulkPairEditor({
|
|||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
|
stateKey,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const pairsText = useMemo(() => {
|
const pairsText = useMemo(() => {
|
||||||
return pairs
|
return pairs
|
||||||
@@ -33,6 +34,7 @@ export function BulkPairEditor({
|
|||||||
<Editor
|
<Editor
|
||||||
useTemplating
|
useTemplating
|
||||||
autocompleteVariables
|
autocompleteVariables
|
||||||
|
stateKey={`bulk_pair.${stateKey}`}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
|
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
|
||||||
defaultValue={pairsText}
|
defaultValue={pairsText}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
|||||||
close() {
|
close() {
|
||||||
handleClose();
|
handleClose();
|
||||||
},
|
},
|
||||||
}));
|
}), [handleClose, isOpen, setIsOpen]);
|
||||||
|
|
||||||
useHotKey(hotKeyAction ?? null, () => {
|
useHotKey(hotKeyAction ?? null, () => {
|
||||||
setDefaultSelectedIndex(0);
|
setDefaultSelectedIndex(0);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap, historyField } from '@codemirror/commands';
|
||||||
import { forceParsing } from '@codemirror/language';
|
import { foldState, forceParsing } from '@codemirror/language';
|
||||||
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
import { Compartment, EditorState, type Extension } from '@codemirror/state';
|
||||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||||
@@ -8,12 +8,12 @@ import classNames from 'classnames';
|
|||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
import type { MutableRefObject, ReactNode } from 'react';
|
import type { MutableRefObject, ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
|
useEffect,
|
||||||
Children,
|
Children,
|
||||||
cloneElement,
|
cloneElement,
|
||||||
forwardRef,
|
forwardRef,
|
||||||
isValidElement,
|
isValidElement,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
@@ -71,6 +71,7 @@ export interface EditorProps {
|
|||||||
extraExtensions?: Extension[];
|
extraExtensions?: Extension[];
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
hideGutter?: boolean;
|
hideGutter?: boolean;
|
||||||
|
stateKey: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emptyVariables: EnvironmentVariable[] = [];
|
const emptyVariables: EnvironmentVariable[] = [];
|
||||||
@@ -102,6 +103,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
actions,
|
actions,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
hideGutter,
|
hideGutter,
|
||||||
|
stateKey,
|
||||||
}: EditorProps,
|
}: EditorProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -115,7 +117,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
}
|
}
|
||||||
|
|
||||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||||
useImperativeHandle(ref, () => cm.current?.view);
|
useImperativeHandle(ref, () => cm.current?.view, []);
|
||||||
|
|
||||||
// Use ref so we can update the handler without re-initializing the editor
|
// Use ref so we can update the handler without re-initializing the editor
|
||||||
const handleChange = useRef<EditorProps['onChange']>(onChange);
|
const handleChange = useRef<EditorProps['onChange']>(onChange);
|
||||||
@@ -289,7 +291,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let view: EditorView;
|
|
||||||
try {
|
try {
|
||||||
const languageCompartment = new Compartment();
|
const languageCompartment = new Compartment();
|
||||||
const langExt = getLanguageExtension({
|
const langExt = getLanguageExtension({
|
||||||
@@ -303,32 +304,38 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
onClickMissingVariable,
|
onClickMissingVariable,
|
||||||
onClickPathParameter,
|
onClickPathParameter,
|
||||||
});
|
});
|
||||||
|
const extensions = [
|
||||||
|
languageCompartment.of(langExt),
|
||||||
|
placeholderCompartment.current.of(
|
||||||
|
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||||
|
),
|
||||||
|
wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []),
|
||||||
|
...getExtensions({
|
||||||
|
container,
|
||||||
|
readOnly,
|
||||||
|
singleLine,
|
||||||
|
hideGutter,
|
||||||
|
stateKey,
|
||||||
|
onChange: handleChange,
|
||||||
|
onPaste: handlePaste,
|
||||||
|
onPasteOverwrite: handlePasteOverwrite,
|
||||||
|
onFocus: handleFocus,
|
||||||
|
onBlur: handleBlur,
|
||||||
|
onKeyDown: handleKeyDown,
|
||||||
|
}),
|
||||||
|
...(extraExtensions ?? []),
|
||||||
|
];
|
||||||
|
|
||||||
const state = EditorState.create({
|
const cachedJsonState = getCachedEditorState(stateKey);
|
||||||
doc: `${defaultValue ?? ''}`,
|
const state = cachedJsonState
|
||||||
extensions: [
|
? EditorState.fromJSON(
|
||||||
languageCompartment.of(langExt),
|
cachedJsonState,
|
||||||
placeholderCompartment.current.of(
|
{ extensions },
|
||||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
{ fold: foldState, history: historyField },
|
||||||
),
|
)
|
||||||
wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []),
|
: EditorState.create({ doc: `${defaultValue ?? ''}`, extensions });
|
||||||
...getExtensions({
|
|
||||||
container,
|
|
||||||
readOnly,
|
|
||||||
singleLine,
|
|
||||||
hideGutter,
|
|
||||||
onChange: handleChange,
|
|
||||||
onPaste: handlePaste,
|
|
||||||
onPasteOverwrite: handlePasteOverwrite,
|
|
||||||
onFocus: handleFocus,
|
|
||||||
onBlur: handleBlur,
|
|
||||||
onKeyDown: handleKeyDown,
|
|
||||||
}),
|
|
||||||
...(extraExtensions ?? []),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
view = new EditorView({ state, parent: container });
|
const view = new EditorView({ state, parent: container });
|
||||||
|
|
||||||
// For large documents, the parser may parse the max number of lines and fail to add
|
// For large documents, the parser may parse the max number of lines and fail to add
|
||||||
// things like fold markers because of it.
|
// things like fold markers because of it.
|
||||||
@@ -459,6 +466,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getExtensions({
|
function getExtensions({
|
||||||
|
stateKey,
|
||||||
container,
|
container,
|
||||||
readOnly,
|
readOnly,
|
||||||
singleLine,
|
singleLine,
|
||||||
@@ -470,6 +478,7 @@ function getExtensions({
|
|||||||
onBlur,
|
onBlur,
|
||||||
onKeyDown,
|
onKeyDown,
|
||||||
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||||
|
stateKey: EditorProps['stateKey'];
|
||||||
container: HTMLDivElement | null;
|
container: HTMLDivElement | null;
|
||||||
onChange: MutableRefObject<EditorProps['onChange']>;
|
onChange: MutableRefObject<EditorProps['onChange']>;
|
||||||
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
onPaste: MutableRefObject<EditorProps['onPaste']>;
|
||||||
@@ -519,6 +528,7 @@ function getExtensions({
|
|||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (onChange && update.docChanged) {
|
if (onChange && update.docChanged) {
|
||||||
onChange.current?.(update.state.doc.toString());
|
onChange.current?.(update.state.doc.toString());
|
||||||
|
saveCachedEditorState(stateKey, update.state);
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
@@ -529,3 +539,21 @@ const placeholderElFromText = (text: string) => {
|
|||||||
el.innerHTML = text.replaceAll('\n', '<br/>');
|
el.innerHTML = text.replaceAll('\n', '<br/>');
|
||||||
return el;
|
return el;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function saveCachedEditorState(stateKey: string | null, state: EditorState | null) {
|
||||||
|
if (!stateKey || state == null) return;
|
||||||
|
const stateJson = state.toJSON({ history: historyField, folds: foldState });
|
||||||
|
sessionStorage.setItem(stateKey, JSON.stringify(stateJson));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCachedEditorState(stateKey: string | null) {
|
||||||
|
if (stateKey == null) return;
|
||||||
|
const serializedState = stateKey ? sessionStorage.getItem(stateKey) : null;
|
||||||
|
if (serializedState == null) return;
|
||||||
|
try {
|
||||||
|
return JSON.parse(serializedState);
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import type { HTMLAttributes, ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||||
import type { EditorProps } from './Editor/Editor';
|
import type { EditorProps } from './Editor/Editor';
|
||||||
@@ -8,44 +8,41 @@ import { Editor } from './Editor/Editor';
|
|||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
export type InputProps = Omit<
|
export type InputProps = Pick<
|
||||||
HTMLAttributes<HTMLInputElement>,
|
EditorProps,
|
||||||
'onChange' | 'onFocus' | 'onKeyDown' | 'onPaste'
|
| 'language'
|
||||||
> &
|
| 'useTemplating'
|
||||||
Pick<
|
| 'autocomplete'
|
||||||
EditorProps,
|
| 'forceUpdateKey'
|
||||||
| 'language'
|
| 'autoFocus'
|
||||||
| 'useTemplating'
|
| 'autoSelect'
|
||||||
| 'autocomplete'
|
| 'autocompleteVariables'
|
||||||
| 'forceUpdateKey'
|
| 'onKeyDown'
|
||||||
| 'autoFocus'
|
| 'readOnly'
|
||||||
| 'autoSelect'
|
> & {
|
||||||
| 'autocompleteVariables'
|
name?: string;
|
||||||
| 'onKeyDown'
|
type?: 'text' | 'password';
|
||||||
| 'readOnly'
|
label: ReactNode;
|
||||||
> & {
|
hideLabel?: boolean;
|
||||||
name?: string;
|
labelPosition?: 'top' | 'left';
|
||||||
type?: 'text' | 'password';
|
labelClassName?: string;
|
||||||
label: ReactNode;
|
containerClassName?: string;
|
||||||
hideLabel?: boolean;
|
onChange?: (value: string) => void;
|
||||||
labelPosition?: 'top' | 'left';
|
onFocus?: () => void;
|
||||||
labelClassName?: string;
|
onBlur?: () => void;
|
||||||
containerClassName?: string;
|
onPaste?: (value: string) => void;
|
||||||
onChange?: (value: string) => void;
|
onPasteOverwrite?: (value: string) => void;
|
||||||
onFocus?: () => void;
|
defaultValue?: string;
|
||||||
onBlur?: () => void;
|
leftSlot?: ReactNode;
|
||||||
onPaste?: (value: string) => void;
|
rightSlot?: ReactNode;
|
||||||
onPasteOverwrite?: (value: string) => void;
|
size?: 'xs' | 'sm' | 'md' | 'auto';
|
||||||
defaultValue?: string;
|
className?: string;
|
||||||
leftSlot?: ReactNode;
|
placeholder?: string;
|
||||||
rightSlot?: ReactNode;
|
validate?: boolean | ((v: string) => boolean);
|
||||||
size?: 'xs' | 'sm' | 'md' | 'auto';
|
require?: boolean;
|
||||||
className?: string;
|
wrapLines?: boolean;
|
||||||
placeholder?: string;
|
stateKey: EditorProps['stateKey'];
|
||||||
validate?: boolean | ((v: string) => boolean);
|
};
|
||||||
require?: boolean;
|
|
||||||
wrapLines?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Input = forwardRef<EditorView | undefined, InputProps>(function Input(
|
export const Input = forwardRef<EditorView | undefined, InputProps>(function Input(
|
||||||
{
|
{
|
||||||
@@ -71,6 +68,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
type = 'text',
|
type = 'text',
|
||||||
validate,
|
validate,
|
||||||
readOnly,
|
readOnly,
|
||||||
|
stateKey,
|
||||||
...props
|
...props
|
||||||
}: InputProps,
|
}: InputProps,
|
||||||
ref,
|
ref,
|
||||||
@@ -172,6 +170,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={id}
|
id={id}
|
||||||
singleLine
|
singleLine
|
||||||
|
stateKey={stateKey}
|
||||||
wrapLines={wrapLines}
|
wrapLines={wrapLines}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
type={type === 'password' && !obscured ? 'text' : type}
|
type={type === 'password' && !obscured ? 'text' : type}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { deepEqual } from '@tanstack/react-router';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { EditorView } from 'codemirror';
|
import type { EditorView } from 'codemirror';
|
||||||
import {
|
import {
|
||||||
Fragment,
|
|
||||||
forwardRef,
|
forwardRef,
|
||||||
|
Fragment,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
@@ -12,8 +13,8 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import type { XYCoord } from 'react-dnd';
|
import type { XYCoord } from 'react-dnd';
|
||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { v4 as uuid } from 'uuid';
|
|
||||||
import { usePrompt } from '../../hooks/usePrompt';
|
import { usePrompt } from '../../hooks/usePrompt';
|
||||||
|
import { generateId } from '../../lib/generateId';
|
||||||
import { DropMarker } from '../DropMarker';
|
import { DropMarker } from '../DropMarker';
|
||||||
import { SelectFile } from '../SelectFile';
|
import { SelectFile } from '../SelectFile';
|
||||||
import { Checkbox } from './Checkbox';
|
import { Checkbox } from './Checkbox';
|
||||||
@@ -31,21 +32,22 @@ export interface PairEditorRef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
allowFileValues?: boolean;
|
||||||
onChange: (pairs: Pair[]) => void;
|
|
||||||
forceUpdateKey?: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
|
forceUpdateKey?: string;
|
||||||
|
nameAutocomplete?: GenericCompletionConfig;
|
||||||
|
nameAutocompleteVariables?: boolean;
|
||||||
namePlaceholder?: string;
|
namePlaceholder?: string;
|
||||||
|
nameValidate?: InputProps['validate'];
|
||||||
|
noScroll?: boolean;
|
||||||
|
onChange: (pairs: Pair[]) => void;
|
||||||
|
pairs: Pair[];
|
||||||
|
stateKey: InputProps['stateKey'];
|
||||||
|
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||||
|
valueAutocompleteVariables?: boolean;
|
||||||
valuePlaceholder?: string;
|
valuePlaceholder?: string;
|
||||||
valueType?: 'text' | 'password';
|
valueType?: 'text' | 'password';
|
||||||
nameAutocomplete?: GenericCompletionConfig;
|
|
||||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
|
||||||
nameAutocompleteVariables?: boolean;
|
|
||||||
valueAutocompleteVariables?: boolean;
|
|
||||||
allowFileValues?: boolean;
|
|
||||||
nameValidate?: InputProps['validate'];
|
|
||||||
valueValidate?: InputProps['validate'];
|
valueValidate?: InputProps['validate'];
|
||||||
noScroll?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Pair = {
|
export type Pair = {
|
||||||
@@ -65,21 +67,22 @@ type PairContainer = {
|
|||||||
|
|
||||||
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
|
export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function PairEditor(
|
||||||
{
|
{
|
||||||
|
stateKey,
|
||||||
|
allowFileValues,
|
||||||
className,
|
className,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
nameAutocompleteVariables,
|
nameAutocompleteVariables,
|
||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
nameValidate,
|
nameValidate,
|
||||||
valueType,
|
|
||||||
onChange,
|
|
||||||
noScroll,
|
noScroll,
|
||||||
|
onChange,
|
||||||
pairs: originalPairs,
|
pairs: originalPairs,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
valueAutocompleteVariables,
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
|
valueType,
|
||||||
valueValidate,
|
valueValidate,
|
||||||
allowFileValues,
|
|
||||||
}: PairEditorProps,
|
}: PairEditorProps,
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -93,20 +96,28 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
return [...pairs, newPairContainer()];
|
return [...pairs, newPairContainer()];
|
||||||
});
|
});
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(
|
||||||
focusValue(index: number) {
|
ref,
|
||||||
const id = pairs[index]?.id ?? 'n/a';
|
() => ({
|
||||||
setForceFocusValuePairId(id);
|
focusValue(index: number) {
|
||||||
},
|
const id = pairs[index]?.id ?? 'n/a';
|
||||||
}));
|
setForceFocusValuePairId(id);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
[pairs],
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Remove empty headers on initial render
|
// Remove empty headers on initial render
|
||||||
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
||||||
// sort of diff method or deterministic IDs based on array index and update key
|
// sort of diff method or deterministic IDs based on array index and update key
|
||||||
const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
|
const nonEmpty = originalPairs.filter(
|
||||||
const pairs = nonEmpty.map((pair) => newPairContainer(pair));
|
(h, i) => i !== originalPairs.length - 1 && !(h.name === '' && h.value === ''),
|
||||||
setPairs([...pairs, newPairContainer()]);
|
);
|
||||||
|
const newPairs = nonEmpty.map((pair) => newPairContainer(pair));
|
||||||
|
if (!deepEqual(pairs, newPairs)) {
|
||||||
|
setPairs(pairs);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [forceUpdateKey]);
|
}, [forceUpdateKey]);
|
||||||
@@ -211,28 +222,29 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
|||||||
<Fragment key={p.id}>
|
<Fragment key={p.id}>
|
||||||
{hoveredIndex === i && <DropMarker />}
|
{hoveredIndex === i && <DropMarker />}
|
||||||
<PairEditorRow
|
<PairEditorRow
|
||||||
pairContainer={p}
|
|
||||||
className="py-1"
|
|
||||||
isLast={isLast}
|
|
||||||
allowFileValues={allowFileValues}
|
allowFileValues={allowFileValues}
|
||||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
className="py-1"
|
||||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
|
||||||
valueType={valueType}
|
|
||||||
forceFocusNamePairId={forceFocusNamePairId}
|
forceFocusNamePairId={forceFocusNamePairId}
|
||||||
forceFocusValuePairId={forceFocusValuePairId}
|
forceFocusValuePairId={forceFocusValuePairId}
|
||||||
forceUpdateKey={forceUpdateKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
index={i}
|
||||||
|
isLast={isLast}
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
valueAutocomplete={valueAutocomplete}
|
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||||
namePlaceholder={namePlaceholder}
|
namePlaceholder={namePlaceholder}
|
||||||
valuePlaceholder={valuePlaceholder}
|
|
||||||
nameValidate={nameValidate}
|
nameValidate={nameValidate}
|
||||||
valueValidate={valueValidate}
|
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onFocus={handleFocus}
|
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
|
onFocus={handleFocus}
|
||||||
onMove={handleMove}
|
onMove={handleMove}
|
||||||
index={i}
|
pairContainer={p}
|
||||||
|
stateKey={stateKey}
|
||||||
|
valueAutocomplete={valueAutocomplete}
|
||||||
|
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||||
|
valuePlaceholder={valuePlaceholder}
|
||||||
|
valueType={valueType}
|
||||||
|
valueValidate={valueValidate}
|
||||||
/>
|
/>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
);
|
);
|
||||||
@@ -260,17 +272,18 @@ type PairEditorRowProps = {
|
|||||||
index: number;
|
index: number;
|
||||||
} & Pick<
|
} & Pick<
|
||||||
PairEditorProps,
|
PairEditorProps,
|
||||||
| 'nameAutocomplete'
|
|
||||||
| 'valueAutocomplete'
|
|
||||||
| 'nameAutocompleteVariables'
|
|
||||||
| 'valueAutocompleteVariables'
|
|
||||||
| 'valueType'
|
|
||||||
| 'namePlaceholder'
|
|
||||||
| 'valuePlaceholder'
|
|
||||||
| 'nameValidate'
|
|
||||||
| 'valueValidate'
|
|
||||||
| 'forceUpdateKey'
|
|
||||||
| 'allowFileValues'
|
| 'allowFileValues'
|
||||||
|
| 'forceUpdateKey'
|
||||||
|
| 'nameAutocomplete'
|
||||||
|
| 'nameAutocompleteVariables'
|
||||||
|
| 'namePlaceholder'
|
||||||
|
| 'nameValidate'
|
||||||
|
| 'stateKey'
|
||||||
|
| 'valueAutocomplete'
|
||||||
|
| 'valueAutocompleteVariables'
|
||||||
|
| 'valuePlaceholder'
|
||||||
|
| 'valueType'
|
||||||
|
| 'valueValidate'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
function PairEditorRow({
|
function PairEditorRow({
|
||||||
@@ -279,8 +292,8 @@ function PairEditorRow({
|
|||||||
forceFocusNamePairId,
|
forceFocusNamePairId,
|
||||||
forceFocusValuePairId,
|
forceFocusValuePairId,
|
||||||
forceUpdateKey,
|
forceUpdateKey,
|
||||||
isLast,
|
|
||||||
index,
|
index,
|
||||||
|
isLast,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
nameAutocompleteVariables,
|
nameAutocompleteVariables,
|
||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
@@ -291,6 +304,7 @@ function PairEditorRow({
|
|||||||
onFocus,
|
onFocus,
|
||||||
onMove,
|
onMove,
|
||||||
pairContainer,
|
pairContainer,
|
||||||
|
stateKey,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
valueAutocompleteVariables,
|
valueAutocompleteVariables,
|
||||||
valuePlaceholder,
|
valuePlaceholder,
|
||||||
@@ -434,6 +448,7 @@ function PairEditorRow({
|
|||||||
ref={nameInputRef}
|
ref={nameInputRef}
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
|
stateKey={`name.${pairContainer.id}.${stateKey}`}
|
||||||
wrapLines={false}
|
wrapLines={false}
|
||||||
readOnly={pairContainer.pair.readOnlyName}
|
readOnly={pairContainer.pair.readOnlyName}
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -476,6 +491,7 @@ function PairEditorRow({
|
|||||||
ref={valueInputRef}
|
ref={valueInputRef}
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
|
stateKey={`value.${pairContainer.id}.${stateKey}`}
|
||||||
wrapLines={false}
|
wrapLines={false}
|
||||||
size="sm"
|
size="sm"
|
||||||
containerClassName={classNames(isLast && 'border-dashed')}
|
containerClassName={classNames(isLast && 'border-dashed')}
|
||||||
@@ -575,7 +591,7 @@ function PairEditorRow({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newPairContainer = (initialPair?: Pair): PairContainer => {
|
const newPairContainer = (initialPair?: Pair): PairContainer => {
|
||||||
const id = initialPair?.id ?? uuid();
|
const id = initialPair?.id ?? generateId();
|
||||||
const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
|
const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
|
||||||
return { id, pair };
|
return { id, pair };
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { HTMLAttributes } from 'react';
|
import type { HTMLAttributes, FocusEvent } from 'react';
|
||||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||||
import { IconButton } from './IconButton';
|
import { IconButton } from './IconButton';
|
||||||
import type { InputProps } from './Input';
|
import type { InputProps } from './Input';
|
||||||
import { HStack } from './Stacks';
|
import { HStack } from './Stacks';
|
||||||
|
|
||||||
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type'> &
|
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type' | 'stateKey'> &
|
||||||
Pick<HTMLAttributes<HTMLInputElement>, 'onKeyDownCapture'> & {
|
Pick<HTMLAttributes<HTMLInputElement>, 'onKeyDownCapture'> & {
|
||||||
|
onFocusRaw?: HTMLAttributes<HTMLInputElement>['onFocus'];
|
||||||
type?: 'text' | 'password' | 'number';
|
type?: 'text' | 'password' | 'number';
|
||||||
step?: number;
|
step?: number;
|
||||||
};
|
};
|
||||||
@@ -33,7 +34,10 @@ export function PlainInput({
|
|||||||
type = 'text',
|
type = 'text',
|
||||||
validate,
|
validate,
|
||||||
autoSelect,
|
autoSelect,
|
||||||
...props
|
placeholder,
|
||||||
|
autoFocus,
|
||||||
|
onKeyDownCapture,
|
||||||
|
onFocusRaw,
|
||||||
}: PlainInputProps) {
|
}: PlainInputProps) {
|
||||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||||
@@ -41,14 +45,15 @@ export function PlainInput({
|
|||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
|
||||||
const handleFocus = useCallback(() => {
|
const handleFocus = useCallback((e: FocusEvent<HTMLInputElement>) => {
|
||||||
|
onFocusRaw?.(e);
|
||||||
setFocused(true);
|
setFocused(true);
|
||||||
if (autoSelect) {
|
if (autoSelect) {
|
||||||
inputRef.current?.select();
|
inputRef.current?.select();
|
||||||
textareaRef.current?.select();
|
textareaRef.current?.select();
|
||||||
}
|
}
|
||||||
onFocus?.();
|
onFocus?.();
|
||||||
}, [autoSelect, onFocus]);
|
}, [autoSelect, onFocus, onFocusRaw]);
|
||||||
|
|
||||||
const handleBlur = useCallback(() => {
|
const handleBlur = useCallback(() => {
|
||||||
setFocused(false);
|
setFocused(false);
|
||||||
@@ -135,7 +140,9 @@ export function PlainInput({
|
|||||||
className={classNames(commonClassName, 'h-auto')}
|
className={classNames(commonClassName, 'h-auto')}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
{...props}
|
autoFocus={autoFocus}
|
||||||
|
placeholder={placeholder}
|
||||||
|
onKeyDownCapture={onKeyDownCapture}
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
{type === 'password' && (
|
{type === 'password' && (
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ function ActualEventStreamViewer({ response }: Props) {
|
|||||||
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
|
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
|
||||||
const formatted = useFormatText({ text, language, pretty: true });
|
const formatted = useFormatText({ text, language, pretty: true });
|
||||||
if (formatted.data == null) return null;
|
if (formatted.data == null) return null;
|
||||||
return <Editor readOnly defaultValue={formatted.data} language={language} />;
|
return <Editor readOnly defaultValue={formatted.data} language={language} stateKey={null} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EventStreamEventsVirtual({
|
function EventStreamEventsVirtual({
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ export function TextViewer({
|
|||||||
defaultValue={filterText}
|
defaultValue={filterText}
|
||||||
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
|
onKeyDown={(e) => e.key === 'Escape' && toggleSearch()}
|
||||||
onChange={setFilterText}
|
onChange={setFilterText}
|
||||||
|
stateKey={`filter.${responseId}`}
|
||||||
/>
|
/>
|
||||||
</div>,
|
</div>,
|
||||||
);
|
);
|
||||||
@@ -113,6 +114,7 @@ export function TextViewer({
|
|||||||
isSearching,
|
isSearching,
|
||||||
language,
|
language,
|
||||||
requestId,
|
requestId,
|
||||||
|
responseId,
|
||||||
setFilterText,
|
setFilterText,
|
||||||
toggleSearch,
|
toggleSearch,
|
||||||
]);
|
]);
|
||||||
@@ -165,6 +167,7 @@ export function TextViewer({
|
|||||||
language={language}
|
language={language}
|
||||||
actions={actions}
|
actions={actions}
|
||||||
extraExtensions={extraExtensions}
|
extraExtensions={extraExtensions}
|
||||||
|
stateKey={null}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ const fallback: string[] = [];
|
|||||||
export function useRecentWorkspaces() {
|
export function useRecentWorkspaces() {
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const kv = useKeyValue<string[]>({
|
const {value, set} = useKeyValue<string[]>({
|
||||||
key: kvKey(),
|
key: kvKey(),
|
||||||
namespace,
|
namespace,
|
||||||
fallback,
|
fallback,
|
||||||
@@ -19,7 +19,7 @@ export function useRecentWorkspaces() {
|
|||||||
|
|
||||||
// Set history when active request changes
|
// Set history when active request changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
kv.set((currentHistory: string[]) => {
|
set((currentHistory: string[]) => {
|
||||||
if (activeWorkspace === null) return currentHistory;
|
if (activeWorkspace === null) return currentHistory;
|
||||||
const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspace.id);
|
const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspace.id);
|
||||||
return [activeWorkspace.id, ...withoutCurrent];
|
return [activeWorkspace.id, ...withoutCurrent];
|
||||||
@@ -28,8 +28,8 @@ export function useRecentWorkspaces() {
|
|||||||
}, [activeWorkspace]);
|
}, [activeWorkspace]);
|
||||||
|
|
||||||
const onlyValidIds = useMemo(
|
const onlyValidIds = useMemo(
|
||||||
() => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
|
() => value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
|
||||||
[kv.value, workspaces],
|
[value, workspaces],
|
||||||
);
|
);
|
||||||
|
|
||||||
return onlyValidIds;
|
return onlyValidIds;
|
||||||
|
|||||||
@@ -1,3 +1,7 @@
|
|||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
|
||||||
|
const nanoid = customAlphabet('023456789abcdefghijkmnpqrstuvwxyzABCDEFGHIJKMNPQRSTUVWXYZ', 10);
|
||||||
|
|
||||||
export function generateId(): string {
|
export function generateId(): string {
|
||||||
return Math.random().toString(36).slice(2);
|
return nanoid();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ type TauriCmd =
|
|||||||
| 'cmd_write_file_dev';
|
| 'cmd_write_file_dev';
|
||||||
|
|
||||||
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
export async function invokeCmd<T>(cmd: TauriCmd, args?: InvokeArgs): Promise<T> {
|
||||||
console.log('RUN COMMAND', cmd, args);
|
// console.log('RUN COMMAND', cmd, args);
|
||||||
try {
|
try {
|
||||||
return await invoke(cmd, args);
|
return await invoke(cmd, args);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"jotai": "^2.9.3",
|
"jotai": "^2.9.3",
|
||||||
"lucide-react": "^0.439.0",
|
"lucide-react": "^0.439.0",
|
||||||
"mime": "^4.0.4",
|
"mime": "^4.0.4",
|
||||||
|
"nanoid": "^5.0.9",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"parse-color": "^1.0.0",
|
"parse-color": "^1.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
Reference in New Issue
Block a user