lib, gui: Default ignores for new folders (fixes #7428) (#7530)

This commit is contained in:
Simon Frei
2022-01-13 23:38:21 +01:00
committed by GitHub
parent 40bb52fdd8
commit 21d04b895a
17 changed files with 718 additions and 218 deletions
+209 -106
View File
@@ -58,6 +58,9 @@ angular.module('syncthing.core')
text: '',
error: null,
disabled: false,
originalLines: [],
defaultLines: [],
saved: false,
};
resetRemoteNeed();
@@ -409,8 +412,14 @@ angular.module('syncthing.core')
console.log("FolderScanProgress", data);
});
// May be called through .error with the presented arguments, or through
// .catch with the http response object containing the same arguments.
$scope.emitHTTPError = function (data, status, headers, config) {
$scope.$emit('HTTPError', { data: data, status: status, headers: headers, config: config });
var out = data;
if (data && !data.data) {
out = { data: data, status: status, headers: headers, config: config };
}
$scope.$emit('HTTPError', out);
};
var debouncedFuncs = {};
@@ -741,7 +750,7 @@ angular.module('syncthing.core')
}
function shouldSetDefaultFolderPath() {
return $scope.config.defaults.folder.path && !$scope.editingExisting && $scope.folderEditor.folderPath.$pristine && !$scope.editingDefaults;
return $scope.config.defaults.folder.path && $scope.folderEditor.folderPath.$pristine && $scope.currentFolder._editing == "add";
}
function resetRemoteNeed() {
@@ -750,7 +759,6 @@ angular.module('syncthing.core')
$scope.remoteNeedDevice = undefined;
}
function setDefaultTheme() {
if (!document.getElementById("fallback-theme-css")) {
@@ -767,13 +775,9 @@ angular.module('syncthing.core')
}
}
function saveIgnores(ignores, cb) {
$http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
function saveIgnores(ignores) {
return $http.post(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
ignore: ignores
}).success(function () {
if (cb) {
cb();
}
});
};
@@ -1268,8 +1272,9 @@ angular.module('syncthing.core')
if (cfg) {
cfg.paused = pause;
$scope.config.folders = folderList($scope.folders);
$scope.saveConfig();
return $scope.saveConfig();
}
return $q.when();
};
$scope.showListenerStatus = function () {
@@ -1421,18 +1426,14 @@ angular.module('syncthing.core')
});
};
$scope.saveConfig = function (callback) {
$scope.saveConfig = function () {
var cfg = JSON.stringify($scope.config);
var opts = {
headers: {
'Content-Type': 'application/json'
}
};
$http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).then(function() {
if (callback) {
callback();
}
}, $scope.emitHTTPError);
return $http.put(urlbase + '/config', cfg, opts).finally(refreshConfig).catch($scope.emitHTTPError);
};
$scope.urVersions = function () {
@@ -1512,7 +1513,7 @@ angular.module('syncthing.core')
// here as well...
$scope.devices = deviceMap($scope.config.devices);
$scope.saveConfig(function () {
$scope.saveConfig.then(function () {
if (themeChanged) {
document.location.reload(true);
}
@@ -1578,11 +1579,11 @@ angular.module('syncthing.core')
}
$scope.editDeviceModalTitle = function() {
if ($scope.editingDefaults) {
if ($scope.editingDeviceDefaults()) {
return $translate.instant("Edit Device Defaults");
}
var title = '';
if ($scope.editingExisting) {
if ($scope.editingDeviceExisting()) {
title += $translate.instant("Edit Device");
} else {
title += $translate.instant("Add Device");
@@ -1595,16 +1596,23 @@ angular.module('syncthing.core')
};
$scope.editDeviceModalIcon = function() {
if ($scope.editingDefaults || $scope.editingExisting) {
if ($scope.has(["existing", "defaults"], $scope.currentDevice._editing)) {
return 'fas fa-pencil-alt';
}
return 'fas fa-desktop';
};
$scope.editingDeviceDefaults = function() {
return $scope.currentDevice._editing == 'defaults';
}
$scope.editingDeviceExisting = function() {
return $scope.currentDevice._editing == 'existing';
}
$scope.editDeviceExisting = function (deviceCfg) {
$scope.currentDevice = $.extend({}, deviceCfg);
$scope.editingExisting = true;
$scope.editingDefaults = false;
$scope.currentDevice._editing = "existing";
$scope.willBeReintroducedBy = undefined;
if (deviceCfg.introducedBy) {
var introducerDevice = $scope.devices[deviceCfg.introducedBy];
@@ -1633,7 +1641,7 @@ angular.module('syncthing.core')
$scope.editDeviceDefaults = function () {
$http.get(urlbase + '/config/defaults/device').then(function (p) {
$scope.currentDevice = p.data;
$scope.editingDefaults = true;
$scope.currentDevice._editing = "defaults";
editDeviceModal();
}, $scope.emitHTTPError);
};
@@ -1671,8 +1679,7 @@ angular.module('syncthing.core')
$scope.currentDevice = p.data;
$scope.currentDevice.name = name;
$scope.currentDevice.deviceID = deviceID;
$scope.editingExisting = false;
$scope.editingDefaults = false;
$scope.currentDevice._editing = "add";
initShareEditing('device');
$scope.currentSharing.unrelated = $scope.folderList();
editDeviceModal();
@@ -1682,7 +1689,7 @@ angular.module('syncthing.core')
$scope.deleteDevice = function () {
$('#editDevice').modal('hide');
if (!$scope.editingExisting) {
if ($scope.currentDevice._editing != "existing") {
return;
}
@@ -1705,13 +1712,13 @@ angular.module('syncthing.core')
return x.trim();
});
delete $scope.currentDevice._addressesStr;
if ($scope.editingDefaults) {
if ($scope.currentDevice._editing == "defaults") {
$scope.config.defaults.device = $scope.currentDevice;
} else {
setDeviceConfig();
}
delete $scope.currentSharing;
delete $scope.currentDevice;
$scope.currentDevice = {};
$scope.saveConfig();
};
@@ -1955,20 +1962,34 @@ angular.module('syncthing.core')
$('#folder-ignores textarea').focus();
}
}).one('hidden.bs.modal', function () {
window.location.hash = "";
$scope.currentFolder = {};
var p = $q.when();
// If the modal was closed default patterns should still apply
if ($scope.currentFolder._editing == "add-ignores" && !$scope.ignores.saved && $scope.ignores.defaultLines) {
p = saveFolderAddIgnores($scope.currentFolder.id, true);
}
p.then(function () {
window.location.hash = "";
$scope.currentFolder = {};
$scope.ignores = {};
});
});
};
$scope.editFolderModalTitle = function() {
if ($scope.editingDefaults) {
if ($scope.editingFolderDefaults()) {
return $translate.instant("Edit Folder Defaults");
}
var title = '';
if ($scope.editingExisting) {
title += $translate.instant("Edit Folder");
} else {
title += $translate.instant("Add Folder");
switch ($scope.currentFolder._editing) {
case "existing":
title = $translate.instant("Edit Folder");
break;
case "add":
title = $translate.instant("Add Folder");
break;
case "add-ignores":
title = $translate.instant("Set Ignores on Added Folder");
break;
}
if ($scope.currentFolder.id !== '') {
title += ' (' + $scope.folderLabel($scope.currentFolder.id) + ')';
@@ -1977,12 +1998,20 @@ angular.module('syncthing.core')
};
$scope.editFolderModalIcon = function() {
if ($scope.editingDefaults || $scope.editingExisting) {
if ($scope.has(["existing", "defaults"], $scope.currentFolder._editing)) {
return 'fas fa-pencil-alt';
}
return 'fas fa-folder';
};
$scope.editingFolderDefaults = function() {
return $scope.currentFolder._editing == 'defaults';
}
$scope.editingFolderExisting = function() {
return $scope.currentFolder._editing == 'existing';
}
function editFolder(initialTab) {
if ($scope.currentFolder.path.length > 1 && $scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
$scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
@@ -2033,39 +2062,60 @@ angular.module('syncthing.core')
};
$scope.editFolderExisting = function(folderCfg, initialTab) {
$scope.editingExisting = true;
$scope.editingDefaults = false;
$scope.currentFolder = angular.copy(folderCfg);
$scope.ignores.text = 'Loading...';
$scope.ignores.error = null;
$scope.ignores.disabled = true;
$http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
.success(function (data) {
$scope.currentFolder.ignores = data.ignore || [];
$scope.ignores.text = $scope.currentFolder.ignores.join('\n');
$scope.ignores.error = data.error;
$scope.ignores.disabled = false;
})
.error(function (err) {
$scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
$scope.emitHTTPError(err);
});
$scope.currentFolder._editing = "existing";
editFolderLoadIgnores();
editFolder(initialTab);
};
$scope.editFolderDefaults = function() {
$http.get(urlbase + '/config/defaults/folder')
.success(function (data) {
$scope.currentFolder = data;
$scope.editingExisting = false;
$scope.editingDefaults = true;
editFolder();
})
.error($scope.emitHTTPError);
function editFolderLoadingIgnores() {
$scope.ignores.text = 'Loading...';
$scope.ignores.error = null;
$scope.ignores.disabled = true;
}
function editFolderGetIgnores() {
return $http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
.then(function(r) {
return r.data;
}, function (response) {
$scope.ignores.text = $translate.instant("Failed to load ignore patterns.");
return $q.reject(response);
});
};
function editFolderLoadIgnores() {
editFolderLoadingIgnores();
return editFolderGetIgnores().then(editFolderInitIgnores, $scope.emitHTTPError);
}
$scope.editFolderDefaults = function() {
$q.all([
$http.get(urlbase + '/config/defaults/folder').then(function (response) {
$scope.currentFolder = response.data;
$scope.currentFolder._editing = "defaults";
}),
getDefaultIgnores().then(editFolderInitIgnores),
]).then(editFolder, $scope.emitHTTPError);
};
function getDefaultIgnores() {
return $http.get(urlbase + '/config/defaults/ignores').then(function (r) {
return r.data.lines;
});
}
function editFolderInitIgnores(data) {
$scope.ignores.originalLines = data.ignore || [];
setIgnoresText(data.ignore);
$scope.ignores.error = data.error;
$scope.ignores.disabled = false;
}
function setIgnoresText(lines) {
$scope.ignores.text = lines ? lines.join('\n') : "";
}
$scope.selectAllSharedDevices = function (state) {
var devices = $scope.currentSharing.shared;
for (var i = 0; i < devices.length; i++) {
@@ -2093,9 +2143,6 @@ angular.module('syncthing.core')
$scope.addFolderAndShare = function (folderID, pendingFolder, device) {
addFolderInit(folderID).then(function() {
$scope.currentFolder.viewFlags = {
importFromOtherDevice: true
};
$scope.currentSharing.selected[device] = true;
$scope.currentFolder.label = pendingFolder.offeredBy[device].label;
for (var k in pendingFolder.offeredBy) {
@@ -2110,19 +2157,16 @@ angular.module('syncthing.core')
};
function addFolderInit(folderID) {
$scope.editingExisting = false;
$scope.editingDefaults = false;
return $http.get(urlbase + '/config/defaults/folder').then(function(p) {
$scope.currentFolder = p.data;
return $http.get(urlbase + '/config/defaults/folder').then(function (response) {
$scope.currentFolder = response.data;
$scope.currentFolder._editing = "add";
$scope.currentFolder.id = folderID;
initShareEditing('folder');
$scope.currentSharing.unrelated = $scope.currentSharing.unrelated.concat($scope.currentSharing.shared);
$scope.currentSharing.shared = [];
$scope.ignores.text = '';
$scope.ignores.error = null;
$scope.ignores.disabled = false;
// Ignores don't need to be initialized here, as that happens in
// a second step if the user indicates in the creation modal
// that they want to set ignores
}, $scope.emitHTTPError);
}
@@ -2142,7 +2186,14 @@ angular.module('syncthing.core')
};
$scope.saveFolder = function () {
$('#editFolder').modal('hide');
if ($scope.currentFolder._editing == "add-ignores") {
// On modal being hidden without clicking save, the defaults will be saved.
$scope.ignores.saved = true;
saveFolderAddIgnores($scope.currentFolder.id);
hideFolderModal();
return;
}
var folderCfg = angular.copy($scope.currentFolder);
$scope.currentSharing.selected[$scope.myID] = true;
var newDevices = [];
@@ -2191,44 +2242,88 @@ angular.module('syncthing.core')
}
delete folderCfg._guiVersioning;
if ($scope.editingDefaults) {
if ($scope.currentFolder._editing == "defaults") {
hideFolderModal();
$scope.config.defaults.ignores.lines = ignoresArray();
$scope.config.defaults.folder = folderCfg;
$scope.saveConfig();
} else {
saveFolderExisting(folderCfg);
return;
}
// This is a new folder where ignores should apply before it first starts.
if ($scope.currentFolder._addIgnores) {
folderCfg.paused = true;
}
$scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders);
if ($scope.currentFolder._editing == "existing") {
hideFolderModal();
saveFolderIgnoresExisting();
$scope.saveConfig();
return;
}
// No ignores to be set on the new folder, save directly.
if (!$scope.currentFolder._addIgnores) {
hideFolderModal();
$scope.saveConfig();
return;
}
// Add folder (paused), load existing ignores and if there are none,
// load default ignores, then let the user edit them.
$scope.saveConfig().then(function() {
editFolderLoadingIgnores();
$scope.currentFolder._editing = "add-ignores";
$('.nav-tabs a[href="#folder-ignores"]').tab('show');
return editFolderGetIgnores();
}).then(function(data) {
// Error getting ignores -> leave error message.
if (!data) {
return;
}
if ((data.ignore && data.ignore.length > 0) || data.error) {
editFolderInitIgnores(data);
} else {
getDefaultIgnores().then(function(lines) {
setIgnoresText(lines);
$scope.ignores.defaultLines = lines;
$scope.ignores.disabled = false;
});
}
}, $scope.emitHTTPError);
};
function saveFolderExisting(folderCfg) {
var ignoresLoaded = !$scope.ignores.disabled;
function saveFolderIgnoresExisting() {
if ($scope.ignores.disabled) {
return;
}
var ignores = ignoresArray();
function arrayDiffers(a, b) {
return !a !== !b || a.length !== b.length || a.some(function(v, i) { return v !== b[i]; });
}
if (arrayDiffers(ignores, $scope.ignores.originalLines)) {
return saveIgnores(ignores);
};
}
function saveFolderAddIgnores(folderID, useDefault) {
var ignores = useDefault ? $scope.ignores.defaultLines : ignoresArray();
return saveIgnores(ignores).then(function () {
return $scope.setFolderPause(folderID, $scope.currentFolder.paused);
});
};
function ignoresArray() {
var ignores = $scope.ignores.text.split('\n');
// Split always returns a minimum 1-length array even for no patterns
if (ignores.length === 1 && ignores[0] === "") {
ignores = [];
}
if (!$scope.editingExisting && ignores.length) {
folderCfg.paused = true;
};
$scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders);
function arrayEquals(a, b) {
return a.length === b.length && a.every(function(v, i) { return v === b[i] });
}
if (ignoresLoaded && $scope.editingExisting && !arrayEquals(ignores, folderCfg.ignores)) {
saveIgnores(ignores);
};
$scope.saveConfig(function () {
if (!$scope.editingExisting && ignores.length) {
saveIgnores(ignores, function () {
$scope.setFolderPause(folderCfg.id, false);
});
}
});
};
return ignores;
}
$scope.ignoreFolder = function (device, folderID, offeringDevice) {
var ignoredFolder = {
@@ -2282,8 +2377,8 @@ angular.module('syncthing.core')
};
$scope.deleteFolder = function (id) {
$('#editFolder').modal('hide');
if (!$scope.editingExisting) {
hideFolderModal();
if ($scope.currentFolder._editing != "existing") {
return;
}
@@ -2295,6 +2390,10 @@ angular.module('syncthing.core')
$scope.saveConfig();
};
function hideFolderModal() {
$('#editFolder').modal('hide');
}
function resetRestoreVersions() {
$scope.restoreVersions = {
folder: null,
@@ -2839,6 +2938,10 @@ angular.module('syncthing.core')
return Object.keys(dict).length;
};
$scope.has = function (array, element) {
return array.indexOf(element) >= 0;
};
$scope.dismissNotification = function (id) {
var idx = $scope.config.options.unackedNotificationIDs.indexOf(id);
if (idx > -1) {
@@ -4,7 +4,7 @@ angular.module('syncthing.core')
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
if (scope.editingExisting) {
if (scope.currentFolder._editing != "add") {
// we shouldn't validate
ctrl.$setValidity('uniqueFolder', true);
} else if (scope.folders.hasOwnProperty(viewValue)) {
@@ -4,7 +4,7 @@ angular.module('syncthing.core')
require: 'ngModel',
link: function (scope, elm, attrs, ctrl) {
ctrl.$parsers.unshift(function (viewValue) {
if (scope.editingExisting) {
if (scope.currentDevice._editing != "add") {
// we shouldn't validate
ctrl.$setValidity('validDeviceid', true);
} else {
@@ -3,14 +3,14 @@
<form role="form" name="deviceEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(deviceEditor)">
<li class="active"><a data-toggle="tab" href="#device-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
<li ng-if="!editingDefaults"><a data-toggle="tab" href="#device-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
<li ng-if="!editingDeviceDefaults()"><a data-toggle="tab" href="#device-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
<li><a data-toggle="tab" href="#device-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
</ul>
<div class="tab-content">
<div id="device-general" class="tab-pane in active">
<div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
<div ng-if="!editingDeviceDefaults()" class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
<label translate for="deviceID">Device ID</label>
<div ng-if="!editingExisting">
<div ng-if="!editingDeviceExisting()">
<div class="input-group">
<input name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required="" valid-deviceid list="discovery-list" aria-required="true" />
<div class="input-group-btn">
@@ -40,7 +40,7 @@
<span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
</p>
</div>
<div ng-if="editingExisting" class="input-group">
<div ng-if="editingDeviceExisting()" class="input-group">
<div class="well well-sm text-monospace form-control" style="height: auto;" select-on-click>{{currentDevice.deviceID}}</div>
<div class="input-group-btn">
<button type="button" class="btn btn-default" data-toggle="modal" data-target="#idqr">
@@ -56,7 +56,7 @@
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
</div>
</div>
<div ng-if="!editingDefaults" id="device-sharing" class="tab-pane">
<div ng-if="!editingDeviceDefaults()" id="device-sharing" class="tab-pane">
<div class="row">
<div class="col-md-6">
<div class="form-group">
@@ -172,7 +172,7 @@
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
<div ng-if="editingExisting && !editingDefaults" class="pull-left">
<div ng-if="has(['existing', 'defaults'], currentDevice._editing)" class="pull-left">
<button type="button" class="btn btn-warning btn-sm" data-toggle="modal" data-target="#remove-device-confirmation">
<span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
@@ -2,44 +2,44 @@
<div class="modal-body">
<form role="form" name="folderEditor">
<ul class="nav nav-tabs" ng-init="loadFormIntoScope(folderEditor)">
<li class="active"><a data-toggle="tab" href="#folder-general"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
<li><a data-toggle="tab" href="#folder-sharing"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
<li><a data-toggle="tab" href="#folder-versioning"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
<li ng-if="!editingDefaults" ng-class="{'disabled': currentFolder._recvEnc}"><a ng-attr-data-toggle="{{ currentFolder._recvEnc ? undefined : 'tab'}}" href="{{currentFolder._recvEnc ? '#' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
<li><a data-toggle="tab" href="#folder-advanced"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
<li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}" class="active"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-general'}}"><span class="fas fa-cog"></span> <span translate>General</span></a></li>
<li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-sharing'}}"><span class="fas fa-share-alt"></span> <span translate>Sharing</span></a></li>
<li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-versioning'}}"><span class="fas fa-copy"></span> <span translate>File Versioning</span></a></li>
<li ng-class="{'disabled': currentFolder._recvEnc}"><a data-toggle="tab" href="{{currentFolder._recvEnc ? '' : '#folder-ignores'}}"><span class="fas fa-filter"></span> <span translate>Ignore Patterns</span></a></li>
<li ng-class="{'disabled': currentFolder._editing == 'add-ignores'}"><a data-toggle="tab" href="{{currentFolder._editing == 'add-ignores' ? '' : '#folder-advanced'}}"><span class="fas fa-cogs"></span> <span translate>Advanced</span></a></li>
</ul>
<div class="tab-content">
<div id="folder-general" class="tab-pane in active">
<div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty && !editingDefaults}">
<div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty && !editingFolderDefaults()}">
<label for="folderLabel"><span translate>Folder Label</span></label>
<input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}" />
<p class="help-block">
<span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
</p>
</div>
<div ng-if="!editingDefaults" class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
<div ng-if="!editingFolderDefaults()" class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
<label for="folderID"><span translate>Folder ID</span></label>
<input name="folderID" ng-readonly="editingExisting || (!editingExisting && currentFolder.viewFlags.importFromOtherDevice)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
<input name="folderID" ng-readonly="has(['existing', 'add'], currentFolder._editing)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required="" aria-required="true" unique-folder value="{{currentFolder.id}}" />
<p class="help-block">
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
<span translate ng-show="!editingExisting">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</span>
<span translate ng-show="!editingFolderExisting()">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</span>
</p>
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty && !editingDefaults}">
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty && !editingFolderDefaults()}">
<label translate for="folderPath">Folder Path</label>
<input name="folderPath" ng-readonly="editingExisting && !editingDefaults" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" ng-required="!editingDefaults" ng-aria-required="!editingDefaults" path-is-sub-dir />
<input name="folderPath" ng-readonly="editingFolderExisting()" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" ng-required="!editingFolderDefaults()" ng-aria-required="!editingFolderDefaults()" path-is-sub-dir />
<datalist id="directory-list">
<option ng-repeat="directory in directoryList" value="{{ directory }}" />
</datalist>
<p class="help-block">
<span ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine"><span translate>Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.</br></span>
<span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty && !editingDefaults">The folder path cannot be blank.</span>
<span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty && !editingFolderDefaults()">The folder path cannot be blank.</span>
<span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length == 0">Warning, this path is a subdirectory of an existing folder "{%otherFolder%}".</span>
<span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.isSub && folderPathErrors.otherLabel.length != 0">Warning, this path is a subdirectory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
<span ng-if="folderPathErrors.isParent && !editingDefaults">
<span ng-if="folderPathErrors.isParent && !editingFolderDefaults()">
<span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" ng-if="folderPathErrors.otherLabel.length == 0">Warning, this path is a parent directory of an existing folder "{%otherFolder%}".</span>
<span class="text-danger" translate translate-value-other-folder="{{folderPathErrors.otherID}}" translate-value-other-folder-label="{{folderPathErrors.otherLabel}}" ng-if="folderPathErrors.otherLabel.length != 0">Warning, this path is a parent directory of an existing folder "{%otherFolderLabel%}" ({%otherFolder%}).</span>
</span>
@@ -148,33 +148,42 @@
</div>
</div>
<div ng-if="!editingDefaults" id="folder-ignores" class="tab-pane">
<p translate>Enter ignore patterns, one per line.</p>
<div ng-class="{'has-error': ignores.error != null}">
<textarea class="form-control" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea>
<p class="help-block" ng-if="ignores.error">
{{ignores.error}}
</p>
<div id="folder-ignores" class="tab-pane" ng-switch="currentFolder._editing">
<div ng-switch-when="add">
<label>
<input type="checkbox" ng-model="currentFolder._addIgnores" >&nbsp;<span translate>Add ignore patterns</span>
</label>
<p translate class="help-block">Ignore patterns can only be added after the folder is created. If checked, an input field to enter ignore patterns will be presented after saving.</p>
</div>
<div ng-switch-default>
<p translate>Enter ignore patterns, one per line.</p>
<div ng-class="{'has-error': ignores.error != null}">
<textarea class="form-control" name="ignoresText" rows="5" ng-model="ignores.text" ng-disabled="ignores.disabled"></textarea>
<p class="help-block" ng-if="ignores.error">
{{ignores.error}}
</p>
</div>
<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>
<div ng-if="!editingFolderDefaults()">
<hr />
<span translate translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span>
</div>
</div>
<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 />
<span translate ng-show="editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Editing {%path%}.</span>
<span translate ng-show="!editingExisting" translate-value-path="{{currentFolder.path}}{{system.pathSeparator}}.stignore">Creating ignore patterns, overwriting an existing file at {%path%}.</span>
</div>
<div id="folder-advanced" class="tab-pane">
@@ -205,17 +214,17 @@
<div class="col-md-6 form-group">
<label translate>Folder Type</label>
&nbsp;<a href="https://docs.syncthing.net/users/foldertypes.html" target="_blank"><span class="fas fa-question-circle"></span>&nbsp;<span translate>Help</span></a>
<select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingExisting && currentFolder.type == 'receiveencrypted'">
<select class="form-control" ng-change="setDefaultsForFolderType()" ng-model="currentFolder.type" ng-disabled="editingFolderExisting() && currentFolder.type == 'receiveencrypted'">
<option value="sendreceive" translate>Send &amp; Receive</option>
<option value="sendonly" translate>Send Only</option>
<option value="receiveonly" translate>Receive Only</option>
<option value="receiveencrypted" ng-disabled="editingExisting" translate>Receive Encrypted</option>
<option value="receiveencrypted" ng-disabled="editingFolderExisting()" translate>Receive Encrypted</option>
</select>
<p ng-if="currentFolder.type == 'sendonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
<p ng-if="currentFolder.type == 'receiveonly'" translate class="help-block">Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.</p>
<p ng-if="currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Stores and syncs only encrypted data. Folders on all connected devices need to be set up with the same password or be of type "{%receiveEncrypted%}" too.</p>
<p ng-if="editingExisting && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
<p ng-if="editingExisting && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
<p ng-if="editingFolderExisting() && currentFolder.type == 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" cannot be changed after adding the folder. You need to remove the folder, delete or decrypt the data on disk, and add the folder again.</p>
<p ng-if="editingFolderExisting() && currentFolder.type != 'receiveencrypted'" translate class="help-block" translate-value-receive-encrypted="{{'Receive Encrypted' | translate}}">Folder type "{%receiveEncrypted%}" can only be set when adding a new folder.</p>
</div>
<div class="col-md-6 form-group">
<label translate>File Pull Order</label>
@@ -274,7 +283,7 @@
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
<button type="button" class="btn btn-warning pull-left btn-sm" data-toggle="modal" data-target="#remove-folder-confirmation" ng-if="editingExisting && !editingDefaults">
<button type="button" class="btn btn-warning pull-left btn-sm" data-toggle="modal" data-target="#remove-folder-confirmation" ng-if="editingFolderExisting()">
<span class="fas fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
</div>