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

View File

@@ -1,18 +1,19 @@
import { memo, useCallback } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useCookieJars } from '../hooks/useCookieJars';
import { cookieJarsAtom } from '../hooks/useCookieJars';
import { useCreateCookieJar } from '../hooks/useCreateCookieJar';
import { useDeleteCookieJar } from '../hooks/useDeleteCookieJar';
import { useDialog } from '../hooks/useDialog';
import { usePrompt } from '../hooks/usePrompt';
import { useUpdateCookieJar } from '../hooks/useUpdateCookieJar';
import { jotaiStore } from '../lib/jotai';
import { CookieDialog } from './CookieDialog';
import { Dropdown, type DropdownItem } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import { useDialog } from '../hooks/useDialog';
export function CookieDropdown() {
const cookieJars = useCookieJars() ?? [];
export const CookieDropdown = memo(function CookieDropdown() {
const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar();
const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null);
const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null);
@@ -20,77 +21,88 @@ export function CookieDropdown() {
const dialog = useDialog();
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 (
<Dropdown
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(),
},
]}
>
<Dropdown items={getItems}>
<IconButton size="sm" icon="cookie" title="Cookie Jar" />
</Dropdown>
);
}
});

View File

@@ -1,4 +1,3 @@
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import type { DropdownProps } 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) {
const activeRequest = useActiveRequest();
const folderId = activeRequest?.folderId ?? null;
const items = useCreateDropdownItems({
const getItems = useCreateDropdownItems({
hideFolder,
hideIcons: true,
folderId,
folderId: 'active-folder',
});
return (
<Dropdown items={items} {...props}>
<Dropdown items={getItems} {...props}>
{children}
</Dropdown>
);

View File

@@ -2,6 +2,7 @@ import { useNavigate } from '@tanstack/react-router';
import classNames from 'classnames';
import { useCallback, useMemo, useRef } from 'react';
import { useKeyPressEvent } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { getActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHotKey } from '../hooks/useHotKey';
@@ -15,12 +16,11 @@ import { Dropdown } from './core/Dropdown';
import { HttpMethodTag } from './core/HttpMethodTag';
interface Props {
activeRequestId: string | null;
activeRequestName: string;
className?: string;
}
export function RecentRequestsDropdown({ className, activeRequestId, activeRequestName }: Props) {
export function RecentRequestsDropdown({ className }: Props) {
const activeRequest = useActiveRequest();
const dropdownRef = useRef<DropdownRef>(null);
const [allRecentRequestIds] = useRecentRequests();
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
@@ -93,10 +93,10 @@ export function RecentRequestsDropdown({ className, activeRequestId, activeReque
className={classNames(
className,
'truncate pointer-events-auto',
activeRequestId === null && 'text-text-subtlest italic',
activeRequest == null && 'text-text-subtlest italic',
)}
>
{activeRequestName}
{fallbackRequestName(activeRequest)}
</Button>
</Dropdown>
);

View File

@@ -1,20 +1,24 @@
import type { HttpRequest } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { atom, useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { useLocalStorage } from 'react-use';
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useImportCurl } from '../hooks/useImportCurl';
import { useImportQuerystring } from '../hooks/useImportQuerystring';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
import { useRequests } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useToast } from '../hooks/useToast';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { deepEqualAtom } from '../lib/atoms';
import { languageFromContentType } from '../lib/contentType';
import { tryFormatJson } from '../lib/formatters';
import { generateId } from '../lib/generateId';
@@ -67,15 +71,24 @@ const TAB_HEADERS = 'headers';
const TAB_AUTH = 'auth';
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({
style,
fullHeight,
className,
activeRequest,
}: Props) {
const requests = useRequests();
const activeRequestId = activeRequest.id;
const updateRequest = useUpdateAnyHttpRequest();
const { mutateAsync: updateRequestAsync, mutate: updateRequest } = useUpdateAnyHttpRequest();
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
'requestPaneActiveTabs',
{},
@@ -102,12 +115,12 @@ export const RequestPane = memo(function RequestPane({
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
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
},
[activeRequest, updateRequest],
[activeRequest, updateRequestAsync],
);
const toast = useToast();
@@ -206,7 +219,7 @@ export const RequestPane = memo(function RequestPane({
showMethodToast(patch.method);
}
await updateRequest.mutateAsync({ id: activeRequestId, update: patch });
await updateRequestAsync({ id: activeRequestId, update: patch });
if (newContentType !== undefined) {
await handleContentTypeChange(newContentType);
@@ -247,7 +260,7 @@ export const RequestPane = memo(function RequestPane({
token: authentication.token ?? '',
};
}
await updateRequest.mutateAsync({
updateRequest({
id: activeRequestId,
update: { authenticationType, authentication },
});
@@ -267,25 +280,26 @@ export const RequestPane = memo(function RequestPane({
numParams,
toast,
updateRequest,
updateRequestAsync,
urlParameterPairs.length,
],
);
const sendRequest = useSendAnyHttpRequest();
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
const cancelResponse = useCancelHttpResponse(activeResponse?.id ?? null);
const isLoading = useIsResponseLoading(activeRequestId);
const { mutate: sendRequest } = useSendAnyHttpRequest();
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
const { updateKey } = useRequestUpdateKey(activeRequestId);
const importCurl = useImportCurl();
const importQuerystring = useImportQuerystring(activeRequestId);
const { mutate: importCurl } = useImportCurl();
const { mutate: importQuerystring } = useImportQuerystring(activeRequestId);
const handleBodyChange = useCallback(
(body: HttpRequest['body']) => updateRequest.mutate({ id: activeRequestId, update: { body } }),
(body: HttpRequest['body']) => updateRequest({ id: activeRequestId, update: { body } }),
[activeRequestId, updateRequest],
);
const handleBodyTextChange = useCallback(
(text: string) => updateRequest.mutate({ id: activeRequestId, update: { body: { text } } }),
(text: string) => updateRequest({ id: activeRequestId, update: { body: { text } } }),
[activeRequestId, updateRequest],
);
@@ -301,20 +315,48 @@ export const RequestPane = memo(function RequestPane({
setActiveTab(TAB_PARAMS);
});
const autocomplete: GenericCompletionConfig = {
minMatch: 3,
options:
requests.length > 0
? [
...requests
.filter((r) => r.id !== activeRequestId)
.map((r): GenericCompletionOption => ({ type: 'constant', label: r.url })),
]
: [
{ label: 'http://', type: 'constant' },
{ label: 'https://', type: 'constant' },
],
};
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
const autocomplete: GenericCompletionConfig = useMemo(
() => ({
minMatch: 3,
options:
autocompleteUrls.length > 0
? autocompleteUrls
: [
{ label: 'http://', 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 (
<div
@@ -329,23 +371,12 @@ export const RequestPane = memo(function RequestPane({
url={activeRequest.url}
method={activeRequest.method}
placeholder="https://example.com"
onPasteOverwrite={(text) => {
if (text.startsWith('curl ')) {
importCurl.mutate({ overwriteRequestId: activeRequestId, command: text });
} else {
// Only import query if pasted text contains entire querystring
importQuerystring.mutate(text);
}
}}
onPasteOverwrite={handlePaste}
autocomplete={autocomplete}
onSend={() => sendRequest.mutateAsync(activeRequest.id ?? null)}
onCancel={cancelResponse.mutate}
onMethodChange={(method) =>
updateRequest.mutate({ id: activeRequestId, update: { method } })
}
onUrlChange={(url: string) =>
updateRequest.mutate({ id: activeRequestId, update: { url } })
}
onSend={handleSend}
onCancel={cancelResponse}
onMethodChange={handleMethodChange}
onUrlChange={handleUrlChange}
forceUpdateKey={updateKey}
isLoading={isLoading}
/>
@@ -372,9 +403,7 @@ export const RequestPane = memo(function RequestPane({
<HeadersEditor
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
request={activeRequest}
onChange={(headers) =>
updateRequest.mutate({ id: activeRequestId, update: { headers } })
}
onChange={(headers) => updateRequest({ id: activeRequestId, update: { headers } })}
/>
</TabContent>
<TabContent value={TAB_PARAMS}>
@@ -383,7 +412,7 @@ export const RequestPane = memo(function RequestPane({
forceUpdateKey={forceUpdateKey + urlParametersKey}
pairs={urlParameterPairs}
onChange={(urlParameters) =>
updateRequest.mutate({ id: activeRequestId, update: { urlParameters } })
updateRequest({ id: activeRequestId, update: { urlParameters } })
}
/>
</TabContent>
@@ -437,9 +466,7 @@ export const RequestPane = memo(function RequestPane({
requestId={activeRequest.id}
contentType={contentType}
body={activeRequest.body}
onChange={(body) =>
updateRequest.mutate({ id: activeRequestId, update: { body } })
}
onChange={(body) => updateRequest({ id: activeRequestId, update: { body } })}
onChangeContentType={handleContentTypeChange}
/>
) : typeof activeRequest.bodyType === 'string' ? (
@@ -467,9 +494,7 @@ export const RequestPane = memo(function RequestPane({
className="font-sans !text-xl !px-0"
containerClassName="border-0"
placeholder={activeRequest.id}
onChange={(name) =>
updateRequest.mutate({ id: activeRequestId, update: { name } })
}
onChange={(name) => updateRequest({ id: activeRequestId, update: { name } })}
/>
<MarkdownEditor
name="request-description"
@@ -477,7 +502,7 @@ export const RequestPane = memo(function RequestPane({
defaultValue={activeRequest.description}
stateKey={`description.${activeRequest.id}`}
onChange={(description) =>
updateRequest.mutate({ id: activeRequestId, update: { description } })
updateRequest({ id: activeRequestId, update: { description } })
}
/>
</div>

View File

@@ -317,7 +317,7 @@ export function Sidebar({ className }: Props) {
setShowMainContextMenu({ x: e.clientX, y: e.clientY });
}, []);
const mainContextMenuItems = useCreateDropdownItems();
const mainContextMenuItems = useCreateDropdownItems({ folderId: null });
// Not ready to render yet
if (tree == null) {

View File

@@ -1,13 +1,12 @@
import deepEqual from '@gilbarbara/deep-equal';
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
import { atom } from 'jotai';
import { selectAtom } from 'jotai/utils';
import { activeWorkspaceAtom } from '../hooks/useActiveWorkspace';
import { foldersAtom } from '../hooks/useFolders';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { deepEqualAtom } from '../lib/atoms';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import type { SidebarTreeNode } from './Sidebar';
@@ -27,11 +26,7 @@ const allPotentialChildrenAtom = atom((get) => {
}));
});
const memoAllPotentialChildrenAtom = selectAtom(
allPotentialChildrenAtom,
(v) => v,
(a, b) => deepEqual(a, b),
);
const memoAllPotentialChildrenAtom = deepEqualAtom(allPotentialChildrenAtom);
export const sidebarTreeAtom = atom<{
tree: SidebarTreeNode | null;

View File

@@ -1,4 +1,4 @@
import React, { useMemo } from 'react';
import React, { useCallback } from 'react';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
@@ -42,7 +42,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
folderId: child.model === 'folder' ? child.id : null,
});
const items = useMemo<DropdownItem[]>(() => {
const items = useCallback((): DropdownItem[] => {
if (child.model === 'folder') {
return [
{
@@ -77,7 +77,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
onSelect: () => deleteFolder.mutate(),
},
{ type: 'separator' },
...createDropdownItems,
...createDropdownItems(),
];
} else {
const requestItems: DropdownItem[] =

View File

@@ -1,8 +1,6 @@
import classNames from 'classnames';
import React, { memo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { CookieDropdown } from './CookieDropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
@@ -21,7 +19,6 @@ interface Props {
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const togglePalette = useToggleCommandPalette();
const activeRequest = useActiveRequest();
return (
<div
@@ -40,10 +37,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
</HStack>
</HStack>
<div className="pointer-events-none w-full max-w-[30vw] mx-auto flex justify-center">
<RecentRequestsDropdown
activeRequestId={activeRequest?.id ?? null}
activeRequestName={fallbackRequestName(activeRequest)}
/>
<RecentRequestsDropdown />
</div>
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none pr-1">
<LicenseBadge />

View File

@@ -176,7 +176,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
interface ContextMenuProps {
triggerPosition: { x: number; y: number } | null;
className?: string;
items: DropdownItem[];
items: DropdownProps['items'];
onClose: () => void;
}
@@ -201,7 +201,7 @@ export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function Co
isOpen={true} // Always open because we return null if not
className={className}
ref={ref}
items={items}
items={typeof items === 'function' ? items() : items}
onClose={onClose}
triggerShape={triggerShape}
/>

View File

@@ -67,7 +67,7 @@ export function Tabs({
ref={ref}
className={classNames(
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

View File

@@ -1,7 +1,6 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
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 { grpcRequestsAtom } from './useGrpcRequests';
import { httpRequestsAtom } from './useHttpRequests';
@@ -21,11 +20,6 @@ export function getActiveRequest() {
return jotaiStore.get(activeRequestAtom);
}
export const activeRequestNameAtom = atom(get => {
const activeRequest = get(activeRequestAtom);
return fallbackRequestName(activeRequest);
});
export function useActiveRequest<T extends keyof TypeMap>(
model?: T | undefined,
): TypeMap[T] | null {

View File

@@ -1,8 +1,9 @@
import { useMemo } from 'react';
import { useCallback } from 'react';
import type { DropdownItem } from '../components/core/Dropdown';
import { Icon } from '../components/core/Icon';
import { generateId } from '../lib/generateId';
import { BODY_TYPE_GRAPHQL } from '../lib/model_util';
import { getActiveRequest } from './useActiveRequest';
import { useCreateFolder } from './useCreateFolder';
import { useCreateGrpcRequest } from './useCreateGrpcRequest';
import { useCreateHttpRequest } from './useCreateHttpRequest';
@@ -14,19 +15,26 @@ export function useCreateDropdownItems({
}: {
hideFolder?: boolean;
hideIcons?: boolean;
folderId?: string | null;
} = {}): DropdownItem[] {
folderId?: string | null | 'active-folder';
} = {}): () => DropdownItem[] {
const { mutate: createHttpRequest } = useCreateHttpRequest();
const { mutate: createGrpcRequest } = useCreateGrpcRequest();
const { mutate: createFolder } = useCreateFolder();
return useMemo<DropdownItem[]>(
() => [
return useCallback(
(): DropdownItem[] => [
{
key: 'create-http-request',
label: 'HTTP Request',
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',

11
src-web/lib/atoms.ts Normal file
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),
);
}