Skip to content
99 changes: 78 additions & 21 deletions src/providers/RequestEditorProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ export class RequestEditorProvider {
// Track open panels by request ID
private static openPanels: Map<string, vscode.WebviewPanel> = new Map();

// Tracks the folder a request's open panel currently belongs to, kept in
// sync when the request (or an ancestor folder) is moved elsewhere. The
// panel's message handlers read from this map instead of the folderId
// that was captured when the panel was first opened.
private static panelFolderIds: Map<string, string> = new Map();

constructor(private readonly context: vscode.ExtensionContext) {}

// Update panel title for an open request editor
Expand Down Expand Up @@ -74,6 +80,38 @@ export class RequestEditorProvider {
});
}

/**
* Called when a request (or an ancestor folder) is moved to a new
* location. Updates the folder this panel's handlers resolve inherited
* config against, and — if the panel is open — pushes the new folder's
* inherited config/environment to the webview without touching the
* in-progress (possibly unsaved) request fields.
*/
public static notifyRequestMoved(
requestId: string,
newFolderId: string,
sidebarProvider: SidebarProvider,
): void {
RequestEditorProvider.panelFolderIds.set(requestId, newFolderId);

const panel = RequestEditorProvider.openPanels.get(requestId);
if (!panel) return;

const folderConfig = sidebarProvider.getInheritedConfig(newFolderId);
const envVariables = sidebarProvider.getActiveEnvVariables(newFolderId);
const collectionData = sidebarProvider.getCollectionData(newFolderId);
const collectionId = sidebarProvider.getRootCollectionId(newFolderId);

panel.webview.postMessage({
type: "folderConfigUpdated",
folderConfig,
envVariables,
environments: collectionData.environments,
activeEnvironmentId: collectionData.activeEnvironmentId,
collectionId,
});
}

