mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 18:31:16 +01:00
Request Inheritance (#209)
This commit is contained in:
@@ -1,36 +1,91 @@
|
||||
import { foldersAtom, patchModel } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
|
||||
interface Props {
|
||||
folderId: string | null;
|
||||
tab?: FolderSettingsTab;
|
||||
}
|
||||
|
||||
export function FolderSettingsDialog({ folderId }: Props) {
|
||||
const TAB_AUTH = 'auth';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_GENERAL = 'general';
|
||||
|
||||
export type FolderSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
|
||||
|
||||
export function FolderSettingsDialog({ folderId, tab }: Props) {
|
||||
const folders = useAtomValue(foldersAtom);
|
||||
const folder = folders.find((f) => f.id === folderId);
|
||||
const folder = folders.find((f) => f.id === folderId) ?? null;
|
||||
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
|
||||
const authTab = useAuthTab(TAB_AUTH, folder);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, folder);
|
||||
const inheritedHeaders = useInheritedHeaders(folder);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(() => {
|
||||
if (folder == null) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
value: TAB_GENERAL,
|
||||
label: 'General',
|
||||
},
|
||||
...authTab,
|
||||
...headersTab,
|
||||
];
|
||||
}, [authTab, folder, headersTab]);
|
||||
|
||||
if (folder == null) return null;
|
||||
|
||||
return (
|
||||
<VStack space={3} className="pb-3">
|
||||
<Input
|
||||
label="Folder Name"
|
||||
defaultValue={folder.name}
|
||||
onChange={(name) => patchModel(folder, { name })}
|
||||
stateKey={`name.${folder.id}`}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Folder Settings"
|
||||
className="px-1.5 pb-2"
|
||||
addBorders
|
||||
tabs={tabs}
|
||||
>
|
||||
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<HttpAuthenticationEditor model={folder} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<VStack space={3} className="pb-3">
|
||||
<Input
|
||||
label="Folder Name"
|
||||
defaultValue={folder.name}
|
||||
onChange={(name) => patchModel(folder, { name })}
|
||||
stateKey={`name.${folder.id}`}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
name="folder-description"
|
||||
placeholder="Folder description"
|
||||
className="min-h-[10rem] border border-border px-2"
|
||||
defaultValue={folder.description}
|
||||
stateKey={`description.${folder.id}`}
|
||||
onChange={(description) => patchModel(folder, { description })}
|
||||
/>
|
||||
</VStack>
|
||||
<MarkdownEditor
|
||||
name="folder-description"
|
||||
placeholder="Folder description"
|
||||
className="min-h-[10rem] border border-border px-2"
|
||||
defaultValue={folder.description}
|
||||
stateKey={`description.${folder.id}`}
|
||||
onChange={(description) => patchModel(folder, { description })}
|
||||
/>
|
||||
</VStack>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<HeadersEditor
|
||||
inheritedHeaders={inheritedHeaders}
|
||||
forceUpdateKey={folder.id}
|
||||
headers={folder.headers}
|
||||
onChange={(headers) => patchModel(folder, { headers })}
|
||||
stateKey={`headers.${folder.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -343,9 +343,7 @@ function SetupSyncDropdown({ workspaceMeta }: { workspaceMeta: WorkspaceMeta })
|
||||
color: 'success',
|
||||
label: 'Open Workspace Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect() {
|
||||
openWorkspaceSettings.mutate();
|
||||
},
|
||||
onSelect: openWorkspaceSettings,
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { type GrpcMetadataEntry, type GrpcRequest, patchModel } from '@yaakapp-internal/models';
|
||||
import { type GrpcRequest, type HttpRequestHeader, patchModel } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useRef } from 'react';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useContainerSize } from '../hooks/useContainerQuery';
|
||||
import type { ReflectResponseService } from '../hooks/useGrpc';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
@@ -12,13 +14,13 @@ import { Button } from './core/Button';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { GrpcEditor } from './GrpcEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
@@ -64,7 +66,9 @@ export function GrpcRequestPane({
|
||||
onCancel,
|
||||
onSend,
|
||||
}: Props) {
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const metadataTab = useHeadersTab(TAB_METADATA, activeRequest, 'Metadata');
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
const { value: activeTabs, set: setActiveTabs } = useKeyValue<Record<string, string>>({
|
||||
namespace: 'no_sync',
|
||||
key: 'grpcRequestActiveTabs',
|
||||
@@ -130,42 +134,15 @@ export function GrpcRequestPane({
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
{ value: TAB_MESSAGE, label: 'Message' },
|
||||
{
|
||||
value: TAB_AUTH,
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: GrpcRequest['authentication'] = activeRequest.authentication;
|
||||
if (activeRequest.authenticationType !== authenticationType) {
|
||||
authentication = {
|
||||
// Reset auth if changing types
|
||||
};
|
||||
}
|
||||
await patchModel(activeRequest, {
|
||||
authenticationType,
|
||||
authentication,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
{ value: TAB_METADATA, label: 'Metadata' },
|
||||
...metadataTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: 'Info',
|
||||
rightSlot: activeRequest.description && <CountBadge count={true} />,
|
||||
},
|
||||
],
|
||||
[activeRequest, authentication],
|
||||
[activeRequest.description, authTab, metadataTab],
|
||||
);
|
||||
|
||||
const activeTab = activeTabs?.[activeRequest.id];
|
||||
@@ -177,7 +154,7 @@ export function GrpcRequestPane({
|
||||
);
|
||||
|
||||
const handleMetadataChange = useCallback(
|
||||
(metadata: GrpcMetadataEntry[]) => patchModel(activeRequest, { metadata }),
|
||||
(metadata: HttpRequestHeader[]) => patchModel(activeRequest, { metadata }),
|
||||
[activeRequest],
|
||||
);
|
||||
|
||||
@@ -307,17 +284,15 @@ export function GrpcRequestPane({
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_AUTH}>
|
||||
<HttpAuthenticationEditor request={activeRequest} />
|
||||
<HttpAuthenticationEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_METADATA}>
|
||||
<PairOrBulkEditor
|
||||
preferenceName="grpc_metadata"
|
||||
valueAutocompleteVariables
|
||||
nameAutocompleteVariables
|
||||
pairs={activeRequest.metadata}
|
||||
onChange={handleMetadataChange}
|
||||
<HeadersEditor
|
||||
inheritedHeaders={inheritedHeaders}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
stateKey={`grpc_metadata.${activeRequest.id}`}
|
||||
headers={activeRequest.metadata}
|
||||
stateKey={`headers.${activeRequest.id}`}
|
||||
onChange={handleMetadataChange}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_DESCRIPTION}>
|
||||
|
||||
@@ -5,36 +5,81 @@ import { connections } from '../lib/data/connections';
|
||||
import { encodings } from '../lib/data/encodings';
|
||||
import { headerNames } from '../lib/data/headerNames';
|
||||
import { mimeTypes } from '../lib/data/mimetypes';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import type { InputProps } from './core/Input';
|
||||
import type { Pair, PairEditorProps } from './core/PairEditor';
|
||||
import { ensurePairId, PairEditorRow } from './core/PairEditor';
|
||||
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
type Props = {
|
||||
forceUpdateKey: string;
|
||||
headers: HttpRequestHeader[];
|
||||
inheritedHeaders?: HttpRequestHeader[];
|
||||
stateKey: string;
|
||||
onChange: (headers: HttpRequestHeader[]) => void;
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export function HeadersEditor({ stateKey, headers, onChange, forceUpdateKey }: Props) {
|
||||
export function HeadersEditor({
|
||||
stateKey,
|
||||
headers,
|
||||
inheritedHeaders,
|
||||
onChange,
|
||||
forceUpdateKey,
|
||||
}: Props) {
|
||||
const validInheritedHeaders =
|
||||
inheritedHeaders?.filter((pair) => pair.enabled && (pair.name || pair.value)) ?? [];
|
||||
return (
|
||||
<PairOrBulkEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteFunctions
|
||||
nameAutocompleteVariables
|
||||
namePlaceholder="Header-Name"
|
||||
nameValidate={validateHttpHeader}
|
||||
onChange={onChange}
|
||||
pairs={headers}
|
||||
preferenceName="headers"
|
||||
stateKey={stateKey}
|
||||
valueType={valueType}
|
||||
valueAutocomplete={valueAutocomplete}
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
/>
|
||||
<div className="@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
{validInheritedHeaders.length > 0 ? (
|
||||
<Banner className="!py-0 mb-1.5 border-dashed" color="secondary">
|
||||
<details>
|
||||
<summary className="py-1.5 text-sm !cursor-default !select-none opacity-70 hover:opacity-100">
|
||||
<HStack>
|
||||
Inherited <CountBadge count={validInheritedHeaders.length} />
|
||||
</HStack>
|
||||
</summary>
|
||||
<div className="pb-2">
|
||||
{validInheritedHeaders?.map((pair, i) => (
|
||||
<PairEditorRow
|
||||
key={pair.id + '.' + i}
|
||||
index={i}
|
||||
disabled
|
||||
disableDrag
|
||||
className="py-1"
|
||||
onChange={() => {}}
|
||||
onEnd={() => {}}
|
||||
onMove={() => {}}
|
||||
pair={ensurePairId(pair)}
|
||||
stateKey={null}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</details>
|
||||
</Banner>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<PairOrBulkEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteFunctions
|
||||
nameAutocompleteVariables
|
||||
namePlaceholder="Header-Name"
|
||||
nameValidate={validateHttpHeader}
|
||||
onChange={onChange}
|
||||
pairs={headers}
|
||||
preferenceName="headers"
|
||||
stateKey={stateKey}
|
||||
valueType={valueType}
|
||||
valueAutocomplete={valueAutocomplete}
|
||||
valueAutocompleteFunctions
|
||||
valueAutocompleteVariables
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -51,14 +96,14 @@ const headerOptionsMap: Record<string, string[]> = {
|
||||
const valueType = (pair: Pair): InputProps['type'] => {
|
||||
const name = pair.name.toLowerCase().trim();
|
||||
if (
|
||||
name.includes('authorization') ||
|
||||
name.includes('api-key') ||
|
||||
name.includes('access-token') ||
|
||||
name.includes('auth') ||
|
||||
name.includes('secret') ||
|
||||
name.includes('token') ||
|
||||
name === 'cookie' ||
|
||||
name === 'set-cookie'
|
||||
name.includes('authorization') ||
|
||||
name.includes('api-key') ||
|
||||
name.includes('access-token') ||
|
||||
name.includes('auth') ||
|
||||
name.includes('secret') ||
|
||||
name.includes('token') ||
|
||||
name === 'cookie' ||
|
||||
name === 'set-cookie'
|
||||
) {
|
||||
return 'password';
|
||||
} else {
|
||||
|
||||
@@ -1,34 +1,84 @@
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import type {
|
||||
Folder,
|
||||
GrpcRequest,
|
||||
HttpRequest,
|
||||
WebsocketRequest,
|
||||
Workspace,
|
||||
} from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import React, { useCallback } from 'react';
|
||||
import { openFolderSettings } from '../commands/openFolderSettings';
|
||||
import { openWorkspaceSettings } from '../commands/openWorkspaceSettings';
|
||||
import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
|
||||
import { useInheritedAuthentication } from '../hooks/useInheritedAuthentication';
|
||||
import { resolvedModelName } from '../lib/resolvedModelName';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { DynamicForm } from './DynamicForm';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
model: HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;
|
||||
}
|
||||
|
||||
export function HttpAuthenticationEditor({ request }: Props) {
|
||||
export function HttpAuthenticationEditor({ model }: Props) {
|
||||
const inheritedAuth = useInheritedAuthentication(model);
|
||||
const authConfig = useHttpAuthenticationConfig(
|
||||
request.authenticationType,
|
||||
request.authentication,
|
||||
request.id,
|
||||
model.authenticationType,
|
||||
model.authentication,
|
||||
model.id,
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(authentication: Record<string, boolean>) => patchModel(request, { authentication }),
|
||||
[request],
|
||||
async (authentication: Record<string, boolean>) => await patchModel(model, { authentication }),
|
||||
[model],
|
||||
);
|
||||
|
||||
if (authConfig.data == null) {
|
||||
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
|
||||
if (model.authenticationType === 'none') {
|
||||
return <EmptyStateText>No authentication</EmptyStateText>;
|
||||
}
|
||||
|
||||
if (model.authenticationType != null && authConfig.data == null) {
|
||||
return (
|
||||
<EmptyStateText>
|
||||
Unknown authentication <InlineCode>{authConfig.data}</InlineCode>
|
||||
</EmptyStateText>
|
||||
);
|
||||
}
|
||||
|
||||
if (inheritedAuth == null) {
|
||||
return <EmptyStateText>Authentication not configured</EmptyStateText>;
|
||||
}
|
||||
|
||||
if (inheritedAuth.authenticationType === 'none') {
|
||||
return <EmptyStateText>No authentication</EmptyStateText>;
|
||||
}
|
||||
|
||||
const wasAuthInherited = inheritedAuth?.id !== model.id;
|
||||
if (wasAuthInherited) {
|
||||
const name = resolvedModelName(inheritedAuth);
|
||||
const cta = inheritedAuth.model === 'workspace' ? 'Workspace' : name;
|
||||
return (
|
||||
<EmptyStateText>
|
||||
<p>
|
||||
Inherited from{' '}
|
||||
<button
|
||||
className="underline hover:text-text"
|
||||
onClick={() => {
|
||||
if (inheritedAuth.model === 'folder') openFolderSettings(inheritedAuth.id, 'auth');
|
||||
else openWorkspaceSettings('auth');
|
||||
}}
|
||||
>
|
||||
{cta}
|
||||
</button>
|
||||
</p>
|
||||
</EmptyStateText>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -36,17 +86,17 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
<HStack space={2} className="mb-2" alignItems="center">
|
||||
<Checkbox
|
||||
className="w-full"
|
||||
checked={!request.authentication.disabled}
|
||||
onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
|
||||
checked={!model.authentication.disabled}
|
||||
onChange={(disabled) => handleChange({ ...model.authentication, disabled: !disabled })}
|
||||
title="Enabled"
|
||||
/>
|
||||
{authConfig.data.actions && authConfig.data.actions.length > 0 && (
|
||||
{authConfig.data?.actions && authConfig.data.actions.length > 0 && (
|
||||
<Dropdown
|
||||
items={authConfig.data.actions.map(
|
||||
(a): DropdownItem => ({
|
||||
label: a.label,
|
||||
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
|
||||
onSelect: () => a.call(request),
|
||||
onSelect: () => a.call(model),
|
||||
}),
|
||||
)}
|
||||
>
|
||||
@@ -55,12 +105,12 @@ export function HttpAuthenticationEditor({ request }: Props) {
|
||||
)}
|
||||
</HStack>
|
||||
<DynamicForm
|
||||
disabled={request.authentication.disabled}
|
||||
disabled={model.authentication.disabled}
|
||||
autocompleteVariables
|
||||
autocompleteFunctions
|
||||
stateKey={`auth.${request.id}.${request.authenticationType}`}
|
||||
inputs={authConfig.data.args}
|
||||
data={request.authentication}
|
||||
stateKey={`auth.${model.id}.${model.authenticationType}`}
|
||||
inputs={authConfig.data?.args ?? []}
|
||||
data={model.authentication}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -6,13 +6,15 @@ import { atom, useAtomValue } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { allRequestsAtom } from '../hooks/useAllRequests';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||
import { allRequestsAtom } from '../hooks/useAllRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
@@ -44,12 +46,12 @@ import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
import { GraphQLEditor } from './GraphQLEditor';
|
||||
|
||||
interface Props {
|
||||
style: CSSProperties;
|
||||
@@ -85,7 +87,9 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||
const contentType = getContentTypeFromHeaders(activeRequest.headers);
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null, patch: Partial<Omit<HttpRequest, 'headers'>> = {}) => {
|
||||
@@ -214,42 +218,21 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
rightSlot: <CountBadge count={urlParameterPairs.length} />,
|
||||
label: 'Params',
|
||||
},
|
||||
{
|
||||
value: TAB_HEADERS,
|
||||
label: 'Headers',
|
||||
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
|
||||
},
|
||||
{
|
||||
value: TAB_AUTH,
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
|
||||
if (activeRequest.authenticationType !== authenticationType) {
|
||||
authentication = {
|
||||
// Reset auth if changing types
|
||||
};
|
||||
}
|
||||
await patchModel(activeRequest, { authenticationType, authentication });
|
||||
},
|
||||
},
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: 'Info',
|
||||
},
|
||||
],
|
||||
[activeRequest, authentication, handleContentTypeChange, numParams, urlParameterPairs.length],
|
||||
[
|
||||
activeRequest,
|
||||
authTab,
|
||||
handleContentTypeChange,
|
||||
headersTab,
|
||||
numParams,
|
||||
urlParameterPairs.length,
|
||||
],
|
||||
);
|
||||
|
||||
const { mutate: sendRequest } = useSendAnyHttpRequest();
|
||||
@@ -372,10 +355,11 @@ export function HttpRequestPane({ style, fullHeight, className, activeRequest }:
|
||||
tabListClassName="mt-2 !mb-1.5"
|
||||
>
|
||||
<TabContent value={TAB_AUTH}>
|
||||
<HttpAuthenticationEditor request={activeRequest} />
|
||||
<HttpAuthenticationEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<HeadersEditor
|
||||
inheritedHeaders={inheritedHeaders}
|
||||
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||
headers={activeRequest.headers}
|
||||
stateKey={`headers.${activeRequest.id}`}
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function RouteError({ error }: { error: unknown }) {
|
||||
<FormattedError>
|
||||
{message}
|
||||
{stack && (
|
||||
<details className="mt-3 select-autotext-xs">
|
||||
<details className="mt-3 select-auto text-xs">
|
||||
<summary className="!cursor-default !select-none">Stack Trace</summary>
|
||||
<div className="mt-2 text-xs">{stack}</div>
|
||||
</details>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import type { WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { patchModel } from '@yaakapp-internal/models';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { closeWebsocket, connectWebsocket, sendWebsocket } from '@yaakapp-internal/ws';
|
||||
@@ -9,13 +9,15 @@ import React, { useCallback, useMemo } from 'react';
|
||||
import { getActiveCookieJar } from '../hooks/useActiveCookieJar';
|
||||
import { getActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
|
||||
import { allRequestsAtom } from '../hooks/useAllRequests';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { activeWebsocketConnectionAtom } from '../hooks/usePinnedWebsocketConnection';
|
||||
import { useRequestEditor, useRequestEditorEvent } from '../hooks/useRequestEditor';
|
||||
import {allRequestsAtom} from "../hooks/useAllRequests";
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { deepEqualAtom } from '../lib/atoms';
|
||||
import { languageFromContentType } from '../lib/contentType';
|
||||
@@ -69,7 +71,9 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
});
|
||||
const forceUpdateKey = useRequestUpdateKey(activeRequest.id);
|
||||
const [{ urlKey }, { focusParamsTab, forceUrlRefresh, forceParamsRefresh }] = useRequestEditor();
|
||||
const authentication = useHttpAuthenticationSummaries();
|
||||
const authTab = useAuthTab(TAB_AUTH, activeRequest);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, activeRequest);
|
||||
const inheritedHeaders = useInheritedHeaders(activeRequest);
|
||||
|
||||
const { urlParameterPairs, urlParametersKey } = useMemo(() => {
|
||||
const placeholderNames = Array.from(activeRequest.url.matchAll(/\/(:[^/]+)/g)).map(
|
||||
@@ -99,45 +103,14 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
rightSlot: <CountBadge count={urlParameterPairs.length} />,
|
||||
label: 'Params',
|
||||
},
|
||||
{
|
||||
value: TAB_HEADERS,
|
||||
label: 'Headers',
|
||||
rightSlot: <CountBadge count={activeRequest.headers.filter((h) => h.name).length} />,
|
||||
},
|
||||
{
|
||||
value: TAB_AUTH,
|
||||
label: 'Auth',
|
||||
options: {
|
||||
value: activeRequest.authenticationType,
|
||||
items: [
|
||||
...authentication.map((a) => ({
|
||||
label: a.label || 'UNKNOWN',
|
||||
shortLabel: a.shortLabel,
|
||||
value: a.name,
|
||||
})),
|
||||
{ type: 'separator' },
|
||||
{ label: 'No Authentication', shortLabel: 'Auth', value: null },
|
||||
],
|
||||
onChange: async (authenticationType) => {
|
||||
let authentication: HttpRequest['authentication'] = activeRequest.authentication;
|
||||
if (activeRequest.authenticationType !== authenticationType) {
|
||||
authentication = {
|
||||
// Reset auth if changing types
|
||||
};
|
||||
}
|
||||
await patchModel(activeRequest, {
|
||||
authenticationType,
|
||||
authentication,
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
...headersTab,
|
||||
...authTab,
|
||||
{
|
||||
value: TAB_DESCRIPTION,
|
||||
label: 'Info',
|
||||
},
|
||||
];
|
||||
}, [activeRequest, authentication, urlParameterPairs.length]);
|
||||
}, [authTab, headersTab, urlParameterPairs.length]);
|
||||
|
||||
const { activeResponse } = usePinnedHttpResponse(activeRequestId);
|
||||
const { mutate: cancelResponse } = useCancelHttpResponse(activeResponse?.id ?? null);
|
||||
@@ -266,10 +239,11 @@ export function WebsocketRequestPane({ style, fullHeight, className, activeReque
|
||||
tabListClassName="mt-2 !mb-1.5"
|
||||
>
|
||||
<TabContent value={TAB_AUTH}>
|
||||
<HttpAuthenticationEditor request={activeRequest} />
|
||||
<HttpAuthenticationEditor model={activeRequest} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS}>
|
||||
<HeadersEditor
|
||||
inheritedHeaders={inheritedHeaders}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
headers={activeRequest.headers}
|
||||
stateKey={`headers.${activeRequest.id}`}
|
||||
|
||||
@@ -49,7 +49,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
label: 'Workspace Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
hotKeyAction: 'workspace_settings.show',
|
||||
onSelect: () => openWorkspaceSettings.mutate(),
|
||||
onSelect: openWorkspaceSettings,
|
||||
},
|
||||
{
|
||||
label: revealInFinderText,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { patchModel, workspaceMetasAtom, workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useState } from 'react';
|
||||
import { useAuthTab } from '../hooks/useAuthTab';
|
||||
import { useHeadersTab } from '../hooks/useHeadersTab';
|
||||
import { useInheritedHeaders } from '../hooks/useInheritedHeaders';
|
||||
import { deleteModelWithConfirm } from '../lib/deleteModelWithConfirm';
|
||||
import { router } from '../lib/router';
|
||||
import { Banner } from './core/Banner';
|
||||
@@ -8,6 +12,9 @@ import { InlineCode } from './core/InlineCode';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { Separator } from './core/Separator';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { SyncToFilesystemSetting } from './SyncToFilesystemSetting';
|
||||
import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
|
||||
@@ -15,11 +22,22 @@ import { WorkspaceEncryptionSetting } from './WorkspaceEncryptionSetting';
|
||||
interface Props {
|
||||
workspaceId: string | null;
|
||||
hide: () => void;
|
||||
tab?: WorkspaceSettingsTab;
|
||||
}
|
||||
|
||||
export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
||||
const TAB_AUTH = 'auth';
|
||||
const TAB_HEADERS = 'headers';
|
||||
const TAB_GENERAL = 'general';
|
||||
|
||||
export type WorkspaceSettingsTab = typeof TAB_AUTH | typeof TAB_HEADERS | typeof TAB_GENERAL;
|
||||
|
||||
export function WorkspaceSettingsDialog({ workspaceId, hide, tab }: Props) {
|
||||
const workspace = useAtomValue(workspacesAtom).find((w) => w.id === workspaceId);
|
||||
const workspaceMeta = useAtomValue(workspaceMetasAtom).find((m) => m.workspaceId === workspaceId);
|
||||
const [activeTab, setActiveTab] = useState<string>(tab ?? TAB_GENERAL);
|
||||
const authTab = useAuthTab(TAB_AUTH, workspace ?? null);
|
||||
const headersTab = useHeadersTab(TAB_HEADERS, workspace ?? null);
|
||||
const inheritedHeaders = useInheritedHeaders(workspace ?? null);
|
||||
|
||||
if (workspace == null) {
|
||||
return (
|
||||
@@ -37,53 +55,76 @@ export function WorkspaceSettingsDialog({ workspaceId, hide }: Props) {
|
||||
);
|
||||
|
||||
return (
|
||||
<VStack space={4} alignItems="start" className="pb-3 h-full">
|
||||
<PlainInput
|
||||
required
|
||||
hideLabel
|
||||
placeholder="Workspace Name"
|
||||
label="Name"
|
||||
defaultValue={workspace.name}
|
||||
className="!text-base font-sans"
|
||||
onChange={(name) => patchModel(workspace, { name })}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
onChangeValue={setActiveTab}
|
||||
label="Folder Settings"
|
||||
className="px-1.5 pb-2"
|
||||
addBorders
|
||||
tabs={[{ value: TAB_GENERAL, label: 'General' }, ...authTab, ...headersTab]}
|
||||
>
|
||||
<TabContent value={TAB_AUTH} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<HttpAuthenticationEditor model={workspace} />
|
||||
</TabContent>
|
||||
<TabContent value={TAB_HEADERS} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<HeadersEditor
|
||||
inheritedHeaders={inheritedHeaders}
|
||||
forceUpdateKey={workspace.id}
|
||||
headers={workspace.headers}
|
||||
onChange={(headers) => patchModel(workspace, { headers })}
|
||||
stateKey={`headers.${workspace.id}`}
|
||||
/>
|
||||
</TabContent>
|
||||
<TabContent value={TAB_GENERAL} className="pt-3 overflow-y-auto h-full px-4">
|
||||
<VStack space={4} alignItems="start" className="pb-3 h-full">
|
||||
<PlainInput
|
||||
required
|
||||
hideLabel
|
||||
placeholder="Workspace Name"
|
||||
label="Name"
|
||||
defaultValue={workspace.name}
|
||||
className="!text-base font-sans"
|
||||
onChange={(name) => patchModel(workspace, { name })}
|
||||
/>
|
||||
|
||||
<MarkdownEditor
|
||||
name="workspace-description"
|
||||
placeholder="Workspace description"
|
||||
className="min-h-[3rem] max-h-[25rem] border border-border px-2"
|
||||
defaultValue={workspace.description}
|
||||
stateKey={`description.${workspace.id}`}
|
||||
onChange={(description) => patchModel(workspace, { description })}
|
||||
heightMode="auto"
|
||||
/>
|
||||
<MarkdownEditor
|
||||
name="workspace-description"
|
||||
placeholder="Workspace description"
|
||||
className="min-h-[3rem] max-h-[25rem] border border-border px-2"
|
||||
defaultValue={workspace.description}
|
||||
stateKey={`description.${workspace.id}`}
|
||||
onChange={(description) => patchModel(workspace, { description })}
|
||||
heightMode="auto"
|
||||
/>
|
||||
|
||||
<SyncToFilesystemSetting
|
||||
value={{ filePath: workspaceMeta.settingSyncDir }}
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||
/>
|
||||
<WorkspaceEncryptionSetting size="xs" />
|
||||
<SyncToFilesystemSetting
|
||||
value={{ filePath: workspaceMeta.settingSyncDir }}
|
||||
onCreateNewWorkspace={hide}
|
||||
onChange={({ filePath }) => patchModel(workspaceMeta, { settingSyncDir: filePath })}
|
||||
/>
|
||||
<WorkspaceEncryptionSetting size="xs" />
|
||||
|
||||
<Separator className="my-4" />
|
||||
<Separator className="my-4" />
|
||||
|
||||
<HStack alignItems="center" justifyContent="between" className="w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const didDelete = await deleteModelWithConfirm(workspace);
|
||||
if (didDelete) {
|
||||
hide(); // Only hide if actually deleted workspace
|
||||
await router.navigate({ to: '/' });
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
|
||||
</HStack>
|
||||
</VStack>
|
||||
<HStack alignItems="center" justifyContent="between" className="w-full">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const didDelete = await deleteModelWithConfirm(workspace);
|
||||
if (didDelete) {
|
||||
hide(); // Only hide if actually deleted workspace
|
||||
await router.navigate({ to: '/' });
|
||||
}
|
||||
}}
|
||||
color="danger"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Delete Workspace
|
||||
</Button>
|
||||
<InlineCode className="select-text cursor-text">{workspaceId}</InlineCode>
|
||||
</HStack>
|
||||
</VStack>
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export function Checkbox({
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon
|
||||
size="sm"
|
||||
className={classNames(disabled && 'opacity-disabled')}
|
||||
icon={checked === 'indeterminate' ? 'minus' : checked ? 'check' : 'empty'}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -290,6 +290,8 @@ type PairEditorRowProps = {
|
||||
onFocus?: (pair: PairWithId) => void;
|
||||
onSubmit?: (pair: PairWithId) => void;
|
||||
isLast?: boolean;
|
||||
disabled?: boolean;
|
||||
disableDrag?: boolean;
|
||||
index: number;
|
||||
} & Pick<
|
||||
PairEditorProps,
|
||||
@@ -311,21 +313,23 @@ type PairEditorRowProps = {
|
||||
| 'valueValidate'
|
||||
>;
|
||||
|
||||
function PairEditorRow({
|
||||
export function PairEditorRow({
|
||||
allowFileValues,
|
||||
allowMultilineValues,
|
||||
className,
|
||||
forcedEnvironmentId,
|
||||
disableDrag,
|
||||
disabled,
|
||||
forceFocusNamePairId,
|
||||
forceFocusValuePairId,
|
||||
forceUpdateKey,
|
||||
forcedEnvironmentId,
|
||||
index,
|
||||
isLast,
|
||||
nameAutocomplete,
|
||||
namePlaceholder,
|
||||
nameValidate,
|
||||
nameAutocompleteFunctions,
|
||||
nameAutocompleteVariables,
|
||||
namePlaceholder,
|
||||
nameValidate,
|
||||
onChange,
|
||||
onDelete,
|
||||
onEnd,
|
||||
@@ -461,12 +465,12 @@ function PairEditorRow({
|
||||
<Checkbox
|
||||
hideLabel
|
||||
title={pair.enabled ? 'Disable item' : 'Enable item'}
|
||||
disabled={isLast}
|
||||
disabled={isLast || disabled}
|
||||
checked={isLast ? false : !!pair.enabled}
|
||||
className={classNames(isLast && '!opacity-disabled')}
|
||||
onChange={handleChangeEnabled}
|
||||
/>
|
||||
{!isLast ? (
|
||||
{!isLast && !disableDrag ? (
|
||||
<div
|
||||
className={classNames(
|
||||
'py-2 h-7 w-4 flex items-center',
|
||||
@@ -502,6 +506,7 @@ function PairEditorRow({
|
||||
ref={nameInputRef}
|
||||
hideLabel
|
||||
stateKey={`name.${pair.id}.${stateKey}`}
|
||||
disabled={disabled}
|
||||
wrapLines={false}
|
||||
readOnly={pair.readOnlyName}
|
||||
size="sm"
|
||||
@@ -523,12 +528,19 @@ function PairEditorRow({
|
||||
)}
|
||||
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
|
||||
{pair.isFile ? (
|
||||
<SelectFile inline size="xs" filePath={pair.value} onChange={handleChangeValueFile} />
|
||||
<SelectFile
|
||||
disabled={disabled}
|
||||
inline
|
||||
size="xs"
|
||||
filePath={pair.value}
|
||||
onChange={handleChangeValueFile}
|
||||
/>
|
||||
) : isLast ? (
|
||||
// Use PlainInput for last ones because there's a unique bug where clicking below
|
||||
// the Codemirror input focuses it.
|
||||
<PlainInput
|
||||
hideLabel
|
||||
disabled={disabled}
|
||||
size="sm"
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
label="Value"
|
||||
@@ -553,6 +565,7 @@ function PairEditorRow({
|
||||
stateKey={`value.${pair.id}.${stateKey}`}
|
||||
wrapLines={false}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
containerClassName={classNames(isLast && 'border-dashed')}
|
||||
validate={valueValidate}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
@@ -585,8 +598,9 @@ function PairEditorRow({
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast ? 'empty' : 'chevron_down'}
|
||||
icon={(isLast || disabled) ? 'empty' : 'chevron_down'}
|
||||
title="Select form data type"
|
||||
className="text-text-subtle"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
@@ -8,7 +8,7 @@ export type RadioDropdownItem<T = string | null> =
|
||||
| {
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
shortLabel?: string;
|
||||
shortLabel?: ReactNode;
|
||||
value: T;
|
||||
rightSlot?: ReactNode;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { duplicateModelById, getModel, workspacesAtom } from '@yaakapp-internal/models';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import React, { useMemo } from 'react';
|
||||
import { openFolderSettings } from '../../commands/openFolderSettings';
|
||||
import { useCreateDropdownItems } from '../../hooks/useCreateDropdownItems';
|
||||
import { useHttpRequestActions } from '../../hooks/useHttpRequestActions';
|
||||
import { useMoveToWorkspace } from '../../hooks/useMoveToWorkspace';
|
||||
@@ -8,13 +9,11 @@ import { useSendAnyHttpRequest } from '../../hooks/useSendAnyHttpRequest';
|
||||
import { useSendManyRequests } from '../../hooks/useSendManyRequests';
|
||||
import { deleteModelWithConfirm } from '../../lib/deleteModelWithConfirm';
|
||||
|
||||
import { showDialog } from '../../lib/dialog';
|
||||
import { duplicateRequestAndNavigate } from '../../lib/duplicateRequestAndNavigate';
|
||||
import { renameModelWithPrompt } from '../../lib/renameModelWithPrompt';
|
||||
import type { DropdownItem } from '../core/Dropdown';
|
||||
import { ContextMenu } from '../core/Dropdown';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { FolderSettingsDialog } from '../FolderSettingsDialog';
|
||||
import type { SidebarTreeNode } from './Sidebar';
|
||||
|
||||
interface Props {
|
||||
@@ -44,13 +43,7 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
|
||||
{
|
||||
label: 'Settings',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () =>
|
||||
showDialog({
|
||||
id: 'folder-settings',
|
||||
title: 'Folder Settings',
|
||||
size: 'md',
|
||||
render: () => <FolderSettingsDialog folderId={child.id} />,
|
||||
}),
|
||||
onSelect: () => openFolderSettings(child.id),
|
||||
},
|
||||
{
|
||||
label: 'Duplicate',
|
||||
|
||||
Reference in New Issue
Block a user