mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-07-06 12:55:24 +02:00
Prevent a bunch more stuff from re-rendering
This commit is contained in:
@@ -1,18 +1,19 @@
|
|||||||
|
import { memo, useCallback } from 'react';
|
||||||
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||||
import { useCookieJars } from '../hooks/useCookieJars';
|
import { cookieJarsAtom } from '../hooks/useCookieJars';
|
||||||
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
|
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
|
||||||
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
|
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
|
||||||
|
import { useDialog } from '../hooks/useDialog';
|
||||||
import { usePrompt } from '../hooks/usePrompt';
|
import { usePrompt } from '../hooks/usePrompt';
|
||||||
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
|
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
|
||||||
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import { CookieDialog } from './CookieDialog';
|
import { CookieDialog } from './CookieDialog';
|
||||||
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
import { Dropdown, type DropdownItem } from './core/Dropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import { useDialog } from '../hooks/useDialog';
|
|
||||||
|
|
||||||
export function CookieDropdown() {
|
export const CookieDropdown = memo(function CookieDropdown() {
|
||||||
const cookieJars = useCookieJars() ?? [];
|
|
||||||
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
|
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
|
||||||
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
|
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
|
||||||
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
|
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
|
||||||
@@ -20,77 +21,88 @@ export function CookieDropdown() {
|
|||||||
const dialog = useDialog();
|
const dialog = useDialog();
|
||||||
const prompt = usePrompt();
|
const prompt = usePrompt();
|
||||||
|
|
||||||
|
const getItems = useCallback((): DropdownItem[] => {
|
||||||
|
const cookieJars = jotaiStore.get(cookieJarsAtom) ?? [];
|
||||||
|
return [
|
||||||
|
...cookieJars.map((j) => ({
|
||||||
|
key: j.id,
|
||||||
|
label: j.name,
|
||||||
|
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
|
||||||
|
onSelect: () => setActiveCookieJarId(j.id),
|
||||||
|
})),
|
||||||
|
...((cookieJars.length > 0 && activeCookieJar != null
|
||||||
|
? [
|
||||||
|
{ type: 'separator', label: activeCookieJar.name },
|
||||||
|
{
|
||||||
|
key: 'manage',
|
||||||
|
label: 'Manage Cookies',
|
||||||
|
leftSlot: <Icon icon="cookie" />,
|
||||||
|
onSelect: () => {
|
||||||
|
if (activeCookieJar == null) return;
|
||||||
|
dialog.show({
|
||||||
|
id: 'cookies',
|
||||||
|
title: 'Manage Cookies',
|
||||||
|
size: 'full',
|
||||||
|
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'rename',
|
||||||
|
label: 'Rename',
|
||||||
|
leftSlot: <Icon icon="pencil" />,
|
||||||
|
onSelect: async () => {
|
||||||
|
const name = await prompt({
|
||||||
|
id: 'rename-cookie-jar',
|
||||||
|
title: 'Rename Cookie Jar',
|
||||||
|
description: (
|
||||||
|
<>
|
||||||
|
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
label: 'Name',
|
||||||
|
confirmText: 'Save',
|
||||||
|
placeholder: 'New name',
|
||||||
|
defaultValue: activeCookieJar?.name,
|
||||||
|
});
|
||||||
|
if (name == null) return;
|
||||||
|
updateCookieJar.mutate({ name });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...((cookieJars.length > 1 // Never delete the last one
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
key: 'delete',
|
||||||
|
label: 'Delete',
|
||||||
|
leftSlot: <Icon icon="trash" />,
|
||||||
|
variant: 'danger',
|
||||||
|
onSelect: () => deleteCookieJar.mutateAsync(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []) as DropdownItem[]),
|
||||||
|
]
|
||||||
|
: []) as DropdownItem[]),
|
||||||
|
{ type: 'separator' },
|
||||||
|
{
|
||||||
|
key: 'create-cookie-jar',
|
||||||
|
label: 'New Cookie Jar',
|
||||||
|
leftSlot: <Icon icon="plus" />,
|
||||||
|
onSelect: () => createCookieJar.mutate(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}, [
|
||||||
|
activeCookieJar,
|
||||||
|
createCookieJar,
|
||||||
|
deleteCookieJar,
|
||||||
|
dialog,
|
||||||
|
prompt,
|
||||||
|
setActiveCookieJarId,
|
||||||
|
updateCookieJar,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown
|
<Dropdown items={getItems}>
|
||||||
items={[
|
|
||||||
...cookieJars.map((j) => ({
|
|
||||||
key: j.id,
|
|
||||||
label: j.name,
|
|
||||||
leftSlot: <Icon icon={j.id === activeCookieJar?.id ? 'check' : 'empty'} />,
|
|
||||||
onSelect: () => setActiveCookieJarId(j.id),
|
|
||||||
})),
|
|
||||||
...((cookieJars.length > 0 && activeCookieJar != null
|
|
||||||
? [
|
|
||||||
{ type: 'separator', label: activeCookieJar.name },
|
|
||||||
{
|
|
||||||
key: 'manage',
|
|
||||||
label: 'Manage Cookies',
|
|
||||||
leftSlot: <Icon icon="cookie" />,
|
|
||||||
onSelect: () => {
|
|
||||||
if (activeCookieJar == null) return;
|
|
||||||
dialog.show({
|
|
||||||
id: 'cookies',
|
|
||||||
title: 'Manage Cookies',
|
|
||||||
size: 'full',
|
|
||||||
render: () => <CookieDialog cookieJarId={activeCookieJar.id} />,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'rename',
|
|
||||||
label: 'Rename',
|
|
||||||
leftSlot: <Icon icon="pencil" />,
|
|
||||||
onSelect: async () => {
|
|
||||||
const name = await prompt({
|
|
||||||
id: 'rename-cookie-jar',
|
|
||||||
title: 'Rename Cookie Jar',
|
|
||||||
description: (
|
|
||||||
<>
|
|
||||||
Enter a new name for <InlineCode>{activeCookieJar?.name}</InlineCode>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
label: 'Name',
|
|
||||||
confirmText: 'Save',
|
|
||||||
placeholder: 'New name',
|
|
||||||
defaultValue: activeCookieJar?.name,
|
|
||||||
});
|
|
||||||
if (name == null) return;
|
|
||||||
updateCookieJar.mutate({ name });
|
|
||||||
},
|
|
||||||
},
|
|
||||||
...((cookieJars.length > 1 // Never delete the last one
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
key: 'delete',
|
|
||||||
label: 'Delete',
|
|
||||||
leftSlot: <Icon icon="trash" />,
|
|
||||||
variant: 'danger',
|
|
||||||
onSelect: () => deleteCookieJar.mutateAsync(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []) as DropdownItem[]),
|
|
||||||
]
|
|
||||||
: []) as DropdownItem[]),
|
|
||||||
{ type: 'separator' },
|
|
||||||
{
|
|
||||||
key: 'create-cookie-jar',
|
|
||||||
label: 'New Cookie Jar',
|
|
||||||
leftSlot: <Icon icon="plus" />,
|
|
||||||
onSelect: () => createCookieJar.mutate(),
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
|
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
|
||||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||||
import type { DropdownProps } from './core/Dropdown';
|
import type { DropdownProps } from './core/Dropdown';
|
||||||
import { Dropdown } from './core/Dropdown';
|
import { Dropdown } from './core/Dropdown';
|
||||||
@@ -8,15 +7,14 @@ interface Props extends Omit<DropdownProps, 'items'> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function CreateDropdown({ hideFolder, children, ...props }: Props) {
|
export function CreateDropdown({ hideFolder, children, ...props }: Props) {
|
||||||
const activeRequest = useActiveRequest();
|
const getItems = useCreateDropdownItems({
|
||||||
const folderId = activeRequest?.folderId ?? null;
|
|
||||||
const items = useCreateDropdownItems({
|
|
||||||
hideFolder,
|
hideFolder,
|
||||||
hideIcons: true,
|
hideIcons: true,
|
||||||
folderId,
|
folderId: 'active-folder',
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dropdown items={items} {...props}>
|
<Dropdown items={getItems} {...props}>
|
||||||
{children}
|
{children}
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { useNavigate } from '@tanstack/react-router';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useCallback, useMemo, useRef } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { useKeyPressEvent } from 'react-use';
|
import { useKeyPressEvent } from 'react-use';
|
||||||
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
|
||||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||||
import { useHotKey } from '../hooks/useHotKey';
|
import { useHotKey } from '../hooks/useHotKey';
|
||||||
@@ -15,12 +16,11 @@ import { Dropdown } from './core/Dropdown';
|
|||||||
import { HttpMethodTag } from './core/HttpMethodTag';
|
import { HttpMethodTag } from './core/HttpMethodTag';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
activeRequestId: string | null;
|
|
||||||
activeRequestName: string;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecentRequestsDropdown({ className, activeRequestId, activeRequestName }: Props) {
|
export function RecentRequestsDropdown({ className }: Props) {
|
||||||
|
const activeRequest = useActiveRequest();
|
||||||
const dropdownRef = useRef<DropdownRef>(null);
|
const dropdownRef = useRef<DropdownRef>(null);
|
||||||
const [allRecentRequestIds] = useRecentRequests();
|
const [allRecentRequestIds] = useRecentRequests();
|
||||||
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
||||||
@@ -93,10 +93,10 @@ export function RecentRequestsDropdown({ className, activeRequestId, activeReque
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'truncate pointer-events-auto',
|
'truncate pointer-events-auto',
|
||||||
activeRequestId === null && 'text-text-subtlest italic',
|
activeRequest == null && 'text-text-subtlest italic',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{activeRequestName}
|
{fallbackRequestName(activeRequest)}
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,20 +1,24 @@
|
|||||||
import type { HttpRequest } from '@yaakapp-internal/models';
|
import type { HttpRequest } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import React, { memo, useCallback, useMemo, useState } from 'react';
|
import React, { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useLocalStorage } from 'react-use';
|
import { useLocalStorage } from 'react-use';
|
||||||
|
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||||
|
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||||
|
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||||
import { useImportCurl } from '../hooks/useImportCurl';
|
import { useImportCurl } from '../hooks/useImportCurl';
|
||||||
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
import { useImportQuerystring } from '../hooks/useImportQuerystring';
|
||||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
import { useRequests } from '../hooks/useRequests';
|
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||||
import { useToast } from '../hooks/useToast';
|
import { useToast } from '../hooks/useToast';
|
||||||
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||||
|
import { deepEqualAtom } from '../lib/atoms';
|
||||||
import { languageFromContentType } from '../lib/contentType';
|
import { languageFromContentType } from '../lib/contentType';
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import { generateId } from '../lib/generateId';
|
import { generateId } from '../lib/generateId';
|
||||||
@@ -67,15 +71,24 @@ const TAB_HEADERS = 'headers';
|
|||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
const TAB_DESCRIPTION = 'description';
|
const TAB_DESCRIPTION = 'description';
|
||||||
|
|
||||||
|
const nonActiveRequestUrlsAtom = atom((get) => {
|
||||||
|
const activeRequestId = get(activeRequestIdAtom);
|
||||||
|
const requests = [...get(httpRequestsAtom), ...get(grpcRequestsAtom)];
|
||||||
|
return requests
|
||||||
|
.filter((r) => r.id !== activeRequestId)
|
||||||
|
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
|
||||||
|
|
||||||
export const RequestPane = memo(function RequestPane({
|
export const RequestPane = memo(function RequestPane({
|
||||||
style,
|
style,
|
||||||
fullHeight,
|
fullHeight,
|
||||||
className,
|
className,
|
||||||
activeRequest,
|
activeRequest,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const requests = useRequests();
|
|
||||||
const activeRequestId = activeRequest.id;
|
const activeRequestId = activeRequest.id;
|
||||||
const updateRequest = useUpdateAnyHttpRequest();
|
const { mutateAsync: updateRequestAsync, mutate: updateRequest } = useUpdateAnyHttpRequest();
|
||||||
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
||||||
'requestPaneActiveTabs',
|
'requestPaneActiveTabs',
|
||||||
{},
|
{},
|
||||||
@@ -102,12 +115,12 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
id: generateId(),
|
id: generateId(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
await updateRequest.mutateAsync({ id: activeRequest.id, update: { headers } });
|
await updateRequestAsync({ id: activeRequest.id, update: { headers } });
|
||||||
|
|
||||||
// Force update header editor so any changed headers are reflected
|
// Force update header editor so any changed headers are reflected
|
||||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||||
},
|
},
|
||||||
[activeRequest, updateRequest],
|
[activeRequest, updateRequestAsync],
|
||||||
);
|
);
|
||||||
|
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
@@ -206,7 +219,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
showMethodToast(patch.method);
|
showMethodToast(patch.method);
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateRequest.mutateAsync({ id: activeRequestId, update: patch });
|
await updateRequestAsync({ id: activeRequestId, update: patch });
|
||||||
|
|
||||||
if (newContentType !== undefined) {
|
if (newContentType !== undefined) {
|
||||||
await handleContentTypeChange(newContentType);
|
await handleContentTypeChange(newContentType);
|
||||||
@@ -247,7 +260,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
token: authentication.token ?? '',
|
token: authentication.token ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
await updateRequest.mutateAsync({
|
updateRequest({
|
||||||
id: activeRequestId,
|
id: activeRequestId,
|
||||||
update: { authenticationType, authentication },
|
update: { authenticationType, authentication },
|
||||||
});
|
});
|
||||||
@@ -267,25 +280,26 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
numParams,
|
numParams,
|
||||||
toast,
|
toast,
|
||||||
updateRequest,
|
updateRequest,
|
||||||
|
updateRequestAsync,
|
||||||
urlParameterPairs.length,
|
urlParameterPairs.length,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
const sendRequest = useSendAnyHttpRequest();
|
|
||||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
|
||||||
const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null);
|
|
||||||
const isLoading = useIsResponseLoading(activeRequestId);
|
const isLoading = useIsResponseLoading(activeRequestId);
|
||||||
|
const { mutate: sendRequest } = useSendAnyHttpRequest();
|
||||||
|
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||||
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
const { updateKey } = useRequestUpdateKey(activeRequestId);
|
||||||
const importCurl = useImportCurl();
|
const { mutate: importCurl } = useImportCurl();
|
||||||
const importQuerystring = useImportQuerystring(activeRequestId);
|
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
|
||||||
|
|
||||||
const handleBodyChange = useCallback(
|
const handleBodyChange = useCallback(
|
||||||
(body: HttpRequest['body']) => updateRequest.mutate({ id: activeRequestId, update: { body } }),
|
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
|
||||||
[activeRequestId, updateRequest],
|
[activeRequestId, updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleBodyTextChange = useCallback(
|
const handleBodyTextChange = useCallback(
|
||||||
(text: string) => updateRequest.mutate({ id: activeRequestId, update: { body: { text } } }),
|
(text: string) => updateRequest({ id: activeRequestId, update: { body: { text } } }),
|
||||||
[activeRequestId, updateRequest],
|
[activeRequestId, updateRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -301,20 +315,48 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
setActiveTab(TAB_PARAMS);
|
setActiveTab(TAB_PARAMS);
|
||||||
});
|
});
|
||||||
|
|
||||||
const autocomplete: GenericCompletionConfig = {
|
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||||
minMatch: 3,
|
|
||||||
options:
|
const autocomplete: GenericCompletionConfig = useMemo(
|
||||||
requests.length > 0
|
() => ({
|
||||||
? [
|
minMatch: 3,
|
||||||
...requests
|
options:
|
||||||
.filter((r) => r.id !== activeRequestId)
|
autocompleteUrls.length > 0
|
||||||
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url })),
|
? autocompleteUrls
|
||||||
]
|
: [
|
||||||
: [
|
{ label: 'http://', type: 'constant' },
|
||||||
{ label: 'http://', type: 'constant' },
|
{ label: 'https://', type: 'constant' },
|
||||||
{ label: 'https://', type: 'constant' },
|
],
|
||||||
],
|
}),
|
||||||
};
|
[autocompleteUrls],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handlePaste = useCallback(
|
||||||
|
(text: string) => {
|
||||||
|
if (text.startsWith('curl ')) {
|
||||||
|
importCurl({ overwriteRequestId: activeRequestId, command: text });
|
||||||
|
} else {
|
||||||
|
// Only import query if pasted text contains entire querystring
|
||||||
|
importQuerystring(text);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[activeRequestId, importCurl, importQuerystring],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSend = useCallback(
|
||||||
|
() => sendRequest(activeRequest.id ?? null),
|
||||||
|
[activeRequest.id, sendRequest],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleMethodChange = useCallback(
|
||||||
|
(method: string) => updateRequest({ id: activeRequestId, update: { method } }),
|
||||||
|
[activeRequestId, updateRequest],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleUrlChange = useCallback(
|
||||||
|
(url: string) => updateRequest({ id: activeRequestId, update: { url } }),
|
||||||
|
[activeRequestId, updateRequest],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -329,23 +371,12 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
url={activeRequest.url}
|
url={activeRequest.url}
|
||||||
method={activeRequest.method}
|
method={activeRequest.method}
|
||||||
placeholder="https://example.com"
|
placeholder="https://example.com"
|
||||||
onPasteOverwrite={(text) => {
|
onPasteOverwrite={handlePaste}
|
||||||
if (text.startsWith('curl ')) {
|
|
||||||
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
|
|
||||||
} else {
|
|
||||||
// Only import query if pasted text contains entire querystring
|
|
||||||
importQuerystring.mutate(text);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
autocomplete={autocomplete}
|
autocomplete={autocomplete}
|
||||||
onSend={() => sendRequest.mutateAsync(activeRequest.id ?? null)}
|
onSend={handleSend}
|
||||||
onCancel={cancelResponse.mutate}
|
onCancel={cancelResponse}
|
||||||
onMethodChange={(method) =>
|
onMethodChange={handleMethodChange}
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { method } })
|
onUrlChange={handleUrlChange}
|
||||||
}
|
|
||||||
onUrlChange={(url: string) =>
|
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { url } })
|
|
||||||
}
|
|
||||||
forceUpdateKey={updateKey}
|
forceUpdateKey={updateKey}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
/>
|
/>
|
||||||
@@ -372,9 +403,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
<HeadersEditor
|
<HeadersEditor
|
||||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||||
request={activeRequest}
|
request={activeRequest}
|
||||||
onChange={(headers) =>
|
onChange={(headers) => updateRequest({ id: activeRequestId, update: { headers } })}
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { headers } })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PARAMS}>
|
<TabContent value={TAB_PARAMS}>
|
||||||
@@ -383,7 +412,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
forceUpdateKey={forceUpdateKey + urlParametersKey}
|
||||||
pairs={urlParameterPairs}
|
pairs={urlParameterPairs}
|
||||||
onChange={(urlParameters) =>
|
onChange={(urlParameters) =>
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { urlParameters } })
|
updateRequest({ id: activeRequestId, update: { urlParameters } })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
@@ -437,9 +466,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
requestId={activeRequest.id}
|
requestId={activeRequest.id}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
body={activeRequest.body}
|
body={activeRequest.body}
|
||||||
onChange={(body) =>
|
onChange={(body) => updateRequest({ id: activeRequestId, update: { body } })}
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { body } })
|
|
||||||
}
|
|
||||||
onChangeContentType={handleContentTypeChange}
|
onChangeContentType={handleContentTypeChange}
|
||||||
/>
|
/>
|
||||||
) : typeof activeRequest.bodyType === 'string' ? (
|
) : typeof activeRequest.bodyType === 'string' ? (
|
||||||
@@ -467,9 +494,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
className="font-sans !text-xl !px-0"
|
className="font-sans !text-xl !px-0"
|
||||||
containerClassName="border-0"
|
containerClassName="border-0"
|
||||||
placeholder={activeRequest.id}
|
placeholder={activeRequest.id}
|
||||||
onChange={(name) =>
|
onChange={(name) => updateRequest({ id: activeRequestId, update: { name } })}
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { name } })
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
<MarkdownEditor
|
<MarkdownEditor
|
||||||
name="request-description"
|
name="request-description"
|
||||||
@@ -477,7 +502,7 @@ export const RequestPane = memo(function RequestPane({
|
|||||||
defaultValue={activeRequest.description}
|
defaultValue={activeRequest.description}
|
||||||
stateKey={`description.${activeRequest.id}`}
|
stateKey={`description.${activeRequest.id}`}
|
||||||
onChange={(description) =>
|
onChange={(description) =>
|
||||||
updateRequest.mutate({ id: activeRequestId, update: { description } })
|
updateRequest({ id: activeRequestId, update: { description } })
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ export function Sidebar({ className }: Props) {
|
|||||||
setShowMainContextMenu({ x: e.clientX, y: e.clientY });
|
setShowMainContextMenu({ x: e.clientX, y: e.clientY });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const mainContextMenuItems = useCreateDropdownItems();
|
const mainContextMenuItems = useCreateDropdownItems({ folderId: null });
|
||||||
|
|
||||||
// Not ready to render yet
|
// Not ready to render yet
|
||||||
if (tree == null) {
|
if (tree == null) {
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import deepEqual from '@gilbarbara/deep-equal';
|
|
||||||
import type { Folder, GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
import type { Folder, GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||||
|
|
||||||
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
|
// This is an atom so we can use it in the child items to avoid re-rendering the entire list
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
import { selectAtom } from 'jotai/utils';
|
|
||||||
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
|
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
|
||||||
import { foldersAtom } from '../hooks/useFolders';
|
import { foldersAtom } from '../hooks/useFolders';
|
||||||
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
|
||||||
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
import { httpRequestsAtom } from '../hooks/useHttpRequests';
|
||||||
|
import { deepEqualAtom } from '../lib/atoms';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||||
import type { SidebarTreeNode } from './Sidebar';
|
import type { SidebarTreeNode } from './Sidebar';
|
||||||
|
|
||||||
@@ -27,11 +26,7 @@ const allPotentialChildrenAtom = atom((get) => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
const memoAllPotentialChildrenAtom = selectAtom(
|
const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom);
|
||||||
allPotentialChildrenAtom,
|
|
||||||
(v) => v,
|
|
||||||
(a, b) => deepEqual(a, b),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sidebarTreeAtom = atom<{
|
export const sidebarTreeAtom = atom<{
|
||||||
tree: SidebarTreeNode | null;
|
tree: SidebarTreeNode | null;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||||
@@ -42,7 +42,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
|||||||
folderId: child.model === 'folder' ? child.id : null,
|
folderId: child.model === 'folder' ? child.id : null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const items = useMemo<DropdownItem[]>(() => {
|
const items = useCallback((): DropdownItem[] => {
|
||||||
if (child.model === 'folder') {
|
if (child.model === 'folder') {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -77,7 +77,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
|||||||
onSelect: () => deleteFolder.mutate(),
|
onSelect: () => deleteFolder.mutate(),
|
||||||
},
|
},
|
||||||
{ type: 'separator' },
|
{ type: 'separator' },
|
||||||
...createDropdownItems,
|
...createDropdownItems(),
|
||||||
];
|
];
|
||||||
} else {
|
} else {
|
||||||
const requestItems: DropdownItem[] =
|
const requestItems: DropdownItem[] =
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { memo } from 'react';
|
import React, { memo } from 'react';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
|
||||||
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
|
||||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
|
||||||
import { CookieDropdown } from './CookieDropdown';
|
import { CookieDropdown } from './CookieDropdown';
|
||||||
import { Icon } from './core/Icon';
|
import { Icon } from './core/Icon';
|
||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
@@ -21,7 +19,6 @@ interface Props {
|
|||||||
|
|
||||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||||
const togglePalette = useToggleCommandPalette();
|
const togglePalette = useToggleCommandPalette();
|
||||||
const activeRequest = useActiveRequest();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -40,10 +37,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
|||||||
</HStack>
|
</HStack>
|
||||||
</HStack>
|
</HStack>
|
||||||
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
|
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
|
||||||
<RecentRequestsDropdown
|
<RecentRequestsDropdown />
|
||||||
activeRequestId={activeRequest?.id ?? null}
|
|
||||||
activeRequestName={fallbackRequestName(activeRequest)}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
|
||||||
<LicenseBadge />
|
<LicenseBadge />
|
||||||
|
|||||||
@@ -176,7 +176,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
|||||||
interface ContextMenuProps {
|
interface ContextMenuProps {
|
||||||
triggerPosition: { x: number; y: number } | null;
|
triggerPosition: { x: number; y: number } | null;
|
||||||
className?: string;
|
className?: string;
|
||||||
items: DropdownItem[];
|
items: DropdownProps['items'];
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function Co
|
|||||||
isOpen={true} // Always open because we return null if not
|
isOpen={true} // Always open because we return null if not
|
||||||
className={className}
|
className={className}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
items={items}
|
items={typeof items === 'function' ? items() : items}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
triggerShape={triggerShape}
|
triggerShape={triggerShape}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export function Tabs({
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 overflow-x-hidden',
|
'h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 ',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import {fallbackRequestName} from "../lib/fallbackRequestName";
|
import { jotaiStore } from '../lib/jotai';
|
||||||
import {jotaiStore} from "../lib/jotai";
|
|
||||||
import { activeRequestIdAtom } from './useActiveRequestId';
|
import { activeRequestIdAtom } from './useActiveRequestId';
|
||||||
import { grpcRequestsAtom } from './useGrpcRequests';
|
import { grpcRequestsAtom } from './useGrpcRequests';
|
||||||
import { httpRequestsAtom } from './useHttpRequests';
|
import { httpRequestsAtom } from './useHttpRequests';
|
||||||
@@ -21,11 +20,6 @@ export function getActiveRequest() {
|
|||||||
return jotaiStore.get(activeRequestAtom);
|
return jotaiStore.get(activeRequestAtom);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const activeRequestNameAtom = atom(get => {
|
|
||||||
const activeRequest = get(activeRequestAtom);
|
|
||||||
return fallbackRequestName(activeRequest);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function useActiveRequest<T extends keyof TypeMap>(
|
export function useActiveRequest<T extends keyof TypeMap>(
|
||||||
model?: T | undefined,
|
model?: T | undefined,
|
||||||
): TypeMap[T] | null {
|
): TypeMap[T] | null {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useMemo } from 'react';
|
import { useCallback } from 'react';
|
||||||
import type { DropdownItem } from '../components/core/Dropdown';
|
import type { DropdownItem } from '../components/core/Dropdown';
|
||||||
import { Icon } from '../components/core/Icon';
|
import { Icon } from '../components/core/Icon';
|
||||||
import { generateId } from '../lib/generateId';
|
import { generateId } from '../lib/generateId';
|
||||||
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
|
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
|
||||||
|
import { getActiveRequest } from './useActiveRequest';
|
||||||
import { useCreateFolder } from './useCreateFolder';
|
import { useCreateFolder } from './useCreateFolder';
|
||||||
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
|
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
|
||||||
import { useCreateHttpRequest } from './useCreateHttpRequest';
|
import { useCreateHttpRequest } from './useCreateHttpRequest';
|
||||||
@@ -14,19 +15,26 @@ export function useCreateDropdownItems({
|
|||||||
}: {
|
}: {
|
||||||
hideFolder?: boolean;
|
hideFolder?: boolean;
|
||||||
hideIcons?: boolean;
|
hideIcons?: boolean;
|
||||||
folderId?: string | null;
|
folderId?: string | null | 'active-folder';
|
||||||
} = {}): DropdownItem[] {
|
} = {}): () => DropdownItem[] {
|
||||||
const { mutate: createHttpRequest } = useCreateHttpRequest();
|
const { mutate: createHttpRequest } = useCreateHttpRequest();
|
||||||
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
|
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
|
||||||
const { mutate: createFolder } = useCreateFolder();
|
const { mutate: createFolder } = useCreateFolder();
|
||||||
|
|
||||||
return useMemo<DropdownItem[]>(
|
return useCallback(
|
||||||
() => [
|
(): DropdownItem[] => [
|
||||||
{
|
{
|
||||||
key: 'create-http-request',
|
key: 'create-http-request',
|
||||||
label: 'HTTP Request',
|
label: 'HTTP Request',
|
||||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||||
onSelect: () => createHttpRequest({ folderId }),
|
onSelect: () => {
|
||||||
|
const args = { folderId };
|
||||||
|
if (folderId === 'active-folder') {
|
||||||
|
const activeRequest = getActiveRequest();
|
||||||
|
args.folderId = activeRequest?.folderId ?? undefined;
|
||||||
|
}
|
||||||
|
createHttpRequest(args);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'create-graphql-request',
|
key: 'create-graphql-request',
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import deepEqual from '@gilbarbara/deep-equal';
|
||||||
|
import type { Atom } from 'jotai';
|
||||||
|
import { selectAtom } from 'jotai/utils';
|
||||||
|
|
||||||
|
export function deepEqualAtom<T>(a: Atom<T>) {
|
||||||
|
return selectAtom(
|
||||||
|
a,
|
||||||
|
(v) => v,
|
||||||
|
(a, b) => deepEqual(a, b),
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user