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

lib/ignore Have a nice treeview for handling ignores - Next Gen Ignores #5132

Closed
wants to merge 3 commits into from
Closed
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
235 changes: 233 additions & 2 deletions gui/default/syncthing/core/syncthingController.js
Expand Up @@ -1569,6 +1569,17 @@ angular.module('syncthing.core')
});
};

$scope.syncedFiles = {
parseable: false,
all: false,
files: [],
reset: function() {
$scope.syncedFiles.parseable = false;
$scope.syncedFiles.all = false;
$scope.syncedFiles.files = [];
}
};

$scope.editFolder = function (folderCfg) {
$scope.editingExisting = true;
$scope.currentFolder = angular.copy(folderCfg);
Expand Down Expand Up @@ -1613,13 +1624,233 @@ angular.module('syncthing.core')
}
$scope.currentFolder.externalCommand = $scope.currentFolder.externalCommand || "";

function enableBasicRules(enable) {
if (enable) {
console.log('enabling basic rules');
$('#folder-ignores #folderIgnoresTree').removeAttr('disabled');
//$('#folder-ignores #folderIgnoresTree').show();
} else {
console.log('disabling basic rules');
$('#folder-ignores #folderIgnoresTree').attr('disabled', 'disabled');
//$('#folder-ignores #folderIgnoresTree').hide();
}
}

function writeAdvancedRules() {
console.log('writeAdvancedRules');
if ($scope.syncedFiles.all) {
$('#folder-ignores textarea').val('');
return;
}

var rules = ['*'];
$scope.syncedFiles.files.forEach(function (file) {
console.log('file', file);
var fileSplit = file.split('/');
var fileDir = fileSplit.slice(0, fileSplit.length - 1).join('/');
rules.push('!/' + file);
if (fileDir === '') {
rules.push('*');
} else {
rules.push('/' + fileDir + '/*');
}
});

rules = rules.sort(function(a, b) {
var a_toSort = a;
if (a_toSort.startsWith('!')) {
a_toSort = a_toSort.slice(1, a_toSort.length).concat('1');
} else {
a_toSort = a_toSort.concat('2');
}

var b_toSort = b;
if (b_toSort.startsWith('!')) {
b_toSort = b_toSort.slice(1, b_toSort.length).concat('1');
} else {
b_toSort = b_toSort.concat('2');
}

return a_toSort <= b_toSort;
}).filter(function(value, index, self) {
return self.indexOf(value) === index
});

console.log('writeAdvancedRules - rules:', rules);
$('#folder-ignores textarea').val(rules.join('\n'));
}

function parseAdvancedRules() {
/*
* parse:
*
* !/c/c
* !/c/a
* /c/*
* !/b
* !/a
* *
*
* into object with:
*
* {
* parseable: true,
* all: false,
* files: ['/c/c', '/c/a', '/b', '/a'],
* }
*
*/
console.log('parseAdvancedRules');
var rules = $('#folder-ignores textarea').val().split('\n');
$scope.syncedFiles.reset();
$scope.syncedFiles.parseable = true;
$scope.syncedFiles.all = true;

var prevRule = null;
for (const rule of rules) {
console.log('rule', rule, 'prevRule', prevRule);
if (rule === '') {
continue;
} else {
$scope.syncedFiles.all = false;
}

// TODO: handles cases with multiple prevRule and one rule (multiple files in same dir)
if (rule === '*') {
continue;
} else if (prevRule === null) {
if (!rule.startsWith('!')) {
console.log('not start with !')
$scope.syncedFiles.parseable = false;
console.log('parseAdvancedRules - syncedFiles:', $scope.syncedFiles);
return;
} else {
prevRule = rule;
}
} else {
var prevRuleSplit = prevRule.split('/');
//var prevRulePath = ([prevRuleSplit[0].slice(1, prevRuleSplit[0].length)].concat(prevRuleSplit.slice(1, prevRuleSplit.length - 1))).join('/');
// Remove leading ! and basename
var prevRulePath = prevRuleSplit.slice(0, prevRuleSplit.length - 1).join('/');
// Remove leading !
var rulePathWithBasename = prevRule.slice(1, prevRule.length);
// Remove last *
var ruleSplit = rule.split('/');
var rulePath = ruleSplit.slice(0, prevRuleSplit.length - 1).join('/');
console.log('prevRulePath', prevRulePath, 'rulePath', rulePath)

if (prevRulePath !== rulePath) {
console.log('prevRulePath !== rulePath')
$scope.syncedFiles.parseable = false;
console.log('parseAdvancedRules - syncedFiles:', $scope.syncedFiles);
return;
} else {
console.log(rulePathWithBasename)
$scope.syncedFiles.files.push(rulePathWithBasename);
prevRule = null;
}
}
}

console.log('parseAdvancedRules - syncedFiles:', $scope.syncedFiles);
}

enableBasicRules(false);
$('#folder-ignores #folderIgnoresTree').fancytree({
source: [
{title: $scope.currentFolder.id, key: '', folder: true, lazy: true, selected: $scope.syncedFiles.all}
],
checkbox: true,
clickFolderMode: 2, // Expand and not select when clicking on folder
selectMode: 3, // Hierarchical select of nodes
tabindex: "0", // Tree control can be reached using TAB keys
lazyLoad: function(event, ft_data) {
var dfd = $.Deferred();
ft_data.result = dfd.promise();
$http.get(urlbase + '/db/browse?folder=' + encodeURIComponent($scope.currentFolder.id) + '&prefix=' + encodeURIComponent(ft_data.node.key) + '&levels=0')
.success(function (tree) {
var result = $.map(tree, function(obj, title) {
var key = ft_data.node.key ? ft_data.node.key + '/' + title : title;
var selected = ft_data.node.isSelected() || $scope.syncedFiles.files.includes(key);
if (Array.isArray(obj)) {
return {title: title, key: key, selected: selected, folder: false};
} else {
return {title: title, key: key, selected: selected, folder: true, lazy: true};
}
});
dfd.resolve(result);
});
}
});

