sort folders by name first and then sequence (#5063)

* sort folders by name first and then sequence
---------

Co-authored-by: lohit <lohit@usebruno.com>
This commit is contained in:
lohit
2025-07-14 22:17:11 +05:30
committed by GitHub
parent 31e555812c
commit 4e4c94d73f
13 changed files with 537 additions and 94 deletions

View File

@@ -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();

View File

@@ -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 (
<StyledWrapper className="flex flex-col">

View File

@@ -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'));
}

View File

@@ -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',

View File

@@ -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;

View File

@@ -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 }
]);
});
});
});

View File

@@ -219,4 +219,45 @@ export const formatSize = (bytes) => {
}
return (bytes / (1024 * 1024 * 1024)).toFixed(1) + 'GB';
}
}
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();
};