Merge pull request #4962 from Jojo-1000/fix-filter-ui

Fix filter UI
This commit is contained in:
gpatel-fr
2023-12-10 00:22:31 +01:00
committed by GitHub
6 changed files with 81 additions and 28 deletions
@@ -54,11 +54,18 @@ backupApp.controller('CommandlineController', function($scope, $routeParams, $lo
combined.unshift($scope.Command);
for(n in opts) {
if (opts[n] == null)
for (n in opts) {
var value = opts[n];
if (n == 'include' || n == 'exclude') {
// Handle filters that appear multiple times
for (var i = 0; i < value.length; ++i) {
combined.push('--' + n + '=' + value[i]);
}
} else if (value == null) {
combined.push(n);
else
combined.push(n + '=' + opts[n]);
} else {
combined.push(n + '=' + value);
}
}
options = {
@@ -3,11 +3,13 @@ backupApp.directive('parseFilterType', function(AppUtils, SystemInfo) {
restrict: 'A',
require: ['ngModel'],
link: function(scope, element, attr, ctrl) {
var body = null;
ctrl[0].$parsers.push(function(txt) {
var dirsep = SystemInfo.state.DirectorySeparator || '/';
return AppUtils.buildFilter(txt, body, dirsep);
// Store type in scope for parseFilterContent directive
// This works because ng-repeat creates a child scope for each iteration
// It is still glitchy in some edge cases, but should be more consistent
scope.filterType = txt;
return AppUtils.buildFilter(txt, scope.filterBody, dirsep);
});
ctrl[0].$formatters.push(function(src) {
@@ -18,8 +20,7 @@ backupApp.directive('parseFilterType', function(AppUtils, SystemInfo) {
return null;
}
body = parts[1];
scope.filterType = parts[0];
return parts[0];
});
}
@@ -31,23 +32,23 @@ backupApp.directive('parseFilterContent', function(AppUtils, SystemInfo) {
restrict: 'A',
require: ['ngModel'],
link: function(scope, element, attr, ctrl) {
var type = null;
ctrl[0].$parsers.push(function(txt) {
var dirsep = SystemInfo.state.DirectorySeparator || '/';
return AppUtils.buildFilter(type, txt, dirsep);
// Store type in scope for parseFilterType directive
scope.filterBody = txt;
return AppUtils.buildFilter(scope.filterType, txt, dirsep);
});
ctrl[0].$formatters.push(function(src) {
var dirsep = SystemInfo.state.DirectorySeparator || '/';
// Remove double trailing slashes
var parts = AppUtils.splitFilterIntoTypeAndBody(src, dirsep);
if (parts == null) {
type = null;
return null;
}
type = parts[0];
scope.filterBody = parts[1];
return parts[1];
});
}
@@ -21,6 +21,7 @@ backupApp.directive('restoreFilePicker', function() {
$scope.systeminfo = SystemInfo.watch($scope);
$scope.treedata = [];
$scope.AppUtils = AppUtils;
function compareablePath(path) {
return scope.systeminfo.CaseSensitiveFilesystem ? path : path.toLowerCase();
@@ -178,7 +178,9 @@ backupApp.directive('sourceFolderPicker', function() {
for(var i = 0; i < (scope.ngFilters || []).length; i++) {
var f = AppUtils.splitFilterIntoTypeAndBody(scope.ngFilters[i], dirsep);
if (f != null) {
if (f[0] == '-path')
if (f[1].indexOf('?') != -1 || f[1].indexOf('*') != -1)
anySpecials = true;
else if (f[0] == '-path')
excludemap[compareablePath(f[1])] = true;
else if (f[0] == '-folder')
excludemap[compareablePath(f[1] + dirsep)] = true;
@@ -138,11 +138,13 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
key: '-folder',
prefix: '-',
suffix: '!',
stripSep: true,
rx: '\\-(.*)\\!'
}, {
name: gettextCatalog.getString('Exclude file'),
key: '-path',
prefix: '-',
stripSep: true,
exclude: ['*', '?', '{'],
rx: '\\-([^\\[\\{\\*\\?]+)'
}, {
@@ -257,12 +259,14 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
return new Date(dt);
};
this.parseOptionStrings = function(val, dict, validateCallback) {
this.parseOptionStrings = function (val, dict, validateCallback) {
// Parse options and return a dict with dict['--key']=value options
// Include and exclude filters are returned as arrays in dict['include'], dict['exclude'] if present
dict = dict || {};
var lines = null;
if (val != null && typeof(val) == typeof([]))
if (val != null && Array.isArray(val))
lines = val;
else
lines = this.replace_all(val || '', '\r', '\n').split('\n');
@@ -287,8 +291,18 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
if (validateCallback)
if (!validateCallback(dict, key, value))
return null;
dict['--' + key] = value;
// Special handing for --include and --exclude, which are the only options that can appear multiple times (compare FilterCollector.DoExtractOptions)
// In all other cases, the last value overrides the earlier values
var lower = key.toLowerCase();
if (lower == 'include' || lower == 'exclude') {
if (!Array.isArray(dict[lower])) {
dict[lower] = [value];
} else {
dict[lower].push(value);
}
} else {
dict['--' + key] = value;
}
}
}
@@ -300,6 +314,10 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
if (d['--' + k] !== undefined) {
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Duplicate option {{opt}}', { opt: k }));
return false;
} else if (k == 'include' || k == 'exclude') {
// Cannot specify filters in extra options
DialogService.dialog(gettextCatalog.getString('Error'), gettextCatalog.getString('Cannot specify filter include or excludes in extra options'));
return false;
}
return true;
@@ -422,6 +440,13 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
return str.replace( new RegExp( "(" + this.preg_quote(pattern) + ")" , 'g' ), replacement );
};
this.globToRegexp = function (str) {
// Escape special chars, except ? and *
str = (str + '').replace(/([\\\.\+\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, "\\$1");
// Replace ? and * with .? and .*
return str.replace(/(\?|\*)/g, ".$1");
};
this.format = function() {
if (arguments == null || arguments.length < 1)
return null;
@@ -568,13 +593,25 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
return [type, body];
}
for(var i in this.filterClasses) {
var m = [];
for (var i in this.filterClasses) {
var n = matches(src, this.filterClasses[i]);
if (n != null)
return n;
m.push(n);
}
return null;
if (m.length == 0) {
return null;
}
// Select match with shortest body, meaning longest prefix and suffix
var shortestIdx = 0;
var shortestLen = src.length;
for (i = 0; i < m.length; ++i) {
if (m[i][1].length < shortestLen) {
shortestIdx = i;
shortestLen = m[i][1].length;
}
}
return m[shortestIdx];
};
this.buildFilter = function(type, body, dirsep) {
@@ -590,7 +627,10 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
if (pre.length >= 2 && pre[1] == '[') {
//Regexp encode body ....
}
if (f.stripSep == true && body[body.length - 1] == dirsep) {
// Remove trailing dirsep
body = body.slice(0, -1);
}
return pre + body + suf;
};
@@ -609,7 +649,7 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
if (rx)
filter = filter.substr(1, filter.length - 2);
else
filter = this.replace_all(this.replace_all(this.preg_quote(filter), '*', '.*'), '?', '.');
filter = this.globToRegexp(filter);
try {
res.push([flag == '+', new RegExp(filter, caseSensitive ? 'g' : 'gi')]);
@@ -621,9 +661,11 @@ backupApp.service('AppUtils', function($rootScope, $timeout, $cookies, DialogSer
};
this.evalFilter = function(path, filters, include) {
for(var i = 0; i < filters.length; i++) {
for (var i = 0; i < filters.length; i++) {
var m = path.match(filters[i][1]);
if (m && m.length == 1 && m[0].length == path.length)
// Regex such as .* might match empty string at the end which is unwanted
// Check that the first match covers the full string
if (m && m.length >= 1 && m[0].length == path.length)
return filters[i][0];
}
@@ -5,7 +5,7 @@
<a class="check" ng-click="toggleCheck(node)" ng-class="{include: node.include == '+', exclude: node.include == '-', partial: node.include == ' ', root: node.root}" alt="{{node.include == ' ' ? 'partially included' : (node.include == '-' ? 'excluded' : (node.include == '+' ? 'included' : 'not checked'))}}"></a>
<a class="type {{node.iconCls}}" alt="{{loading ? 'loading' : node.entrytype}}" ng-class="{invisible: node.hidden, loading: node.loading}"></a>
<span ng-click="toggleSelected(node)" ng-bind-html="node.text | highlight:ngSearchFilter"></span>
<span ng-click="toggleSelected(node)" ng-bind-html="node.text | highlight:AppUtils.globToRegexp(ngSearchFilter)"></span>
</div>
<ul ng-class="{expanded: node.expanded}" dx-connect="node" alt="{{node.expanded ? 'expanded' : 'contracted'}}"/>
</li>