mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 01:28:35 +02:00
Consolidate tab persistence logic into Tabs component
- Move active tab persistence into Tabs component with storageKey + activeTabKey props - Change value prop to defaultValue so callers don't manage tab state - Add TabsRef with setActiveTab method for programmatic tab switching - Restore request_pane.focus_tab listener for :param placeholder clicks - Update all Tab consumers to use new pattern Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
|
import { createWorkspaceModel, foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useMemo, useState } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useAuthTab } from '../hooks/useAuthTab';
|
import { useAuthTab } from '../hooks/useAuthTab';
|
||||||
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
import { useEnvironmentsBreakdown } from '../hooks/useEnvironmentsBreakdown';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
@@ -37,7 +37,6 @@ export type FolderSettingsTab =
|
|||||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||||
const folders = useAtomValue(foldersAtom);
|
const folders = useAtomValue(foldersAtom);
|
||||||
const folder = folders.find((f) => f.id === folderId) ?? null;
|
const folder = folders.find((f) => f.id === folderId) ?? null;
|
||||||
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
|
|
||||||
const authTab = useAuthTab(TAB_AUTH, folder);
|
const authTab = useAuthTab(TAB_AUTH, folder);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
||||||
const inheritedHeaders = useInheritedHeaders(folder);
|
const inheritedHeaders = useInheritedHeaders(folder);
|
||||||
@@ -69,8 +68,7 @@ export function FolderSettingsDialog({ folderId, tab }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
defaultValue={tab ?? TAB_GENERAL}
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
label="Folder Settings"
|
label="Folder Settings"
|
||||||
className="pt-2 pb-2 pl-3 pr-1"
|
className="pt-2 pb-2 pl-3 pr-1"
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { useContainerSize } from '../hooks/useContainerQuery';
|
|||||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||||
import { Button } from './core/Button';
|
import { Button } from './core/Button';
|
||||||
@@ -69,11 +68,6 @@ export function GrpcRequestPane({
|
|||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
|
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
|
|
||||||
namespace: 'no_sync',
|
|
||||||
key: 'grpcRequestActiveTabs',
|
|
||||||
fallback: {},
|
|
||||||
});
|
|
||||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
|
|
||||||
const urlContainerEl = useRef<HTMLDivElement>(null);
|
const urlContainerEl = useRef<HTMLDivElement>(null);
|
||||||
@@ -145,14 +139,6 @@ export function GrpcRequestPane({
|
|||||||
[activeRequest.description, authTab, metadataTab],
|
[activeRequest.description, authTab, metadataTab],
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeTab = activeTabs?.[activeRequest.id];
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
async (tab: string) => {
|
|
||||||
await setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
|
||||||
},
|
|
||||||
[activeRequest.id, setActiveTabs],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleMetadataChange = useCallback(
|
const handleMetadataChange = useCallback(
|
||||||
(metadata: HttpRequestHeader[]) => patchModel(activeRequest, { metadata }),
|
(metadata: HttpRequestHeader[]) => patchModel(activeRequest, { metadata }),
|
||||||
[activeRequest],
|
[activeRequest],
|
||||||
@@ -265,12 +251,11 @@ export function GrpcRequestPane({
|
|||||||
</HStack>
|
</HStack>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
|
||||||
label="Request"
|
label="Request"
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-1 !mb-1.5"
|
tabListClassName="mt-1 !mb-1.5"
|
||||||
storageKey="grpc_request_tabs_order"
|
storageKey="grpc_request_tabs"
|
||||||
|
activeTabKey={activeRequest.id}
|
||||||
>
|
>
|
||||||
<TabContent value="message">
|
<TabContent value="message">
|
||||||
<GrpcEditor
|
<GrpcEditor
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useMemo, useState } from 'react';
|
import { lazy, Suspense, useCallback, useMemo, useRef, useState } from 'react';
|
||||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||||
import { allRequestsAtom } from '../hooks/useAllRequests';
|
import { allRequestsAtom } from '../hooks/useAllRequests';
|
||||||
import { useAuthTab } from '../hooks/useAuthTab';
|
import { useAuthTab } from '../hooks/useAuthTab';
|
||||||
@@ -12,7 +12,6 @@ import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
|||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useImportCurl } from '../hooks/useImportCurl';
|
import { useImportCurl } from '../hooks/useImportCurl';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
@@ -42,8 +41,8 @@ import { Editor } from './core/Editor/LazyEditor';
|
|||||||
import { InlineCode } from './core/InlineCode';
|
import { InlineCode } from './core/InlineCode';
|
||||||
import type { Pair } from './core/PairEditor';
|
import type { Pair } from './core/PairEditor';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import type { TabItem } from './core/Tabs/Tabs';
|
import type { TabItem, TabsRef } from './core/Tabs/Tabs';
|
||||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
import { setActiveTab, TabContent, Tabs } from './core/Tabs/Tabs';
|
||||||
import { EmptyStateText } from './EmptyStateText';
|
import { EmptyStateText } from './EmptyStateText';
|
||||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||||
@@ -70,6 +69,7 @@ const TAB_PARAMS = 'params';
|
|||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
const TAB_DESCRIPTION = 'description';
|
const TAB_DESCRIPTION = 'description';
|
||||||
|
const TABS_STORAGE_KEY = 'http_request_tabs';
|
||||||
|
|
||||||
const nonActiveRequestUrlsAtom = atom((get) => {
|
const nonActiveRequestUrlsAtom = atom((get) => {
|
||||||
const activeRequestId = get(activeRequestIdAtom);
|
const activeRequestId = get(activeRequestIdAtom);
|
||||||
@@ -83,19 +83,20 @@ const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
|
|||||||
|
|
||||||
export function HttpRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
export function HttpRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
||||||
const activeRequestId = activeRequest.id;
|
const activeRequestId = activeRequest.id;
|
||||||
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
|
const tabsRef = useRef<TabsRef>(null);
|
||||||
namespace: 'no_sync',
|
|
||||||
key: 'httpRequestActiveTabs',
|
|
||||||
fallback: {},
|
|
||||||
});
|
|
||||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
const [{ urlKey }, { forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||||
const contentType = getContentTypeFromHeaders(activeRequest.headers);
|
const contentType = getContentTypeFromHeaders(activeRequest.headers);
|
||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
|
|
||||||
|
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||||
|
useRequestEditorEvent('request_pane.focus_tab', () => {
|
||||||
|
tabsRef.current?.setActiveTab(TAB_PARAMS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleContentTypeChange = useCallback(
|
const handleContentTypeChange = useCallback(
|
||||||
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
|
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
|
||||||
if (activeRequest == null) {
|
if (activeRequest == null) {
|
||||||
@@ -260,18 +261,6 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
[activeRequest],
|
[activeRequest],
|
||||||
);
|
);
|
||||||
|
|
||||||
const activeTab = activeTabs?.[activeRequestId];
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
async (tab: string) => {
|
|
||||||
await setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
|
||||||
},
|
|
||||||
[activeRequest.id, setActiveTabs],
|
|
||||||
);
|
|
||||||
|
|
||||||
useRequestEditorEvent('request_pane.focus_tab', async () => {
|
|
||||||
await setActiveTab(TAB_PARAMS);
|
|
||||||
});
|
|
||||||
|
|
||||||
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||||
|
|
||||||
const autocomplete: GenericCompletionConfig = useMemo(
|
const autocomplete: GenericCompletionConfig = useMemo(
|
||||||
@@ -298,7 +287,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
e.preventDefault(); // Prevent input onChange
|
e.preventDefault(); // Prevent input onChange
|
||||||
|
|
||||||
await patchModel(activeRequest, patch);
|
await patchModel(activeRequest, patch);
|
||||||
focusParamsTab();
|
await setActiveTab({
|
||||||
|
storageKey: TABS_STORAGE_KEY,
|
||||||
|
activeTabKey: activeRequestId,
|
||||||
|
value: TAB_PARAMS,
|
||||||
|
});
|
||||||
|
|
||||||
// Wait for request to update, then refresh the UI
|
// Wait for request to update, then refresh the UI
|
||||||
// TODO: Somehow make this deterministic
|
// TODO: Somehow make this deterministic
|
||||||
@@ -309,14 +302,7 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh, importCurl],
|
||||||
activeRequest,
|
|
||||||
activeRequestId,
|
|
||||||
focusParamsTab,
|
|
||||||
forceParamsRefresh,
|
|
||||||
forceUrlRefresh,
|
|
||||||
importCurl,
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
const handleSend = useCallback(
|
const handleSend = useCallback(
|
||||||
() => sendRequest(activeRequest.id ?? null),
|
() => sendRequest(activeRequest.id ?? null),
|
||||||
@@ -354,12 +340,12 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
|||||||
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
|
isLoading={activeResponse != null && activeResponse.state !== 'closed'}
|
||||||
/>
|
/>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
ref={tabsRef}
|
||||||
label="Request"
|
label="Request"
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-1 -mb-1.5"
|
tabListClassName="mt-1 -mb-1.5"
|
||||||
storageKey="http_request_tabs_order"
|
storageKey={TABS_STORAGE_KEY}
|
||||||
|
activeTabKey={activeRequestId}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_AUTH}>
|
<TabContent value={TAB_AUTH}>
|
||||||
<HttpAuthenticationEditor model={activeRequest} />
|
<HttpAuthenticationEditor model={activeRequest} />
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ComponentType, CSSProperties } from 'react';
|
import type { ComponentType, CSSProperties } from 'react';
|
||||||
import { lazy, Suspense, useCallback, useMemo } from 'react';
|
import { lazy, Suspense, useMemo } from 'react';
|
||||||
import { useLocalStorage } from 'react-use';
|
|
||||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||||
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
|
import { useHttpResponseEvents } from '../hooks/useHttpResponseEvents';
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
@@ -58,10 +57,6 @@ const TAB_TIMELINE = 'timeline';
|
|||||||
export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
||||||
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId);
|
const { activeResponse, setPinnedResponseId, responses } = usePinnedHttpResponse(activeRequestId);
|
||||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||||
const [activeTabs, setActiveTabs] = useLocalStorage<Record<string, string>>(
|
|
||||||
'responsePaneActiveTabs',
|
|
||||||
{},
|
|
||||||
);
|
|
||||||
const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null);
|
const contentType = getContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||||
const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence;
|
const mimeType = contentType == null ? null : getMimeTypeFromContentType(contentType).essence;
|
||||||
|
|
||||||
@@ -129,13 +124,6 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
viewMode,
|
viewMode,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
const activeTab = activeTabs?.[activeRequestId];
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
(tab: string) => {
|
|
||||||
setActiveTabs((r) => ({ ...r, [activeRequestId]: tab }));
|
|
||||||
},
|
|
||||||
[activeRequestId, setActiveTabs],
|
|
||||||
);
|
|
||||||
|
|
||||||
const cancel = useCancelHttpResponse(activeResponse?.id ?? null);
|
const cancel = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
|
|
||||||
@@ -199,14 +187,12 @@ export function HttpResponsePane({ style, className, activeRequestId }: Props) {
|
|||||||
)}
|
)}
|
||||||
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
{/* Show tabs if we have any data (headers, body, etc.) even if there's an error */}
|
||||||
<Tabs
|
<Tabs
|
||||||
key={activeRequestId} // Freshen tabs on request change
|
|
||||||
value={activeTab}
|
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
label="Response"
|
label="Response"
|
||||||
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
className="ml-3 mr-3 mb-3 min-h-0 flex-1"
|
||||||
tabListClassName="mt-0.5 -mb-1.5"
|
tabListClassName="mt-0.5 -mb-1.5"
|
||||||
storageKey="http_response_tabs_order"
|
storageKey="http_response_tabs"
|
||||||
|
activeTabKey={activeRequestId}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_BODY}>
|
<TabContent value={TAB_BODY}>
|
||||||
<ErrorBoundary name="Http Response Viewer">
|
<ErrorBoundary name="Http Response Viewer">
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useLicense } from '@yaakapp-internal/license';
|
|||||||
import { pluginsAtom, settingsAtom } from '@yaakapp-internal/models';
|
import { pluginsAtom, settingsAtom } from '@yaakapp-internal/models';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useKeyPressEvent } from 'react-use';
|
import { useKeyPressEvent } from 'react-use';
|
||||||
import { appInfo } from '../../lib/appInfo';
|
import { appInfo } from '../../lib/appInfo';
|
||||||
import { capitalize } from '../../lib/capitalize';
|
import { capitalize } from '../../lib/capitalize';
|
||||||
@@ -51,7 +50,6 @@ export default function Settings({ hide }: Props) {
|
|||||||
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
|
const { tab: tabFromQuery } = useSearch({ from: '/workspaces/$workspaceId/settings' });
|
||||||
// Parse tab and subtab (e.g., "plugins:installed")
|
// Parse tab and subtab (e.g., "plugins:installed")
|
||||||
const [mainTab, subtab] = tabFromQuery?.split(':') ?? [];
|
const [mainTab, subtab] = tabFromQuery?.split(':') ?? [];
|
||||||
const [tab, setTab] = useState<string | undefined>(mainTab || tabFromQuery);
|
|
||||||
const settings = useAtomValue(settingsAtom);
|
const settings = useAtomValue(settingsAtom);
|
||||||
const plugins = useAtomValue(pluginsAtom);
|
const plugins = useAtomValue(pluginsAtom);
|
||||||
const licenseCheck = useLicense();
|
const licenseCheck = useLicense();
|
||||||
@@ -91,11 +89,10 @@ export default function Settings({ hide }: Props) {
|
|||||||
)}
|
)}
|
||||||
<Tabs
|
<Tabs
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
value={tab}
|
defaultValue={mainTab || tabFromQuery}
|
||||||
addBorders
|
addBorders
|
||||||
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3"
|
tabListClassName="min-w-[10rem] bg-surface x-theme-sidebar border-r border-border pl-3"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
onChangeValue={setTab}
|
|
||||||
tabs={tabs.map(
|
tabs={tabs.map(
|
||||||
(value): TabItem => ({
|
(value): TabItem => ({
|
||||||
value,
|
value,
|
||||||
@@ -145,7 +142,7 @@ export default function Settings({ hide }: Props) {
|
|||||||
<SettingsHotkeys />
|
<SettingsHotkeys />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
|
<TabContent value={TAB_PLUGINS} className="h-full grid grid-rows-1 px-6 !py-4">
|
||||||
<SettingsPlugins defaultSubtab={tab === TAB_PLUGINS ? subtab : undefined} />
|
<SettingsPlugins defaultSubtab={mainTab === TAB_PLUGINS ? subtab : undefined} />
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
<TabContent value={TAB_PROXY} className="overflow-y-auto h-full px-6 !py-4">
|
||||||
<SettingsProxy />
|
<SettingsProxy />
|
||||||
|
|||||||
@@ -54,13 +54,11 @@ export function SettingsPlugins({ defaultSubtab }: SettingsPluginsProps) {
|
|||||||
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
|
const installedPlugins = plugins.filter((p) => !isPluginBundled(p, appInfo.vendoredPluginDir));
|
||||||
const createPlugin = useInstallPlugin();
|
const createPlugin = useInstallPlugin();
|
||||||
const refreshPlugins = useRefreshPlugins();
|
const refreshPlugins = useRefreshPlugins();
|
||||||
const [tab, setTab] = useState<string | undefined>(defaultSubtab);
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full">
|
<div className="h-full">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tab}
|
defaultValue={defaultSubtab}
|
||||||
label="Plugins"
|
label="Plugins"
|
||||||
onChangeValue={setTab}
|
|
||||||
addBorders
|
addBorders
|
||||||
tabs={[
|
tabs={[
|
||||||
{ label: 'Discover', value: 'search' },
|
{ label: 'Discover', value: 'search' },
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-intern
|
|||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { atom, useAtomValue } from 'jotai';
|
import { atom, useAtomValue } from 'jotai';
|
||||||
import type { CSSProperties } from 'react';
|
import type { CSSProperties } from 'react';
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo, useRef } from 'react';
|
||||||
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
|
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||||
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||||
@@ -14,7 +14,6 @@ import { useAuthTab } from '../hooks/useAuthTab';
|
|||||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
|
||||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||||
import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection';
|
import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection';
|
||||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||||
@@ -30,8 +29,8 @@ import { Editor } from './core/Editor/LazyEditor';
|
|||||||
import { IconButton } from './core/IconButton';
|
import { IconButton } from './core/IconButton';
|
||||||
import type { Pair } from './core/PairEditor';
|
import type { Pair } from './core/PairEditor';
|
||||||
import { PlainInput } from './core/PlainInput';
|
import { PlainInput } from './core/PlainInput';
|
||||||
import type { TabItem } from './core/Tabs/Tabs';
|
import type { TabItem, TabsRef } from './core/Tabs/Tabs';
|
||||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
import { setActiveTab, TabContent, Tabs } from './core/Tabs/Tabs';
|
||||||
import { HeadersEditor } from './HeadersEditor';
|
import { HeadersEditor } from './HeadersEditor';
|
||||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||||
import { MarkdownEditor } from './MarkdownEditor';
|
import { MarkdownEditor } from './MarkdownEditor';
|
||||||
@@ -50,6 +49,7 @@ const TAB_PARAMS = 'params';
|
|||||||
const TAB_HEADERS = 'headers';
|
const TAB_HEADERS = 'headers';
|
||||||
const TAB_AUTH = 'auth';
|
const TAB_AUTH = 'auth';
|
||||||
const TAB_DESCRIPTION = 'description';
|
const TAB_DESCRIPTION = 'description';
|
||||||
|
const TABS_STORAGE_KEY = 'websocket_request_tabs';
|
||||||
|
|
||||||
const nonActiveRequestUrlsAtom = atom((get) => {
|
const nonActiveRequestUrlsAtom = atom((get) => {
|
||||||
const activeRequestId = get(activeRequestIdAtom);
|
const activeRequestId = get(activeRequestIdAtom);
|
||||||
@@ -63,17 +63,18 @@ const memoNotActiveRequestUrlsAtom = deepEqualAtom(nonActiveRequestUrlsAtom);
|
|||||||
|
|
||||||
export function WebsocketRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
export function WebsocketRequestPane({ style, fullHeight, className, activeRequest }: Props) {
|
||||||
const activeRequestId = activeRequest.id;
|
const activeRequestId = activeRequest.id;
|
||||||
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
|
const tabsRef = useRef<TabsRef>(null);
|
||||||
namespace: 'no_sync',
|
|
||||||
key: 'websocketRequestActiveTabs',
|
|
||||||
fallback: {},
|
|
||||||
});
|
|
||||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id);
|
const forceUpdateKey = useRequestUpdateKey(activeRequest.id);
|
||||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
const [{ urlKey }, { forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||||
|
|
||||||
|
// Listen for event to focus the params tab (e.g., when clicking a :param in the URL)
|
||||||
|
useRequestEditorEvent('request_pane.focus_tab', () => {
|
||||||
|
tabsRef.current?.setActiveTab(TAB_PARAMS);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
||||||
(m) => m[1] ?? '',
|
(m) => m[1] ?? '',
|
||||||
@@ -115,18 +116,6 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||||
const connection = useAtomValue(activeWebsocketConnectionAtom);
|
const connection = useAtomValue(activeWebsocketConnectionAtom);
|
||||||
|
|
||||||
const activeTab = activeTabs?.[activeRequestId];
|
|
||||||
const setActiveTab = useCallback(
|
|
||||||
async (tab: string) => {
|
|
||||||
await setActiveTabs((r) => ({ ...r, [activeRequest.id]: tab }));
|
|
||||||
},
|
|
||||||
[activeRequest.id, setActiveTabs],
|
|
||||||
);
|
|
||||||
|
|
||||||
useRequestEditorEvent('request_pane.focus_tab', async () => {
|
|
||||||
await setActiveTab(TAB_PARAMS);
|
|
||||||
});
|
|
||||||
|
|
||||||
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
const autocompleteUrls = useAtomValue(memoNotActiveRequestUrlsAtom);
|
||||||
|
|
||||||
const autocomplete: GenericCompletionConfig = useMemo(
|
const autocomplete: GenericCompletionConfig = useMemo(
|
||||||
@@ -176,7 +165,11 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
e.preventDefault(); // Prevent input onChange
|
e.preventDefault(); // Prevent input onChange
|
||||||
|
|
||||||
await patchModel(activeRequest, patch);
|
await patchModel(activeRequest, patch);
|
||||||
focusParamsTab();
|
await setActiveTab({
|
||||||
|
storageKey: TABS_STORAGE_KEY,
|
||||||
|
activeTabKey: activeRequestId,
|
||||||
|
value: TAB_PARAMS,
|
||||||
|
});
|
||||||
|
|
||||||
// Wait for request to update, then refresh the UI
|
// Wait for request to update, then refresh the UI
|
||||||
// TODO: Somehow make this deterministic
|
// TODO: Somehow make this deterministic
|
||||||
@@ -186,7 +179,7 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[activeRequest, focusParamsTab, forceParamsRefresh, forceUrlRefresh],
|
[activeRequest, activeRequestId, forceParamsRefresh, forceUrlRefresh],
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageLanguage = languageFromContentType(null, activeRequest.message);
|
const messageLanguage = languageFromContentType(null, activeRequest.message);
|
||||||
@@ -229,12 +222,12 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
ref={tabsRef}
|
||||||
label="Request"
|
label="Request"
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
tabs={tabs}
|
tabs={tabs}
|
||||||
tabListClassName="mt-1 !mb-1.5"
|
tabListClassName="mt-1 !mb-1.5"
|
||||||
storageKey="websocket_request_tabs_order"
|
storageKey={TABS_STORAGE_KEY}
|
||||||
|
activeTabKey={activeRequestId}
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_AUTH}>
|
<TabContent value={TAB_AUTH}>
|
||||||
<HttpAuthenticationEditor model={activeRequest} />
|
<HttpAuthenticationEditor model={activeRequest} />
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
|
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
|
||||||
import { useAtomValue } from 'jotai';
|
import { useAtomValue } from 'jotai';
|
||||||
import { useState } from 'react';
|
|
||||||
import { useAuthTab } from '../hooks/useAuthTab';
|
import { useAuthTab } from '../hooks/useAuthTab';
|
||||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||||
@@ -45,7 +44,6 @@ const DEFAULT_TAB: WorkspaceSettingsTab = TAB_GENERAL;
|
|||||||
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||||
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
|
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
|
||||||
const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId);
|
const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId);
|
||||||
const [activeTab, setActiveTab] = useState<string>(tab ?? DEFAULT_TAB);
|
|
||||||
const authTab = useAuthTab(TAB_AUTH, workspace ?? null);
|
const authTab = useAuthTab(TAB_AUTH, workspace ?? null);
|
||||||
const headersTab = useHeadersTab(TAB_HEADERS, workspace ?? null);
|
const headersTab = useHeadersTab(TAB_HEADERS, workspace ?? null);
|
||||||
const inheritedHeaders = useInheritedHeaders(workspace ?? null);
|
const inheritedHeaders = useInheritedHeaders(workspace ?? null);
|
||||||
@@ -67,8 +65,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab}
|
defaultValue={tab ?? DEFAULT_TAB}
|
||||||
onChangeValue={setActiveTab}
|
|
||||||
label="Folder Settings"
|
label="Folder Settings"
|
||||||
className="pt-4 pb-2 px-3"
|
className="pt-4 pb-2 px-3"
|
||||||
tabListClassName="pl-4"
|
tabListClassName="pl-4"
|
||||||
@@ -90,7 +87,7 @@ export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
|||||||
) : null,
|
) : null,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
storageKey="workspace_settings_tabs_order"
|
storageKey="workspace_settings_tabs"
|
||||||
>
|
>
|
||||||
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
<TabContent value={TAB_AUTH} className="overflow-y-auto h-full px-4">
|
||||||
<HttpAuthenticationEditor model={workspace} />
|
<HttpAuthenticationEditor model={workspace} />
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
useSensors,
|
useSensors,
|
||||||
} from '@dnd-kit/core';
|
} from '@dnd-kit/core';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode, Ref } from 'react';
|
||||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
|
||||||
import { useKeyValue } from '../../../hooks/useKeyValue';
|
import { useKeyValue } from '../../../hooks/useKeyValue';
|
||||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||||
import { DropMarker } from '../../DropMarker';
|
import { DropMarker } from '../../DropMarker';
|
||||||
@@ -37,22 +37,37 @@ export type TabItem =
|
|||||||
rightSlot?: ReactNode;
|
rightSlot?: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
interface TabsStorage {
|
||||||
|
order: string[];
|
||||||
|
activeTabs: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TabsRef {
|
||||||
|
/** Programmatically set the active tab */
|
||||||
|
setActiveTab: (value: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
label: string;
|
label: string;
|
||||||
value?: string;
|
/** Default tab value. If not provided, defaults to first tab. */
|
||||||
onChangeValue: (value: string) => void;
|
defaultValue?: string;
|
||||||
|
/** Called when active tab changes */
|
||||||
|
onChangeValue?: (value: string) => void;
|
||||||
tabs: TabItem[];
|
tabs: TabItem[];
|
||||||
tabListClassName?: string;
|
tabListClassName?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
addBorders?: boolean;
|
addBorders?: boolean;
|
||||||
layout?: 'horizontal' | 'vertical';
|
layout?: 'horizontal' | 'vertical';
|
||||||
|
/** Storage key for persisting tab order and active tab. When provided, enables drag-to-reorder and active tab persistence. */
|
||||||
storageKey?: string | string[];
|
storageKey?: string | string[];
|
||||||
|
/** Key to identify which context this tab belongs to (e.g., request ID). Used for per-context active tab persistence. */
|
||||||
|
activeTabKey?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Tabs({
|
export const Tabs = forwardRef<TabsRef, Props>(function Tabs({
|
||||||
value,
|
defaultValue,
|
||||||
onChangeValue,
|
onChangeValue: onChangeValueProp,
|
||||||
label,
|
label,
|
||||||
children,
|
children,
|
||||||
tabs: originalTabs,
|
tabs: originalTabs,
|
||||||
@@ -61,17 +76,74 @@ export function Tabs({
|
|||||||
addBorders,
|
addBorders,
|
||||||
layout = 'vertical',
|
layout = 'vertical',
|
||||||
storageKey,
|
storageKey,
|
||||||
}: Props) {
|
activeTabKey,
|
||||||
|
}: Props, forwardedRef: Ref<TabsRef>) {
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const reorderable = !!storageKey;
|
const reorderable = !!storageKey;
|
||||||
|
|
||||||
// Use key-value storage for persistence if storageKey is provided
|
// Use key-value storage for persistence if storageKey is provided
|
||||||
const { value: savedOrder, set: setSavedOrder } = useKeyValue<string[]>({
|
// Handle migration from old format (string[]) to new format (TabsStorage)
|
||||||
namespace: 'global',
|
const { value: rawStorage, set: setStorage } = useKeyValue<TabsStorage | string[]>({
|
||||||
key: storageKey ?? ['tabs_order', 'default'],
|
namespace: 'no_sync',
|
||||||
fallback: [],
|
key: storageKey ?? ['tabs', 'default'],
|
||||||
|
fallback: { order: [], activeTabs: {} },
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Migrate old format (string[]) to new format (TabsStorage)
|
||||||
|
const storage: TabsStorage = Array.isArray(rawStorage)
|
||||||
|
? { order: rawStorage, activeTabs: {} }
|
||||||
|
: rawStorage ?? { order: [], activeTabs: {} };
|
||||||
|
|
||||||
|
const savedOrder = storage.order;
|
||||||
|
|
||||||
|
// Get the active tab value - prefer storage (if activeTabKey), then defaultValue, then first tab
|
||||||
|
const storedActiveTab = activeTabKey ? storage?.activeTabs?.[activeTabKey] : undefined;
|
||||||
|
const [internalValue, setInternalValue] = useState<string | undefined>(undefined);
|
||||||
|
const value = storedActiveTab ?? internalValue ?? defaultValue ?? originalTabs[0]?.value;
|
||||||
|
|
||||||
|
// Helper to normalize storage (handle migration from old format)
|
||||||
|
const normalizeStorage = useCallback(
|
||||||
|
(s: TabsStorage | string[]): TabsStorage =>
|
||||||
|
Array.isArray(s) ? { order: s, activeTabs: {} } : s,
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle tab change - update internal state, storage if we have a key, and call prop callback
|
||||||
|
const onChangeValue = useCallback(
|
||||||
|
async (newValue: string) => {
|
||||||
|
setInternalValue(newValue);
|
||||||
|
if (storageKey && activeTabKey) {
|
||||||
|
await setStorage((s) => {
|
||||||
|
const normalized = normalizeStorage(s);
|
||||||
|
return {
|
||||||
|
...normalized,
|
||||||
|
activeTabs: { ...normalized.activeTabs, [activeTabKey]: newValue },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
onChangeValueProp?.(newValue);
|
||||||
|
},
|
||||||
|
[storageKey, activeTabKey, setStorage, onChangeValueProp, normalizeStorage],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Expose imperative methods via ref
|
||||||
|
useImperativeHandle(forwardedRef, () => ({
|
||||||
|
setActiveTab: (value: string) => {
|
||||||
|
onChangeValue(value);
|
||||||
|
},
|
||||||
|
}), [onChangeValue]);
|
||||||
|
|
||||||
|
// Helper to save order
|
||||||
|
const setSavedOrder = useCallback(
|
||||||
|
async (order: string[]) => {
|
||||||
|
await setStorage((s) => {
|
||||||
|
const normalized = normalizeStorage(s);
|
||||||
|
return { ...normalized, order };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[setStorage, normalizeStorage],
|
||||||
|
);
|
||||||
|
|
||||||
// State for ordered tabs
|
// State for ordered tabs
|
||||||
const [orderedTabs, setOrderedTabs] = useState<TabItem[]>(originalTabs);
|
const [orderedTabs, setOrderedTabs] = useState<TabItem[]>(originalTabs);
|
||||||
const [isDragging, setIsDragging] = useState<TabItem | null>(null);
|
const [isDragging, setIsDragging] = useState<TabItem | null>(null);
|
||||||
@@ -112,8 +184,6 @@ export function Tabs({
|
|||||||
|
|
||||||
const tabs = storageKey ? orderedTabs : originalTabs;
|
const tabs = storageKey ? orderedTabs : originalTabs;
|
||||||
|
|
||||||
value = value ?? tabs[0]?.value;
|
|
||||||
|
|
||||||
// Update tabs when value changes
|
// Update tabs when value changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>('[data-tab]');
|
const tabs = ref.current?.querySelectorAll<HTMLDivElement>('[data-tab]');
|
||||||
@@ -320,7 +390,7 @@ export function Tabs({
|
|||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
interface TabButtonProps {
|
interface TabButtonProps {
|
||||||
tab: TabItem;
|
tab: TabItem;
|
||||||
@@ -329,7 +399,7 @@ interface TabButtonProps {
|
|||||||
layout: 'horizontal' | 'vertical';
|
layout: 'horizontal' | 'vertical';
|
||||||
reorderable: boolean;
|
reorderable: boolean;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
onChangeValue: (value: string) => void;
|
onChangeValue?: (value: string) => void;
|
||||||
overlay?: boolean;
|
overlay?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,7 +443,7 @@ function TabButton({
|
|||||||
? undefined
|
? undefined
|
||||||
: (e: React.MouseEvent) => {
|
: (e: React.MouseEvent) => {
|
||||||
e.preventDefault(); // Prevent dropdown from opening on first click
|
e.preventDefault(); // Prevent dropdown from opening on first click
|
||||||
onChangeValue(tab.value);
|
onChangeValue?.(tab.value);
|
||||||
},
|
},
|
||||||
className: classNames(
|
className: classNames(
|
||||||
'flex items-center rounded whitespace-nowrap',
|
'flex items-center rounded whitespace-nowrap',
|
||||||
@@ -478,3 +548,32 @@ export const TabContent = memo(function TabContent({
|
|||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Programmatically set the active tab for a Tabs component that uses storageKey + activeTabKey.
|
||||||
|
* This is useful when you need to change the tab from outside the component (e.g., in response to an event).
|
||||||
|
*/
|
||||||
|
export async function setActiveTab({
|
||||||
|
storageKey,
|
||||||
|
activeTabKey,
|
||||||
|
value,
|
||||||
|
}: {
|
||||||
|
storageKey: string;
|
||||||
|
activeTabKey: string;
|
||||||
|
value: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
const { getKeyValue, setKeyValue } = await import('../../../lib/keyValueStore');
|
||||||
|
const current = getKeyValue<TabsStorage>({
|
||||||
|
namespace: 'no_sync',
|
||||||
|
key: storageKey,
|
||||||
|
fallback: { order: [], activeTabs: {} },
|
||||||
|
});
|
||||||
|
await setKeyValue({
|
||||||
|
namespace: 'no_sync',
|
||||||
|
key: storageKey,
|
||||||
|
value: {
|
||||||
|
...current,
|
||||||
|
activeTabs: { ...current.activeTabs, [activeTabKey]: value },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { type MultipartPart, parseMultipart } from '@mjackson/multipart-parser';
|
import { type MultipartPart, parseMultipart } from '@mjackson/multipart-parser';
|
||||||
import { lazy, Suspense, useMemo, useState } from 'react';
|
import { lazy, Suspense, useMemo } from 'react';
|
||||||
import { languageFromContentType } from '../../lib/contentType';
|
import { languageFromContentType } from '../../lib/contentType';
|
||||||
import { Banner } from '../core/Banner';
|
import { Banner } from '../core/Banner';
|
||||||
import { Icon } from '../core/Icon';
|
import { Icon } from '../core/Icon';
|
||||||
@@ -22,8 +22,6 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MultipartViewer({ data, boundary, idPrefix = 'multipart' }: Props) {
|
export function MultipartViewer({ data, boundary, idPrefix = 'multipart' }: Props) {
|
||||||
const [tab, setTab] = useState<string>();
|
|
||||||
|
|
||||||
const parseResult = useMemo(() => {
|
const parseResult = useMemo(() => {
|
||||||
try {
|
try {
|
||||||
const maxFileSize = 1024 * 1024 * 10; // 10MB
|
const maxFileSize = 1024 * 1024 * 10; // 10MB
|
||||||
@@ -55,12 +53,10 @@ export function MultipartViewer({ data, boundary, idPrefix = 'multipart' }: Prop
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs
|
<Tabs
|
||||||
value={tab}
|
|
||||||
addBorders
|
addBorders
|
||||||
label="Multipart"
|
label="Multipart"
|
||||||
layout="horizontal"
|
layout="horizontal"
|
||||||
tabListClassName="border-r border-r-border"
|
tabListClassName="border-r border-r-border"
|
||||||
onChangeValue={setTab}
|
|
||||||
tabs={parts.map((part) => ({
|
tabs={parts.map((part) => ({
|
||||||
label: part.name ?? '',
|
label: part.name ?? '',
|
||||||
value: part.name ?? '',
|
value: part.name ?? '',
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export function useRequestEditor() {
|
|||||||
const focusParamValue = useCallback(
|
const focusParamValue = useCallback(
|
||||||
(name: string) => {
|
(name: string) => {
|
||||||
focusParamsTab();
|
focusParamsTab();
|
||||||
setTimeout(() => emitter.emit('request_params.focus_value', name), 50);
|
requestAnimationFrame(() => emitter.emit('request_params.focus_value', name));
|
||||||
},
|
},
|
||||||
[focusParamsTab],
|
[focusParamsTab],
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user