Get a working Sieve scripts manager

This commit is contained in:
djmaze 2021-01-19 16:01:30 +01:00
parent a31834458b
commit e3125ebfff
24 changed files with 439 additions and 304 deletions

View file

@ -379,6 +379,8 @@ export const Notification = {
CantSaveFilters: 351,
CantGetFilters: 352,
CantActivateFiltersScript: 351, // TODO: 353
CantDeleteFiltersScript: 351, // TODO: 354
FiltersAreNotCorrect: 355,
CantCreateFolder: 400,

View file

@ -10,21 +10,23 @@ class SieveScriptModel extends AbstractModel
this.addObservables({
name: '',
nameError: false,
nameFocused: false,
active: false,
body: '',
nameError: false,
bodyError: false,
deleteAccess: false,
canBeDeleted: false
canBeDeleted: false,
hasChanges: false
});
this.filters = ko.observableArray([]);
// this.saving = ko.observable(false).extend({ throttle: 200 });
this.addSubscribables({
name: sValue => this.nameError(!sValue)
name: () => this.hasChanges(true),
filters: () => this.hasChanges(true),
body: () => this.hasChanges(true)
});
}
@ -34,20 +36,17 @@ class SieveScriptModel extends AbstractModel
}
verify() {
if (!this.name()) {
this.nameError(true);
return false;
}
this.nameError(false);
return true;
this.nameError(!this.name().trim());
this.bodyError(this.allowFilters() ? !this.filters().length : !this.body().trim());
return !this.nameError() && !this.bodyError();
}
toJson() {
return {
name: this.name(),
active: this.active ? '1' : '0',
body: this.body,
// filters: this.filters()
active: this.active() ? '1' : '0',
body: this.body(),
filters: this.filters().map(item => item.toJson())
};
}
@ -66,12 +65,14 @@ class SieveScriptModel extends AbstractModel
static reviveFromJson(json) {
const script = super.reviveFromJson(json);
if (script) {
script.filters([]);
if (script.allowFilters() && Array.isNotEmpty(json.filters)) {
script.filters(
json.filters.map(aData => FilterModel.reviveFromJson(aData)).filter(v => v)
);
} else {
script.filters([]);
}
script.hasChanges(false);
}
return script;
}

View file

