Skip to content
Draft
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
33 changes: 31 additions & 2 deletions src/components/workspace/request-editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -219,13 +219,23 @@ function KVTable({
);
}

interface HistoryInitialData {
method: HttpMethod;
url: string;
headers: KVRow[];
params: KVRow[];
bodyType: string;
bodyContent: string;
}

interface RequestEditorProps {
requestId: string | null;
collectionRelPath: string | null;
workspacePath: string | null;
environments?: EnvFile[];
onOpenEnvTab?: (envName: string) => void;
onHistoryEntry?: (entry: HistoryEntry) => void;
historyInitialData?: HistoryInitialData;
}

const AUTH_TYPES: { value: AuthConfig["type"]; label: string }[] = [
Expand All @@ -235,7 +245,7 @@ const AUTH_TYPES: { value: AuthConfig["type"]; label: string }[] = [
{ value: "apikey", label: "API Key" },
];

export function RequestEditor({ requestId, collectionRelPath, workspacePath, environments, onOpenEnvTab, onHistoryEntry }: RequestEditorProps) {
export function RequestEditor({ requestId, collectionRelPath, workspacePath, environments, onOpenEnvTab, onHistoryEntry, historyInitialData }: RequestEditorProps) {
const [details, setDetails] = useState<RequestDetails | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
Expand Down Expand Up @@ -310,6 +320,19 @@ export function RequestEditor({ requestId, collectionRelPath, workspacePath, env
};
}, [requestId, collectionRelPath, workspacePath]);

// Initialize from history data when opened from history panel
useEffect(() => {
if (!historyInitialData) return;
setMethod(historyInitialData.method);
setUrl(historyInitialData.url);
setHeaders(historyInitialData.headers);
setParams(historyInitialData.params);
setBodyType(historyInitialData.bodyType);
setBodyContent(historyInitialData.bodyContent);
setResponseData(null);
setSendError(null);
}, [historyInitialData]);

// Load auth config when request changes
useEffect(() => {
if (!requestId) {
Expand Down Expand Up @@ -566,6 +589,9 @@ export function RequestEditor({ requestId, collectionRelPath, workspacePath, env
statusText: result.statusText,
timeMs: result.timeMs,
timestamp: new Date().toISOString(),
headers: headers.filter((h) => h.key.trim() !== "" || h.value.trim() !== ""),
params: params.filter((p) => p.key.trim() !== "" || p.value.trim() !== ""),
body: { type: bodyType as "none" | "json" | "text" | "form-data" | "x-www-form-urlencoded", content: bodyContent },
});
} catch (err: unknown) {
let msg: string;
Expand All @@ -592,6 +618,9 @@ export function RequestEditor({ requestId, collectionRelPath, workspacePath, env
statusText: null,
timeMs: null,
timestamp: new Date().toISOString(),
headers: headers.filter((h) => h.key.trim() !== "" || h.value.trim() !== ""),
params: params.filter((p) => p.key.trim() !== "" || p.value.trim() !== ""),
body: { type: bodyType as "none" | "json" | "text" | "form-data" | "x-www-form-urlencoded", content: bodyContent },
});
} finally {
if (unlistenProgress) unlistenProgress();
Expand Down Expand Up @@ -688,7 +717,7 @@ export function RequestEditor({ requestId, collectionRelPath, workspacePath, env
{ id: "settings", label: "Settings" },
];

if (!requestId) {
if (!requestId && !historyInitialData) {
return (
<div className="flex h-full items-center justify-center select-none">
<div className="text-center">
Expand Down
15 changes: 12 additions & 3 deletions src/components/workspace/request-tab-bar.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { cn } from "@/lib/utils";
import type { HttpMethod } from "@/lib/types";
import type { HistoryEntry, HttpMethod } from "@/lib/types";
import { BookOpen, Globe, Plus, X } from "lucide-react";
import { useState, useRef, useEffect, useCallback } from "react";

Expand Down Expand Up @@ -48,7 +48,16 @@ export interface CollectionDocTabItem {
isDirty?: boolean;
}

export type TabItem = RequestTabItem | EnvironmentTabItem | FolderReadmeTabItem | CollectionDocTabItem;
export interface HistoryRequestTabItem {
id: string;
kind: "history-request";
name: string;
method: HttpMethod;
historyEntry: HistoryEntry;
isDirty?: boolean;
}

export type TabItem = RequestTabItem | EnvironmentTabItem | FolderReadmeTabItem | CollectionDocTabItem | HistoryRequestTabItem;

interface RequestTabBarProps {
tabs: TabItem[];
Expand Down Expand Up @@ -155,7 +164,7 @@ export function RequestTabBar({
{activeTabId === tab.id && (
<div className="absolute left-0 right-0 top-0 h-[2px] rounded-b bg-primary" />
)}
{tab.kind === "request" ? (
{(tab.kind === "request" || tab.kind === "history-request") ? (
<span className={cn("text-[10px] font-bold uppercase tracking-wide", METHOD_COLORS[tab.method])}>
{tab.method}
</span>
Expand Down
66 changes: 61 additions & 5 deletions src/components/workspace/workspace-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,9 @@ import {
updateEnvVariable,
type EnvFile
} from "@/lib/environments";
import { addHistoryEntry, clearRequestHistory, getRequestHistory } from "@/lib/settings";
import { addHistoryEntry, clearRequestHistory, deleteHistoryEntry, getRequestHistory } from "@/lib/settings";
import type { HistoryEntry } from "@/lib/types";
import { Loader2, Plus, Trash2 } from "lucide-react";
import { Loader2, Plus, Trash2, X } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react";
import { ActivityBar, type ActivityTab } from "./activity-bar";
import { CollectionDocView } from "./collection-doc-view";
Expand All @@ -47,6 +47,7 @@ import {
type CollectionDocTabItem,
type EnvironmentTabItem,
type FolderReadmeTabItem,
type HistoryRequestTabItem,
type RequestTabItem,
type TabItem,
} from "./request-tab-bar";
Expand Down Expand Up @@ -129,6 +130,29 @@ function WorkspaceContent() {
setHistory([]);
}, []);

const handleDeleteHistoryEntry = useCallback(async (entryId: string) => {
const updated = await deleteHistoryEntry(entryId);
setHistory(updated);
}, []);

const handleSelectHistoryEntry = useCallback(
(entry: HistoryEntry) => {
const tabId = `history-${entry.id}`;
if (!openTabs.some((t) => t.id === tabId)) {
const newTab: HistoryRequestTabItem = {
id: tabId,
kind: "history-request",
name: entry.url,
method: entry.method,
historyEntry: entry,
};
setOpenTabs((prev) => [...prev, newTab]);
}
setActiveTabId(tabId);
},
[openTabs]
);

const loadEnvs = useCallback(async () => {
if (!folderPath) return;
try {
Expand Down Expand Up @@ -710,10 +734,14 @@ function WorkspaceContent() {
) : (
<div className="flex-1 overflow-y-auto">
{history.map((entry) => (
<button
<div
key={entry.id}
className="flex w-full items-center gap-2 border-b border-border/50 px-3 py-2 text-left hover:bg-muted/40 transition-colors cursor-pointer"
className="group flex w-full items-center gap-2 border-b border-border/50 px-3 py-2 text-left hover:bg-muted/40 transition-colors cursor-pointer"
title={entry.url}
onClick={() => handleSelectHistoryEntry(entry)}
role="button"
tabIndex={0}
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); handleSelectHistoryEntry(entry); } }}
>
<span className={`text-[10px] font-bold uppercase shrink-0 w-12 ${HISTORY_METHOD_COLORS[entry.method] ?? "text-muted-foreground"}`}>
{entry.method}
Expand All @@ -738,7 +766,17 @@ function WorkspaceContent() {
</span>
</div>
</div>
</button>
<button
onClick={(e) => {
e.stopPropagation();
handleDeleteHistoryEntry(entry.id);
}}
className="shrink-0 text-muted-foreground/30 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all cursor-pointer"
title="Delete entry"
>
<X className="size-3" />
</button>
</div>
))}
</div>
)}
Expand Down Expand Up @@ -831,6 +869,24 @@ function WorkspaceContent() {
environments={environments}
onOpenEnvTab={handleSelectEnv}
/>
) : activeTab?.kind === "history-request" ? (
<RequestEditor
key={activeTab.id}
requestId={null}
collectionRelPath={null}
workspacePath={folderPath}
environments={environments}
onOpenEnvTab={handleSelectEnv}
onHistoryEntry={handleHistoryEntry}
historyInitialData={{
method: activeTab.historyEntry.method,
url: activeTab.historyEntry.url,
headers: activeTab.historyEntry.headers ?? [],
params: activeTab.historyEntry.params ?? [],
bodyType: activeTab.historyEntry.body?.type ?? "none",
bodyContent: activeTab.historyEntry.body?.content ?? "",
}}
/>
) : (
<RequestEditor
requestId={activeRequest?.id ?? null}
Expand Down
16 changes: 16 additions & 0 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,22 @@ export async function addHistoryEntry(entry: HistoryEntry): Promise<HistoryEntry
}
}

export async function deleteHistoryEntry(entryId: string): Promise<HistoryEntry[]> {
try {
const store = await getStore();
if (!store) return [];

const existing =
(await store.get<HistoryEntry[]>(KEYS.REQUEST_HISTORY)) ?? [];
const updated = existing.filter((e) => e.id !== entryId);
await store.set(KEYS.REQUEST_HISTORY, updated);
await store.save();
return updated;
} catch {
return [];
}
}

export async function clearRequestHistory(): Promise<void> {
try {
const store = await getStore();
Expand Down
3 changes: 3 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,7 @@ export interface HistoryEntry {
statusText: string | null;
timeMs: number | null;
timestamp: string; // ISO 8601
headers: RequestHeader[];
params: QueryParam[];
body: RequestBody;
}