mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-17 23:13:51 +01: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 { 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>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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[] =
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
11
src-web/lib/atoms.ts
Normal 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),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user