/** Push a fresh configLoaded payload to a single open panel, if it exists. Used after a global-history restore, which has no open editor form of its own to update. */
/** Push a fresh historyUpdated payload to a single open panel, if it exists. Used after a global-history delete/clear affecting that request. */
public static refreshPanelHistory(requestId: string, historyManager: HistoryManager): void {
Expand Down Expand Up @@ -174,13 +212,20 @@ export class RequestEditorProvider {

// Store the panel reference
RequestEditorProvider.openPanels.set(requestId, panel);
RequestEditorProvider.panelFolderIds.set(requestId, folderId);

// Resolves the request's current folder, kept up to date across moves
// (see notifyRequestMoved) instead of the folderId captured above.
const getFolderId = () =>
RequestEditorProvider.panelFolderIds.get(requestId) ?? folderId;

// Notify sidebar of the initially active panel
sidebarProvider?.notifyActiveRequest(requestId);

// Remove from map when panel is closed
panel.onDidDispose(() => {
RequestEditorProvider.openPanels.delete(requestId);
RequestEditorProvider.panelFolderIds.delete(requestId);
sidebarProvider?.notifyActiveRequest(null);
});

Expand All @@ -191,27 +236,33 @@ export class RequestEditorProvider {
}
if (e.webviewPanel.visible) {
// Send updated folder config to webview
const currentFolderId = getFolderId();
const folderConfig = sidebarProvider
? sidebarProvider.getInheritedConfig(folderId)
? sidebarProvider.getInheritedConfig(currentFolderId)
: context.globalState.get<{
baseUrl?: string;
headers?: { key: string; value: string }[];
}>(`restlab.folder.${folderId}`) || {};
}>(`restlab.folder.${currentFolderId}`) || {};

const envVariables = sidebarProvider
? sidebarProvider.getActiveEnvVariables(folderId)
? sidebarProvider.getActiveEnvVariables(currentFolderId)
: {};

const collectionData = sidebarProvider
? sidebarProvider.getCollectionData(folderId)
? sidebarProvider.getCollectionData(currentFolderId)
: { environments: [], activeEnvironmentId: null };

const collectionId = sidebarProvider
? sidebarProvider.getRootCollectionId(currentFolderId)
: currentFolderId;

panel.webview.postMessage({
type: "folderConfigUpdated",
folderConfig: folderConfig,
envVariables: envVariables,
environments: collectionData.environments,
activeEnvironmentId: collectionData.activeEnvironmentId,
collectionId,
});
}
});
Expand All @@ -227,39 +278,40 @@ export class RequestEditorProvider {
// Handle messages from webview
panel.webview.onDidReceiveMessage(async (message) => {
switch (message.type) {
case "getConfig":
case "getConfig": {
// Always read fresh config from globalState to get latest folder settings
const currentFolderId = getFolderId();
const savedRequest = context.globalState.get<RequestConfig>(
`restlab.request.${requestId}`,
);

// Get inherited config from sidebar provider (walks up parent chain)
const folderConfig = sidebarProvider
? sidebarProvider.getInheritedConfig(folderId)
? sidebarProvider.getInheritedConfig(currentFolderId)
: context.globalState.get<{
baseUrl?: string;
headers?: { key: string; value: string }[];
}>(`restlab.folder.${folderId}`) || {};
}>(`restlab.folder.${currentFolderId}`) || {};

// Get active environment variables
const envVariables = sidebarProvider
? sidebarProvider.getActiveEnvVariables(folderId)
? sidebarProvider.getActiveEnvVariables(currentFolderId)
: {};

const collectionId = sidebarProvider
? sidebarProvider.getRootCollectionId(folderId)
: folderId;
? sidebarProvider.getRootCollectionId(currentFolderId)
: currentFolderId;

const collectionData = sidebarProvider
? sidebarProvider.getCollectionData(folderId)
? sidebarProvider.getCollectionData(currentFolderId)
: { environments: [], activeEnvironmentId: null };

panel.webview.postMessage({
type: "configLoaded",
config: {
id: requestId,
name: requestName,
folderId,
folderId: currentFolderId,
method: savedRequest?.method || "GET",
url: savedRequest?.url || "",
headers: savedRequest?.headers || [],
Expand All @@ -278,39 +330,43 @@ export class RequestEditorProvider {
history: historyManager.getForRequest(requestId),
});
break;
case "saveConfig":
}
case "saveConfig": {
await context.globalState.update(
`restlab.request.${requestId}`,
message.config,
);
const currentFolderId = getFolderId();
// Update method in sidebar if it changed
if (sidebarProvider && message.config.method) {
sidebarProvider.updateRequestMethod(
folderId,
currentFolderId,
requestId,
message.config.method,
);
}
// Update name in sidebar if it changed
if (sidebarProvider && message.config.name) {
sidebarProvider.updateRequestName(
folderId,
currentFolderId,
requestId,
message.config.name,
);
// Update panel title
panel.title = message.config.name;
}
break;
case "setActiveEnvironment":
}
case "setActiveEnvironment": {
if (sidebarProvider) {
const currentFolderId = getFolderId();
await sidebarProvider.setCollectionActiveEnvironment(
folderId,
currentFolderId,
message.envId ?? null,
);
const newEnvVars = sidebarProvider.getActiveEnvVariables(folderId);
const newCollData = sidebarProvider.getCollectionData(folderId);
const rootId = sidebarProvider.getRootCollectionId(folderId);
const newEnvVars = sidebarProvider.getActiveEnvVariables(currentFolderId);
const newCollData = sidebarProvider.getCollectionData(currentFolderId);
const rootId = sidebarProvider.getRootCollectionId(currentFolderId);
RequestEditorProvider.broadcastToAllPanels({
type: "environmentUpdated",
collectionId: rootId,
Expand All @@ -320,6 +376,7 @@ export class RequestEditorProvider {
});
}
break;
}
case "sendRequest": {
const recordHistory = async (response: ResponseData) => {
const snapshot = message.historySnapshot || {};
Expand All @@ -332,7 +389,7 @@ export class RequestEditorProvider {
await historyManager.addEntry({
requestId,
requestName,
folderId,
folderId: getFolderId(),
request: {
method: snapshot.method || message.method,
url: snapshot.url || "",
Expand Down
8 changes: 8 additions & 0 deletions src/providers/SidebarProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,11 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
);
}

// If this request is open in an editor panel, refresh it with the
// target folder's inherited config instead of leaving it pointed at
// the folder it was moved out of.
RequestEditorProvider.notifyRequestMoved(requestId, targetFolderId, this);

this._saveFolders();
this._sendFoldersToWebview();
vscode.window.showInformationMessage(
Expand Down Expand Up @@ -882,6 +887,9 @@ export class SidebarProvider implements vscode.WebviewViewProvider {
requestConfig,
);
}
// Refresh any open editor panel for this request with the new
// (possibly new-collection) inherited config.
RequestEditorProvider.notifyRequestMoved(request.id, folder.id, this);
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/webview/components/icons/CheckIcon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from "react";
const CheckIcon = () => (
<svg
width="10"
height="10"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
strokeLinejoin="round"
>
<polyline points="20 6 9 17 4 12" />
</svg>
);

export default CheckIcon;
61 changes: 42 additions & 19 deletions src/webview/editor/EnvVarInput.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { useRef } from "react";
import ReactDOM from "react-dom";

interface EnvVarInputProps {
value: string;
Expand All @@ -23,7 +24,9 @@ const EnvVarInput: React.FC<EnvVarInputProps> = ({
const [showPopup, setShowPopup] = React.useState(false);
const [filterText, setFilterText] = React.useState("");
const [activeIdx, setActiveIdx] = React.useState(0);
const [popupStyle, setPopupStyle] = React.useState<React.CSSProperties>({});
const inputRef = useRef<HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);

React.useEffect(() => {
const el = inputRef.current;
Expand Down Expand Up @@ -58,6 +61,23 @@ const EnvVarInput: React.FC<EnvVarInputProps> = ({
}
};

// Position the popup via a portal anchored to the input's live screen
// position instead of a locally `position: absolute` element. A locally
// absolute popup sits inside this input's fieldset stacking context, so a
// later sibling fieldset (e.g. "Headers") paints over it regardless of
// z-index — portaling to <body> with `position: fixed` escapes that.
React.useEffect(() => {
if (showPopup && containerRef.current) {
const rect = containerRef.current.getBoundingClientRect();
setPopupStyle({
position: "fixed",
top: rect.bottom + 4,
left: rect.left,
minWidth: Math.max(rect.width, 280),
});
}
}, [showPopup]);

const getFiltered = () =>
varKeys.filter((k) => k.toLowerCase().includes(filterText.toLowerCase()));

Expand Down Expand Up @@ -102,7 +122,7 @@ const EnvVarInput: React.FC<EnvVarInputProps> = ({
const filtered = getFiltered();

return (
<div className="var-input-container">
<div ref={containerRef} className="var-input-container">
<textarea
ref={inputRef}
rows={1}
Expand All @@ -114,24 +134,27 @@ const EnvVarInput: React.FC<EnvVarInputProps> = ({
className={`autogrow-textarea${className ? ` ${className}` : ""}`}
autoComplete="off"
/>
{showPopup && filtered.length > 0 && (
<div className="var-popup">
{filtered.map((k, i) => (
<div
key={k}
className={`var-popup-item ${i === activeIdx ? "active" : ""}`}
onMouseDown={(e) => {
e.preventDefault();
insertVar(k);
}}
onMouseEnter={() => setActiveIdx(i)}
>
<span className="var-popup-key">{`{{${k}}}`}</span>
<span className="var-popup-value">{envVariables[k]}</span>
</div>
))}
</div>
)}
{showPopup &&
filtered.length > 0 &&
ReactDOM.createPortal(
<div className="var-popup" style={popupStyle}>
{filtered.map((k, i) => (
<div
key={k}
className={`var-popup-item ${i === activeIdx ? "active" : ""}`}
onMouseDown={(e) => {
e.preventDefault();
insertVar(k);
}}
onMouseEnter={() => setActiveIdx(i)}
>
<span className="var-popup-key">{`{{${k}}}`}</span>
<span className="var-popup-value">{envVariables[k]}</span>
</div>
))}
</div>,
document.body,
)}
</div>
);
};
Expand Down
Loading