Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 26 additions & 15 deletions src-web/components/HeadersEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import type { HttpRequestHeader } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { Fragment } from 'react';
import type { InheritedHeader } from '../hooks/useInheritedHeaders';
import { charsets } from '../lib/data/charsets';
import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings';
Expand All @@ -8,6 +10,7 @@ import { mimeTypes } from '../lib/data/mimetypes';
import { CountBadge } from './core/CountBadge';
import { DetailsBanner } from './core/DetailsBanner';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import { Icon } from './core/Icon';
import type { InputProps } from './core/Input';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairEditorRow } from './core/PairEditor';
Expand All @@ -18,7 +21,7 @@ import { HStack } from './core/Stacks';
type Props = {
forceUpdateKey: string;
headers: HttpRequestHeader[];
inheritedHeaders?: HttpRequestHeader[];
inheritedHeaders?: InheritedHeader[];
inheritedHeadersLabel?: string;
stateKey: string;
onChange: (headers: HttpRequestHeader[]) => void;
Expand Down Expand Up @@ -62,21 +65,29 @@ export function HeadersEditor({
</HStack>
}
>
<div className="pb-2">
<div className="pb-2 grid grid-cols-[minmax(0,1fr)_auto] items-center">
{validInheritedHeaders?.map((pair, i) => (
<PairEditorRow
key={`${pair.id}.${i}`}
index={i}
disabled
disableDrag
className="py-1"
pair={ensurePairId(pair)}
stateKey={null}
nameAutocompleteFunctions
nameAutocompleteVariables
valueAutocompleteFunctions
valueAutocompleteVariables
/>
<Fragment key={`${pair.id}.${i}`}>
<PairEditorRow
index={i}
disabled
disableDrag
className="py-1"
pair={ensurePairId(pair)}
stateKey={null}
nameAutocompleteFunctions
nameAutocompleteVariables
valueAutocompleteFunctions
valueAutocompleteVariables
/>
<span className="text-xs text-text-subtlest whitespace-nowrap pl-1 pr-2 flex items-center gap-1">
{pair.source.model === 'default' && <Icon icon="sparkles" size="xs" />}
{pair.source.model === 'workspace' && <Icon icon="house" size="xs" />}
{pair.source.model === 'workspace' && pair.source.name}
{pair.source.model === 'folder' && <Icon icon="folder" size="xs" />}
{pair.source.model === 'folder' && pair.source.name}
</span>
</Fragment>
))}
</div>
</DetailsBanner>
Expand Down
35 changes: 29 additions & 6 deletions src-web/hooks/useInheritedHeaders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,34 @@ const ancestorsAtom = atom((get) => [...get(foldersAtom), ...get(workspacesAtom)

export type HeaderModel = HttpRequest | GrpcRequest | WebsocketRequest | Folder | Workspace;

export function useInheritedHeaders(baseModel: HeaderModel | null) {
export type HeaderSource = {
id: string;
name: string;
model: 'workspace' | 'folder' | 'default';
};

export type InheritedHeader = HttpRequestHeader & {
source: HeaderSource;
};

export function useInheritedHeaders(baseModel: HeaderModel | null): InheritedHeader[] {
const parents = useAtomValue(ancestorsAtom);

const defaultSource: HeaderSource = { id: 'default', name: 'Default', model: 'default' };

if (baseModel == null) return [];
if (baseModel.model === 'workspace') return defaultHeaders;
if (baseModel.model === 'workspace') {
return defaultHeaders.map((h) => ({ ...h, source: defaultSource }));
}

const next = (child: HeaderModel): HttpRequestHeader[] => {
const next = (child: HeaderModel): InheritedHeader[] => {
// Short-circuit at workspace level - return global defaults + workspace headers
if (child.model === 'workspace') {
return [...defaultHeaders, ...child.headers];
const workspaceSource: HeaderSource = { id: child.id, name: child.name, model: 'workspace' };
return [
...defaultHeaders.map((h) => ({ ...h, source: defaultSource })),
...child.headers.map((h) => ({ ...h, source: workspaceSource })),
];
}

// Recurse up the tree
Expand All @@ -38,13 +56,18 @@ export function useInheritedHeaders(baseModel: HeaderModel | null) {
}

const headers = next(parent);
return [...headers, ...parent.headers];
const parentSource: HeaderSource = {
id: parent.id,
name: parent.name,
model: parent.model as 'workspace' | 'folder',
};
return [...headers, ...parent.headers.map((h) => ({ ...h, source: parentSource }))];
};

const allHeaders = next(baseModel);

// Deduplicate by header name (case-insensitive), keeping the latest (most specific) value
const headersByName = new Map<string, HttpRequestHeader>();
const headersByName = new Map<string, InheritedHeader>();
for (const header of allHeaders) {
headersByName.set(header.name.toLowerCase(), header);
}
Expand Down