Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tree selector for ignore patterns #9132

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
18 changes: 18 additions & 0 deletions gui/default/assets/css/overrides.css
Expand Up @@ -341,6 +341,24 @@ ul.three-columns li, ul.two-columns li {
z-index: 980;
}

/*
* Folder Basic Ignores Tree tweaks
*/

#folderGlobalTree-container {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The styles here seem like a copy from #restoreTree-container. Is this correct? If yes, then I think it would be nice to just create a common class with these to add to the both elements to avoid the repetition.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I just copied that.

I will look at this later, when I know the style is final. min-height and max-height might need some tuning.

overflow-y: scroll;
resize: vertical;
/* Limit height to prevent vertical screen overflow. */
max-height: calc(100vh - 390px);
/* Always fit at least one folder with dropdown open. */
min-height: 136px;
}

/* Ignore fixed height when manually resized. */
#folderGlobalTree-container[style*="height"] {
max-height: none;
}

/*
* Restore Versions tweaks
*/
Expand Down
10 changes: 10 additions & 0 deletions gui/default/assets/css/tree.css
Expand Up @@ -10,6 +10,11 @@
.fancytree-container {
cursor: pointer;
width: 100%;
list-style-type: none;
}

.fancytree-container ul {
list-style-type: none;
}

.fancytree-hide {
Expand All @@ -33,10 +38,15 @@
.fancytree-expander,
.fancytree-icon {
margin-top: 4px;
margin-left: 10px;
vertical-align: top;
width: 16px;
}

.fancytree-helper-indeterminate-cb:before {
content: "\f14a";
}

.fancytree-childcounter {
background: #777;
border-radius: 10px;
Expand Down
66 changes: 66 additions & 0 deletions gui/default/fancyTreeGlobalDataWorker.js
@@ -0,0 +1,66 @@
const generatedByTreeSelector = "// Generated by tree selector";

var ignores = [];
var invalidPattern = false;

// To prevent running multiple jobs at once.
// When new job starts old one cancel itself.
var latestReqId = 0;

self.onmessage = function (msg) {
console.log("fancyTreeGlobalDataWorker | start", "data", msg.data)
const newReqId = Date.now();
latestReqId = newReqId;

// Just cancellation message.
if (!msg.data?.data) {
console.log("fancyTreeGlobalDataWorker | cancel requested")
return;
}

ignores = msg.data.ignorePatterns;
if (ignores[0] !== generatedByTreeSelector) {
invalidPattern = true;
ignores = [];
} else {
ignores = ignores.slice(1, -1)
}
self.postMessage({
data: formatGlobalTreeNodes(msg.data.data, "", newReqId),
invalidPattern: invalidPattern
});
inProgress = false;
console.log("fancyTreeGlobalDataWorker | finished");
}

function formatGlobalTreeNodes(data, parentKey, reqId, parentIgnored = true) {
if (reqId != latestReqId) {
console.log("fancyTreeGlobalDataWorker | cancelled", reqId)
return null;
}

var result = [];
// console.log("formatGlobalTreeNodes(worker)", "data", data, "parentKey", parentKey, "parentIgnored", parentIgnored)
data.forEach(function(node) {
const isFolder = node.type == "FILE_INFO_TYPE_DIRECTORY";
const key = parentKey + "/" + node.name;
const found = ignores.find(function (ignoreKey) {
return key == ignoreKey.slice(1);
});

const ignored = parentIgnored && !found;
result.push({
children: isFolder && node.children?.length > 0 ? formatGlobalTreeNodes(node.children, key, reqId, ignored) : [],
key: key,
title: node.name,
folder: isFolder,
selected: !ignored
});
if (reqId != latestReqId) {
console.log("fancyTreeGlobalDataWorker | cancelled", reqId)
return null;
}
});

return result;
}
209 changes: 209 additions & 0 deletions gui/default/syncthing/core/syncthingController.js
Expand Up @@ -6,6 +6,8 @@ angular.module('syncthing.core')
'use strict';

// private/helper definitions
const generatedByTreeSelector = "// Generated by tree selector";
var fancyTreeGlobalDataWorker;

var prevDate = 0;
var navigatingAway = false;
Expand Down Expand Up @@ -76,6 +78,12 @@ angular.module('syncthing.core')
defaultLines: [],
saved: false,
};
$scope.folderGlobalTreeData = {
data: [],
fancyData: [],
error: null,
loading: false,
};
resetRemoteNeed();

try {
Expand Down Expand Up @@ -2107,6 +2115,7 @@ angular.module('syncthing.core')
};

$scope.setDefaultsForFolderType = function () {
$scope.currentFolder.advancedIgnores = true;
if ($scope.currentFolder.type === 'receiveencrypted') {
$scope.currentFolder.fsWatcherEnabled = false;
$scope.currentFolder.ignorePerms = true;
Expand Down Expand Up @@ -2142,6 +2151,12 @@ angular.module('syncthing.core')
initialTab = "#folder-general";
}
$('.nav-tabs a[href="' + initialTab + '"]').tab('show');
$(document).on('shown.bs.tab', 'a[data-toggle="tab"]', function (e) {
// console.log("tree document href=", e.target.href,e.target.attributes.href.value);
if (e.target.href, e.target.attributes.href.value == "#folder-ignores") {
setFolderGlobalTree();
}
})
$('#editFolder').one('shown.bs.tab', function (e) {
if (e.target.attributes.href.value === "#folder-ignores") {
$('#folder-ignores textarea').focus();
Expand All @@ -2156,6 +2171,13 @@ angular.module('syncthing.core')
window.location.hash = "";
$scope.currentFolder = {};
$scope.ignores = {};
$scope.folderGlobalTreeData = {
data: [],
fancyData: [],
error: null,
loading: false,
};
destroyFolderGlobalTree();
});
});
showModal('#editFolder');
Expand Down Expand Up @@ -2256,6 +2278,10 @@ angular.module('syncthing.core')
$scope.currentFolder._editing = "existing";
editFolderLoadIgnores();
editFolder(initialTab);
loadFolderGlobalTreeData();
console.log("initial tab tree", initialTab)
if (initialTab == "#folder-ignores")
setFolderGlobalTree();
};

