Skip to content
1 change: 1 addition & 0 deletions src/extensions/default/Git/styles/git-styles.less
Original file line number Diff line number Diff line change
Expand Up @@ -907,6 +907,7 @@
white-space: nowrap;
padding: 2px 5px;
margin-left: -5px;
margin-right: 2em;
.dropdown-arrow {
display: inline-block;
width: 7px;
Expand Down
80 changes: 80 additions & 0 deletions src/extensionsIntegrated/CollapseFolders/main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/* Displays a Collapse button in the sidebar area */
/* when the button gets clicked, it closes all the directories recursively that are opened */
/* Styling for the button is done in `../../styles/Extn-CollapseFolders.less` */
define(function (require, exports, module) {
const AppInit = require("utils/AppInit");
const ProjectManager = require("project/ProjectManager");

/**
* This is the main function that handles the closing of all the directories
*/
function handleCollapseBtnClick() {
// this will give us an array of array's
// the root level directories will be at index 0, its next level will be at index 1 and so on
const openNodes = ProjectManager._actionCreator.model.getOpenNodes();
if (!openNodes || openNodes.length === 0) {
return;
}

// traversing from the back because the deepest nested directories should be closed first
// Note: this is an array of all the directories at the deepest level
for (let i = openNodes.length - 1; i >= 0; i--) {
// close all the directories
openNodes[i].forEach(function (folderPath) {
try {
// to close each dir
ProjectManager._actionCreator.setDirectoryOpen(folderPath, false);
} catch (error) {
console.error("Failed to close folder:", folderPath, error);
}
});
}
}

/**
* This function is responsible to create the 'Collapse All' button
* and append it to the sidebar area on the project-files-header
*/
function createCollapseButton() {
const $projectFilesHeader = $("#project-files-header");
// make sure that we were able to get the project-files-header DOM element
if ($projectFilesHeader.length === 0) {
return;
}

// create the collapse btn
const $collapseBtn = $(`
<div id="collapse-folders" class="btn-alt-quiet" title="Collapse All">
<i class="fa-solid fa-chevron-down collapse-icon" aria-hidden="true"></i>
<i class="fa-solid fa-chevron-up collapse-icon" aria-hidden="true"></i>
</div>
`);

$collapseBtn.on("click", handleCollapseBtnClick);
$projectFilesHeader.append($collapseBtn); // append the btn to the project-files-header
}

AppInit.appReady(function () {
createCollapseButton();
});
});
1 change: 1 addition & 0 deletions src/extensionsIntegrated/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,5 @@ define(function (require, exports, module) {
require("./CSSColorPreview/main");
require("./TabBar/main");
require("./CustomSnippets/main");
require("./CollapseFolders/main");
});
25 changes: 25 additions & 0 deletions src/styles/Extn-CollapseFolders.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#collapse-folders {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 0.2em 0.65em;
margin-top: 0.1em;
position: absolute !important;
right: 0;
opacity: 0;
visibility: hidden;
transition:
opacity 0.2s ease-in-out,
visibility 0.2s ease-in-out;

.collapse-icon {
font-size: 0.5em;
line-height: 1;
}
}

#sidebar:hover #collapse-folders {
opacity: 1;
visibility: visible;
}
1 change: 1 addition & 0 deletions src/styles/brackets.less
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
@import "Extn-DisplayShortcuts.less";
@import "Extn-CSSColorPreview.less";
@import "Extn-CustomSnippets.less";
@import "Extn-CollapseFolders.less";
@import "UserProfile.less";

