mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-12 10:21:30 +00:00
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:
@@ -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;
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import styled from 'styled-components';
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
.menu-icon {
|
||||
color: ${(props) => props.theme.sidebar.dropdownIcon.color};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'] {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -341,7 +341,8 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => {
|
||||
collectionUid,
|
||||
pathname,
|
||||
name,
|
||||
seq
|
||||
seq,
|
||||
uid: getRequestUid(pathname)
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user