function updateBasicRulesFromSyncedFiles() {
console.log('updateBasicRulesFromSyncedFiles');
enableBasicRules($scope.syncedFiles.parseable);

if ($scope.syncedFiles.all === true) {
$('#folder-ignores #folderIgnoresTree').fancytree('getTree').rootNode.children[0].setSelected(true);
} else {
$('#folder-ignores #folderIgnoresTree').fancytree('getTree').visit(function(node) {
if ($scope.syncedFiles.files.includes(node.key)) {
node.setSelected(true);
}
});
}
}

function updateSyncedFilesFromBasicRules() {
console.log('updateSyncedFilesFromBasicRules');
$scope.syncedFiles.reset();
$scope.syncedFiles.parseable = true;

$('#folder-ignores #folderIgnoresTree').fancytree('getTree').visit(function(node) {
if (node.isSelected()) {
if (node.key === '') {
// If we include the whole tree, no need
// for any rule so we break now
$scope.syncedFiles.all = true;
return false;
} else if (!node.parent.isSelected()) {
$scope.syncedFiles.files.push(node.key);
}
}
return true;
});

}

// When switching to the Basic tab, parse the current ignore
// rules and either disable the tree if the rules are too
// complicated or pre-check the currently not-ignored files
// and folders.
$('#folder-ignores .nav-tabs a').on('shown.bs.tab', function(event) {
console.log('change tab', event);
if (event.target.getAttribute('href') === '#folder-ignores-basic') {
parseAdvancedRules();
updateBasicRulesFromSyncedFiles();
} else {
updateSyncedFilesFromBasicRules();
writeAdvancedRules();
}
});

$('#folder-ignores textarea').val($translate.instant("Loading..."));
$('#folder-ignores textarea').attr('disabled', 'disabled');
$('#folder-ignores #folderIgnoresTree').attr('disabled', 'disabled');
$http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
.success(function (data) {
$scope.currentFolder.ignores = data.ignore || [];
$('#folder-ignores textarea').val($scope.currentFolder.ignores.join('\n'));
data.ignore = data.ignore || [];

$('#folder-ignores textarea').val(data.ignore.join('\n'));
$('#folder-ignores textarea').removeAttr('disabled');

$('#folder-ignores').modal()
.one('shown.bs.modal', function () {
textArea.focus();
});
$('#folder-ignores #folderIgnoresTree').removeAttr('disabled');

parseAdvancedRules();
})
.error(function (err) {
$('#folder-ignores textarea').val($translate.instant("Failed to load ignore patterns."));
Expand Down
42 changes: 27 additions & 15 deletions gui/default/syncthing/folder/editFolderModalView.html
Expand Up @@ -116,21 +116,33 @@
</div>
</div>
<div id="folder-ignores" class="tab-pane">
<p translate>Enter ignore patterns, one per line.</p>
<textarea class="form-control" rows="5"></textarea>
<hr/>
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
<dl class="dl-horizontal dl-narrow small">
<dt><code>(?d)</code></dt> <dd><b><span translate>Prefix indicating that the file can be deleted if preventing directory removal</span></b></dd>
<dt><code>(?i)</code></dt> <dd><span translate>Prefix indicating that the pattern should be matched without case sensitivity</span></dd>
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
</dl>
<hr/>
<div class="pull-left" ng-show="editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span></div>
<div class="pull-left" ng-show="!editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span></div>
<ul class="nav nav-tabs">
<li class="active"><a data-toggle="tab" href="#folder-ignores-basic"><span class="fas fa-filter-basic"></span> <span translate>Basic</span></a></li>
<li><a data-toggle="tab" href="#folder-ignores-advanced"><span class="fas fa-filter-advanced"></span> <span translate>Advanced</span></a></li>
</ul>
<div class="tab-content">
<div id="folder-ignores-basic" class="tab-pane in active">
<p translate>Select files and folders to sync.</p>
<div id="folderIgnoresTree"></div>
</div>
<div id="folder-ignores-advanced" class="tab-pane">
<p translate>Enter patterns, one per line.</p>
<textarea class="form-control" rows="5"></textarea>
<hr/>
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="https://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
<dl class="dl-horizontal dl-narrow small">
<dt><code>(?d)</code></dt> <dd><b><span translate>Prefix indicating that the file can be deleted if preventing directory removal</span></b></dd>
<dt><code>(?i)</code></dt> <dd><span translate>Prefix indicating that the pattern should be matched without case sensitivity</span></dd>
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
</dl>
<hr/>
<div class="pull-left" ng-show="editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span></div>
<div class="pull-left" ng-show="!editingExisting"><span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span></div>
</div>
</div>
</div>
<div id="folder-advanced" class="tab-pane">
<div class="row form-group" ng-class="{'has-error': folderEditor.rescanIntervalS.$invalid && folderEditor.rescanIntervalS.$dirty}">
Expand Down