function editFolderLoadingIgnores() {
Expand Down Expand Up @@ -2284,6 +2310,189 @@ angular.module('syncthing.core')
}, $scope.emitHTTPError);
}

function getFolderGlobalTreeData() {
console.log("getFolderGlobalTreeData");
return $http.get(urlbase + '/db/browse?folder=' + encodeURIComponent($scope.currentFolder.id))
.then(function (r) {
return r.data;
}, function (response) {
$scope.folderGlobalTreeData.error = $translate.instant("Failed to load folder global tree.");
return $q.reject(response);
});
}

// By reseting this variable current global tree request
// can be cancelled. Used to skip data parsing job if not
// necessary.
var loadFolderGlobalTreeDataReq;
function loadFolderGlobalTreeData() {
console.log("loadFolderGlobalTreeData");
$scope.folderGlobalTreeData.data = [];
$scope.folderGlobalTreeData.fancyData = [];
$scope.folderGlobalTreeData.error = null;
$scope.folderGlobalTreeData.loading = true;

const req = getFolderGlobalTreeData().then(function (data) {
if (!data) {
return;
}

// Preventing race conditions when user clicks like a crazy
// and opening/closing the folder modal on filter page.
// Replicable with tens of thousands files in global tree.
if (loadFolderGlobalTreeDataReq != req) {
console.log("loadFolderGlobalTreeData | cancelled", data.error)
return;
}

console.log("loadFolderGlobalTreeData | loaded", data.error)
setFolderGlobalTreeData(data);
$scope.folderGlobalTreeData.error = data.error;
}, $scope.emitHTTPError);

return loadFolderGlobalTreeDataReq = req;
}

function setFolderGlobalTreeData(data) {
$scope.folderGlobalTreeData.data = data || [];

// Pre-build $scope.folderGlobalTreeData.data.fancyData
// used by fancyTree to speed up fancytree opening later.

// Worker is preferred if supported so UI don't lag when
// parsing tens of thousands files.
initFancyTreeGlobalDataWorker();
if (fancyTreeGlobalDataWorker) {
fancyTreeGlobalDataWorker.postMessage({
data: $scope.folderGlobalTreeData.data,
ignorePatterns: ignoresArray(),
});
} else {
setFolderGlobalTreeFancyData(formatGlobalTreeNodes($scope.folderGlobalTreeData.data, ""))
}
}
function setFolderGlobalTreeFancyData(data) {
console.log("setFolderGlobalTreeFancyData", $scope.folderGlobalTreeData.loading, data)
$scope.folderGlobalTreeData.fancyData = data;
$scope.folderGlobalTree?.reload();
$scope.folderGlobalTreeData.loading = false;
}

