persist request/folder uids after request/folder resequencing and ui updates (#4611)

* move file/folder uids to new paths

* drag file/folder preview ui updates, can item be dropped ui hint check

---------

Co-authored-by: lohit <lohit@usebruno.com>
This commit is contained in:
lohit
2025-05-06 22:20:59 +05:30
committed by GitHub
parent 38c307d6f1
commit 2ee7ce5829
12 changed files with 152 additions and 33 deletions

View File

@@ -0,0 +1,9 @@
import styled from 'styled-components';
const StyledWrapper = styled.div`
.drag-preview {
background-color: ${(props) => props.theme.sidebar.collection.item.hoverBg};
}
`;
export default StyledWrapper;

View File

@@ -0,0 +1,49 @@
import { useDragLayer } from 'react-dnd';
import {
IconFile,
IconFolder,
} from '@tabler/icons';
import StyledWrapper from './StyledWrapper';
function getItemStyles({ x, y }) {
if (Number.isNaN(x) || Number.isNaN(y)) return { display: 'none' };
const transform = `translate(${x}px, ${y}px)`;
return {
position: 'fixed',
pointerEvents: 'none',
top: 0,
transform,
WebkitTransform: transform,
zIndex: 100,
};
}
export const CollectionItemDragPreview = () => {
const {
item,
isDragging,
clientOffset
} = useDragLayer((monitor) => ({
item: monitor.getItem(),
isDragging: monitor.isDragging(),
clientOffset: monitor.getClientOffset(),
}));
if (!isDragging) return null;
const { x, y } = clientOffset || {};
const shouldShowFolderIcon = !item.type || item.type === 'folder';
return (
<StyledWrapper>
<div style={getItemStyles({ x, y })} className='p-2'>
<div className='flex items-center gap-2 border border-gray-500/10 rounded-md px-2 py-1 drag-preview'>
{shouldShowFolderIcon ? (
<IconFolder size={16} />
) : (
<IconFile size={16} />
)}
{item.name}
</div>
</div>
</StyledWrapper>
);
};

View File

@@ -1,6 +1,7 @@
import styled from 'styled-components';
const Wrapper = styled.div`
position: relative;
.menu-icon {
color: ${(props) => props.theme.sidebar.dropdownIcon.color};

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef, forwardRef } from 'react';
import React, { useState, useRef, forwardRef, useEffect } from 'react';
import { getEmptyImage } from 'react-dnd-html5-backend';
import range from 'lodash/range';
import filter from 'lodash/filter';
import classnames from 'classnames';
@@ -28,6 +29,7 @@ import CollectionItemIcon from './CollectionItemIcon';
import { scrollToTheActiveTab } from 'utils/tabs';
import { isTabForItemActive as isTabForItemActiveSelector, isTabForItemPresent as isTabForItemPresentSelector } from 'src/selectors/tab';
import { isEqual } from 'lodash';
import { calculateDraggedItemNewPathname } from 'utils/collections/index';
const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => {
const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid });
@@ -56,9 +58,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
const [dropType, setDropType] = useState(null); // 'adjacent' or 'inside'
const [{ isDragging }, drag] = useDrag({
const [{ isDragging }, drag, dragPreview] = useDrag({
type: `collection-item-${collectionUid}`,
item: item,
item,
collect: (monitor) => ({
isDragging: monitor.isDragging()
}),
@@ -67,6 +69,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
}
});
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
const determineDropType = (monitor) => {
const hoverBoundingRect = ref.current?.getBoundingClientRect();
const clientOffset = monitor.getClientOffset();
@@ -83,6 +89,20 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
}
};
const canItemBeDropped = ({ draggedItem, targetItem, dropType }) => {
const { uid: targetItemUid, pathname: targetItemPathname } = targetItem;
const { uid: draggedItemUid, pathname: draggedItemPathname } = draggedItem;
if (draggedItemUid === targetItemUid) return false;
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname });
if (!newPathname) return false;
if (targetItemPathname?.startsWith(draggedItemPathname)) return false;
return true;
};
const [{ isOver, canDrop }, drop] = useDrop({
accept: `collection-item-${collectionUid}`,
hover: (draggedItem, monitor) => {
@@ -92,7 +112,10 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText })
if (draggedItemUid === targetItemUid) return;
const dropType = determineDropType(monitor);
setDropType(dropType);
const _canItemBeDropped = canItemBeDropped({ draggedItem, targetItem: item, dropType });
setDropType(_canItemBeDropped ? dropType : null);
},
drop: async (draggedItem, monitor) => {
const { uid: targetItemUid } = item;

View File

@@ -13,7 +13,8 @@ const Wrapper = styled.div`
}
&.item-hovered {
background: ${(props) => props.theme.sidebar.collection.item.hoverBg};
border-top: ${(props) => props.theme.dragAndDrop.borderStyle} ${(props) => props.theme.dragAndDrop.border};
border-bottom: 2px solid transparent;
.collection-actions {
.dropdown {
div[aria-expanded='false'] {

View File

@@ -1,4 +1,5 @@
import React, { useState, forwardRef, useRef, useEffect } from 'react';
import { getEmptyImage } from 'react-dnd-html5-backend';
import classnames from 'classnames';
import { uuid } from 'utils/common';
import filter from 'lodash/filter';
@@ -7,7 +8,7 @@ import { IconChevronRight, IconDots, IconLoader2 } from '@tabler/icons';
import Dropdown from 'components/Dropdown';
import { collapseCollection } from 'providers/ReduxStore/slices/collections';
import { mountCollection, moveCollectionAndPersist, handleCollectionItemDrop } from 'providers/ReduxStore/slices/collections/actions';
import { useDispatch, useSelector } from 'react-redux';
import { useDispatch } from 'react-redux';
import { addTab, makeTabPermanent } from 'providers/ReduxStore/slices/tabs';
import NewRequest from 'components/Sidebar/NewRequest';
import NewFolder from 'components/Sidebar/NewFolder';
@@ -19,9 +20,10 @@ import { isItemAFolder, isItemARequest } from 'utils/collections';
import RenameCollection from './RenameCollection';
import StyledWrapper from './StyledWrapper';
import CloneCollection from './CloneCollection';
import { areItemsLoading, findItemInCollection } from 'utils/collections';
import { areItemsLoading } from 'utils/collections';
import { scrollToTheActiveTab } from 'utils/tabs';
import ShareCollection from 'components/ShareCollection/index';
import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index';
const Collection = ({ collection, searchText }) => {
const [showNewFolderModal, setShowNewFolderModal] = useState(false);
@@ -127,8 +129,8 @@ const Collection = ({ collection, searchText }) => {
const isCollectionItem = (itemType) => {
return itemType.startsWith('collection-item');
};
const [{ isDragging }, drag] = useDrag({
const [{ isDragging }, drag, dragPreview] = useDrag({
type: "collection",
item: collection,
collect: (monitor) => ({
@@ -157,7 +159,9 @@ const Collection = ({ collection, searchText }) => {
}),
});
drag(drop(collectionRef));
useEffect(() => {
dragPreview(getEmptyImage(), { captureDraggingState: true });
}, []);
if (searchText && searchText.length) {
if (!doesCollectionHaveItemsMatchingSearchText(collection, searchText)) {
@@ -193,8 +197,12 @@ const Collection = ({ collection, searchText }) => {
{showCloneCollectionModalOpen && (
<CloneCollection collectionUid={collection.uid} collectionPathname={collection.pathname} onClose={() => setShowCloneCollectionModalOpen(false)} />
)}
<CollectionItemDragPreview />
<div className={collectionRowClassName}
ref={collectionRef}
ref={(node) => {
collectionRef.current = node;
drag(drop(node));
}}
>
<div
className="flex flex-grow items-center overflow-hidden"
@@ -291,7 +299,6 @@ const Collection = ({ collection, searchText }) => {
</Dropdown>
</div>
</div>
<div>
{!collectionIsCollapsed ? (
<div>

View File

@@ -44,7 +44,7 @@ import { closeAllCollectionTabs } from 'providers/ReduxStore/slices/tabs';
import { resolveRequestFilename } from 'utils/common/platform';
import { parsePathParams, parseQueryParams, splitOnFirst } from 'utils/url/index';
import { sendCollectionOauth2Request as _sendCollectionOauth2Request } from 'utils/network/index';
import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory } from 'utils/collections/index';
import { getGlobalEnvironmentVariables, findCollectionByPathname, findEnvironmentInCollectionByName, getReorderedItemsInTargetDirectory, resetSequencesInFolder, getReorderedItemsInSourceDirectory, calculateDraggedItemNewPathname } from 'utils/collections/index';
import { sanitizeName } from 'utils/common/regex';
import { safeParseJSON, safeStringifyJSON } from 'utils/common/index';
@@ -649,21 +649,6 @@ export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, co
const draggedItemDirectory = findParentItemInCollection(collection, draggedItemUid) || collection;
const draggedItemDirectoryItems = cloneDeep(draggedItemDirectory.items);
const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType }) => {
const { pathname: targetItemPathname } = targetItem;
const { filename: draggedItemFilename } = draggedItem;
const targetItemDirname = path.dirname(targetItemPathname);
const isTargetTheCollection = targetItemPathname === collection.pathname;
const isTargetItemAFolder = isItemAFolder(targetItem);
if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {
return path.join(targetItemPathname, draggedItemFilename)
} else if (dropType === 'adjacent') {
return path.join(targetItemDirname, draggedItemFilename)
}
return null;
};
const handleMoveToNewLocation = async ({ draggedItem, draggedItemDirectoryItems, targetItem, targetItemDirectoryItems, newPathname, dropType }) => {
const { uid: targetItemUid } = targetItem;
const { pathname: draggedItemPathname, uid: draggedItemUid } = draggedItem;
@@ -725,7 +710,7 @@ export const handleCollectionItemDrop = ({ targetItem, draggedItem, dropType, co
return new Promise(async (resolve, reject) => {
try {
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType });
const newPathname = calculateDraggedItemNewPathname({ draggedItem, targetItem, dropType, collectionPathname: collection.pathname });
if (!newPathname) return;
if (targetItemPathname?.startsWith(draggedItemPathname)) return;
if (newPathname !== draggedItemPathname) {

View File

@@ -1798,7 +1798,7 @@ export const collectionsSlice = createSlice({
currentPath = path.join(currentPath, directoryName);
if (!childItem) {
childItem = {
uid: uuid(),
uid: dir?.meta?.uid || uuid(),
pathname: currentPath,
name: dir?.meta?.name || directoryName,
seq: dir?.meta?.seq || 1,

View File

@@ -1068,5 +1068,20 @@ export const getReorderedItemsInSourceDirectory = ({ items }) => {
);
};
export const calculateDraggedItemNewPathname = ({ draggedItem, targetItem, dropType, collectionPathname }) => {
const { pathname: targetItemPathname } = targetItem;
const { filename: draggedItemFilename } = draggedItem;
const targetItemDirname = path.dirname(targetItemPathname);
const isTargetTheCollection = targetItemPathname === collectionPathname;
const isTargetItemAFolder = isItemAFolder(targetItem);
if (dropType === 'inside' && (isTargetItemAFolder || isTargetTheCollection)) {
return path.join(targetItemPathname, draggedItemFilename)
} else if (dropType === 'adjacent') {
return path.join(targetItemDirname, draggedItemFilename)
}
return null;
};
// item sequence utils - END

View File

@@ -341,7 +341,8 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
collectionUid,
pathname,
name,
seq
seq,
uid: getRequestUid(pathname)
}
};

View File

@@ -25,7 +25,8 @@ const {
sizeInMB,
safeWriteFileSync,
copyPath,
removePath
removePath,
getPaths
} = require('../utils/filesystem');
const { openCollectionDialog } = require('../app/collections');
const { generateUidBasedOnHash, stringifyJson, safeParseJSON, safeStringifyJSON } = require('../utils/common');
@@ -799,8 +800,15 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection
ipcMain.handle('renderer:move-item', async (event, { targetDirname, sourcePathname }) => {
try {
if (fs.existsSync(targetDirname)) {
const sourceDirname = path.dirname(sourcePathname);
const pathnamesBefore = await getPaths(sourcePathname);
const pathnamesAfter = pathnamesBefore?.map(p => p?.replace(sourceDirname, targetDirname));
await copyPath(sourcePathname, targetDirname);
await removePath(sourcePathname);
// move the request uids of the previous file/folders to the new file/folder items
pathnamesAfter?.forEach((_, index) => {
moveRequestUid(pathnamesBefore[index], pathnamesAfter[index]);
});
}
} catch (error) {
return Promise.reject(error);

View File

@@ -324,6 +324,25 @@ const removePath = async (source) => {
}
}
// Recursively gets paths.
const getPaths = async (source) => {
let paths = [];
const _getPaths = async (source) => {
const stat = await fsPromises.lstat(source);
paths.push(source);
if (stat.isDirectory()) {
const entries = await fsPromises.readdir(source);
for (const entry of entries) {
const entryPath = path.join(source, entry);
await _getPaths(entryPath);
}
}
}
await _getPaths(source);
return paths;
}
module.exports = {
isValidPathname,
exists,
@@ -352,5 +371,6 @@ module.exports = {
safeWriteFile,
safeWriteFileSync,
copyPath,
removePath
removePath,
getPaths
};