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
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React, { useCallback, useState, useRef, Fragment, useMemo, useEffect } from 'react';
import get from 'lodash/get';
import { makeTabPermanent, syncTabUid } from 'providers/ReduxStore/slices/tabs';
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment, saveCollectionSettings, closeTabs } from 'providers/ReduxStore/slices/collections/actions';
import { saveRequest, saveCollectionRoot, saveFolderRoot, saveEnvironment, saveCollectionSettings, closeTabs, cloneItemAsTransient } from 'providers/ReduxStore/slices/collections/actions';
import useKeybinding from 'hooks/useKeybinding';
import { deleteRequestDraft, deleteCollectionDraft, deleteFolderDraft, clearEnvironmentsDraft } from 'providers/ReduxStore/slices/collections';
import { clearGlobalEnvironmentDraft } from 'providers/ReduxStore/slices/global-environments';
import { saveGlobalEnvironment } from 'providers/ReduxStore/slices/global-environments';
import { useTheme } from 'providers/Theme';
import { useDispatch, useSelector } from 'react-redux';
import { findItemInCollection, findItemInCollectionByPathname, hasRequestChanges, areItemsLoading } from 'utils/collections';
import { findItemInCollection, findItemInCollectionByPathname, hasRequestChanges, areItemsLoading, isItemARequest } from 'utils/collections';
import ConfirmRequestClose from './ConfirmRequestClose';
import ConfirmCollectionClose from './ConfirmCollectionClose';
import ConfirmFolderClose from './ConfirmFolderClose';
Expand Down Expand Up @@ -706,6 +706,17 @@ function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, t
await handleCloseMultipleTabs(collectionRequestTabs);
}

const isRequestTab = currentTabItem && isItemARequest(currentTabItem);

const handleDuplicateAsTab = () => {
if (!currentTabItem?.uid || !collection?.uid) {
return;
}
dispatch(cloneItemAsTransient(currentTabItem.uid, collection.uid)).catch((err) => {
toast.error(err?.message || 'An error occurred while duplicating the request');
});
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

const menuItems = useMemo(() => [
{
id: 'new-request',
Expand All @@ -717,6 +728,12 @@ function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, t
label: 'Clone Request',
onClick: () => setShowCloneRequestModal(true)
},
{
id: 'duplicate-as-tab',
label: 'Duplicate as Tab',
onClick: handleDuplicateAsTab,
disabled: !isRequestTab
},
{
id: 'revert-changes',
label: 'Revert Changes',
Expand Down Expand Up @@ -756,7 +773,7 @@ function RequestTabMenu({ menuDropdownRef, tabLabelRef, collectionRequestTabs, t
label: 'Close All',
onClick: handleCloseAllTabs
}
], [currentTabUid, currentTabItem, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);
], [currentTabUid, currentTabItem, isRequestTab, hasOtherTabs, hasLeftTabs, hasRightTabs, collection, collectionRequestTabs, tabIndex, dispatch]);

const menuDropdown = (
<MenuDropdown
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -967,6 +967,68 @@ export const cloneItem = (newName, newFilename, itemUid, collectionUid) => (disp
});
};

// Duplicates a request into the collection's temp directory as a transient (unsaved) request,
// mirroring Postman's "Duplicate Tab" behavior. Folders are not supported — they require a
// committed name on disk to be meaningful.
export const cloneItemAsTransient = (itemUid, collectionUid) => (dispatch, getState) => {
const state = getState();
const collection = findCollectionByUid(state.collections.collections, collectionUid);

return new Promise((resolve, reject) => {
if (!collection) {
return reject(new Error('Collection not found'));
}

const tempDirectory = state.collections.tempDirectories?.[collectionUid];
if (!tempDirectory) {
return reject(new Error('Temporary directory is not initialized for this collection'));
}

const item = findItemInCollection(cloneDeep(collection), itemUid);
if (!item) {
return reject(new Error('Unable to locate item'));
}
if (isItemAFolder(item)) {
return reject(new Error('Folders cannot be duplicated as a tab'));
}

const transientRequests = filter(
flattenItems(collection.items),
(i) => isItemARequest(i) && i.pathname && i.pathname.startsWith(tempDirectory)
);
const { newName, newFilename } = generateUniqueName(`${item.name} copy`, transientRequests, false);
const filename = resolveRequestFilename(newFilename, collection.format);

// The `isTransient` flag is inferred at load time from the file's path
// (it's never persisted to disk), so we don't set it here — the schema is strict.
const itemToSave = refreshUidsInItem(transformRequestToSaveToFilesystem(item));
set(itemToSave, 'name', trim(newName));
set(itemToSave, 'filename', trim(filename));
const items = filter(collection.items, (i) => isItemAFolder(i) || isItemARequest(i));
itemToSave.seq = items.length + 1;

const fullPathname = path.join(tempDirectory, filename);
const { ipcRenderer } = window;

itemSchema
.validate(itemToSave)
.then(() => ipcRenderer.invoke('renderer:new-request', fullPathname, itemToSave))
.then(() => {
dispatch(
insertTaskIntoQueue({
uid: uuid(),
type: 'OPEN_REQUEST',
collectionUid,
itemPathname: fullPathname,
preview: false
})
);
resolve();
})
.catch(reject);
});
};

export const pasteItem = (targetCollectionUid, targetItemUid = null) => (dispatch, getState) => {
const state = getState();

Expand Down