@ -236,6 +236,30 @@ class RemoteUserFetch extends AbstractFetchRemote {
});
}
/**
* @param {?Function} fCallback
* @param {SieveScriptModel} script
*/
filtersScriptSave(fCallback, script) {
this.defaultRequest(fCallback, 'FiltersScriptSave', script.toJson());
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptActivate(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptActivate', {name:name});
}
/**
* @param {?Function} fCallback
* @param {string} name
*/
filtersScriptDelete(fCallback, name) {
this.defaultRequest(fCallback, 'FiltersScriptDelete', {name:name});
}
/**
* @param {?Function} fCallback
*/

View file

@ -1,22 +1,22 @@
import ko from 'ko';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { StorageResultType, Notification } from 'Common/Enums';
import { getNotification } from 'Common/Translator';
import FilterStore from 'Stores/User/Filter';
import SieveStore from 'Stores/User/Sieve';
import Remote from 'Remote/User/Fetch';
import { FilterModel } from 'Model/Filter';
import { SieveScriptModel } from 'Model/SieveScript';
import { showScreenPopup } from 'Knoin/Knoin';
class FiltersUserSettings {
constructor() {
this.filters = FilterStore.filters;
this.sieve = SieveStore;
this.scripts = SieveStore.scripts;
this.loading = ko.observable(false).extend({ throttle: 200 });
ko.addObservablesTo(this, {
serverError: false,
@ -33,23 +33,17 @@ class FiltersUserSettings {
}
updateList() {
if (!this.filters.loading()) {
this.filters.loading(true);
if (!this.loading()) {
this.loading(true);
Remote.filtersGet((result, data) => {
this.filters.loading(false);
this.loading(false);
this.serverError(false);
this.scripts([]);
if (StorageResultType.Success === result && data && data.Result && Array.isArray(data.Result.Filters)) {
if (StorageResultType.Success === result && data && data.Result) {
this.serverError(false);
this.filters(
data.Result.Filters.map(aItem => FilterModel.reviveFromJson(aItem)).filter(v => v)
);
FilterStore.modules(data.Result.Capa);
SieveStore.capa(data.Result.Capa);
/*
this.scripts(
@ -60,20 +54,9 @@ class FiltersUserSettings {
value = SieveScriptModel.reviveFromJson(value);
value && this.scripts.push(value)
});
FilterStore.raw(data.Result.Scripts['rainloop.user.raw'].body);
FilterStore.capa(data.Result.Capa.join(' '));
// this.filterRaw.active(data.Result.Scripts['rainloop.user.raw'].active);
// this.filterRaw.allow(!!data.Result.RawIsAllow);
} else {
this.filters([]);
FilterStore.modules({});
this.scripts([]);
SieveStore.capa([]);
FilterStore.raw('');
FilterStore.capa({});
this.serverError(true);
this.serverErrorDesc(
data && data.ErrorCode ? getNotification(data.ErrorCode) : getNotification(Notification.CantGetFilters)
@ -106,13 +89,42 @@ class FiltersUserSettings {
]);
}
deleteScript() {
// TODO
deleteScript(script) {
if (!script.active()) {
Remote.filtersScriptDelete(
(result, data) => {
if (StorageResultType.Success === result && data && data.Result) {
this.scripts.remove(script);
delegateRunOnDestroy(script);
} else {
this.saveError(true);
this.saveErrorText((data && data.ErrorCode)
? (data.ErrorMessageAdditional || getNotification(data.ErrorCode))
: getNotification(Notification.CantActivateFiltersScript)
);
}
},
script.name()
);
}
}
toggleScript(script) {
// TODO: activate/deactivate script
script.active(!script.active());
let name = script.active() ? '' : script.name();
Remote.filtersScriptActivate(
(result, data) => {
if (StorageResultType.Success === result && data && data.Result) {
this.scripts().forEach(script => script.active(script.name() === name));
} else {
this.saveError(true);
this.saveErrorText((data && data.ErrorCode)
? (data.ErrorMessageAdditional || getNotification(data.ErrorCode))
: getNotification(Notification.CantActivateFiltersScript)
);
}
},
name
);
}
onBuild(oDom) {

View file

@ -1,18 +0,0 @@
import ko from 'ko';
class FilterUserStore {
constructor() {
ko.addObservablesTo(this, {
capa: '',
modules: [],
raw: ''
});
this.filters = ko.observableArray([]);
this.filters.loading = ko.observable(false).extend({ throttle: 200 });
this.filters.saving = ko.observable(false).extend({ throttle: 200 });
}
}
export default new FilterUserStore();

View file

@ -50,3 +50,8 @@ html.rl-mobile .b-settings-filters {
.filter-item .filter-sub-name {
color: #aaa;
}
.b-filter-script textarea {
height: 300px;
font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;
}

View file

@ -4,8 +4,8 @@ import { FiltersAction, FilterConditionField, FilterConditionType } from 'Common
import { defaultOptionsAfterRender } from 'Common/Utils';
import { i18n, initOnStartOrLangChange } from 'Common/Translator';
import FilterStore from 'Stores/User/Filter';
import FolderStore from 'Stores/User/Folder';
import SieveStore from 'Stores/User/Sieve';
import { popup, command } from 'Knoin/Knoin';
import { AbstractViewNext } from 'Knoin/AbstractViewNext';
@ -25,7 +25,7 @@ class FilterPopupView extends AbstractViewNext {
selectedFolderValue: ''
});
this.modules = FilterStore.modules;
this.modules = SieveStore.capa;
this.fTrueCallback = null;

View file

@ -3,12 +3,13 @@ import ko from 'ko';
import { delegateRunOnDestroy } from 'Common/UtilsUser';
import { StorageResultType, Notification } from 'Common/Enums';
import { getNotification } from 'Common/Translator';
import { i18nToNodes } from 'Common/Translator';
import Remote from 'Remote/User/Fetch';
import FilterStore from 'Stores/User/Filter';
import FilterModel from 'Model/Filter';
import SieveStore from 'Stores/User/Sieve';
import { popup, showScreenPopup, command } from 'Knoin/Knoin';
import { popup, showScreenPopup/*, command*/ } from 'Knoin/Knoin';
import { AbstractViewNext } from 'Knoin/AbstractViewNext';
@popup({
@ -19,78 +20,46 @@ class SieveScriptPopupView extends AbstractViewNext {
constructor() {
super();
// this.filters = FilterStore.filters;
this.filters = ko.observableArray([]);
this.filters.loading = ko.observable(false).extend({ throttle: 200 });
this.filters.saving = ko.observable(false).extend({ throttle: 200 });
this.modules = FilterStore.modules;
this.script = {
filters: FilterStore.filters,
saving: () => FilterStore.filters.saving()
};
ko.addObservablesTo(this, {
isNew: true,
serverError: false,
serverErrorDesc: '',
saveError: false,
saveErrorText: '',
haveChanges: false,
filterRaw: ''
rawActive: false,
script: null
});
this.serverError.subscribe(value => value || this.serverErrorDesc(''), this);
// this.filterRaw = FilterStore.raw;
this.filterRaw.capa = FilterStore.capa;
this.filterRaw.active = ko.observable(false);
this.filterRaw.allow = ko.observable(false);
this.filterRaw.error = ko.observable(false);
this.sieveCapabilities = SieveStore.capa.join(' ');
this.saving = false;
this.filterForDeletion = ko.observable(null).deleteAccessHelper();
this.filters.subscribe(() => this.haveChanges(true));
this.filterRaw.subscribe(() => {
this.haveChanges(true);
this.filterRaw.error(false);
});
this.haveChanges.subscribe(() => this.saveErrorText(''));
this.filterRaw.active.subscribe(() => {
this.haveChanges(true);
this.filterRaw.error(false);
});
}
@command((self) => self.haveChanges())
// @command()
saveScriptCommand() {
if (!this.filters.saving()) {
if (this.filterRaw.active() && !this.filterRaw().trim()) {
this.filterRaw.error(true);
let script = this.script();
if (!this.saving/* && script.hasChanges()*/) {
if (!script.verify()) {
return false;
}
this.filters.saving(true);
this.saveErrorText('');
this.saving = true;
this.saveError(false);
Remote.filtersSave(
Remote.filtersScriptSave(
(result, data) => {
this.filters.saving(false);
this.saving = false;
if (StorageResultType.Success === result && data && data.Result) {
this.haveChanges(false);
} else if (data && data.ErrorCode) {
this.saveErrorText(data.ErrorMessageAdditional || getNotification(data.ErrorCode));
script.hasChanges(false);
} else {
this.saveErrorText(getNotification(Notification.CantSaveFilters));
this.saveError(true);
this.saveErrorText((data && data.ErrorCode)
? (data.ErrorMessageAdditional || getNotification(data.ErrorCode))
: getNotification(Notification.CantSaveFilters)
);
}
},
this.filters(),
this.filterRaw(),
this.filterRaw.active()
script
);
}
@ -98,19 +67,18 @@ class SieveScriptPopupView extends AbstractViewNext {
}
deleteFilter(filter) {
this.filters.remove(filter);
this.script().filters.remove(filter);
delegateRunOnDestroy(filter);
}
addFilter() {
/* this = SieveScriptModel */
const filter = new FilterModel();
filter.generateID();
showScreenPopup(require('View/Popup/Filter'), [
filter,
() => {
this.filters.push(filter);
this.filterRaw.active(false);
},
false
]);
@ -118,19 +86,16 @@ class SieveScriptPopupView extends AbstractViewNext {
editFilter(filter) {
const clonedFilter = filter.cloneSelf();
showScreenPopup(require('View/Popup/Filter'), [
clonedFilter,
() => {
const filters = this.filters(),
const script = this.script(),
filters = script.filters(),
index = filters.indexOf(filter);
if (-1 < index && filters[index]) {
if (-1 < index) {
delegateRunOnDestroy(filters[index]);
filters[index] = clonedFilter;
this.filters(filters);
this.haveChanges(true);
script.filters(filters);
}
},
true
@ -141,36 +106,22 @@ class SieveScriptPopupView extends AbstractViewNext {
oDom.addEventListener('click', event => {
const el = event.target.closestWithin('.filter-item .e-action', oDom),
filter = el && ko.dataFor(el);
filter && this.editFilter(ko.dataFor(el));
filter && this.editFilter(filter);
});
}
onShow(oScript, fTrueCallback, bEdit) {
this.clearPopup();
this.fTrueCallback = fTrueCallback;
this.filters(oScript.filters());
this.filterRaw(oScript.body());
this.filterRaw.active(!oScript.allowFilters());
this.filterRaw.error(false);
this.script(oScript);
this.rawActive(!oScript.allowFilters());
this.isNew(!bEdit);
if (!bEdit && oScript) {
// oScript.nameFocused(true);
}
this.saveError(false);
}
onShowWithDelay() {
// Sometimes not everything is translated, try again
i18nToNodes(this.viewModelDom);
}
clearPopup() {
this.isNew(true);
this.fTrueCallback = null;
this.filters([]);
}
}
export { SieveScriptPopupView, SieveScriptPopupView as default };

View file

@ -326,6 +326,17 @@ class ManageSieveClient extends \MailSo\Net\NetClient
return $this;
}
/**
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Sieve\Exceptions\NegativeResponseException
*/
public function RenameScript(string $sOldName, string $sNewName) : self
{
$this->sendRequestWithCheck('RENAMESCRIPT "'.$sOldName.'" "'.$sNewName.'"');
return $this;
}
/**
* @throws \MailSo\Net\Exceptions\Exception
* @throws \MailSo\Sieve\Exceptions\NegativeResponseException

View file

@ -76,6 +76,64 @@ trait Filters
$aFilters, $sRaw, $bRawIsActive));
}
public function DoFiltersScriptSave() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::FILTERS, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
$sName = $this->GetActionParam('name', '');
$aFilters = array();
if (\RainLoop\Providers\Filters\SieveStorage::SIEVE_FILE_NAME === $sName) {
$aIncFilters = $this->GetActionParam('filters', array());
foreach ($aIncFilters as $aFilter) {
if (\is_array($aFilter)) {
$oFilter = new \RainLoop\Providers\Filters\Classes\Filter();
if ($oFilter->FromJSON($aFilter)) {
$aFilters[] = $oFilter;
}
}
}
}
if ($this->GetActionParam('active', false)) {
// $this->FiltersProvider()->ActivateScript($oAccount, $sName);
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->Save(
$oAccount, $sName, $aFilters, $this->GetActionParam('body', '')
));
}
public function DoFiltersScriptActivate() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::FILTERS, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->ActivateScript(
$oAccount, $this->GetActionParam('name', '')
));
}
public function DoFiltersScriptDelete() : array
{
$oAccount = $this->getAccountFromToken();
if (!$this->GetCapa(false, false, Capa::FILTERS, $oAccount)) {
return $this->FalseResponse(__FUNCTION__);
}
return $this->DefaultResponse(__FUNCTION__, $this->FiltersProvider()->DeleteScript(
$oAccount, $this->GetActionParam('name', '')
));
}
protected function FiltersProvider() : \RainLoop\Providers\Filters
{
if (!$this->oFiltersProvider) {

View file

@ -35,6 +35,8 @@ class Notifications
const CantSaveFilters = 351;
const CantGetFilters = 352;
const CantActivateFiltersScript = 351; // TODO: 353
const CantDeleteFiltersScript = 351; // TODO: 354
const FiltersAreNotCorrect = 355;
const CantCreateFolder = 400;

View file

@ -14,47 +14,73 @@ class Filters extends \RainLoop\Providers\AbstractProvider
$this->oDriver = $oDriver;
}
private static function handleException(\Throwable $oException, int $defNotification) : void
{
if ($oException instanceof \MailSo\Net\Exceptions\SocketCanNotConnectToHostException) {
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ConnectionError, $oException);
}
if ($oException instanceof \MailSo\Sieve\Exceptions\NegativeResponseException) {
throw new \RainLoop\Exceptions\ClientException(
\RainLoop\Notifications::ClientViewError, $oException, \implode("\r\n", $oException->GetResponses())
);
}
throw new \RainLoop\Exceptions\ClientException($defNotification, $oException);
}
public function Load(\RainLoop\Model\Account $oAccount, bool $bAllowRaw = false) : array
{
try
{
return $this->IsActive() ? $this->oDriver->Load($oAccount, $bAllowRaw) : array();
}
catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException)
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ConnectionError, $oException);
}
catch (\Throwable $oException)
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantGetFilters, $oException);
static::handleException($oException, \RainLoop\Notifications::CantGetFilters);
}
return false;
}
public function Save(\RainLoop\Model\Account $oAccount, array $aFilters, string $sRaw = '', bool $bRawIsActive = false) : bool
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool
{
try
{
return $this->IsActive() ? $this->oDriver->Save(
$oAccount, $aFilters, $sRaw, $bRawIsActive) : false;
}
catch (\MailSo\Net\Exceptions\SocketCanNotConnectToHostException $oException)
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::ConnectionError, $oException);
}
catch (\MailSo\Sieve\Exceptions\NegativeResponseException $oException)
{
throw new \RainLoop\Exceptions\ClientException(
\RainLoop\Notifications::ClientViewError, $oException,
\implode("\r\n", $oException->GetResponses()));
return $this->IsActive()
? $this->oDriver->Save($oAccount, $sScriptName, $aFilters, $sRaw)
: false;
}
catch (\Throwable $oException)
{
throw new \RainLoop\Exceptions\ClientException(\RainLoop\Notifications::CantSaveFilters, $oException);
static::handleException($oException. \RainLoop\Notifications::CantSaveFilters);
}
}
return false;
public function ActivateScript(\RainLoop\Model\Account $oAccount, string $sScriptName)
{
try
{
return $this->IsActive()
? $this->oDriver->Activate($oAccount, $sScriptName)
: false;
}
catch (\Throwable $oException)
{
static::handleException($oException. \RainLoop\Notifications::CantActivateFiltersScript);
}
}
public function DeleteScript(\RainLoop\Model\Account $oAccount, string $sScriptName)
{
try
{
return $this->IsActive()
? $this->oDriver->Delete($oAccount, $sScriptName)
: false;
}
catch (\Throwable $oException)
{
static::handleException($oException. \RainLoop\Notifications::CantDeleteFiltersScript);
}
}
public function IsActive() : bool

View file

@ -6,5 +6,9 @@ interface FiltersInterface
{
public function Load(\RainLoop\Model\Account $oAccount, bool $bAllowRaw = false) : array;
public function Save(\RainLoop\Model\Account $oAccount, array $aFilters) : bool;
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool;
public function Activate(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool;
public function Delete(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool;
}

View file

@ -39,52 +39,67 @@ class SieveStorage implements FiltersInterface
$this->bUtf8FolderName = !!$this->oConfig->Get('labs', 'sieve_utf8_folder_name', true);
}
protected function getConnection(\RainLoop\Model\Account $oAccount) : ?\MailSo\Sieve\ManageSieveClient
{
$oSieveClient = new \MailSo\Sieve\ManageSieveClient();
$oSieveClient->SetLogger($this->oLogger);
$oSieveClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'sieve_timeout', 10));
return $oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig)
? $oSieveClient
: null;
}
public function Load(\RainLoop\Model\Account $oAccount, bool $bAllowRaw = false) : array
{
$aModules = array();
$aFilters = array();
$aScripts = array();
$oSieveClient = new \MailSo\Sieve\ManageSieveClient();
$oSieveClient->SetLogger($this->oLogger);
$oSieveClient->SetTimeOuts(10, (int) $this->oConfig->Get('labs', 'sieve_timeout', 10));
if ($oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig)) {
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
$aModules = $oSieveClient->Modules();
\sort($aModules);
$aList = $oSieveClient->ListScripts();
if (!empty($aList[self::SIEVE_FILE_NAME])) {
$sS = $oSieveClient->GetScript(self::SIEVE_FILE_NAME);
if ($sS) {
$aFilters = $this->fileStringToCollection($sS);
}
}
if ($bAllowRaw) {
foreach ($aList as $name => $active) {
if ($name != self::SIEVE_FILE_NAME) {
foreach ($aList as $name => $active) {
if ($name != self::SIEVE_FILE_NAME) {
if ($bAllowRaw) {
$aScripts[$name] = array(
'@Object' => 'Object/SieveScript',
'name' => $name,
'active' => $active,
'body' => $oSieveClient->GetScript($name) // \trim() ?
);
} else {
$aScripts[$name] = array(
'@Object' => 'Object/SieveScript',
'name' => $name,
'active' => $active,
'body' => $oSieveClient->GetScript($name), // \trim() ?
'filters' => $aFilters
);
}
} else {
$sS = $oSieveClient->GetScript(self::SIEVE_FILE_NAME);
if ($sS) {
$aFilters = $this->fileStringToCollection($sS);
}
$aScripts[$name] = array(
'@Object' => 'Object/SieveScript',
'name' => $name,
'active' => $active,
'body' => $oSieveClient->GetScript($name), // \trim() ?
'filters' => $aFilters
);
}
}
$oSieveClient->LogoutAndDisconnect();
}
if (!isset($aList[self::SIEVE_FILE_NAME_RAW])) {
if (!isset($aList[self::SIEVE_FILE_NAME])) {
$aScripts[$name] = array(
'@Object' => 'Object/SieveScript',
'name' => self::SIEVE_FILE_NAME,
'active' => false,
'body' => '',
'filters' => []
);
}
if ($bAllowRaw && !isset($aList[self::SIEVE_FILE_NAME_RAW])) {
$aScripts[$name] = array(
'@Object' => 'Object/SieveScript',
'name' => self::SIEVE_FILE_NAME_RAW,
@ -96,55 +111,52 @@ class SieveStorage implements FiltersInterface
\ksort($aScripts);
return array(
'RawIsAllow' => $bAllowRaw,
'Filters' => $aFilters,
'Capa' => $aModules,
'Scripts' => $aScripts
);
}
public function Save(\RainLoop\Model\Account $oAccount, array $aFilters, string $sRaw = '', bool $bRawIsActive = false) : bool
public function Save(\RainLoop\Model\Account $oAccount, string $sScriptName, array $aFilters, string $sRaw = '') : bool
{
$oSieveClient = new \MailSo\Sieve\ManageSieveClient();
$oSieveClient->SetLogger($this->oLogger);
$oSieveClient->SetTimeOuts(10, (int) \RainLoop\Api::Config()->Get('labs', 'sieve_timeout', 10));
if ($oAccount->SieveConnectAndLoginHelper($this->oPlugins, $oSieveClient, $this->oConfig))
{
$aList = $oSieveClient->ListScripts();
if ($bRawIsActive)
{
if (!empty($sRaw))
{
$oSieveClient->PutScript(self::SIEVE_FILE_NAME_RAW, $sRaw);
$oSieveClient->SetActiveScript(self::SIEVE_FILE_NAME_RAW);
}
else if (isset($aList[self::SIEVE_FILE_NAME_RAW]))
{
$oSieveClient->DeleteScript(self::SIEVE_FILE_NAME_RAW);
if (self::SIEVE_FILE_NAME === $sScriptName) {
$sRaw = $this->collectionToFileString($aFilters);
}
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
if (empty($sRaw)) {
$aList = $oSieveClient->ListScripts();
if (isset($aList[$sScriptName])) {
$oSieveClient->DeleteScript($sScriptName);
}
} else {
$oSieveClient->PutScript($sScriptName, $sRaw);
}
else
{
$sUserFilter = $this->collectionToFileString($aFilters);
if (!empty($sUserFilter))
{
$oSieveClient->PutScript(self::SIEVE_FILE_NAME, $sUserFilter);
$oSieveClient->SetActiveScript(self::SIEVE_FILE_NAME);
}
else if (isset($aList[self::SIEVE_FILE_NAME]))
{
$oSieveClient->DeleteScript(self::SIEVE_FILE_NAME);
}
}
$oSieveClient->LogoutAndDisconnect();
return true;
}
return false;
}
/**
* If $sScriptName is the empty string (i.e., ""), then any active script is disabled.
*/
public function Activate(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool
{
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
$oSieveClient->SetActiveScript(\trim($sScriptName));
return true;
}
return false;
}
public function Delete(\RainLoop\Model\Account $oAccount, string $sScriptName) : bool
{
$oSieveClient = $this->getConnection($oAccount);
if ($oSieveClient) {
$oSieveClient->DeleteScript(\trim($sScriptName));
return true;
}
return false;
}

View file

@ -349,6 +349,15 @@ en:
VACATION_RECIPIENTS_LABEL: "Recipients (comma separated)"
REJECT_MESSAGE_LABEL: "Reject message"
ALL_INCOMING_MESSAGES_DESC: "All incoming messages"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "Name"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
BUTTON_SAVE: "Save"
CAPABILITY_LABEL: "Capabilities"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Select system folders"
SELECT_CHOOSE_ONE: "Choose one"
@ -431,21 +440,15 @@ en:
BUTTON_BACK: "Back"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filters"
LEGEND_SIEVE_SCRIPT: "Sieve Script"
BUTTON_SAVE: "Save"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_DELETE: "Delete"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
SUBNAME_NONE: "None"
SUBNAME_MOVE_TO: "Move to \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Forward to \"%EMAIL%\""
SUBNAME_REJECT: "Reject"
SUBNAME_VACATION_MESSAGE: "Vacation message"
SUBNAME_DISCARD: "Discard"
CAPABILITY_LABEL: "Capability"
LOADING_PROCESS: "Updating filter list"
DELETING_ASK: "Are you sure?"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identity"
LABEL_DISPLAY_NAME: "Name"

View file

@ -348,6 +348,15 @@ de_DE:
VACATION_RECIPIENTS_LABEL: "Empfänger (durch Komma getrennt)"
REJECT_MESSAGE_LABEL: "Ablehnnachricht"
ALL_INCOMING_MESSAGES_DESC: "Alle eingehenden Nachrichten"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Skript erstellen"
TITLE_EDIT: "Skript bearbeiten"
SCRIPT_NAME: "Name"
BUTTON_ADD_FILTER: "Filter hinzufügen"
BUTTON_RAW_SCRIPT: "Benutzerdefiniertes Skript verwenden"
BUTTON_SAVE: "Speichern"
CAPABILITY_LABEL: "Unterstützte Module"
CHANGES_NEED_TO_BE_SAVED_DESC: "Die Änderungen müssen auf dem Server gespeichert werden."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Wählen Sie die Systemordner aus"
SELECT_CHOOSE_ONE: "Wählen Sie einen aus"
@ -431,20 +440,15 @@ de_DE:
BUTTON_BACK: "Zurück"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filter"
BUTTON_SAVE: "Speichern"
BUTTON_ADD_FILTER: "Filter hinzufügen"
BUTTON_DELETE: "Löschen"
BUTTON_RAW_SCRIPT: "Benutzerdefiniertes Skript verwenden"
SUBNAME_NONE: "Keine"
SUBNAME_MOVE_TO: "Verschieben nach \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Weiterleiten nach \"%EMAIL%\""
SUBNAME_REJECT: "Ablehnen"
SUBNAME_VACATION_MESSAGE: "Urlaubsbenachrichtigung"
SUBNAME_DISCARD: "Verwerfen"
CAPABILITY_LABEL: "Unterstützte Module"
LOADING_PROCESS: "Aktualisiere Filterliste"
DELETING_ASK: "Sind Sie sicher?"
CHANGES_NEED_TO_BE_SAVED_DESC: "Die Änderungen müssen auf dem Server gespeichert werden."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identität"
LABEL_DISPLAY_NAME: "Name"

View file

@ -349,6 +349,15 @@ en_GB:
VACATION_RECIPIENTS_LABEL: "Recipients (comma separated)"
REJECT_MESSAGE_LABEL: "Reject message"
ALL_INCOMING_MESSAGES_DESC: "All incoming messages"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "Name"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
BUTTON_SAVE: "Save"
CAPABILITY_LABEL: "Capabilities"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Select system folders"
SELECT_CHOOSE_ONE: "Choose one"
@ -431,20 +440,15 @@ en_GB:
BUTTON_BACK: "Back"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filters"
BUTTON_SAVE: "Save"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_DELETE: "Delete"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
SUBNAME_NONE: "None"
SUBNAME_MOVE_TO: "Move to \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Forward to \"%EMAIL%\""
SUBNAME_REJECT: "Reject"
SUBNAME_VACATION_MESSAGE: "Vacation message"
SUBNAME_DISCARD: "Discard"
CAPABILITY_LABEL: "Capability"
LOADING_PROCESS: "Updating filter list"
DELETING_ASK: "Are you sure?"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identity"
LABEL_DISPLAY_NAME: "Name"

View file

@ -349,6 +349,15 @@ en_US:
VACATION_RECIPIENTS_LABEL: "Recipients (comma separated)"
REJECT_MESSAGE_LABEL: "Reject message"
ALL_INCOMING_MESSAGES_DESC: "All incoming messages"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "Name"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
BUTTON_SAVE: "Save"
CAPABILITY_LABEL: "Capabilities"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Select system folders"
SELECT_CHOOSE_ONE: "Choose one"
@ -431,20 +440,15 @@ en_US:
BUTTON_BACK: "Back"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filters"
BUTTON_SAVE: "Save"
BUTTON_ADD_FILTER: "Add a Filter"
BUTTON_DELETE: "Delete"
BUTTON_RAW_SCRIPT: "Use Custom User Script"
SUBNAME_NONE: "None"
SUBNAME_MOVE_TO: "Move to \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Forward to \"%EMAIL%\""
SUBNAME_REJECT: "Reject"
SUBNAME_VACATION_MESSAGE: "Vacation message"
SUBNAME_DISCARD: "Discard"
CAPABILITY_LABEL: "Capability"
LOADING_PROCESS: "Updating filter list"
DELETING_ASK: "Are you sure?"
CHANGES_NEED_TO_BE_SAVED_DESC: "These changes need to be saved to the server."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identity"
LABEL_DISPLAY_NAME: "Name"

View file

@ -348,6 +348,15 @@ es_ES:
VACATION_RECIPIENTS_LABEL: "Destinatarios (separados por coma)"
REJECT_MESSAGE_LABEL: "Rechazar mensaje"
ALL_INCOMING_MESSAGES_DESC: "Todos los mensajes entrantes"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "Nombre"
BUTTON_ADD_FILTER: "Añadir un Filtro"
BUTTON_RAW_SCRIPT: "Usar Script Personalizado"
BUTTON_SAVE: "Guardar"
CAPABILITY_LABEL: "Capability"
CHANGES_NEED_TO_BE_SAVED_DESC: "Estos cambios deben ser cambiados en el servidor."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Seleccione las carpetas del sistema"
SELECT_CHOOSE_ONE: "Elija una"
@ -432,20 +441,15 @@ es_ES:
BUTTON_BACK: "Atrás"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filtros"
BUTTON_SAVE: "Guardar"
BUTTON_ADD_FILTER: "Añadir un Filtro"
BUTTON_DELETE: "Borrar"
BUTTON_RAW_SCRIPT: "Usar Script Personalizado"
SUBNAME_NONE: "Ninguno"
SUBNAME_MOVE_TO: "Mover a \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Reenviar a \"%EMAIL%\""
SUBNAME_REJECT: "Rechazar"
SUBNAME_VACATION_MESSAGE: "Mensaje de vacaciones"
SUBNAME_DISCARD: "Descartar"
CAPABILITY_LABEL: "Capability"
LOADING_PROCESS: "Actualizando lista de filtros"
DELETING_ASK: "¿Está seguro?"
CHANGES_NEED_TO_BE_SAVED_DESC: "Estos cambios deben ser cambiados en el servidor."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identidad"
LABEL_DISPLAY_NAME: "Nombre"

View file

@ -349,6 +349,15 @@ fr_FR:
VACATION_RECIPIENTS_LABEL: "Destinataires (séparés par des virgules)"
REJECT_MESSAGE_LABEL: "Message rejeté"
ALL_INCOMING_MESSAGES_DESC: "Tous les messages entrants"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "Nom"
BUTTON_ADD_FILTER: "Ajouter un filtre"
BUTTON_RAW_SCRIPT: "Utiliser le script d'un utilisateur"
BUTTON_SAVE: "Enregistrer"
CAPABILITY_LABEL: "Capacité"
CHANGES_NEED_TO_BE_SAVED_DESC: "Ces changements doivent être enregistrés sur le serveur."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Sélectionner les dossiers systèmes"
SELECT_CHOOSE_ONE: "Faites un choix"
@ -432,20 +441,15 @@ fr_FR:
BUTTON_BACK: "Retour"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filtres"
BUTTON_SAVE: "Enregistrer"
BUTTON_ADD_FILTER: "Ajouter un filtre"
BUTTON_DELETE: "Effacer"
BUTTON_RAW_SCRIPT: "Utiliser le script d'un utilisateur"
SUBNAME_NONE: "Aucun"
SUBNAME_MOVE_TO: "Déplacer vers \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Faire suivre à \"%EMAIL%\""
SUBNAME_REJECT: "Rejeter"
SUBNAME_VACATION_MESSAGE: "Message d'absence"
SUBNAME_DISCARD: "Exclure"
CAPABILITY_LABEL: "Capacité"
LOADING_PROCESS: "Mise à jour de la liste de filtres"
DELETING_ASK: "Êtes-vous sûr ?"
CHANGES_NEED_TO_BE_SAVED_DESC: "Ces changements doivent être enregistrés sur le serveur."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identité"
LABEL_DISPLAY_NAME: "Nom"

View file

@ -348,6 +348,15 @@ nl_NL:
VACATION_RECIPIENTS_LABEL: "Ontvangers (comma gescheiden)"
REJECT_MESSAGE_LABEL: "Afwijsbericht"
ALL_INCOMING_MESSAGES_DESC: "Alle inkomende berichten"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Script toevoegen"
TITLE_EDIT: "Script aanpassen"
SCRIPT_NAME: "Naam"
BUTTON_ADD_FILTER: "Filter toevoegen"
BUTTON_RAW_SCRIPT: "Maak een custom script"
BUTTON_SAVE: "Opslaan"
CAPABILITY_LABEL: "Mogelijkheden"
CHANGES_NEED_TO_BE_SAVED_DESC: "Wijzigingen moeten nog opgeslagen worden op de server."
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "Selecteer systeem mappen"
SELECT_CHOOSE_ONE: "Kies één"
@ -430,20 +439,15 @@ nl_NL:
BUTTON_BACK: "Terug"
SETTINGS_FILTERS:
LEGEND_FILTERS: "Filters"
BUTTON_SAVE: "Opslaan"
BUTTON_ADD_FILTER: "Filter toevoegen"
BUTTON_DELETE: "Verwijder"
BUTTON_RAW_SCRIPT: "Maak een custom script"
SUBNAME_NONE: "Geen"
SUBNAME_MOVE_TO: "Verplaats naar map \"%FOLDER%\""
SUBNAME_FORWARD_TO: "Doorsturen naar \"%EMAIL%\""
SUBNAME_REJECT: "Afwijzen"
SUBNAME_VACATION_MESSAGE: "Afwezigheidsbericht"
SUBNAME_DISCARD: "Gooi weg"
CAPABILITY_LABEL: "Mogelijkheden"
LOADING_PROCESS: "Bezig met updaten van de filter lijst"
DELETING_ASK: "Weet u het zeker?"
CHANGES_NEED_TO_BE_SAVED_DESC: "Wijzigingen moeten nog opgeslagen worden op de server."
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "Identiteit"
LABEL_DISPLAY_NAME: "Naam"

View file

@ -348,6 +348,15 @@ zh_CN:
VACATION_RECIPIENTS_LABEL: "收件人 (半角逗号“,”分隔)"
REJECT_MESSAGE_LABEL: "拒收邮件"
ALL_INCOMING_MESSAGES_DESC: "所有来信"
POPUPS_SIEVE_SCRIPT:
TITLE_CREATE: "Create Script"
TITLE_EDIT: "Edit Script"
SCRIPT_NAME: "名称"
BUTTON_ADD_FILTER: "添加筛选条件"
BUTTON_RAW_SCRIPT: "使用自定义脚本"
BUTTON_SAVE: "保存"
CAPABILITY_LABEL: "Capability"
CHANGES_NEED_TO_BE_SAVED_DESC: "这些更改需要保存。"
POPUPS_SYSTEM_FOLDERS:
TITLE_SYSTEM_FOLDERS: "选择系统文件夹"
SELECT_CHOOSE_ONE: "选择一个"
@ -429,20 +438,15 @@ zh_CN:
BUTTON_BACK: "返回"
SETTINGS_FILTERS:
LEGEND_FILTERS: "筛选器"
BUTTON_SAVE: "保存"
BUTTON_ADD_FILTER: "添加筛选条件"
BUTTON_DELETE: "删除"
BUTTON_RAW_SCRIPT: "使用自定义脚本"
SUBNAME_NONE: "无"
SUBNAME_MOVE_TO: "移动到 \"%FOLDER%\""
SUBNAME_FORWARD_TO: "转发到 \"%EMAIL%\""
SUBNAME_REJECT: "拒绝"
SUBNAME_VACATION_MESSAGE: "假期自动回复"
SUBNAME_DISCARD: "取消"
CAPABILITY_LABEL: "Capability"
LOADING_PROCESS: "正在更新筛选规则列表"
DELETING_ASK: "确定删除?"
CHANGES_NEED_TO_BE_SAVED_DESC: "这些更改需要保存。"
SETTINGS_IDENTITY:
LEGEND_IDENTITY: "身份"
LABEL_DISPLAY_NAME: "名称"

View file

@ -1,51 +1,65 @@
<div class="modal fade b-filter-script g-ui-user-select-none" data-bind="modal: modalVisibility">
<div>
<div data-bind="with: script">
<div class="modal-header">
<button type="button" class="close" data-bind="command: cancelCommand">×</button>
<h3 class="i18n" data-i18n="POPUPS_FILTER/LEGEND_SIEVE_SCRIPT"></h3>
<button type="button" class="close" data-bind="command: $root.cancelCommand">×</button>
<h3>
<span class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/TITLE_CREATE" data-bind="visible: $root.isNew"></span>
<span class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/TITLE_EDIT" data-bind="visible: !$root.isNew()"></span>
</h3>
</div>
<div class="modal-body">
<div class="row" data-bind="visible: haveChanges">
<div class="control-group" data-bind="css: {'error': nameError}">
<div class="controls">
<input type="text" class="i18n span5"
data-bind="value: name, hasFocus: $root.isNew, visible: $root.isNew"
autocorrect="off" autocapitalize="off" spellcheck="false"
data-i18n="[placeholder]POPUPS_SIEVE_SCRIPT/SCRIPT_NAME"
/>
<span data-bind="text: name, visible: !$root.isNew()"></span>
</div>
</div>
<div class="row" data-bind="visible: hasChanges">
<div class="span8 width100-on-mobile">
<br />
<div class="alert g-ui-user-select-none" style="margin-bottom: 0">
<div class="alert g-ui-user-select-none">
<i class="icon-warning"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n="SETTINGS_FILTERS/CHANGES_NEED_TO_BE_SAVED_DESC"></span>
<span class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/CHANGES_NEED_TO_BE_SAVED_DESC"></span>
</div>
</div>
</div>
<div class="row" data-bind="visible: serverError">
<div class="row" data-bind="visible: $root.saveError">
<div class="span8 width100-on-mobile">
<div class="alert alert-error g-ui-user-select-none" style="margin-bottom: 0">
<div class="alert alert-error g-ui-user-select-none">
<i class="icon-warning"></i>
&nbsp;&nbsp;
<span data-bind="text: serverErrorDesc"></span>
<span data-bind="text: $root.saveErrorText"></span>
</div>
</div>
</div>
<br />
<div class="row">
<!--
<div class="row" data-bind="visible: allowFilters">
<div class="span5 width100-on-mobile">
<a class="btn" data-bind="click: function () { filterRaw.active(!filterRaw.active()) },
css: {'active': filterRaw.active }, tooltip: 'SETTINGS_FILTERS/BUTTON_RAW_SCRIPT'">
<a class="btn" data-bind="click: function () { $root.rawActive(!$root.rawActive()) },
css: {'active': $root.rawActive }, tooltip: 'POPUPS_SIEVE_SCRIPT/BUTTON_RAW_SCRIPT'">
<i class="icon-file-code"></i>
</a>
</div>
</div>
-->
<div class="row">
<div class="span8 width100-on-mobile">
<div class="control-group" data-bind="css: {'error': filterRaw.error}, visible: filterRaw.active()">
<div class="control-group" data-bind="visible: $root.rawActive">
<div class="controls">
<pre style="word-break: break-word;" data-bind="visible: '' !== filterRaw.capa()">
<b class="i18n" data-i18n="SETTINGS_FILTERS/CAPABILITY_LABEL"></b>:
<span data-bind="text: filterRaw.capa"></span>
<pre>
<b class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/CAPABILITY_LABEL"></b>:
<span data-bind="text: $root.sieveCapabilities"></span>
</pre>
<textarea class="span8" style="height: 300px; font-family: Monaco, Menlo, Consolas, 'Courier New', monospace;"
data-bind="value: filterRaw, valueUpdate: 'afterkeydown'"></textarea>
<textarea class="span8" data-bind="value: body, valueUpdate: 'afterkeydown'"></textarea>
</div>
</div>
<div data-bind="visible: !filterRaw.active()">
<div data-bind="visible: !$root.rawActive()">
<table class="table table-hover list-table filters-list g-ui-user-select-none"
data-bind="i18nUpdate: filters">
<colgroup>
@ -55,10 +69,10 @@
<col style="width: 140px" />
<col style="width: 1%" />
</colgroup>
<tbody data-bind="foreach: filters" style="width: 600px">
<tbody data-bind="foreach: filters">
<tr class="filter-item" draggable="true" data-bind="sortableItem: { list: $root.filters }">
<td>
<span class="disabled-filter" data-bind="click: function () { $root.haveChanges(true); enabled(!enabled()); }">
<span class="disabled-filter" data-bind="click: function () { $parent.hasChanges(true); enabled(!enabled()); }">
<i data-bind="css: {'icon-checkbox-checked': enabled, 'icon-checkbox-unchecked': !enabled()}"></i>
</span>
</td>
@ -83,20 +97,20 @@
</tr>
</tbody>
</table>
<a class="btn" data-bind="click: addFilter">
<a class="btn" data-bind="click: $root.addFilter">
<i class="icon-plus"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n="SETTINGS_FILTERS/BUTTON_ADD_FILTER"></span>
<span class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/BUTTON_ADD_FILTER"></span>
</a>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<a class="btn buttonSave" data-bind="command: saveScriptCommand, tooltipErrorTip: saveErrorText, css: {'btn-danger': '' !== saveErrorText()}">
<i data-bind="css: {'icon-floppy': !script.saving(), 'icon-spinner animated': script.saving()}"></i>
<a class="btn buttonSave" data-bind="visible: hasChanges, click: function() { $root.saveScriptCommand(); }, css: {'btn-danger': $root.saveError}">
<i data-bind="css: {'icon-floppy': !$root.saving, 'icon-spinner animated': $root.saving}"></i>
&nbsp;&nbsp;
<span class="i18n" data-i18n="SETTINGS_FILTERS/BUTTON_SAVE"></span>
<span class="i18n" data-i18n="POPUPS_SIEVE_SCRIPT/BUTTON_SAVE"></span>
</a>
</div>
</div>

View file

@ -3,7 +3,7 @@
<div class="legend">
<span class="i18n" data-i18n="SETTINGS_FILTERS/LEGEND_FILTERS"></span>
&nbsp;&nbsp;&nbsp;
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: filters.loading"></i>
<i class="icon-spinner animated" style="margin-top: 5px" data-bind="visible: loading"></i>
</div>
</div>
<div class="row" data-bind="visible: serverError">