mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-08 22:05:16 +02:00
Optimize sidebar collapsing
This commit is contained in:
+11
-9
@@ -44,11 +44,7 @@ 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::{
|
use yaak_models::models::{CookieJar, Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcConnectionState, GrpcEvent, GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, Settings, Workspace};
|
||||||
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,
|
||||||
@@ -61,10 +57,10 @@ use yaak_models::queries::{
|
|||||||
get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars,
|
get_key_value_raw, get_or_create_settings, get_plugin, get_workspace, list_cookie_jars,
|
||||||
list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events,
|
list_environments, list_folders, list_grpc_connections_for_workspace, list_grpc_events,
|
||||||
list_grpc_requests, list_http_requests, list_http_responses_for_request,
|
list_grpc_requests, list_http_requests, list_http_responses_for_request,
|
||||||
list_http_responses_for_workspace, list_plugins, list_workspaces, set_key_value_raw,
|
list_http_responses_for_workspace, list_key_values_raw, list_plugins, list_workspaces,
|
||||||
update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder,
|
set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||||
upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request,
|
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
|
||||||
upsert_plugin, upsert_workspace,
|
upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
|
||||||
};
|
};
|
||||||
use yaak_plugin_runtime::events::{
|
use yaak_plugin_runtime::events::{
|
||||||
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
BootResponse, CallHttpRequestActionRequest, FilterResponse, FindHttpResponsesResponse,
|
||||||
@@ -1513,6 +1509,11 @@ async fn cmd_list_cookie_jars(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cmd_list_key_values(w: WebviewWindow) -> Result<Vec<KeyValue>, String> {
|
||||||
|
list_key_values_raw(&w).await.map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn cmd_get_environment(id: &str, w: WebviewWindow) -> Result<Environment, String> {
|
async fn cmd_get_environment(id: &str, w: WebviewWindow) -> Result<Environment, String> {
|
||||||
get_environment(&w, id).await.map_err(|e| e.to_string())
|
get_environment(&w, id).await.map_err(|e| e.to_string())
|
||||||
@@ -1797,6 +1798,7 @@ pub fn run() {
|
|||||||
cmd_list_grpc_connections,
|
cmd_list_grpc_connections,
|
||||||
cmd_list_grpc_events,
|
cmd_list_grpc_events,
|
||||||
cmd_list_grpc_requests,
|
cmd_list_grpc_requests,
|
||||||
|
cmd_list_key_values,
|
||||||
cmd_list_http_requests,
|
cmd_list_http_requests,
|
||||||
cmd_list_http_responses,
|
cmd_list_http_responses,
|
||||||
cmd_list_plugins,
|
cmd_list_plugins,
|
||||||
|
|||||||
@@ -126,6 +126,18 @@ pub async fn set_key_value_raw<R: Runtime>(
|
|||||||
(emit_upserted_model(w, kv), existing.is_none())
|
(emit_upserted_model(w, kv), existing.is_none())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn list_key_values_raw<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<KeyValue>> {
|
||||||
|
let dbm = &*mgr.state::<SqliteConnection>();
|
||||||
|
let db = dbm.0.lock().await.get().unwrap();
|
||||||
|
let (sql, params) = Query::select()
|
||||||
|
.from(KeyValueIden::Table)
|
||||||
|
.column(Asterisk)
|
||||||
|
.build_rusqlite(SqliteQueryBuilder);
|
||||||
|
let mut stmt = db.prepare(sql.as_str())?;
|
||||||
|
let items = stmt.query_map(&*params.as_params(), |row| row.try_into())?;
|
||||||
|
Ok(items.map(|v| v.unwrap()).collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn get_key_value_raw<R: Runtime>(
|
pub async fn get_key_value_raw<R: Runtime>(
|
||||||
mgr: &impl Manager<R>,
|
mgr: &impl Manager<R>,
|
||||||
namespace: &str,
|
namespace: &str,
|
||||||
@@ -767,7 +779,7 @@ pub async fn list_environments<R: Runtime>(
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
environments.push(base_environment);
|
environments.push(base_environment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -857,7 +869,7 @@ pub async fn update_settings<R: Runtime>(
|
|||||||
None => None,
|
None => None,
|
||||||
Some(p) => Some(serde_json::to_string(&p)?),
|
Some(p) => Some(serde_json::to_string(&p)?),
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
.returning_all()
|
.returning_all()
|
||||||
@@ -1182,7 +1194,7 @@ pub async fn duplicate_folder<R: Runtime>(
|
|||||||
..src_folder.clone()
|
..src_folder.clone()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
for m in http_requests {
|
for m in http_requests {
|
||||||
upsert_http_request(
|
upsert_http_request(
|
||||||
@@ -1194,7 +1206,7 @@ pub async fn duplicate_folder<R: Runtime>(
|
|||||||
..m
|
..m
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
for m in grpc_requests {
|
for m in grpc_requests {
|
||||||
upsert_grpc_request(
|
upsert_grpc_request(
|
||||||
@@ -1206,7 +1218,7 @@ pub async fn duplicate_folder<R: Runtime>(
|
|||||||
..m
|
..m
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
for m in folders {
|
for m in folders {
|
||||||
// Recurse down
|
// Recurse down
|
||||||
@@ -1217,7 +1229,7 @@ pub async fn duplicate_folder<R: Runtime>(
|
|||||||
..m
|
..m
|
||||||
},
|
},
|
||||||
))
|
))
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1376,7 +1388,7 @@ pub async fn create_default_http_response<R: Runtime>(
|
|||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
|||||||
const recentWorkspaces = useRecentWorkspaces();
|
const recentWorkspaces = useRecentWorkspaces();
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const recentRequests = useRecentRequests();
|
const [recentRequests] = useRecentRequests();
|
||||||
const openWorkspace = useOpenWorkspace();
|
const openWorkspace = useOpenWorkspace();
|
||||||
const createWorkspace = useCreateWorkspace();
|
const createWorkspace = useCreateWorkspace();
|
||||||
const createHttpRequest = useCreateHttpRequest();
|
const createHttpRequest = useCreateHttpRequest();
|
||||||
@@ -78,6 +78,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
|||||||
const [, setSidebarHidden] = useSidebarHidden();
|
const [, setSidebarHidden] = useSidebarHidden();
|
||||||
const openSettings = useOpenSettings();
|
const openSettings = useOpenSettings();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const { baseEnvironment } = useEnvironments();
|
||||||
|
|
||||||
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
|
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
|
||||||
const commands: CommandPaletteItem[] = [
|
const commands: CommandPaletteItem[] = [
|
||||||
@@ -131,7 +132,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
|||||||
{
|
{
|
||||||
key: 'environment.create',
|
key: 'environment.create',
|
||||||
label: 'Create Environment',
|
label: 'Create Environment',
|
||||||
onSelect: createEnvironment.mutate,
|
onSelect: () => createEnvironment.mutate(baseEnvironment),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'sidebar.toggle',
|
key: 'sidebar.toggle',
|
||||||
@@ -180,7 +181,8 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
|||||||
activeCookieJar?.id,
|
activeCookieJar?.id,
|
||||||
activeEnvironment,
|
activeEnvironment,
|
||||||
activeRequest,
|
activeRequest,
|
||||||
createEnvironment.mutate,
|
baseEnvironment,
|
||||||
|
createEnvironment,
|
||||||
createGrpcRequest,
|
createGrpcRequest,
|
||||||
createHttpRequest,
|
createHttpRequest,
|
||||||
createWorkspace.mutate,
|
createWorkspace.mutate,
|
||||||
@@ -375,6 +377,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
|
|||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: KeyboardEvent<HTMLInputElement>) => {
|
(e: KeyboardEvent<HTMLInputElement>) => {
|
||||||
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
|
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
|
||||||
|
console.log("ENDER", e.key);
|
||||||
|
|
||||||
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
|
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
|
||||||
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
|
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { emit } from '@tauri-apps/api/event';
|
import { emit } from '@tauri-apps/api/event';
|
||||||
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
|
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugin';
|
||||||
import { useEnsureActiveCookieJar } from '../hooks/useActiveCookieJar';
|
import { useEnsureActiveCookieJar, useSubscribeActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||||
|
import { useSubscribeActiveEnvironmentId } from '../hooks/useActiveEnvironment';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import {useSubscribeActiveRequestId} from "../hooks/useActiveRequestId";
|
import { useSubscribeActiveRequestId } from '../hooks/useActiveRequestId';
|
||||||
import {useSubscribeActiveWorkspaceId} from "../hooks/useActiveWorkspace";
|
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||||
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
|
||||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||||
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
||||||
@@ -14,13 +15,14 @@ import { useNotificationToast } from '../hooks/useNotificationToast';
|
|||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
|
import { useRecentCookieJars } from '../hooks/useRecentCookieJars';
|
||||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
import { useSubscribeRecentRequests } from '../hooks/useRecentRequests';
|
||||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||||
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
|
import { useSyncFontSizeSetting } from '../hooks/useSyncFontSizeSetting';
|
||||||
import { useSyncModelStores } from '../hooks/useSyncModelStores';
|
import { useSyncModelStores } from '../hooks/useSyncModelStores';
|
||||||
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
|
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
|
||||||
import {useSyncWorkspaceRequestTitle} from "../hooks/useSyncWorkspaceRequestTitle";
|
import { useSyncWorkspaceRequestTitle } from '../hooks/useSyncWorkspaceRequestTitle';
|
||||||
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
|
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
|
||||||
|
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
|
||||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||||
|
|
||||||
export function GlobalHooks() {
|
export function GlobalHooks() {
|
||||||
@@ -36,8 +38,11 @@ export function GlobalHooks() {
|
|||||||
useRecentWorkspaces();
|
useRecentWorkspaces();
|
||||||
useRecentEnvironments();
|
useRecentEnvironments();
|
||||||
useRecentCookieJars();
|
useRecentCookieJars();
|
||||||
useRecentRequests();
|
useSubscribeRecentRequests();
|
||||||
useSyncWorkspaceChildModels();
|
useSyncWorkspaceChildModels();
|
||||||
|
useSubscribeTemplateFunctions();
|
||||||
|
useSubscribeActiveEnvironmentId();
|
||||||
|
useSubscribeActiveCookieJar();
|
||||||
|
|
||||||
// Other useful things
|
// Other useful things
|
||||||
useNotificationToast();
|
useNotificationToast();
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
|||||||
const dropdownRef = useRef<DropdownRef>(null);
|
const dropdownRef = useRef<DropdownRef>(null);
|
||||||
const activeRequest = useActiveRequest();
|
const activeRequest = useActiveRequest();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const allRecentRequestIds = useRecentRequests();
|
const [allRecentRequestIds] = useRecentRequests();
|
||||||
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|||||||
@@ -22,18 +22,19 @@ export function RedirectToLatestWorkspace() {
|
|||||||
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
|
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? null;
|
||||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
|
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? null;
|
||||||
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
|
const requestId = (await getRecentRequests(workspaceId))[0] ?? null;
|
||||||
|
const search = { cookie_jar_id: cookieJarId, environment_id: environmentId };
|
||||||
|
|
||||||
if (workspaceId != null && requestId != null) {
|
if (workspaceId != null && requestId != null) {
|
||||||
await navigate({
|
await navigate({
|
||||||
to: '/workspaces/$workspaceId/requests/$requestId',
|
to: '/workspaces/$workspaceId/requests/$requestId',
|
||||||
params: { workspaceId, requestId },
|
params: { workspaceId, requestId },
|
||||||
search: { cookieJarId, environmentId },
|
search,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await navigate({
|
await navigate({
|
||||||
to: '/workspaces/$workspaceId',
|
to: '/workspaces/$workspaceId',
|
||||||
params: { workspaceId },
|
params: { workspaceId },
|
||||||
search: { cookieJarId, environmentId },
|
search,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -140,8 +140,28 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
value: activeRequest.bodyType,
|
value: activeRequest.bodyType,
|
||||||
items: [
|
items: [
|
||||||
{ type: 'separator', label: 'Form Data' },
|
{ type: 'separator', label: 'Form Data' },
|
||||||
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
{
|
||||||
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
label: (
|
||||||
|
<>
|
||||||
|
Url Encoded
|
||||||
|
<CountBadge
|
||||||
|
count={'form' in activeRequest.body && activeRequest.body.form.length}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
value: BODY_TYPE_FORM_URLENCODED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<>
|
||||||
|
Url Encoded
|
||||||
|
<CountBadge
|
||||||
|
count={'form' in activeRequest.body && activeRequest.body.form.length}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
value: BODY_TYPE_FORM_MULTIPART,
|
||||||
|
},
|
||||||
{ type: 'separator', label: 'Text Content' },
|
{ type: 'separator', label: 'Text Content' },
|
||||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||||
@@ -252,6 +272,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
[
|
[
|
||||||
activeRequest.authentication,
|
activeRequest.authentication,
|
||||||
activeRequest.authenticationType,
|
activeRequest.authenticationType,
|
||||||
|
activeRequest.body,
|
||||||
activeRequest.bodyType,
|
activeRequest.bodyType,
|
||||||
activeRequest.description,
|
activeRequest.description,
|
||||||
activeRequest.headers,
|
activeRequest.headers,
|
||||||
|
|||||||
@@ -1,27 +1,33 @@
|
|||||||
import { useNavigate } from '@tanstack/react-router';
|
|
||||||
import { useRouteError } from 'react-router-dom';
|
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
import { FormattedError } from './core/FormattedError';
|
import { FormattedError } from './core/FormattedError';
|
||||||
import { Heading } from './core/Heading';
|
import { Heading } from './core/Heading';
|
||||||
import { VStack } from './core/Stacks';
|
import { VStack } from './core/Stacks';
|
||||||
|
|
||||||
export default function RouteError() {
|
export default function RouteError({ error }: { error: unknown; reset: () => void }) {
|
||||||
const navigate = useNavigate();
|
|
||||||
const error = useRouteError();
|
|
||||||
console.log('Error', error);
|
console.log('Error', error);
|
||||||
const stringified = JSON.stringify(error);
|
const stringified = JSON.stringify(error);
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const message = (error as any).message ?? stringified;
|
const message = (error as any).message ?? stringified;
|
||||||
|
const stack =
|
||||||
|
typeof error === 'object' && error != null && 'stack' in error ? String(error.stack) : null;
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<VStack space={5} className="max-w-[50rem] !h-auto">
|
<VStack space={5} className="w-[50rem] !h-auto">
|
||||||
<Heading>Route Error 🔥</Heading>
|
<Heading>Route Error 🔥</Heading>
|
||||||
<FormattedError>{message}</FormattedError>
|
<FormattedError>
|
||||||
|
{message}
|
||||||
|
{stack && (
|
||||||
|
<details className="mt-3 select-autotext-xs">
|
||||||
|
<summary className="!cursor-default !select-none">Stack Trace</summary>
|
||||||
|
<div className="mt-2 text-xs">{stack}</div>
|
||||||
|
</details>
|
||||||
|
)}
|
||||||
|
</FormattedError>
|
||||||
<VStack space={2}>
|
<VStack space={2}>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await navigate({ to: '/workspaces' });
|
window.location.assign('/');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Go Home
|
Go Home
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import { useFolders } from '../hooks/useFolders';
|
|||||||
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
import { useGrpcConnections } from '../hooks/useGrpcConnections';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
import { useHttpResponses } from '../hooks/useHttpResponses';
|
import { useHttpResponses } from '../hooks/useHttpResponses';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import { useRequests } from '../hooks/useRequests';
|
import { useRequests } from '../hooks/useRequests';
|
||||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||||
|
import { getSidebarCollapsedMap } from '../hooks/useSidebarItemCollapsed';
|
||||||
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
||||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
@@ -50,13 +50,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
|
const [hoveredTree, setHoveredTree] = useState<SidebarTreeNode | null>(null);
|
||||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { value: collapsed, set: setCollapsed } = useKeyValue<Record<string, boolean>>({
|
|
||||||
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
|
||||||
fallback: {},
|
|
||||||
namespace: 'no_sync',
|
|
||||||
});
|
|
||||||
|
|
||||||
const isCollapsed = useCallback((id: string) => collapsed?.[id] ?? false, [collapsed]);
|
|
||||||
|
|
||||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||||
tree: SidebarTreeNode | null;
|
tree: SidebarTreeNode | null;
|
||||||
@@ -97,7 +90,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
const childItems = childrenMap[node.item.id] ?? [];
|
const childItems = childrenMap[node.item.id] ?? [];
|
||||||
|
|
||||||
// Recurse to children
|
// Recurse to children
|
||||||
const isCollapsed = collapsed?.[node.item.id];
|
|
||||||
const depth = node.depth + 1;
|
const depth = node.depth + 1;
|
||||||
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
|
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
|
||||||
for (const item of childItems) {
|
for (const item of childItems) {
|
||||||
@@ -105,7 +97,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
// Add to children
|
// Add to children
|
||||||
node.children.push(next({ item, children: [], depth }));
|
node.children.push(next({ item, children: [], depth }));
|
||||||
// Add to selectable requests
|
// Add to selectable requests
|
||||||
if (item.model !== 'folder' && !isCollapsed) {
|
if (item.model !== 'folder') {
|
||||||
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
|
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +108,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
||||||
|
|
||||||
return { tree, treeParentMap, selectableRequests, selectedRequest };
|
return { tree, treeParentMap, selectableRequests, selectedRequest };
|
||||||
}, [activeWorkspace, requests, folders, collapsed]);
|
}, [activeWorkspace, requests, folders]);
|
||||||
|
|
||||||
const focusActiveRequest = useCallback(
|
const focusActiveRequest = useCallback(
|
||||||
(
|
(
|
||||||
@@ -160,9 +152,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
|
|
||||||
const { item } = node;
|
const { item } = node;
|
||||||
|
|
||||||
if (item.model === 'folder') {
|
if (item.model === 'http_request' || item.model === 'grpc_request') {
|
||||||
await setCollapsed((c) => ({ ...c, [item.id]: !c[item.id] }));
|
|
||||||
} else {
|
|
||||||
await navigate({
|
await navigate({
|
||||||
to: '/workspaces/$workspaceId/requests/$requestId',
|
to: '/workspaces/$workspaceId/requests/$requestId',
|
||||||
params: {
|
params: {
|
||||||
@@ -177,7 +167,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
setSelectedTree(tree);
|
setSelectedTree(tree);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[treeParentMap, setCollapsed, navigate, setSelectedId],
|
[treeParentMap, navigate, setSelectedId],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleClearSelected = useCallback(() => {
|
const handleClearSelected = useCallback(() => {
|
||||||
@@ -267,13 +257,14 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const handleMove = useCallback<SidebarItemProps['onMove']>(
|
const handleMove = useCallback<SidebarItemProps['onMove']>(
|
||||||
(id, side) => {
|
async (id, side) => {
|
||||||
let hoveredTree = treeParentMap[id] ?? null;
|
let hoveredTree = treeParentMap[id] ?? null;
|
||||||
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
|
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
|
||||||
const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null;
|
const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null;
|
||||||
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||||
|
|
||||||
if (hoveredItem?.model === 'folder' && side === 'below' && !isCollapsed(hoveredItem.id)) {
|
const isHoveredItemCollapsed = hoveredItem != null ? getSidebarCollapsedMap()[hoveredItem.id] : false;
|
||||||
|
if (hoveredItem?.model === 'folder' && side === 'below' && !isHoveredItemCollapsed) {
|
||||||
// Move into the folder if it's open and we're moving below it
|
// Move into the folder if it's open and we're moving below it
|
||||||
hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null;
|
hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null;
|
||||||
hoveredIndex = 0;
|
hoveredIndex = 0;
|
||||||
@@ -282,7 +273,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
setHoveredTree(hoveredTree);
|
setHoveredTree(hoveredTree);
|
||||||
setHoveredIndex(hoveredIndex);
|
setHoveredIndex(hoveredIndex);
|
||||||
},
|
},
|
||||||
[isCollapsed, treeParentMap],
|
[treeParentMap],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleDragStart = useCallback<SidebarItemProps['onDragStart']>((id: string) => {
|
const handleDragStart = useCallback<SidebarItemProps['onDragStart']>((id: string) => {
|
||||||
@@ -385,7 +376,7 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
const mainContextMenuItems = useCreateDropdownItems();
|
const mainContextMenuItems = useCreateDropdownItems();
|
||||||
|
|
||||||
// Not ready to render yet
|
// Not ready to render yet
|
||||||
if (tree == null || collapsed == null) {
|
if (tree == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -415,7 +406,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
|||||||
<SidebarItems
|
<SidebarItems
|
||||||
treeParentMap={treeParentMap}
|
treeParentMap={treeParentMap}
|
||||||
selectedTree={selectedTree}
|
selectedTree={selectedTree}
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
httpResponses={httpResponses}
|
httpResponses={httpResponses}
|
||||||
grpcConnections={grpcConnections}
|
grpcConnections={grpcConnections}
|
||||||
tree={tree}
|
tree={tree}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import type { XYCoord } from 'react-dnd';
|
|||||||
import { useDrag, useDrop } from 'react-dnd';
|
import { useDrag, useDrop } from 'react-dnd';
|
||||||
import { activeRequestAtom } from '../hooks/useActiveRequest';
|
import { activeRequestAtom } from '../hooks/useActiveRequest';
|
||||||
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
import { useScrollIntoView } from '../hooks/useScrollIntoView';
|
||||||
|
import { useSidebarItemCollapsed } from '../hooks/useSidebarItemCollapsed';
|
||||||
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
@@ -15,7 +16,7 @@ import { Icon } from './core/Icon';
|
|||||||
import { StatusTag } from './core/StatusTag';
|
import { StatusTag } from './core/StatusTag';
|
||||||
import { RequestContextMenu } from './RequestContextMenu';
|
import { RequestContextMenu } from './RequestContextMenu';
|
||||||
import type { SidebarTreeNode } from './Sidebar';
|
import type { SidebarTreeNode } from './Sidebar';
|
||||||
import {sidebarSelectedIdAtom} from "./SidebarAtoms";
|
import { sidebarSelectedIdAtom } from './SidebarAtoms';
|
||||||
import type { SidebarItemsProps } from './SidebarItems';
|
import type { SidebarItemsProps } from './SidebarItems';
|
||||||
|
|
||||||
enum ItemTypes {
|
enum ItemTypes {
|
||||||
@@ -35,7 +36,7 @@ export type SidebarItemProps = {
|
|||||||
child: SidebarTreeNode;
|
child: SidebarTreeNode;
|
||||||
latestHttpResponse: HttpResponse | null;
|
latestHttpResponse: HttpResponse | null;
|
||||||
latestGrpcConnection: GrpcConnection | null;
|
latestGrpcConnection: GrpcConnection | null;
|
||||||
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>;
|
} & Pick<SidebarItemsProps, 'onSelect'>;
|
||||||
|
|
||||||
type DragItem = {
|
type DragItem = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -51,7 +52,6 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
onEnd,
|
onEnd,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
onSelect,
|
onSelect,
|
||||||
isCollapsed,
|
|
||||||
className,
|
className,
|
||||||
itemFallbackName,
|
itemFallbackName,
|
||||||
latestHttpResponse,
|
latestHttpResponse,
|
||||||
@@ -59,6 +59,7 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
children,
|
children,
|
||||||
}: SidebarItemProps) {
|
}: SidebarItemProps) {
|
||||||
const ref = useRef<HTMLLIElement>(null);
|
const ref = useRef<HTMLLIElement>(null);
|
||||||
|
const [collapsed, toggleCollapsed] = useSidebarItemCollapsed(itemId);
|
||||||
|
|
||||||
const [, connectDrop] = useDrop<DragItem, void>(
|
const [, connectDrop] = useDrop<DragItem, void>(
|
||||||
{
|
{
|
||||||
@@ -106,20 +107,21 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
const [selected, setSelected] = useState<boolean>(
|
const [selected, setSelected] = useState<boolean>(
|
||||||
jotaiStore.get(sidebarSelectedIdAtom) == itemId,
|
jotaiStore.get(sidebarSelectedIdAtom) == itemId,
|
||||||
);
|
);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
jotaiStore.sub(sidebarSelectedIdAtom, () => {
|
return jotaiStore.sub(sidebarSelectedIdAtom, () => {
|
||||||
const value = jotaiStore.get(sidebarSelectedIdAtom);
|
const value = jotaiStore.get(sidebarSelectedIdAtom);
|
||||||
setSelected(value === itemId);
|
setSelected(value === itemId);
|
||||||
});
|
});
|
||||||
}, [itemId]);
|
}, [itemId]);
|
||||||
|
|
||||||
const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId);
|
const [active, setActive] = useState<boolean>(jotaiStore.get(activeRequestAtom)?.id === itemId);
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
jotaiStore.sub(activeRequestAtom, () => {
|
() =>
|
||||||
const value = jotaiStore.get(activeRequestAtom);
|
jotaiStore.sub(activeRequestAtom, () =>
|
||||||
setActive(value?.id === itemId);
|
setActive(jotaiStore.get(activeRequestAtom)?.id === itemId),
|
||||||
});
|
),
|
||||||
}, [itemId]);
|
[itemId],
|
||||||
|
);
|
||||||
|
|
||||||
useScrollIntoView(ref.current, active);
|
useScrollIntoView(ref.current, active);
|
||||||
|
|
||||||
@@ -175,7 +177,10 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
[handleSubmitNameEdit],
|
[handleSubmitNameEdit],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
|
const handleSelect = useCallback(async () => {
|
||||||
|
if (itemModel === 'folder') toggleCollapsed();
|
||||||
|
else onSelect(itemId);
|
||||||
|
}, [itemModel, toggleCollapsed, onSelect, itemId]);
|
||||||
const [showContextMenu, setShowContextMenu] = useState<{
|
const [showContextMenu, setShowContextMenu] = useState<{
|
||||||
x: number;
|
x: number;
|
||||||
y: number;
|
y: number;
|
||||||
@@ -214,7 +219,7 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
editing && 'ring-1 focus-within:ring-focus',
|
editing && 'ring-1 focus-within:ring-focus',
|
||||||
active && 'bg-surface-highlight text-text',
|
active && 'bg-surface-highlight text-text',
|
||||||
!active && 'text-text-subtle group-hover/item:text-text',
|
!active && 'text-text-subtle group-hover/item:text-text',
|
||||||
showContextMenu && '!text-text', // Show as "active" when context menu is open
|
showContextMenu && '!text-text', // Show as "active" when the context menu is open
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{itemModel === 'folder' && (
|
{itemModel === 'folder' && (
|
||||||
@@ -224,7 +229,7 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
'text-text-subtlest',
|
'text-text-subtlest',
|
||||||
'transition-transform',
|
'transition-transform',
|
||||||
!isCollapsed(itemId) && 'transform rotate-90',
|
!collapsed && 'transform rotate-90',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -259,7 +264,7 @@ export const SidebarItem = memo(function SidebarItem({
|
|||||||
) : null}
|
) : null}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{children}
|
{collapsed ? null : children}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export interface SidebarItemsProps {
|
|||||||
handleEnd: (id: string) => void;
|
handleEnd: (id: string) => void;
|
||||||
handleDragStart: (id: string) => void;
|
handleDragStart: (id: string) => void;
|
||||||
onSelect: (requestId: string) => void;
|
onSelect: (requestId: string) => void;
|
||||||
isCollapsed: (id: string) => boolean;
|
|
||||||
httpResponses: HttpResponse[];
|
httpResponses: HttpResponse[];
|
||||||
grpcConnections: GrpcConnection[];
|
grpcConnections: GrpcConnection[];
|
||||||
}
|
}
|
||||||
@@ -29,7 +28,6 @@ export const SidebarItems = memo(function SidebarItems({
|
|||||||
draggingId,
|
draggingId,
|
||||||
onSelect,
|
onSelect,
|
||||||
treeParentMap,
|
treeParentMap,
|
||||||
isCollapsed,
|
|
||||||
hoveredTree,
|
hoveredTree,
|
||||||
hoveredIndex,
|
hoveredIndex,
|
||||||
handleEnd,
|
handleEnd,
|
||||||
@@ -71,11 +69,9 @@ export const SidebarItems = memo(function SidebarItems({
|
|||||||
onEnd={handleEnd}
|
onEnd={handleEnd}
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
child={child}
|
child={child}
|
||||||
>
|
>
|
||||||
{child.item.model === 'folder' &&
|
{child.item.model === 'folder' &&
|
||||||
!isCollapsed(child.item.id) &&
|
|
||||||
draggingId !== child.item.id && (
|
draggingId !== child.item.id && (
|
||||||
<SidebarItems
|
<SidebarItems
|
||||||
draggingId={draggingId}
|
draggingId={draggingId}
|
||||||
@@ -86,7 +82,6 @@ export const SidebarItems = memo(function SidebarItems({
|
|||||||
hoveredTree={hoveredTree}
|
hoveredTree={hoveredTree}
|
||||||
httpResponses={httpResponses}
|
httpResponses={httpResponses}
|
||||||
grpcConnections={grpcConnections}
|
grpcConnections={grpcConnections}
|
||||||
isCollapsed={isCollapsed}
|
|
||||||
onSelect={onSelect}
|
onSelect={onSelect}
|
||||||
selectedTree={selectedTree}
|
selectedTree={selectedTree}
|
||||||
tree={child}
|
tree={child}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
|
import { useActiveEnvironmentVariables } from '../../../hooks/useActiveEnvironmentVariables';
|
||||||
import {useDialog} from "../../../hooks/useDialog";
|
import { useDialog } from '../../../hooks/useDialog';
|
||||||
import { parseTemplate } from '../../../hooks/useParseTemplate';
|
import { parseTemplate } from '../../../hooks/useParseTemplate';
|
||||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||||
import { useSettings } from '../../../hooks/useSettings';
|
import { useSettings } from '../../../hooks/useSettings';
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ export function FormattedError({ children }: Props) {
|
|||||||
return (
|
return (
|
||||||
<pre
|
<pre
|
||||||
className={classNames(
|
className={classNames(
|
||||||
'font-mono text-sm w-full select-auto cursor-text bg-surface-highlight p-3 rounded',
|
'cursor-text select-auto',
|
||||||
|
'[&_*]:cursor-text [&_*]:select-auto',
|
||||||
|
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
|
||||||
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
|
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -27,16 +27,13 @@ export function PlainInput({
|
|||||||
onChange,
|
onChange,
|
||||||
onFocus,
|
onFocus,
|
||||||
onPaste,
|
onPaste,
|
||||||
placeholder,
|
|
||||||
require,
|
require,
|
||||||
rightSlot,
|
rightSlot,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
type = 'text',
|
type = 'text',
|
||||||
validate,
|
validate,
|
||||||
autoSelect,
|
autoSelect,
|
||||||
step,
|
...props
|
||||||
autoFocus,
|
|
||||||
readOnly,
|
|
||||||
}: 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 ?? '');
|
||||||
@@ -130,7 +127,6 @@ export function PlainInput({
|
|||||||
id={id}
|
id={id}
|
||||||
type={type === 'password' && !obscured ? 'text' : type}
|
type={type === 'password' && !obscured ? 'text' : type}
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
placeholder={placeholder}
|
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
autoCapitalize="off"
|
autoCapitalize="off"
|
||||||
autoCorrect="off"
|
autoCorrect="off"
|
||||||
@@ -139,9 +135,7 @@ export function PlainInput({
|
|||||||
className={classNames(commonClassName, 'h-auto')}
|
className={classNames(commonClassName, 'h-auto')}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
autoFocus={autoFocus}
|
{...props}
|
||||||
step={step}
|
|
||||||
readOnly={readOnly}
|
|
||||||
/>
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
{type === 'password' && (
|
{type === 'password' && (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Icon } from './Icon';
|
|||||||
export type RadioDropdownItem<T = string | null> =
|
export type RadioDropdownItem<T = string | null> =
|
||||||
| {
|
| {
|
||||||
type?: 'default';
|
type?: 'default';
|
||||||
label: string;
|
label: ReactNode;
|
||||||
shortLabel?: string;
|
shortLabel?: string;
|
||||||
value: T;
|
value: T;
|
||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
|
|||||||
@@ -1,18 +1,58 @@
|
|||||||
import { useNavigate, useSearch } from '@tanstack/react-router';
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
||||||
import { useCallback, useEffect, useMemo } from 'react';
|
import type { CookieJar } from '@yaakapp-internal/models';
|
||||||
import { useCookieJars } from './useCookieJars';
|
import { atom, useAtomValue } from 'jotai/index';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
|
import { cookieJarsAtom, useCookieJars } from './useCookieJars';
|
||||||
|
|
||||||
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
|
export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id';
|
||||||
|
|
||||||
|
export const activeCookieJarIdAtom = atom<string>();
|
||||||
|
|
||||||
|
export const activeCookieJarAtom = atom<CookieJar | null>((get) => {
|
||||||
|
const activeId = get(activeCookieJarIdAtom);
|
||||||
|
return get(cookieJarsAtom)?.find((e) => e.id === activeId) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
export function useActiveCookieJar() {
|
export function useActiveCookieJar() {
|
||||||
const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId();
|
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
|
||||||
const cookieJars = useCookieJars();
|
const setId = useCallback(
|
||||||
|
(id: string) =>
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({ ...prev, cookie_jar_id: id }),
|
||||||
|
}),
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
const cookieJar = useAtomValue(activeCookieJarAtom);
|
||||||
|
return [cookieJar, setId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
const activeCookieJar = useMemo(() => {
|
function useActiveCookieJarId() {
|
||||||
return cookieJars?.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null;
|
// NOTE: This query param is accessed from Rust side, so do not change
|
||||||
}, [activeCookieJarId, cookieJars]);
|
const { cookie_jar_id: id } = useSearch({ strict: false });
|
||||||
|
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
|
||||||
|
|
||||||
return [activeCookieJar ?? null, setActiveCookieJarId] as const;
|
const setId = useCallback(
|
||||||
|
(id: string) =>
|
||||||
|
navigate({
|
||||||
|
search: (prev) => ({ ...prev, cookie_jar_id: id }),
|
||||||
|
}),
|
||||||
|
[navigate],
|
||||||
|
);
|
||||||
|
|
||||||
|
return [id, setId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscribeActiveCookieJar() {
|
||||||
|
const { cookie_jar_id } = useSearch({ strict: false });
|
||||||
|
useEffect(
|
||||||
|
() => jotaiStore.set(activeCookieJarIdAtom, cookie_jar_id ?? undefined),
|
||||||
|
[cookie_jar_id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveCookieJar() {
|
||||||
|
return jotaiStore.get(activeCookieJarAtom);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useEnsureActiveCookieJar() {
|
export function useEnsureActiveCookieJar() {
|
||||||
@@ -37,19 +77,3 @@ export function useEnsureActiveCookieJar() {
|
|||||||
setActiveCookieJarId(firstJar.id).catch(console.error);
|
setActiveCookieJarId(firstJar.id).catch(console.error);
|
||||||
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
|
}, [activeCookieJarId, cookieJars, setActiveCookieJarId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function useActiveCookieJarId() {
|
|
||||||
// NOTE: This query param is accessed from Rust side, so do not change
|
|
||||||
const { cookie_jar_id: id } = useSearch({ strict: false });
|
|
||||||
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
|
|
||||||
|
|
||||||
const setId = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
navigate({
|
|
||||||
search: (prev) => ({ ...prev, cookie_jar_id: id }),
|
|
||||||
}),
|
|
||||||
[navigate],
|
|
||||||
);
|
|
||||||
|
|
||||||
return [id, setId] as const;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,28 +1,41 @@
|
|||||||
import { useNavigate, useSearch } from '@tanstack/react-router';
|
import { useNavigate, useSearch } from '@tanstack/react-router';
|
||||||
import { useCallback } from 'react';
|
import type { Environment } from '@yaakapp-internal/models';
|
||||||
import { useEnvironments } from './useEnvironments';
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { atom } from 'jotai/index';
|
||||||
export function useActiveEnvironment() {
|
import { useCallback, useEffect } from 'react';
|
||||||
const [id, setId] = useActiveEnvironmentId();
|
import { jotaiStore } from '../lib/jotai';
|
||||||
const { subEnvironments } = useEnvironments();
|
import { environmentsAtom } from './useEnvironments';
|
||||||
const environment = subEnvironments.find((w) => w.id === id) ?? null;
|
|
||||||
return [environment, setId] as const;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const QUERY_ENVIRONMENT_ID = 'environment_id';
|
export const QUERY_ENVIRONMENT_ID = 'environment_id';
|
||||||
|
|
||||||
function useActiveEnvironmentId() {
|
export const activeEnvironmentIdAtom = atom<string>();
|
||||||
// NOTE: This query param is accessed from Rust side, so do not change
|
|
||||||
const { environment_id: id} = useSearch({ strict: false });
|
|
||||||
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
|
|
||||||
|
|
||||||
|
export const activeEnvironmentAtom = atom<Environment | null>((get) => {
|
||||||
|
const activeEnvironmentId = get(activeEnvironmentIdAtom);
|
||||||
|
return get(environmentsAtom).find((e) => e.id === activeEnvironmentId) ?? null;
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useActiveEnvironment() {
|
||||||
|
const navigate = useNavigate({ from: '/workspaces/$workspaceId' });
|
||||||
const setId = useCallback(
|
const setId = useCallback(
|
||||||
(environmentId: string | null) =>
|
(id: string | null) =>
|
||||||
navigate({
|
navigate({
|
||||||
search: (prev) => ({ ...prev, environment_id: environmentId ?? undefined }),
|
search: (prev) => ({ ...prev, environment_id: id }),
|
||||||
}),
|
}),
|
||||||
[navigate],
|
[navigate],
|
||||||
);
|
);
|
||||||
|
const environment = useAtomValue(activeEnvironmentAtom);
|
||||||
return [id, setId] as const;
|
return [environment, setId] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getActiveEnvironment() {
|
||||||
|
return jotaiStore.get(activeEnvironmentAtom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscribeActiveEnvironmentId() {
|
||||||
|
const { environment_id } = useSearch({ strict: false });
|
||||||
|
useEffect(
|
||||||
|
() => jotaiStore.set(activeEnvironmentIdAtom, environment_id ?? undefined),
|
||||||
|
[environment_id],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useParams } from '@tanstack/react-router';
|
import { useParams } from '@tanstack/react-router';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import {jotaiStore} from "../lib/jotai";
|
import { jotaiStore } from '../lib/jotai';
|
||||||
|
|
||||||
export const activeRequestIdAtom = atom<string>();
|
export const activeRequestIdAtom = atom<string>();
|
||||||
|
|
||||||
@@ -11,7 +11,5 @@ export function useActiveRequestId(): string | null {
|
|||||||
|
|
||||||
export function useSubscribeActiveRequestId() {
|
export function useSubscribeActiveRequestId() {
|
||||||
const { requestId } = useParams({ strict: false });
|
const { requestId } = useParams({ strict: false });
|
||||||
useEffect(() => {
|
useEffect(() => jotaiStore.set(activeRequestIdAtom, requestId), [requestId]);
|
||||||
jotaiStore.set(activeRequestIdAtom, requestId);
|
|
||||||
}, [requestId]);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function useActiveWorkspaceId(): string | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getActiveWorkspaceId() {
|
export function getActiveWorkspaceId() {
|
||||||
return jotaiStore.get(activeWorkspaceIdAtom);
|
return jotaiStore.get(activeWorkspaceIdAtom) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSubscribeActiveWorkspaceId() {
|
export function useSubscribeActiveWorkspaceId() {
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ export function useCreateEnvironment() {
|
|||||||
const workspace = useActiveWorkspace();
|
const workspace = useActiveWorkspace();
|
||||||
const setEnvironments = useSetAtom(environmentsAtom);
|
const setEnvironments = useSetAtom(environmentsAtom);
|
||||||
|
|
||||||
return useFastMutation<Environment | null, unknown, Environment>({
|
return useFastMutation<Environment | null, unknown, Environment | null>({
|
||||||
|
toastyError: true,
|
||||||
mutationKey: ['create_environment'],
|
mutationKey: ['create_environment'],
|
||||||
mutationFn: async (baseEnvironment) => {
|
mutationFn: async (baseEnvironment) => {
|
||||||
|
if (baseEnvironment == null) {
|
||||||
|
throw new Error('No base environment passed');
|
||||||
|
}
|
||||||
|
|
||||||
const name = await prompt({
|
const name = await prompt({
|
||||||
id: 'new-environment',
|
id: 'new-environment',
|
||||||
title: 'New Environment',
|
title: 'New Environment',
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import { useFastMutation } from './useFastMutation';
|
|
||||||
import type { Folder } from '@yaakapp-internal/models';
|
import type { Folder } from '@yaakapp-internal/models';
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { getActiveWorkspaceId } from './useActiveWorkspace';
|
||||||
|
import { useFastMutation } from './useFastMutation';
|
||||||
import { foldersAtom } from './useFolders';
|
import { foldersAtom } from './useFolders';
|
||||||
import { usePrompt } from './usePrompt';
|
import { usePrompt } from './usePrompt';
|
||||||
import { updateModelList } from './useSyncModelStores';
|
import { updateModelList } from './useSyncModelStores';
|
||||||
|
|
||||||
export function useCreateFolder() {
|
export function useCreateFolder() {
|
||||||
const workspace = useActiveWorkspace();
|
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
const setFolders = useSetAtom(foldersAtom);
|
const setFolders = useSetAtom(foldersAtom);
|
||||||
|
|
||||||
@@ -20,8 +19,8 @@ export function useCreateFolder() {
|
|||||||
>({
|
>({
|
||||||
mutationKey: ['create_folder'],
|
mutationKey: ['create_folder'],
|
||||||
mutationFn: async (patch) => {
|
mutationFn: async (patch) => {
|
||||||
console.log("FOLDER", workspace);
|
const workspaceId = getActiveWorkspaceId();
|
||||||
if (workspace === null) {
|
if (workspaceId == null) {
|
||||||
throw new Error("Cannot create folder when there's no active workspace");
|
throw new Error("Cannot create folder when there's no active workspace");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +39,7 @@ export function useCreateFolder() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
patch.sortPriority = patch.sortPriority || -Date.now();
|
patch.sortPriority = patch.sortPriority || -Date.now();
|
||||||
return await invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch });
|
return await invokeCmd('cmd_create_folder', { workspaceId, ...patch });
|
||||||
},
|
},
|
||||||
onSuccess: (folder) => {
|
onSuccess: (folder) => {
|
||||||
if (folder == null) return;
|
if (folder == null) return;
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ export const environmentsAtom = atom<Environment[]>([]);
|
|||||||
|
|
||||||
export function useEnvironments() {
|
export function useEnvironments() {
|
||||||
const allEnvironments = useAtomValue(environmentsAtom);
|
const allEnvironments = useAtomValue(environmentsAtom);
|
||||||
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null);
|
const baseEnvironment = allEnvironments.find((e) => e.environmentId == null) ?? null;
|
||||||
const subEnvironments =
|
const subEnvironments =
|
||||||
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
|
allEnvironments.filter((e) => e.environmentId === (baseEnvironment?.id ?? 'n/a')) ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ export function useFastMutation<TData = unknown, TError = unknown, TVariables =
|
|||||||
if (toastyError) {
|
if (toastyError) {
|
||||||
toast.show({
|
toast.show({
|
||||||
id: 'error-' + mutationKey.join('.'),
|
id: 'error-' + mutationKey.join('.'),
|
||||||
|
color: 'danger',
|
||||||
|
timeout: 8000,
|
||||||
message: String(e),
|
message: String(e),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export function useImportData() {
|
|||||||
await navigate({
|
await navigate({
|
||||||
to: '/workspaces/$workspaceId',
|
to: '/workspaces/$workspaceId',
|
||||||
params: { workspaceId: importedWorkspace.id },
|
params: { workspaceId: importedWorkspace.id },
|
||||||
search: { environmentId },
|
search: { environment_id: environmentId },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation } from '@tanstack/react-query';
|
||||||
|
import type { KeyValue } from '@yaakapp-internal/models';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
|
import { atom } from 'jotai/index';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
|
import { buildKeyValueKey, extractKeyValueOrFallback, setKeyValue } from '../lib/keyValueStore';
|
||||||
|
|
||||||
const DEFAULT_NAMESPACE = 'global';
|
const DEFAULT_NAMESPACE = 'global';
|
||||||
|
|
||||||
|
export const keyValuesAtom = atom<KeyValue[]>([]);
|
||||||
|
|
||||||
export function keyValueQueryKey({
|
export function keyValueQueryKey({
|
||||||
namespace = DEFAULT_NAMESPACE,
|
namespace = DEFAULT_NAMESPACE,
|
||||||
key,
|
key,
|
||||||
@@ -23,44 +29,60 @@ export function useKeyValue<T extends object | boolean | number | string | null>
|
|||||||
key: string | string[];
|
key: string | string[];
|
||||||
fallback: T;
|
fallback: T;
|
||||||
}) {
|
}) {
|
||||||
const query = useQuery<T>({
|
const keyValues = useAtomValue(keyValuesAtom);
|
||||||
queryKey: keyValueQueryKey({ namespace, key }),
|
const keyValue =
|
||||||
queryFn: async () => getKeyValue({ namespace, key, fallback }),
|
keyValues?.find((kv) => buildKeyValueKey(kv.key) === buildKeyValueKey(key)) ?? null;
|
||||||
refetchOnWindowFocus: false,
|
const value = extractKeyValueOrFallback(keyValue, fallback);
|
||||||
});
|
const isLoading = keyValues == null;
|
||||||
|
|
||||||
const mutate = useMutation<void, unknown, T>({
|
const { mutateAsync } = useMutation<void, unknown, T>({
|
||||||
mutationKey: ['set_key_value', namespace, key],
|
mutationKey: ['set_key_value', namespace, key],
|
||||||
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
|
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const set = useCallback(
|
const set = useCallback(
|
||||||
async (value: ((v: T) => T) | T) => {
|
async (valueOrUpdate: ((v: T) => T) | T) => {
|
||||||
if (typeof value === 'function') {
|
if (typeof valueOrUpdate === 'function') {
|
||||||
await getKeyValue({ namespace, key, fallback }).then((kv) => {
|
const newV = valueOrUpdate(value);
|
||||||
const newV = value(kv);
|
if (newV === value) return;
|
||||||
if (newV === kv) return;
|
await mutateAsync(newV);
|
||||||
return mutate.mutateAsync(newV);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
// TODO: Make this only update if the value is different. I tried this but it seems query.data
|
// TODO: Make this only update if the value is different. I tried this but it seems query.data
|
||||||
// is stale.
|
// is stale.
|
||||||
await mutate.mutateAsync(value);
|
await mutateAsync(valueOrUpdate);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
[typeof key === 'string' ? key : key.join('::'), namespace],
|
[typeof key === 'string' ? key : key.join('::'), namespace, value],
|
||||||
);
|
);
|
||||||
|
|
||||||
const reset = useCallback(async () => mutate.mutateAsync(fallback), [mutate, fallback]);
|
const reset = useCallback(async () => mutateAsync(fallback), [fallback, mutateAsync]);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
value: query.data,
|
value,
|
||||||
isLoading: query.isLoading,
|
isLoading,
|
||||||
set,
|
set,
|
||||||
reset,
|
reset,
|
||||||
}),
|
}),
|
||||||
[query.data, query.isLoading, reset, set],
|
[isLoading, reset, set, value],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getKeyValue<T extends object | boolean | number | string | null>({
|
||||||
|
namespace,
|
||||||
|
key,
|
||||||
|
fallback,
|
||||||
|
}: {
|
||||||
|
namespace?: 'global' | 'no_sync' | 'license';
|
||||||
|
key: string | string[];
|
||||||
|
fallback: T;
|
||||||
|
}) {
|
||||||
|
const keyValues = jotaiStore.get(keyValuesAtom);
|
||||||
|
const keyValue =
|
||||||
|
keyValues?.find(
|
||||||
|
(kv) => kv.namespace === namespace && buildKeyValueKey(kv.key) === buildKeyValueKey(key),
|
||||||
|
) ?? null;
|
||||||
|
const value = extractKeyValueOrFallback(keyValue, fallback);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export function useOpenWorkspace() {
|
|||||||
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
|
const environmentId = (await getRecentEnvironments(workspaceId))[0] ?? undefined;
|
||||||
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
|
const requestId = (await getRecentRequests(workspaceId))[0] ?? undefined;
|
||||||
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined;
|
const cookieJarId = (await getRecentCookieJars(workspaceId))[0] ?? undefined;
|
||||||
const search = { environmentId, cookieJarId };
|
const search = { environment_id: environmentId, cookie_jar_id: cookieJarId };
|
||||||
|
|
||||||
if (inNewWindow) {
|
if (inNewWindow) {
|
||||||
const location = router.buildLocation({
|
const location = router.buildLocation({
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useEffect, useMemo } from 'react';
|
import { useEffect, useMemo } from 'react';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { getKeyValue } from '../lib/keyValueStore';
|
import { getKeyValue } from '../lib/keyValueStore';
|
||||||
import { useActiveRequestId } from './useActiveRequestId';
|
import { activeRequestIdAtom } from './useActiveRequestId';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
import { useKeyValue } from './useKeyValue';
|
import { useKeyValue } from './useKeyValue';
|
||||||
import { useRequests } from './useRequests';
|
import { useRequests } from './useRequests';
|
||||||
@@ -12,30 +13,38 @@ const fallback: string[] = [];
|
|||||||
export function useRecentRequests() {
|
export function useRecentRequests() {
|
||||||
const requests = useRequests();
|
const requests = useRequests();
|
||||||
const activeWorkspace = useActiveWorkspace();
|
const activeWorkspace = useActiveWorkspace();
|
||||||
const activeRequestId = useActiveRequestId();
|
|
||||||
|
|
||||||
const kv = useKeyValue<string[]>({
|
const { set: setRecentRequests, value: recentRequests } = useKeyValue<string[]>({
|
||||||
key: kvKey(activeWorkspace?.id ?? 'n/a'),
|
key: kvKey(activeWorkspace?.id ?? 'n/a'),
|
||||||
namespace,
|
namespace,
|
||||||
fallback,
|
fallback,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set history when active request changes
|
|
||||||
useEffect(() => {
|
|
||||||
kv.set((currentHistory) => {
|
|
||||||
if (activeRequestId === null) return currentHistory;
|
|
||||||
const withoutCurrentRequest = currentHistory.filter((id) => id !== activeRequestId);
|
|
||||||
return [activeRequestId, ...withoutCurrentRequest];
|
|
||||||
}).catch(console.error);
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [activeRequestId]);
|
|
||||||
|
|
||||||
const onlyValidIds = useMemo(
|
const onlyValidIds = useMemo(
|
||||||
() => kv.value?.filter((id) => requests.some((r) => r.id === id)) ?? [],
|
() => recentRequests?.filter((id) => requests.some((r) => r.id === id)) ?? [],
|
||||||
[kv.value, requests],
|
[recentRequests, requests],
|
||||||
);
|
);
|
||||||
|
|
||||||
return onlyValidIds;
|
return [onlyValidIds, setRecentRequests] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscribeRecentRequests() {
|
||||||
|
const [recentRequests, setRecentRequests] = useRecentRequests();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return jotaiStore.sub(activeRequestIdAtom, () => {
|
||||||
|
const activeRequestId = jotaiStore.get(activeRequestIdAtom) ?? null;
|
||||||
|
if (recentRequests[0] === activeRequestId) {
|
||||||
|
// Nothing to do
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRecentRequests((currentHistory) => {
|
||||||
|
if (activeRequestId === null) return currentHistory;
|
||||||
|
const withoutCurrentRequest = currentHistory.filter((id) => id !== activeRequestId);
|
||||||
|
return [activeRequestId, ...withoutCurrentRequest];
|
||||||
|
}).catch(console.error);
|
||||||
|
});
|
||||||
|
}, [recentRequests, setRecentRequests]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRecentRequests(workspaceId: string) {
|
export async function getRecentRequests(workspaceId: string) {
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { useFastMutation } from './useFastMutation';
|
|
||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
import { trackEvent } from '../lib/analytics';
|
import { trackEvent } from '../lib/analytics';
|
||||||
import { getHttpRequest } from '../lib/store';
|
import { getHttpRequest } from '../lib/store';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveCookieJar } from './useActiveCookieJar';
|
import { getActiveCookieJar } from './useActiveCookieJar';
|
||||||
import { useActiveEnvironment } from './useActiveEnvironment';
|
import { getActiveEnvironment } from './useActiveEnvironment';
|
||||||
import { useAlert } from './useAlert';
|
import { useAlert } from './useAlert';
|
||||||
|
import { useFastMutation } from './useFastMutation';
|
||||||
|
|
||||||
export function useSendAnyHttpRequest() {
|
export function useSendAnyHttpRequest() {
|
||||||
const alert = useAlert();
|
const alert = useAlert();
|
||||||
const [environment] = useActiveEnvironment();
|
|
||||||
const [activeCookieJar] = useActiveCookieJar();
|
|
||||||
return useFastMutation<HttpResponse | null, string, string | null>({
|
return useFastMutation<HttpResponse | null, string, string | null>({
|
||||||
mutationKey: ['send_any_request'],
|
mutationKey: ['send_any_request'],
|
||||||
mutationFn: async (id) => {
|
mutationFn: async (id) => {
|
||||||
@@ -21,8 +19,8 @@ export function useSendAnyHttpRequest() {
|
|||||||
|
|
||||||
return invokeCmd('cmd_send_http_request', {
|
return invokeCmd('cmd_send_http_request', {
|
||||||
request,
|
request,
|
||||||
environmentId: environment?.id,
|
environmentId: getActiveEnvironment()?.id,
|
||||||
cookieJarId: activeCookieJar?.id,
|
cookieJarId: getActiveCookieJar()?.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onSettled: () => trackEvent('http_request', 'send'),
|
onSettled: () => trackEvent('http_request', 'send'),
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
|
import { setKeyValue } from '../lib/keyValueStore';
|
||||||
|
import { getActiveWorkspaceId } from './useActiveWorkspace';
|
||||||
|
import { getKeyValue, keyValuesAtom } from './useKeyValue';
|
||||||
|
|
||||||
|
function kvKey(workspaceId: string | null) {
|
||||||
|
return ['sidebar_collapsed', workspaceId ?? 'n/a'];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarItemCollapsed(itemId: string) {
|
||||||
|
const [isCollapsed, setIsCollapsed] = useState<boolean>(
|
||||||
|
getSidebarCollapsedMap()[itemId] === true,
|
||||||
|
);
|
||||||
|
useEffect(
|
||||||
|
() =>
|
||||||
|
jotaiStore.sub(keyValuesAtom, () => {
|
||||||
|
setIsCollapsed(getSidebarCollapsedMap()[itemId] === true);
|
||||||
|
}),
|
||||||
|
[itemId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
setKeyValue({
|
||||||
|
key: kvKey(getActiveWorkspaceId()),
|
||||||
|
namespace: 'no_sync',
|
||||||
|
value: { ...getSidebarCollapsedMap(), [itemId]: !isCollapsed },
|
||||||
|
}).catch(console.error);
|
||||||
|
}, [isCollapsed, itemId]);
|
||||||
|
|
||||||
|
return [isCollapsed, toggle] as const;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSidebarCollapsedMap() {
|
||||||
|
const activeWorkspaceId = getActiveWorkspaceId();
|
||||||
|
if (activeWorkspaceId == null) return {};
|
||||||
|
|
||||||
|
const value = getKeyValue<Record<string, boolean>>({
|
||||||
|
key: kvKey(activeWorkspaceId),
|
||||||
|
fallback: {},
|
||||||
|
namespace: 'no_sync',
|
||||||
|
});
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||||
import type { AnyModel } from '@yaakapp-internal/models';
|
import type { AnyModel, KeyValue } from '@yaakapp-internal/models';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { extractKeyValue } from '../lib/keyValueStore';
|
import { buildKeyValueKey } from '../lib/keyValueStore';
|
||||||
import { modelsEq } from '../lib/model_util';
|
import { modelsEq } from '../lib/model_util';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||||
import { cookieJarsAtom } from './useCookieJars';
|
import { cookieJarsAtom } from './useCookieJars';
|
||||||
@@ -13,7 +13,7 @@ import { grpcEventsQueryKey } from './useGrpcEvents';
|
|||||||
import { grpcRequestsAtom } from './useGrpcRequests';
|
import { grpcRequestsAtom } from './useGrpcRequests';
|
||||||
import { httpRequestsAtom } from './useHttpRequests';
|
import { httpRequestsAtom } from './useHttpRequests';
|
||||||
import { httpResponsesAtom } from './useHttpResponses';
|
import { httpResponsesAtom } from './useHttpResponses';
|
||||||
import { keyValueQueryKey } from './useKeyValue';
|
import { keyValueQueryKey, keyValuesAtom } from './useKeyValue';
|
||||||
import { useListenToTauriEvent } from './useListenToTauriEvent';
|
import { useListenToTauriEvent } from './useListenToTauriEvent';
|
||||||
import { pluginsAtom } from './usePlugins';
|
import { pluginsAtom } from './usePlugins';
|
||||||
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
||||||
@@ -71,14 +71,11 @@ export function useSyncModelStores() {
|
|||||||
jotaiStore.set(cookieJarsAtom, updateModelList(model));
|
jotaiStore.set(cookieJarsAtom, updateModelList(model));
|
||||||
} else if (model.model === 'settings') {
|
} else if (model.model === 'settings') {
|
||||||
jotaiStore.set(settingsAtom, model);
|
jotaiStore.set(settingsAtom, model);
|
||||||
|
} else if (model.model === 'key_value') {
|
||||||
|
jotaiStore.set(keyValuesAtom, updateModelList(model));
|
||||||
} else if (queryKey != null) {
|
} else if (queryKey != null) {
|
||||||
// TODO: Convert all models to use Jotai
|
// TODO: Convert all models to use Jotai
|
||||||
queryClient.setQueryData(queryKey, (current: unknown) => {
|
queryClient.setQueryData(queryKey, (current: unknown) => {
|
||||||
if (model.model === 'key_value') {
|
|
||||||
// Special-case for KeyValue
|
|
||||||
return extractKeyValue(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(current)) {
|
if (Array.isArray(current)) {
|
||||||
return updateModelList(model)(current);
|
return updateModelList(model)(current);
|
||||||
}
|
}
|
||||||
@@ -111,7 +108,7 @@ export function useSyncModelStores() {
|
|||||||
} else if (model.model === 'grpc_event') {
|
} else if (model.model === 'grpc_event') {
|
||||||
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
|
queryClient.setQueryData(grpcEventsQueryKey(model), removeModelById(model));
|
||||||
} else if (model.model === 'key_value') {
|
} else if (model.model === 'key_value') {
|
||||||
queryClient.setQueryData(keyValueQueryKey(model), undefined);
|
queryClient.setQueryData(keyValueQueryKey(model), removeModelByKeyValue(model));
|
||||||
} else if (model.model === 'cookie_jar') {
|
} else if (model.model === 'cookie_jar') {
|
||||||
jotaiStore.set(cookieJarsAtom, removeModelById(model));
|
jotaiStore.set(cookieJarsAtom, removeModelById(model));
|
||||||
}
|
}
|
||||||
@@ -136,6 +133,18 @@ export function removeModelById<T extends { id: string }>(model: T) {
|
|||||||
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
|
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function removeModelByKeyValue(model: KeyValue) {
|
||||||
|
return (entries: KeyValue[] | undefined) =>
|
||||||
|
entries?.filter(
|
||||||
|
(e) =>
|
||||||
|
!(
|
||||||
|
e.namespace === model.namespace &&
|
||||||
|
buildKeyValueKey(e.key) === buildKeyValueKey(model.key) &&
|
||||||
|
e.value == model.value
|
||||||
|
),
|
||||||
|
) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
|
const shouldIgnoreModel = (payload: AnyModel, windowLabel: string) => {
|
||||||
if (windowLabel === getCurrentWebviewWindow().label) {
|
if (windowLabel === getCurrentWebviewWindow().label) {
|
||||||
// Never ignore same-window updates
|
// Never ignore same-window updates
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useSetAtom } from 'jotai/index';
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
import { activeWorkspaceIdAtom, getActiveWorkspaceId } from './useActiveWorkspace';
|
||||||
import { cookieJarsAtom } from './useCookieJars';
|
import { cookieJarsAtom } from './useCookieJars';
|
||||||
import { environmentsAtom } from './useEnvironments';
|
import { environmentsAtom } from './useEnvironments';
|
||||||
import { foldersAtom } from './useFolders';
|
import { foldersAtom } from './useFolders';
|
||||||
@@ -9,36 +9,28 @@ import { grpcConnectionsAtom } from './useGrpcConnections';
|
|||||||
import { grpcRequestsAtom } from './useGrpcRequests';
|
import { grpcRequestsAtom } from './useGrpcRequests';
|
||||||
import { httpRequestsAtom } from './useHttpRequests';
|
import { httpRequestsAtom } from './useHttpRequests';
|
||||||
import { httpResponsesAtom } from './useHttpResponses';
|
import { httpResponsesAtom } from './useHttpResponses';
|
||||||
|
import { keyValuesAtom } from './useKeyValue';
|
||||||
|
|
||||||
export function useSyncWorkspaceChildModels() {
|
export function useSyncWorkspaceChildModels() {
|
||||||
const setCookieJars = useSetAtom(cookieJarsAtom);
|
|
||||||
const setFolders = useSetAtom(foldersAtom);
|
|
||||||
const setHttpRequests = useSetAtom(httpRequestsAtom);
|
|
||||||
const setHttpResponses = useSetAtom(httpResponsesAtom);
|
|
||||||
const setGrpcConnections = useSetAtom(grpcConnectionsAtom);
|
|
||||||
const setGrpcRequests = useSetAtom(grpcRequestsAtom);
|
|
||||||
const setEnvironments = useSetAtom(environmentsAtom);
|
|
||||||
|
|
||||||
const workspace = useActiveWorkspace();
|
|
||||||
const workspaceId = workspace?.id;
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (workspaceId == null) {
|
jotaiStore.sub(activeWorkspaceIdAtom, sync);
|
||||||
return;
|
sync().catch(console.error);
|
||||||
}
|
}, []);
|
||||||
(async function () {
|
}
|
||||||
console.log('Syncing model stores', { workspaceId });
|
|
||||||
// Set the things we need first, first
|
async function sync() {
|
||||||
setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId }));
|
const workspaceId = getActiveWorkspaceId();
|
||||||
setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId }));
|
const args = { workspaceId };
|
||||||
setFolders(await invokeCmd('cmd_list_folders', { workspaceId }));
|
console.log('Syncing model stores', args);
|
||||||
|
// Set the things we need first, first
|
||||||
// Then, set the rest
|
jotaiStore.set(httpRequestsAtom, await invokeCmd('cmd_list_http_requests', args));
|
||||||
setCookieJars(await invokeCmd('cmd_list_cookie_jars', { workspaceId }));
|
jotaiStore.set(grpcRequestsAtom, await invokeCmd('cmd_list_grpc_requests', args));
|
||||||
setHttpResponses(await invokeCmd('cmd_list_http_responses', { workspaceId }));
|
jotaiStore.set(foldersAtom, await invokeCmd('cmd_list_folders', args));
|
||||||
setGrpcConnections(await invokeCmd('cmd_list_grpc_connections', { workspaceId }));
|
|
||||||
setEnvironments(await invokeCmd('cmd_list_environments', { workspaceId }));
|
// Then, set the rest
|
||||||
})().catch(console.error);
|
jotaiStore.set(keyValuesAtom, await invokeCmd('cmd_list_key_values', args));
|
||||||
|
jotaiStore.set(cookieJarsAtom, await invokeCmd('cmd_list_cookie_jars', args));
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
jotaiStore.set(httpResponsesAtom, await invokeCmd('cmd_list_http_responses', args));
|
||||||
}, [workspaceId]);
|
jotaiStore.set(grpcConnectionsAtom, await invokeCmd('cmd_list_grpc_connections', args));
|
||||||
|
jotaiStore.set(environmentsAtom, await invokeCmd('cmd_list_environments', args));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,23 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import type { GetTemplateFunctionsResponse } from '@yaakapp-internal/plugin';
|
import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-internal/plugin';
|
||||||
|
import { atom, useAtomValue } from 'jotai';
|
||||||
|
import { useSetAtom } from 'jotai/index';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { invokeCmd } from '../lib/tauri';
|
import { invokeCmd } from '../lib/tauri';
|
||||||
import { usePluginsKey } from './usePlugins';
|
import { usePluginsKey } from './usePlugins';
|
||||||
|
|
||||||
|
const templateFunctionsAtom = atom<TemplateFunction[]>([]);
|
||||||
|
|
||||||
export function useTemplateFunctions() {
|
export function useTemplateFunctions() {
|
||||||
|
return useAtomValue(templateFunctionsAtom);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSubscribeTemplateFunctions() {
|
||||||
const pluginsKey = usePluginsKey();
|
const pluginsKey = usePluginsKey();
|
||||||
const [numFns, setNumFns] = useState<number>(0);
|
const [numFns, setNumFns] = useState<number>(0);
|
||||||
|
const setAtom = useSetAtom(templateFunctionsAtom);
|
||||||
|
|
||||||
const result = useQuery({
|
useQuery({
|
||||||
queryKey: ['template_functions', pluginsKey],
|
queryKey: ['template_functions', pluginsKey],
|
||||||
// Fetch periodically until functions are returned
|
// Fetch periodically until functions are returned
|
||||||
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
|
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
|
||||||
@@ -19,9 +28,9 @@ export function useTemplateFunctions() {
|
|||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
|
const result = await invokeCmd<GetTemplateFunctionsResponse[]>('cmd_template_functions');
|
||||||
setNumFns(result.length);
|
setNumFns(result.length);
|
||||||
return result;
|
const functions = result.flatMap((r) => r.functions) ?? [];
|
||||||
|
setAtom(functions);
|
||||||
|
return functions;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return result.data?.flatMap((r) => r.functions) ?? [];
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export function extractKeyValue<T>(kv: KeyValue | null): T | undefined {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractKeyValueOrFallback<T>(kv: KeyValue | null, fallback: T): T {
|
export function extractKeyValueOrFallback<T>(kv: KeyValue | null, fallback: T): T {
|
||||||
const v = extractKeyValue<T>(kv);
|
const v = extractKeyValue<T>(kv);
|
||||||
if (v === undefined) return fallback;
|
if (v === undefined) return fallback;
|
||||||
return v;
|
return v;
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ type TauriCmd =
|
|||||||
| 'cmd_import_data'
|
| 'cmd_import_data'
|
||||||
| 'cmd_install_plugin'
|
| 'cmd_install_plugin'
|
||||||
| 'cmd_list_cookie_jars'
|
| 'cmd_list_cookie_jars'
|
||||||
|
| 'cmd_list_key_values'
|
||||||
| 'cmd_list_environments'
|
| 'cmd_list_environments'
|
||||||
| 'cmd_list_folders'
|
| 'cmd_list_folders'
|
||||||
| 'cmd_list_grpc_connections'
|
| 'cmd_list_grpc_connections'
|
||||||
@@ -79,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) {
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import React, { Suspense } from 'react';
|
|||||||
import { DndProvider } from 'react-dnd';
|
import { DndProvider } from 'react-dnd';
|
||||||
import { HTML5Backend } from 'react-dnd-html5-backend';
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
||||||
import { HelmetProvider } from 'react-helmet-async';
|
import { HelmetProvider } from 'react-helmet-async';
|
||||||
|
import { DialogProvider, Dialogs } from '../components/Dialogs';
|
||||||
import { GlobalHooks } from '../components/GlobalHooks';
|
import { GlobalHooks } from '../components/GlobalHooks';
|
||||||
|
import RouteError from '../components/RouteError';
|
||||||
|
import { ToastProvider, Toasts } from '../components/Toasts';
|
||||||
import { useOsInfo } from '../hooks/useOsInfo';
|
import { useOsInfo } from '../hooks/useOsInfo';
|
||||||
import { jotaiStore } from '../lib/jotai';
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { ToastProvider, Toasts } from '../components/Toasts';
|
|
||||||
import { DialogProvider, Dialogs } from '../components/Dialogs';
|
|
||||||
|
|
||||||
const queryClient = new QueryClient({
|
const queryClient = new QueryClient({
|
||||||
queryCache: new QueryCache({
|
queryCache: new QueryCache({
|
||||||
@@ -52,6 +53,7 @@ const ReactQueryDevtools =
|
|||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
|
errorComponent: RouteError,
|
||||||
});
|
});
|
||||||
|
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
|
|||||||
Reference in New Issue
Block a user