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