diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js index 5bb23dfdf..a20de70ea 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/CollectionItem/index.js @@ -30,6 +30,7 @@ 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'; +import { sortByNameThenSequence } from 'utils/common/index'; const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) => { const _isTabForItemActiveSelector = isTabForItemActiveSelector({ itemUid: item.uid }); @@ -250,7 +251,7 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) dispatch(makeTabPermanent({ uid: item.uid })); }; - // Sort items by their "seq" property. + // Sort items by their "seq" property. const sortItemsBySequence = (items = []) => { return items.sort((a, b) => a.seq - b.seq); }; @@ -262,9 +263,9 @@ const CollectionItem = ({ item, collectionUid, collectionPathname, searchText }) }); }; - const folderItems = sortItemsBySequence(filter(item.items, (i) => isItemAFolder(i))); + const folderItems = sortByNameThenSequence(filter(item.items, (i) => isItemAFolder(i))); const requestItems = sortItemsBySequence(filter(item.items, (i) => isItemARequest(i))); - + const handleGenerateCode = (e) => { e.stopPropagation(); dropdownTippyRef.current.hide(); diff --git a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js index b2e20f236..404258d8d 100644 --- a/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js +++ b/packages/bruno-app/src/components/Sidebar/Collections/Collection/index.js @@ -25,6 +25,7 @@ import { areItemsLoading } from 'utils/collections'; import { scrollToTheActiveTab } from 'utils/tabs'; import ShareCollection from 'components/ShareCollection/index'; import { CollectionItemDragPreview } from './CollectionItem/CollectionItemDragPreview/index'; +import { sortByNameThenSequence } from 'utils/common/index'; const Collection = ({ collection, searchText }) => { const [showNewFolderModal, setShowNewFolderModal] = useState(false); @@ -185,7 +186,7 @@ const Collection = ({ collection, searchText }) => { }; const requestItems = sortItemsBySequence(filter(collection.items, (i) => isItemARequest(i))); - const folderItems = sortItemsBySequence(filter(collection.items, (i) => isItemAFolder(i))); + const folderItems = sortByNameThenSequence(filter(collection.items, (i) => isItemAFolder(i))); return ( diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js index bbbc9a61e..6c880096e 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/actions.js @@ -383,33 +383,26 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => if (!folderWithSameNameExists) { const fullName = path.join(collection.pathname, directoryName); const { ipcRenderer } = window; + + const folderBruJsonData = { + meta: { + name: folderName, + seq: items?.length + 1 + }, + request: { + auth: { + mode: 'inherit' + } + } + }; + ipcRenderer - .invoke('renderer:new-folder', fullName) - .then(async () => { - const folderData = { - name: folderName, - pathname: fullName, - root: { - meta: { - name: folderName, - seq: items?.length + 1 - }, - request: { - auth: { - mode: 'inherit' - } - } - } - }; - ipcRenderer - .invoke('renderer:save-folder-root', folderData) - .then(resolve) - .catch((err) => { - toast.error('Failed to save folder settings!'); - reject(err); - }); - }) - .catch((error) => reject(error)); + .invoke('renderer:new-folder', { pathname: fullName, folderBruJsonData }) + .then(resolve) + .catch((error) => { + toast.error('Failed to create a new folder!'); + reject(error) + }); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); } @@ -424,33 +417,25 @@ export const newFolder = (folderName, directoryName, collectionUid, itemUid) => const fullName = path.join(currentItem.pathname, directoryName); const { ipcRenderer } = window; + const folderBruJsonData = { + meta: { + name: folderName, + seq: items?.length + 1 + }, + request: { + auth: { + mode: 'inherit' + } + } + }; + ipcRenderer - .invoke('renderer:new-folder', fullName) - .then(async () => { - const folderData = { - name: folderName, - pathname: fullName, - root: { - meta: { - name: folderName, - seq: items?.length + 1 - }, - request: { - auth: { - mode: 'inherit' - } - } - } - }; - ipcRenderer - .invoke('renderer:save-folder-root', folderData) - .then(resolve) - .catch((err) => { - toast.error('Failed to save folder settings!'); - reject(err); - }); - }) - .catch((error) => reject(error)); + .invoke('renderer:new-folder', { pathname: fullName, folderBruJsonData }) + .then(resolve) + .catch((error) => { + toast.error('Failed to create a new folder!'); + reject(error) + }); } else { return reject(new Error('Duplicate folder names under same parent folder are not allowed')); } diff --git a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js index 47a46a287..b904f64b8 100644 --- a/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js +++ b/packages/bruno-app/src/providers/ReduxStore/slices/collections/index.js @@ -1890,7 +1890,7 @@ export const collectionsSlice = createSlice({ uid: dir?.meta?.uid || uuid(), pathname: currentPath, name: dir?.meta?.name || directoryName, - seq: dir?.meta?.seq || 1, + seq: dir?.meta?.seq, filename: directoryName, collapsed: true, type: 'folder', diff --git a/packages/bruno-app/src/utils/collections/index.js b/packages/bruno-app/src/utils/collections/index.js index 9085a5b53..69f15fd46 100644 --- a/packages/bruno-app/src/utils/collections/index.js +++ b/packages/bruno-app/src/utils/collections/index.js @@ -1,5 +1,6 @@ import {cloneDeep, isEqual, sortBy, filter, map, isString, findIndex, find, each, get } from 'lodash'; import { uuid } from 'utils/common'; +import { sortByNameThenSequence } from 'utils/common/index'; import path from 'utils/common/path'; const replaceTabsWithSpaces = (str, numSpaces = 2) => { @@ -1036,7 +1037,7 @@ export const getFormattedCollectionOauth2Credentials = ({ oauth2Credentials = [] export const resetSequencesInFolder = (folderItems) => { const items = folderItems; - const sortedItems = items.sort((a, b) => a.seq - b.seq); + const sortedItems = sortByNameThenSequence(items); return sortedItems.map((item, index) => { item.seq = index + 1; return item; diff --git a/packages/bruno-app/src/utils/common/folders-requests-sorting.spec.js b/packages/bruno-app/src/utils/common/folders-requests-sorting.spec.js new file mode 100644 index 000000000..fbcf71379 --- /dev/null +++ b/packages/bruno-app/src/utils/common/folders-requests-sorting.spec.js @@ -0,0 +1,374 @@ +const { describe, it, expect } = require('@jest/globals'); +const { sortByNameThenSequence } = require('./index'); + +describe('sortByNameThenSequence', () => { + describe('Basic functionality', () => { + it('should return an empty array when given an empty array', () => { + const items = []; + const result = sortByNameThenSequence(items); + expect(result).toEqual([]); + }); + + it('should not mutate the original array', () => { + const items = [ + { name: 'folder_2', seq: 2 }, + { name: 'folder_1', seq: 1 } + ]; + const originalItems = JSON.parse(JSON.stringify(items)); + sortByNameThenSequence(items); + expect(items).toEqual(originalItems); + }); + + it('should return a new array instance', () => { + const items = [{ name: 'folder_1' }]; + const result = sortByNameThenSequence(items); + expect(result).not.toBe(items); + }); + }); + + describe('Alphabetical sorting (no sequence numbers)', () => { + it('should sort items alphabetically by name when no sequence numbers are present', () => { + const items = [ + { name: 'folder_3' }, + { name: 'folder_1' }, + { name: 'folder_2' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1' }, + { name: 'folder_2' }, + { name: 'folder_3' } + ]); + }); + + it('should handle case-sensitive sorting correctly', () => { + const items = [ + { name: 'Folder_2' }, + { name: 'folder_1' }, + { name: 'FOLDER_3' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1' }, + { name: 'Folder_2' }, + { name: 'FOLDER_3' } + ]); + }); + + it('should handle special characters in names', () => { + const items = [ + { name: 'folder-2' }, + { name: 'folder_1' }, + { name: 'folder 3' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder 3' }, + { name: 'folder_1' }, + { name: 'folder-2' } + ]); + }); + }); + + describe('Sequence-based sorting (valid sequence numbers)', () => { + it('should sort items by sequence when all items have valid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: 3 }, + { name: 'folder_1', seq: 1 }, + { name: 'folder_2', seq: 2 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: 1 }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_3', seq: 3 } + ]); + }); + + it('should handle duplicate sequence numbers by inserting them in alphabetical order', () => { + const items = [ + { name: 'folder_3', seq: 1 }, + { name: 'folder_1', seq: 1 }, + { name: 'folder_2', seq: 2 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: 1 }, + { name: 'folder_3', seq: 1 }, + { name: 'folder_2', seq: 2 }, + ]); + }); + + it('should handle large sequence numbers correctly', () => { + const items = [ + { name: 'folder_1', seq: 100 }, + { name: 'folder_2', seq: 1 }, + { name: 'folder_3', seq: 50 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_2', seq: 1 }, + { name: 'folder_3', seq: 50 }, + { name: 'folder_1', seq: 100 } + ]); + }); + }); + + describe('Invalid sequence numbers', () => { + it('should treat undefined sequence as invalid and sort alphabetically', () => { + const items = [ + { name: 'folder_3', seq: undefined }, + { name: 'folder_1', seq: undefined }, + { name: 'folder_2', seq: undefined } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: undefined }, + { name: 'folder_2', seq: undefined }, + { name: 'folder_3', seq: undefined } + ]); + }); + + it('should treat null sequence as invalid and sort alphabetically', () => { + const items = [ + { name: 'folder_3', seq: null }, + { name: 'folder_1', seq: null }, + { name: 'folder_2', seq: null } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: null }, + { name: 'folder_2', seq: null }, + { name: 'folder_3', seq: null } + ]); + }); + + it('should treat boolean values as invalid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: true }, + { name: 'folder_1', seq: false }, + { name: 'folder_2', seq: true } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: false }, + { name: 'folder_2', seq: true }, + { name: 'folder_3', seq: true } + ]); + }); + + it('should treat string values as invalid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: '3' }, + { name: 'folder_1', seq: '1' }, + { name: 'folder_2', seq: 'invalid' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: '1' }, + { name: 'folder_2', seq: 'invalid' }, + { name: 'folder_3', seq: '3' } + ]); + }); + + it('should treat non-integer numbers as invalid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: 3.5 }, + { name: 'folder_1', seq: 1.2 }, + { name: 'folder_2', seq: 2.0 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: 1.2 }, + { name: 'folder_2', seq: 2.0 }, + { name: 'folder_3', seq: 3.5 } + ]); + }); + + it('should treat zero and negative numbers as invalid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: 0 }, + { name: 'folder_1', seq: -1 }, + { name: 'folder_2', seq: -5 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: -1 }, + { name: 'folder_2', seq: -5 }, + { name: 'folder_3', seq: 0 } + ]); + }); + + it('should treat NaN and Infinity as invalid sequence numbers', () => { + const items = [ + { name: 'folder_3', seq: NaN }, + { name: 'folder_1', seq: Infinity }, + { name: 'folder_2', seq: -Infinity } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: Infinity }, + { name: 'folder_2', seq: -Infinity }, + { name: 'folder_3', seq: NaN } + ]); + }); + + it('should handle invalid sequence numbers correctly', () => { + const items = [ + { name: 'folder_4', seq: undefined }, + { name: 'folder_1', seq: false }, + { name: 'folder_5', seq: 'invalid' }, + { name: 'folder_2', seq: true }, + { name: 'folder_3', seq: null } + ]; + const sorted = sortByNameThenSequence(items); + expect(sorted).toEqual([ + { name: 'folder_1', seq: false }, + { name: 'folder_2', seq: true }, + { name: 'folder_3', seq: null }, + { name: 'folder_4', seq: undefined }, + { name: 'folder_5', seq: 'invalid' } + ]); + }); + }); + + describe('Mixed valid and invalid sequence numbers', () => { + it('should handle mixed valid and invalid sequence numbers correctly', () => { + const items = [ + { name: 'folder_4', seq: undefined }, + { name: 'folder_1', seq: false }, + { name: 'folder_5', seq: 3 }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_3', seq: null }, + { name: 'folder_6', seq: 9 }, + { name: 'folder_8', seq: 'invalid' }, + { name: 'folder_7', seq: 4 } + ]; + const sorted = sortByNameThenSequence(items); + expect(sorted).toEqual([ + { name: 'folder_1', seq: false }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_5', seq: 3 }, + { name: 'folder_7', seq: 4 }, + { name: 'folder_3', seq: null }, + { name: 'folder_4', seq: undefined }, + { name: 'folder_8', seq: 'invalid' }, + { name: 'folder_6', seq: 9 } + ]); + }); + + it('should insert sequenced items at their positions among non-sequenced items', () => { + const items = [ + { name: 'folder_6' }, + { name: 'folder_1', seq: 1 }, + { name: 'folder_5' }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_4' }, + { name: 'folder_3', seq: 4 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: 1 }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_4' }, + { name: 'folder_3', seq: 4 }, + { name: 'folder_5' }, + { name: 'folder_6' } + ]); + }); + + it('should handle sequence numbers beyond the array length', () => { + const items = [ + { name: 'folder_1', seq: 10 }, + { name: 'folder_2' }, + { name: 'folder_3', seq: 20 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_2' }, + { name: 'folder_1', seq: 10 }, + { name: 'folder_3', seq: 20 } + ]); + }); + }); + + describe('Edge cases and boundary conditions', () => { + it('should handle items with missing name property without throwing errors', () => { + const items = [ + { seq: 1 }, + { name: 'folder_1' }, + { name: 'folder_2', seq: 2 } + ]; + // Note: This might cause issues in production, but we test the current behavior + expect(() => sortByNameThenSequence(items)).not.toThrow(); + }); + + it('should handle items with no seq property (equivalent to undefined)', () => { + const items = [ + { name: 'folder_3' }, + { name: 'folder_1', seq: 1 }, + { name: 'folder_2' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_1', seq: 1 }, + { name: 'folder_2' }, + { name: 'folder_3' } + ]); + }); + + it('should handle single item arrays', () => { + const items = [{ name: 'folder_1', seq: 1 }]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([{ name: 'folder_1', seq: 1 }]); + }); + + it('should handle items with identical names but different sequences', () => { + const items = [ + { name: 'folder', seq: 2 }, + { name: 'folder', seq: 1 }, + { name: 'folder' } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder', seq: 1 }, + { name: 'folder', seq: 2 }, + { name: 'folder' } + ]); + }); + }); + + describe('Complex scenarios', () => { + it('should handle a comprehensive mix of all scenarios', () => { + const items = [ + { name: 'folder_10', seq: 'invalid' }, + { name: 'folder_1', seq: false }, + { name: 'folder_11', seq: 3 }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_3', seq: null }, + { name: 'folder_12', seq: 9 }, + { name: 'folder_4', seq: undefined }, + { name: 'folder_5' }, + { name: 'folder_6', seq: 0 }, + { name: 'folder_7', seq: 4 }, + { name: 'folder_8', seq: 1 }, + { name: 'folder_9', seq: -1 } + ]; + const result = sortByNameThenSequence(items); + expect(result).toEqual([ + { name: 'folder_8', seq: 1 }, + { name: 'folder_2', seq: 2 }, + { name: 'folder_11', seq: 3 }, + { name: 'folder_7', seq: 4 }, + { name: 'folder_1', seq: false }, + { name: 'folder_10', seq: 'invalid' }, + { name: 'folder_3', seq: null }, + { name: 'folder_4', seq: undefined }, + { name: 'folder_12', seq: 9 }, + { name: 'folder_5' }, + { name: 'folder_6', seq: 0 }, + { name: 'folder_9', seq: -1 } + ]); + }); + }); +}); \ No newline at end of file diff --git a/packages/bruno-app/src/utils/common/index.js b/packages/bruno-app/src/utils/common/index.js index f6621621f..6402d5a1e 100644 --- a/packages/bruno-app/src/utils/common/index.js +++ b/packages/bruno-app/src/utils/common/index.js @@ -219,4 +219,45 @@ export const formatSize = (bytes) => { } return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB'; -} \ No newline at end of file +} + +export const sortByNameThenSequence = items => { + const isSeqValid = seq => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0; + + // Sort folders alphabetically by name + const alphabeticallySorted = [...items].sort((a, b) => a.name && b.name && a.name.localeCompare(b.name)); + + // Extract folders without 'seq' + const withoutSeq = alphabeticallySorted.filter(f => !isSeqValid(f['seq'])); + + // Extract folders with 'seq' and sort them by 'seq' + const withSeq = alphabeticallySorted.filter(f => isSeqValid(f['seq'])).sort((a, b) => a.seq - b.seq); + + const sortedItems = withoutSeq; + + // Insert folders with 'seq' at their specified positions + withSeq.forEach((item) => { + const position = item.seq - 1; + const existingItem = withoutSeq[position]; + + // Check if there's already an item with the same sequence number + const hasItemWithSameSeq = Array.isArray(existingItem) + ? existingItem[0].seq === item.seq + : existingItem?.seq === item.seq; + + if (hasItemWithSameSeq) { + // If there's a conflict, group items with same sequence together + const newGroup = Array.isArray(existingItem) + ? [...existingItem, item] + : [existingItem, item]; + + withoutSeq.splice(position, 1, newGroup); + } else { + // Insert item at the specified position + withoutSeq.splice(position, 0, item); + } + }); + + // return flattened sortedItems + return sortedItems.flat(); +}; \ No newline at end of file diff --git a/packages/bruno-cli/src/utils/bru.js b/packages/bruno-cli/src/utils/bru.js index 07844a455..193354f50 100644 --- a/packages/bruno-cli/src/utils/bru.js +++ b/packages/bruno-cli/src/utils/bru.js @@ -22,8 +22,11 @@ const collectionBruToJson = (bru) => { if (json?.meta) { transformedJson.meta = { name: json.meta.name, - seq: !isNaN(sequence) ? Number(sequence) : 1 }; + + if (sequence) { + transformedJson.meta.seq = Number(sequence); + } } return transformedJson; @@ -59,7 +62,7 @@ const bruToJson = (bru) => { const transformedJson = { type: requestType, name: _.get(json, 'meta.name'), - seq: !isNaN(sequence) ? Number(sequence) : 1, + seq: !_.isNaN(sequence) ? Number(sequence) : 1, request: { method: _.upperCase(_.get(json, 'http.method')), url: _.get(json, 'http.url'), diff --git a/packages/bruno-electron/src/app/collection-watcher.js b/packages/bruno-electron/src/app/collection-watcher.js index 72b161292..d6b6cbfba 100644 --- a/packages/bruno-electron/src/app/collection-watcher.js +++ b/packages/bruno-electron/src/app/collection-watcher.js @@ -326,14 +326,14 @@ const addDirectory = async (win, pathname, collectionUid, collectionPath) => { } let name = path.basename(pathname); - let seq = 1; + let seq; const folderBruFilePath = path.join(pathname, `folder.bru`); if (fs.existsSync(folderBruFilePath)) { let folderBruFileContent = fs.readFileSync(folderBruFilePath, 'utf8'); let folderBruData = await collectionBruToJson(folderBruFileContent); name = folderBruData?.meta?.name || name; - seq = folderBruData?.meta?.seq || seq; + seq = folderBruData?.meta?.seq; } const directory = { diff --git a/packages/bruno-electron/src/bru/index.js b/packages/bruno-electron/src/bru/index.js index 946d95519..cde4067ac 100644 --- a/packages/bruno-electron/src/bru/index.js +++ b/packages/bruno-electron/src/bru/index.js @@ -33,8 +33,11 @@ const collectionBruToJson = async (data, parsed = false) => { if (json?.meta) { transformedJson.meta = { name: json.meta.name, - seq: !isNaN(sequence) ? Number(sequence) : 1 }; + + if (sequence) { + transformedJson.meta.seq = Number(sequence); + } } return transformedJson; @@ -67,8 +70,11 @@ const jsonToCollectionBru = async (json, isFolder) => { if (json?.meta) { collectionBruJson.meta = { name: json.meta.name, - seq: !isNaN(sequence) ? Number(sequence) : 1 }; + + if (sequence) { + collectionBruJson.meta.seq = Number(sequence); + } } return _jsonToCollectionBru(collectionBruJson); @@ -129,7 +135,7 @@ const bruToJson = (data, parsed = false) => { const transformedJson = { type: requestType, name: _.get(json, 'meta.name'), - seq: !isNaN(sequence) ? Number(sequence) : 1, + seq: !_.isNaN(sequence) ? Number(sequence) : 1, request: { method: _.upperCase(_.get(json, 'http.method')), url: _.get(json, 'http.url'), @@ -187,7 +193,7 @@ const jsonToBru = async (json) => { meta: { name: _.get(json, 'name'), type: type, - seq: !isNaN(sequence) ? Number(sequence) : 1 + seq: !_.isNaN(sequence) ? Number(sequence) : 1 }, http: { method: _.lowerCase(_.get(json, 'request.method')), @@ -228,7 +234,7 @@ const jsonToBruViaWorker = async (json) => { meta: { name: _.get(json, 'name'), type: type, - seq: !isNaN(sequence) ? Number(sequence) : 1 + seq: !_.isNaN(sequence) ? Number(sequence) : 1 }, http: { method: _.lowerCase(_.get(json, 'request.method')), diff --git a/packages/bruno-electron/src/ipc/collection.js b/packages/bruno-electron/src/ipc/collection.js index 96322e4b4..9367b2aa7 100644 --- a/packages/bruno-electron/src/ipc/collection.js +++ b/packages/bruno-electron/src/ipc/collection.js @@ -213,8 +213,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection if (!folderRoot.meta) { folderRoot.meta = { - name: folderName, - seq: 1 + name: folderName }; } @@ -400,8 +399,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } else { folderBruFileJsonContent = { meta: { - name: newName, - seq: 1 + name: newName } }; } @@ -451,8 +449,7 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection } else { folderBruFileJsonContent = { meta: { - name: newName, - seq: 1 + name: newName } }; } @@ -526,20 +523,14 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection }); // new folder - ipcMain.handle('renderer:new-folder', async (event, pathname, folderName) => { + ipcMain.handle('renderer:new-folder', async (event, { pathname, folderBruJsonData }) => { const resolvedFolderName = sanitizeName(path.basename(pathname)); pathname = path.join(path.dirname(pathname), resolvedFolderName); try { if (!fs.existsSync(pathname)) { fs.mkdirSync(pathname); const folderBruFilePath = path.join(pathname, 'folder.bru'); - let data = { - meta: { - name: folderName, - seq: 1 - } - }; - const content = await jsonToCollectionBru(data, true); // isFolder flag + const content = await jsonToCollectionBru(folderBruJsonData, true); // isFolder flag await writeFile(folderBruFilePath, content); } else { return Promise.reject(new Error('The directory already exists')); @@ -762,8 +753,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection const folderRootPath = path.join(item.pathname, 'folder.bru'); let folderBruJsonData = { meta: { - name: path.basename(item?.pathname), - seq: item?.seq || 1 + name: path.basename(item.pathname), + seq: item.seq } }; if (fs.existsSync(folderRootPath)) { @@ -771,8 +762,8 @@ const registerRendererEventHandlers = (mainWindow, watcher, lastOpenedCollection folderBruJsonData = await collectionBruToJson(bru); if (!folderBruJsonData?.meta) { folderBruJsonData.meta = { - name: path.basename(item?.pathname), - seq: item?.seq || 1 + name: path.basename(item.pathname), + seq: item.seq }; } if (folderBruJsonData?.meta?.seq === item.seq) { diff --git a/packages/bruno-electron/src/ipc/network/index.js b/packages/bruno-electron/src/ipc/network/index.js index 4f17c06cf..c7a5ef2b1 100644 --- a/packages/bruno-electron/src/ipc/network/index.js +++ b/packages/bruno-electron/src/ipc/network/index.js @@ -24,9 +24,8 @@ const { uuid, safeStringifyJSON, safeParseJSON, parseDataFromResponse, parseData const { chooseFileToSave, writeBinaryFile, writeFile } = require('../../utils/filesystem'); const { addCookieToJar, getDomainsWithCookies, getCookieStringForUrl } = require('../../utils/cookies'); const { createFormData } = require('../../utils/form-data'); -const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars } = require('../../utils/collection'); +const { findItemInCollectionByPathname, sortFolder, getAllRequestsInFolderRecursively, getEnvVars, getTreePathFromCollectionToItem, mergeVars, sortByNameThenSequence } = require('../../utils/collection'); const { getOAuth2TokenUsingAuthorizationCode, getOAuth2TokenUsingClientCredentials, getOAuth2TokenUsingPasswordCredentials, getOAuth2TokenUsingImplicitGrant } = require('../../utils/oauth2'); -const { setupProxyAgents } = require('../../utils/proxy-util'); const { preferencesUtil } = require('../../store/preferences'); const { getProcessEnvVars } = require('../../store/process-env'); const { getBrunoConfig } = require('../../store/bruno-config'); @@ -1003,10 +1002,9 @@ const registerNetworkIpc = (mainWindow) => { } }); + // sort requests by seq property - folderRequests.sort((a, b) => { - return a.seq - b.seq; - }); + folderRequests = sortByNameThenSequence(folderRequests) } let currentRequestIndex = 0; diff --git a/packages/bruno-electron/src/utils/collection.js b/packages/bruno-electron/src/utils/collection.js index 94fa30ec8..038e388fc 100644 --- a/packages/bruno-electron/src/utils/collection.js +++ b/packages/bruno-electron/src/utils/collection.js @@ -346,7 +346,7 @@ const sortCollection = (collection) => { let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.seq - b.seq); + folderItems = sortByNameThenSequence(folderItems); requestItems = requestItems.sort((a, b) => a.seq - b.seq); collection.items = folderItems.concat(requestItems); @@ -361,7 +361,7 @@ const sortFolder = (folder = {}) => { let folderItems = filter(items, (item) => item.type === 'folder'); let requestItems = filter(items, (item) => item.type !== 'folder'); - folderItems = folderItems.sort((a, b) => a.seq - b.seq); + folderItems = sortByNameThenSequence(folderItems); requestItems = requestItems.sort((a, b) => a.seq - b.seq); folder.items = folderItems.concat(requestItems); @@ -467,6 +467,47 @@ const mergeAuth = (collection, request, requestTreePath) => { } }; +const sortByNameThenSequence = items => { + const isSeqValid = seq => Number.isFinite(seq) && Number.isInteger(seq) && seq > 0; + + // Sort folders alphabetically by name + const alphabeticallySorted = [...items].sort((a, b) => a.name && b.name && a.name.localeCompare(b.name)); + + // Extract folders without 'seq' + const withoutSeq = alphabeticallySorted.filter(f => !isSeqValid(f['seq'])); + + // Extract folders with 'seq' and sort them by 'seq' + const withSeq = alphabeticallySorted.filter(f => isSeqValid(f['seq'])).sort((a, b) => a.seq - b.seq); + + const sortedItems = withoutSeq; + + // Insert folders with 'seq' at their specified positions + withSeq.forEach((item) => { + const position = item.seq - 1; + const existingItem = withoutSeq[position]; + + // Check if there's already an item with the same sequence number + const hasItemWithSameSeq = Array.isArray(existingItem) + ? existingItem[0].seq === item.seq + : existingItem?.seq === item.seq; + + if (hasItemWithSameSeq) { + // If there's a conflict, group items with same sequence together + const newGroup = Array.isArray(existingItem) + ? [...existingItem, item] + : [existingItem, item]; + + withoutSeq.splice(position, 1, newGroup); + } else { + // Insert item at the specified position + withoutSeq.splice(position, 0, item); + } + }); + + // return flattened sortedItems + return sortedItems.flat(); +}; + module.exports = { mergeHeaders, mergeVars, @@ -487,5 +528,6 @@ module.exports = { sortFolder, getAllRequestsInFolderRecursively, getEnvVars, - getFormattedCollectionOauth2Credentials + getFormattedCollectionOauth2Credentials, + sortByNameThenSequence }; \ No newline at end of file