/* Overall layout */
Expand Down
1 change: 1 addition & 0 deletions test/UnitTestSuite.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ define(function (require, exports, module) {
require("spec/Extn-JSHint-integ-test");
require("spec/Extn-ESLint-integ-test");
require("spec/Extn-CSSColorPreview-integ-test");
require("spec/Extn-CollapseFolders-integ-test");
// extension integration tests
require("spec/Extn-CSSCodeHints-integ-test");
require("spec/Extn-HTMLCodeHints-Lint-integ-test");
Expand Down
241 changes: 241 additions & 0 deletions test/spec/Extn-CollapseFolders-integ-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
/*
* GNU AGPL-3.0 License
*
* Copyright (c) 2021 - present core.ai . All rights reserved.
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
* for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://opensource.org/licenses/AGPL-3.0.
*
*/

/*global describe, it, expect, beforeAll, afterAll, beforeEach, afterEach, awaitsFor, awaitsForDone, awaits, jsPromise */

define(function (require, exports, module) {
const SpecRunnerUtils = require("spec/SpecRunnerUtils");

describe("integration:Collapse Folders", function () {
let testWindow, ProjectManager, FileSystem, $, testProjectPath, testProjectFolder;

beforeAll(async function () {
// Create the test window
testWindow = await SpecRunnerUtils.createTestWindowAndRun();
// Get reference to useful modules
$ = testWindow.$;
ProjectManager = testWindow.brackets.test.ProjectManager;
FileSystem = testWindow.brackets.test.FileSystem;

// Setup a test project folder with nested directories
testProjectPath = SpecRunnerUtils.getTempDirectory() + "/collapse-folders-test";
testProjectFolder = FileSystem.getDirectoryForPath(testProjectPath);

// Ensure the test directory exists
await SpecRunnerUtils.createTempDirectory();

// Create test project structure
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath);
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1");
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder1/subfolder1");
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2");
await SpecRunnerUtils.ensureExistsDirAsync(testProjectPath + "/folder2/subfolder2");

// Create some test files
await jsPromise(SpecRunnerUtils.createTextFile(testProjectPath + "/file.js", "// Test file", FileSystem));
await jsPromise(
SpecRunnerUtils.createTextFile(testProjectPath + "/folder1/file1.js", "// Test file 1", FileSystem)
);
await jsPromise(
SpecRunnerUtils.createTextFile(
testProjectPath + "/folder1/subfolder1/subfile1.js",
"// Test subfile 1",
FileSystem
)
);
await jsPromise(
SpecRunnerUtils.createTextFile(testProjectPath + "/folder2/file2.js", "// Test file 2", FileSystem)
);
await jsPromise(
SpecRunnerUtils.createTextFile(
testProjectPath + "/folder2/subfolder2/subfile2.js",
"// Test subfile 2",
FileSystem
)
);

// Load the test project
await SpecRunnerUtils.loadProjectInTestWindow(testProjectPath);
}, 30000);

afterAll(async function () {
testWindow = null;
await SpecRunnerUtils.closeTestWindow();
await SpecRunnerUtils.removeTempDirectory();
}, 30000);

/**
* Helper function to open a folder in the project tree
* @param {string} folderPath - The path of the folder to open
*/
async function openFolder(folderPath) {
const folderEntry = FileSystem.getDirectoryForPath(folderPath);
// Call setDirectoryOpen without awaitsForDone since it doesn't return a promise
ProjectManager._actionCreator.setDirectoryOpen(folderEntry.fullPath, true);

// Wait for the folder to be opened in the UI
await awaitsFor(
function () {
const $folderNode = findDirectoryNode(folderPath);
return $folderNode && $folderNode.hasClass("jstree-open");
},
"Folder to be opened: " + folderPath,
1000
);
}

/**
* Helper function to find a directory node in the project tree
* @param {string} path - The path of the directory to find
* @returns {jQuery|null} - The jQuery object for the directory node, or null if not found
*/
function findDirectoryNode(path) {
const dirName = path.split("/").pop();
const $treeItems = $("#project-files-container li");
let $result = null;

$treeItems.each(function () {
const $treeNode = $(this);
if ($treeNode.children("a").text().trim() === dirName) {
$result = $treeNode;
return false; // Break the loop
}
});

return $result;
}

/**
* Helper function to check if a folder is open in the project tree
* @param {string} folderPath - The path of the folder to check
* @returns {boolean} - True if the folder is open, false otherwise
*/
function isFolderOpen(folderPath) {
const $folderNode = findDirectoryNode(folderPath);
// If the folder node can't be found, it's definitely not open
if (!$folderNode) {
return false;
}
return $folderNode.hasClass("jstree-open");
}

describe("UI", function () {
it("should have a collapse button in the project files header", async function () {
// Check if the collapse button exists
const $collapseBtn = $("#collapse-folders");
expect($collapseBtn.length).toBe(1);

// Check if the button has the collapse icons
expect($collapseBtn.find(".collapse-icon").length).toBe(2);
});

it("should collapse all open folders when the collapse button is clicked", async function () {
// Open some folders first
await openFolder(testProjectPath + "/folder1");
await openFolder(testProjectPath + "/folder2");

// Verify folders are open
expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true);
expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true);

// Click the collapse button
$("#collapse-folders").click();

// Wait for folders to be collapsed
await awaitsFor(
function () {
return (
!isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2")
);
},
"Folders to be collapsed",
1000
);

// Verify folders are closed
expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false);
expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false);
});

it("should collapse nested folders when the collapse button is clicked", async function () {
// Open folders with nested structure
await openFolder(testProjectPath + "/folder1");
await openFolder(testProjectPath + "/folder1/subfolder1");
await openFolder(testProjectPath + "/folder2");
await openFolder(testProjectPath + "/folder2/subfolder2");

// Verify folders are open
expect(isFolderOpen(testProjectPath + "/folder1")).toBe(true);
expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(true);
expect(isFolderOpen(testProjectPath + "/folder2")).toBe(true);
expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(true);

// Click the collapse button
$("#collapse-folders").click();

// Wait for all folders to be collapsed
await awaitsFor(
function () {
return (
!isFolderOpen(testProjectPath + "/folder1") &&
!isFolderOpen(testProjectPath + "/folder1/subfolder1") &&
!isFolderOpen(testProjectPath + "/folder2") &&
!isFolderOpen(testProjectPath + "/folder2/subfolder2")
);
},
"All folders to be collapsed",
1000
);

// Verify all folders are closed
expect(isFolderOpen(testProjectPath + "/folder1")).toBe(false);
expect(isFolderOpen(testProjectPath + "/folder1/subfolder1")).toBe(false);
expect(isFolderOpen(testProjectPath + "/folder2")).toBe(false);
expect(isFolderOpen(testProjectPath + "/folder2/subfolder2")).toBe(false);
});

it("should do nothing when no folders are open", async function () {
// Make sure all folders are closed
$("#collapse-folders").click();
await awaitsFor(
function () {
return (
!isFolderOpen(testProjectPath + "/folder1") && !isFolderOpen(testProjectPath + "/folder2")
);
},
"Folders to be collapsed",
1000
);

// Get the current state of the project tree
const initialState = $("#project-files-container").html();

// Click the collapse button again
$("#collapse-folders").click();

// Wait a bit to ensure any potential changes would have happened
await awaits(300);

// Verify the project tree hasn't changed
expect($("#project-files-container").html()).toBe(initialState);
});
});
});
});
Loading