function formatGlobalTreeNodes(data, parentKey, parentIgnored = true) {
var result = [];
var ignores = ignoresArray();
// console.log("formatGlobalTreeNodes", "data", data, "ignores", ignores);
if (ignores[0] !== generatedByTreeSelector) {
ignores = [];
setIgnoresText([generatedByTreeSelector, "*"]);
} else {
ignores = ignores.slice(1, -1)
}

$.each(data, function (index, node) {
const isFolder = node.type == "FILE_INFO_TYPE_DIRECTORY";
const key = parentKey + "/" + node.name;
const found = ignores.find(function (ignoreKey) {
return key == ignoreKey.slice(1);
});
const ignored = parentIgnored && !found;
// console.log("initFolderGlobalTree", "key", key, "ignored ", ignored, "parentIgnored", parentIgnored, "found", found, !found, "isFolder", isFolder, "node.children", node.children);
result.push({
children: isFolder && node.children?.length > 0 ? formatGlobalTreeNodes(node.children, key, ignored) : [],
key: key,
title: node.name,
folder: isFolder,
selected: !ignored
});
});
return result;
}

function setFolderGlobalTree() {
// console.log("setIgnoresGlobalTree")
if (!$scope.currentFolder.advancedIgnorePatterns)
initFolderGlobalTree();
else
destroyFolderGlobalTree();
}

function initFolderGlobalTree() {
if ($scope.folderGlobalTree)
return;

console.log("initFolderGlobalTree | building FancyTree");
$scope.folderGlobalTree = $("#folderGlobalTree").fancytree({
extensions: ["glyph"],
glyph: {
preset: "awesome5",
},
checkbox: true,
checkboxAutoHide: true,
quicksearch: true,
selectMode: 3,
debugLevel: 1,
select: function (event, data) {
console.log("initFolderGlobalTree | fancyTree select");
const rootNode = $scope.folderGlobalTree.getRootNode();
const generatedIgnorePatterns = ["*"];
$.each(rootNode.getSelectedNodes(), function (_, node) {
if (node.parent == rootNode || !node.parent.selected)
generatedIgnorePatterns.unshift("!" + node.key);
});
generatedIgnorePatterns.unshift(generatedByTreeSelector);
setIgnoresText(generatedIgnorePatterns);
$scope.$apply();
},
source: function () {
return $scope.folderGlobalTreeData.fancyData;
}
}).fancytree("getTree");
}

function destroyFolderGlobalTree() {
console.log('destroyFolderGlobalTree', $scope.folderGlobalTree);
resetFancyTreeGlobalDataWorker();
loadFolderGlobalTreeDataReq = null;
if ($scope.folderGlobalTree) {
$("#folderGlobalTree").fancytree("destroy");
$scope.folderGlobalTree = null;
}
}

function initFancyTreeGlobalDataWorker() {
console.log('initFancyTreeGlobalDataWorker', !!$scope.fancyTreeGlobalDataWorker);
if (fancyTreeGlobalDataWorker)
return;

// Worker used to improve ignore patterns global tree data parsing performance.
if (typeof (Worker) !== "undefined") {
fancyTreeGlobalDataWorker = new Worker("fancyTreeGlobalDataWorker.js");

if (fancyTreeGlobalDataWorker)
fancyTreeGlobalDataWorker.onmessage = function (msg) {
console.log("fancyTreeGlobalDataWorker.onmessage", msg.data);
if (msg.data.invalidPatterns)
setIgnoresText([generatedByTreeSelector, "*"]);
setFolderGlobalTreeFancyData(msg.data.data);
}
console.log('initFancyTreeGlobalDataWorker | worker ready', !!$scope.fancyTreeGlobalDataWorker);
}
}

// We don't terminate the worker since it seem to much expensive
// especially loading new instance. So better if single instance
// lives forever.
// The method just cancel current the worker job.
function resetFancyTreeGlobalDataWorker() {
// console.log('resetFancyTreeGlobalDataWorker', !!fancyTreeGlobalDataWorker);
if (fancyTreeGlobalDataWorker) {
fancyTreeGlobalDataWorker.postMessage({
data: null,
ignorePatterns: null
});
}
}

$scope.editFolderDefaults = function() {
$q.all([
$http.get(urlbase + '/config/defaults/folder').then(function (response) {
Expand Down