Prevent a bunch more stuff from re-rendering

This commit is contained in:
Gregory Schier
2024-12-31 23:24:41 -08:00
parent dfca17f9b7
commit 80119f6574
13 changed files with 211 additions and 174 deletions
+86 -74
View File
@@ -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>
); );
} });
+4 -6
View File
@@ -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>
); );
+80 -55
View File
@@ -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>
+1 -1
View File
@@ -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) {
+2 -7
View File
@@ -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 -7
View File
@@ -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 />
+2 -2
View File
@@ -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}
/> />
+1 -1
View File
@@ -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
View File
@@ -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 {
+14 -6
View File
@@ -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',
+11
View File
@@ -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),
);
}