fix(gui): order folders alphabetically and ensure local device stays hidden (ref #10563, ref #10631)

### Purpose

There were some additional regression created during #10563 and #10631 which are:

* Folders were not being ordered by their label within their group
* Local device could still show up randomly with the list of remote devices

The respective fixes in this PR does the following:

* Ensure sorting of the grouped folders (and devices) are done by group name (the top level map key) and then by the specified nested object property or a fallback property if specified property value is empty. So in the case of the folders it's `label` and device is `name`.
* When populating the `devicesGrouped` using `$scope.otherDevices`, do this within a watcher on `$scope.myID` as this is what `$scope.otherDevices` relies on to determine what is a remote device. This is required because `$scope.myID` might not be populated yet given the indeterministic call order being made to `refreshSystem` and `refreshConfig`.

### Testing

Populate folders and devices into different groups, and ensure the ordering is correct and the local device is not showing in remote devices even after many, many refreshes on the UI or restarts of syncthing entirely.

### Screenshots

Before the fix

<img width="1108" height="951" alt="Screenshot_20260409_083910" src="https://github.com/user-attachments/assets/6eb8cacc-5924-4612-aa70-29ed4f691233" />

After the fix

<img width="1090" height="934" alt="Screenshot_20260409_084553" src="https://github.com/user-attachments/assets/f5b74391-228a-43d3-b5ee-433958236d84" />

### Documentation

N/A

## Authorship

Ben Norcombe [bennorcombe@pm.me](mailto:bennorcombe@pm.me)
This commit is contained in:
Ben Norcombe
2026-04-11 13:52:20 +02:00
committed by GitHub
parent 6b9fa76c01
commit 017ef5a57b
@@ -570,14 +570,23 @@ angular.module('syncthing.core')
};
$scope.devicesGrouped = {};
const otherDevices = $scope.otherDevices();
for (var id in otherDevices) {
if ($scope.devicesGrouped[otherDevices[id].group] === undefined) {
$scope.devicesGrouped[otherDevices[id].group] = [];
// myID is watched as $scope.otherDevices() relies on this
// and it can potenitally not be loaded due to this function
// scope being called in an undetermistic manner
$scope.$watch('myID', function(myID) {
if (myID) {
$scope.devicesGrouped = {};
const otherDevices = $scope.otherDevices();
for (var id in otherDevices) {
if ($scope.devicesGrouped[otherDevices[id].group] === undefined) {
$scope.devicesGrouped[otherDevices[id].group] = [];
}
$scope.devicesGrouped[otherDevices[id].group].push(otherDevices[id]);
};
$scope.devicesGrouped = sortByKeyThenProperty($scope.devicesGrouped, "name", "deviceID");
}
$scope.devicesGrouped[otherDevices[id].group].push(otherDevices[id]);
};
});
$scope.folders = folderMap($scope.config.folders);
$scope.foldersGrouped = {};
@@ -593,15 +602,7 @@ angular.module('syncthing.core')
$scope.foldersGrouped[$scope.folders[folder].group].push($scope.folders[folder]);
});
// Sort with blank group first if any then alphabetically
const blankSort = (a, b) => {
if (a[0] === "" && b[0] !== "") return -1;
if (b[0] === "" && a[0] !== "") return 1;
return a[0].localeCompare(b[0]);
};
$scope.foldersGrouped = Object.fromEntries(Object.entries($scope.foldersGrouped).sort(blankSort));
$scope.devicesGrouped = Object.fromEntries(Object.entries($scope.devicesGrouped).sort(blankSort));
$scope.foldersGrouped = sortByKeyThenProperty($scope.foldersGrouped, "label", "id");
refreshNoAuthWarning();
setDefaultTheme();
@@ -611,6 +612,31 @@ angular.module('syncthing.core')
}
}
// Sort firstly by the top level key of the object and then by
// prop name provided for the array of objects for each key.
// If the prop returns has an empty value, then use the
// fallback prop provided.
function sortByKeyThenProperty(obj, prop, fallbackProp) {
const sorted = {};
Object.keys(obj)
.sort()
.forEach((key) => {
sorted[key] = obj[key].sort((a, b) => {
let aProp = prop;
let bProp = prop;
if (!a[aProp]) {
aProp = fallbackProp;
}
if (!b[bProp]) {
bProp = fallbackProp;
}
return a[aProp].localeCompare(b[bProp]);
});
});
return sorted;
}
function refreshSystem() {
return $http.get(urlbase + '/system/status').success(function (data) {
$scope.myID = data.myID;